Java并发-线程同步Synchronized和ReentrantLock的区别

272 阅读9分钟

Java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时,将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。

synchronized是什么?实现原理是怎么样的?什么时候使用?

reentrantLock实现原理是怎么样的?

重入锁和公平锁又是怎么回事?

Synchronized和ReentrantLock比较总结

synchronized是什么?实现原理是怎么样的?什么时候使用?

synchronized实现同步的基础:Java中每个对象都可以作为锁。当线程试图访问同步代码时,必须先获得对象锁,退出或抛出异常时必须释放锁。synchronzied实现同步的表现形式分为:代码块同步和方法同步。

实现原理:JVM基于进入和退出Monitor对象来实现代码块同步和方法同步,两者实现细节不同。

代码块同步:在编译后通过将monitorenter指令插入到同步代码块的开始处,将monitorexit指令插入到方法结束处和异常处,通过反编译字节码可以观察到。任何一个对象都有一个monitor与之关联,线程执行monitorenter指令时,会尝试获取对象对应的monitor的所有权,即尝试获得对象的锁。

方法同步:synchronized方法在method_info结构有ACC_synchronized标记,线程执行时会识别该标记,获取对应的锁,实现方法同步。

两者虽然实现细节不同,但本质上都是对一个对象的监视器(monitor)的获取。任意一个对象都拥有自己的监视器,当同步代码块或同步方法时,执行方法的线程必须先获得该对象的监视器才能进入同步块或同步方法,没有获取到监视器的线程将会被阻塞,并进入同步队列,状态变为BLOCKED。当成功获取监视器的线程释放了锁后,会唤醒阻塞在同步队列的线程,使其重新尝试对监视器的获取。

![](https://upload-images.jianshu.io/upload_images/20887676-2c99940011f8fcdc?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

synchronized使用场景:

a.方法同步

publicsynchronizedvoidmethod(){}

锁住的是该对象,类的其中一个实例,当该对象(仅仅是这一个对象)在不同线程中执行这个同步方法时,线程之间会形成互斥。达到同步效果,但如果不同线程同时对该类的不同对象执行这个同步方法时,则线程之间不会形成互斥,因为他们拥有的是不同的锁。

b.静态方法同步

publicsynchronizedstaticvoidmethod(){}

锁住的是该类,当所有该类的对象(多个对象)在不同线程中调用这个static同步方法时,线程之间会形成互斥,达到同步效果。

c.代码块同步

synchronized(this){//TODO }

这里和a的效果一样,锁住的是对象

d.代码块同步

synchronized(Test.class){//TODO }

这里和b的效果一样,锁住的是类

e.代码块同步

synchronized(object) {}

这里面的object可以是一个任何object对象或数组,并不一定是它本身对象或者类,谁拥有object这个锁,谁就能够操作该块程序代码。

reentrantLock实现原理是怎么样的?

先看ReentrantLock类层次结构

![](https://upload-images.jianshu.io/upload_images/20887676-887f8b6941fde289?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

Lock接口:

void lock():执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁.

boolean tryLock():如果锁可用, 则获取锁, 并立即返回true, 否则返回false. 该方法和lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用, 当前线程仍然继续往下执行代码. 而lock()方法则是一定要获取到锁, 如果锁不可用, 就一直等待, 在未获得锁之前,当前线程并不继续向下执行. 通常采用如下的代码形式调用tryLock()方法:

void unlock():执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程并不持有锁, 却执行该方法, 可能导致异常的发生.

Condition newCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定,当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将缩放锁。

ReentrantLock主要利用CAS+CLH队列来实现。它支持公平锁和非公平锁,两者的实现类似

CAS:Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现。

CLH队列:带头结点的双向非循环链表 (如下图)

![](https://upload-images.jianshu.io/upload_images/20887676-e6004506ff4753a3?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入CLH队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:

非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取。

公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。

ReentrantLocklock=newReentrantLock();//参数默认false,不公平锁lock.lock();//如果被其它资源锁定,会在此等待锁释放,达到暂停的效果 try{//TODO }finally{lock.unlock();//释放锁 }

重入锁和公平锁又是怎么回事?

重入锁:当一个线程得到一个对象后,再次请求该对象锁时是可以再次得到该对象的锁的。具体概念就是:自己可以再次获取自己的内部锁。Java里面内置锁(synchronized)和Lock(ReentrantLock)都是可重入的。

publicclassSynchronizedTest{publicvoidmethod1(){ synchronized (SynchronizedTest.class) {System.out.println("方法1获得ReentrantTest的锁运行了"); method2(); } }publicvoidmethod2(){ synchronized (SynchronizedTest.class) {System.out.println("方法1里面调用的方法2重入锁,也正常运行了"); } }publicstaticvoidmain(String[] args){newSynchronizedTest().method1(); }}

上面的代码是synchronized的重入锁特性,即调用method1()方法时,已经获得了锁,此时内部调用method2()方法时,由于本身已经具有该锁,所以可以再次获取。

publicclassReentrantLockTest{privateLocklock=newReentrantLock();publicvoidmethod1(){lock.lock();try{System.out.println("方法1获得ReentrantLock锁运行了"); method2();}finally{lock.unlock(); } }publicvoidmethod2(){lock.lock();try{System.out.println("方法1里面调用的方法2重入ReentrantLock锁,也正常运行了");}finally{lock.unlock(); } }publicstaticvoidmain(String[] args){newReentrantLockTest().method1(); }}

上面便是ReentrantLock的重入锁特性,即调用method1()方法时,已经获得了锁,此时内部调用method2()方法时,由于本身已经具有该锁,所以可以再次获取。

公平锁:CPU在调度线程的时候是在等待队列里随机挑选一个线程,由于这种随机性所以是无法保证线程先到先得的(synchronized控制的锁就是这种非公平锁)。但这样就会产生饥饿现象,即有些线程(优先级较低的线程)可能永远也无法获取CPU的执行权,优先级高的线程会不断的强制它的资源。那么如何解决饥饿问题呢,这就需要公平锁了。公平锁可以保证线程按照时间的先后顺序执行,避免饥饿现象的产生。但公平锁的效率比较低,因为要实现顺序执行,需要维护一个有序队列。

ReentrantLock便是一种公平锁,通过在构造方法中传入true就是公平锁,传入false,就是非公平锁。

publicclassLockFairTestimplementsRunnable{//创建公平锁privatestaticReentrantLocklock=newReentrantLock(true);publicvoidrun(){while(true){lock.lock();try{System.out.println(Thread.currentThread().getName()+"获得锁");}finally{lock.unlock(); } } }publicstaticvoidmain(String[] args){LockFairTest lft=newLockFairTest();Thread th1=newThread(lft);Thread th2=newThread(lft); th1.start(); th2.start(); }}

Thread-1获得锁Thread-0获得锁Thread-1获得锁Thread-0获得锁Thread-1获得锁Thread-0获得锁

Synchronized和ReentrantLock比较总结

Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现。

synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

Lock可以提高多个线程进行读操作的效率。

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时ReentrantLock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

在JDK1.5中,synchronized是性能低效的。因为这是一个重量级操作,它对性能最大的影响是阻塞的是实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性带来了很大的压力。相比之下使用Java提供的ReentrankLock对象,性能更高一些。到了JDK1.6,发生了变化,对synchronize加入了很多优化措施,有自适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在JDK1.6上synchronized的性能并不比lock差。官方也表示,他们也更支持synchronized,在未来的版本中还有优化余地,所以还是提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。

最后啰嗦一下,其实Java实现线程同步有很多种方法,除了Synchronized和Lock,还可以使用特殊变量域volatile关键字,使用阻塞队列(LinkedBlockingQueue,FIFO-先进先出队列)实现,使用原子变量实现(在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类)。