Java中为了保证每个线程中的原子操作,引入了内置锁,或者称为监视器锁,其中,每个Java对象都可以作为一个实现锁的对象,synchronized关键字修饰的代码块被称为同步代码块,线程进入同步代码块自动获取内置锁,退出同步代码块则释放锁,不需要调用者考虑它的创建以及消除,但是得十分熟悉内置锁的机制。
互斥性、可见性
Java中的锁机制具有可见性、互斥性两大通性,内置锁也不例外,关于互斥性,如其名,即在同一时间只允许一个线程持有某个锁,通过这种特性来实现多线程中的协调机制,这样在同一时间只有一个线程对需同步的代码块(复合操作)进行访问。互斥性我们也往往称为操作的原子性。简单的来说,当线程A需要获取一个来自于线程B中正在持有的锁时,线程A必须等待线程B执行完并释放该锁,才能获取该锁执行。如果线程B处于某些意外一直不释放锁,那么线程A就要一直等待下去。如示例代码:
public class ThreadTest1 extends Thread {
private Object object;
public ThreadTest1(Object object){
this.object=object;
}
public void run(){
synchronized (object){
System.out.println("蕾姆拿到锁啦");
try {
sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("5s后。。。。");
System.out.println("蕾姆释放锁啦");
}
}
}
public class ThreadTest2 extends Thread{
private Object object;
public ThreadTest2(Object object){
this.object=object;
}
@Override
public void run(){
System.out.println("拉姆等锁中。。。。");
synchronized (object){
System.out.println("拉姆拿到锁啦");
}
}
}
public class Test {
public static void main (String args[]){
Object object=new Object();
ThreadTest1 threadTest1=new ThreadTest1(object);
ThreadTest2 threadTest2=new ThreadTest2(object);
threadTest1.start();
threadTest2.start();
}
}
//输出:
//蕾姆拿到锁啦
//拉姆等锁中。。。。
//5s后。。。。
//蕾姆释放锁啦
//拉姆拿到锁啦
在蕾姆拿到锁后拉姆便一直在等待蕾姆释放锁,事实上,上述的输出是我构建的理想状态,不一定是这样的输出,有可能是拉姆先拿到锁,而蕾姆就陷入了等待中,不管是谁先拿到锁,总会保持一点的相同,就是锁的互斥事件,一个线程拿到,另外一个线程便必须等待。而且,这种因为内置锁而陷入等待的线程是不能用Object类的interrupt方法中断的。而关于可见性,简单的来说就是线程在对共享变量做了修改后,能够被其他的线程及时看到,对于内置锁来说,必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值)。
可见性的全称是内存可见性,在上一篇博客讲到,每个线程都有一个独立的内存空间,即线程栈,线程的计算存贮主要在上面进行,当我们要一个线程计算一个值时,线程会把这个值从主存中取出来放入线程栈中计算,计算完毕后才放入主存中,只有在主存中的数据才能让其它线程可见,所以这可能引发一个错误,就是线程A、B同时对主存中某个数据进行运算加1,当A从主存中拿到值为1的数据,还在计算时,B也从主存中获取到了这个数据,当A把计算结果2写入主存后,B也跟着把计算结果2写入主存中,这样线程A,B对该值计算了两次,但是得到的结果却是2。这无疑是恐怖的,而且对于多线程是灾难性的问题,而保证线程之间的可见性方法之一就是利用锁将该数据锁起来,只让一个线程更改,这样就利用线程的封闭性保证了线程之间的可见性。 如图:
可重入性
内置锁的第三个特性是可重入性,即当某个线程请求一个它自己已经在使用的锁时,这个请求可以成功。可重入性避免了死锁的发生,如何理解呢?看下面这个例子:
public class SynchronizedLockTest {
public synchronized void gc(){
gcc();
}
private synchronized void gcc(){
System.out.println("蕾姆进来啦");
}
}
//Test
new SynchronizedLockTest().gc();
//输出:蕾姆进来啦
当主线程获取到SynchronizedLockTest 对象的锁时,再次接着调用另外一个同步方法,并不会陷入等待,也就是说线程在获取自己持有的锁时能得到成功。在JVM中当线程请求一个未被持有的锁时,将记录下锁的持有者,并将获取锁的计数从0加1,当锁的获取者再次获取这个锁时,计数再加1,获取者退出一个同步代码块计数则减1,直到为0表示不再持有该锁。
锁的种类
内置锁分为两类,一类为对象锁,另外一类是类锁。对象锁对应的是对象,任何一个对象都可以成为对象锁,对象锁的使用分为两种,一类是非静态的同步方法,另外一类是同步代码块(入口是对象),当时用非静态的同步方法时,它的调用者便是对象锁,它们的格式如下:
public synchronized void gc(){
}
synchronized (object/this){
}
类锁的使用也分为两种,一类是静态的同步方法,另外一类是同步代码块(入口是类对象)。之所以强调静态与非静态的同步方法,是因为静态的同步方法时是属于类级别的,调用它的是它所属类的Class类对象,所以它是类锁。它们的格式如下:
public static synchronized void gc(){
}
synchronized (类名.class){
}
对象锁是在对象的级别上锁住对应的操作,而类锁是在类级别上,当用类锁锁住一组原子性操作时,其对应的对象锁是不是不起作用了呢?并没有,类锁与对象锁是两种互不相干的锁,同一个类的类锁在被使用时,其对象锁依旧可以使用,两者互不相干。如下代码:
public class ClassLockTest extends Thread {
@Override
public void run(){
synchronized (Object.class){
System.out.println("蕾姆进入类锁了");
try {
sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("蕾姆退出类锁了");
}
}
}
public class ObjectLockTest extends Thread {
private Object object;
public ObjectLockTest(Object object){
this.object=object;
}
@Override
public void run(){
synchronized (object){
System.out.println("拉姆获取到对象锁啦");
}
System.out.println("拉姆释放对象锁啦");
}
}
public class Test {
public static void main (String args[]){
Object object=new Object();
ClassLockTest classLockTest=new ClassLockTest();
ObjectLockTest objectLockTest=new ObjectLockTest(object);
classLockTest.start();
objectLockTest.start();
}
}
//输出:
//蕾姆进入类锁了
//拉姆获取到对象锁啦
//拉姆释放对象锁啦
//蕾姆退出类锁了
当蕾姆获取到Object的类锁时,拉姆依旧能获得Object对象的对象锁。
总结
一个线程可以拥有多个不同的锁,锁与锁之间是不相干扰的(不考虑锁的嵌套)。线程的优点是并发性,但是缺点很多:无序性、不可见性、非原子性等。内置锁解决了线程的这些缺点,但要注意的是,不管是什么锁,给一组行为加锁,让其成为原子性的操作,实质上是让线程的执行操作串行化了,这就引出了锁机制得到一个明显的问题,就是当一组操作耗时很长时,给其加锁成为原子性的操作后就会明显的影响到线程的并发性,所以我们在使用锁机制的时候,应当将加锁的跨度,亦或者称之为加锁的粒度尽可能的小,尽可能的让它在串行化的时候所花费的时间少,这样才不会对线程并发的性能产生严重的影响。