计算机的锁分类有很多种,本书并不打算详细介绍每种锁,而是通过对java.util.concurrent(JUC)包中的基础类的解析来说明锁的本质和特性。Java中常用锁实现的方式有两种。
1、用并发包中的锁类
并发包的类族中,Lock是JUC包的顶层接口,它的实现逻辑并未用到synchronized,而是利用volatile的可见性。先通过Lock来了解JUC包的一些基础类,如图所示:
图为Lock的继承类图,ReentrantLock对于Lock接口的实现主要依赖了Sync,而Sync继承了AbstractQueuedSynchronizer(AQS),它是JUC包实现同步的基础工具。在AQS中,定义了一个volatile int state变量作为共享资源,如果线程获取资源失败,则进入同步FIFO队列中等待;如果成功获取资源就执行临界区代码。执行完释放资源时,会通知同步队列中的等待线程来获取资源后出队并执行。
AQS是抽象类,内置自旋锁实现的同步队列,封装入队和出队的操作,提供独占共享、中断等特性的方法。AQS的子类可以定义不同的资源实现不同性质的方法。比如可重入锁ReentrantLock,定义state为0时可以获取资源并置为1.若以获得资源,state不断加1,在释放资源时state减1,直至为0;CountDownLatch初始时定义了资源总量state=count,countDown()不断将state减1,当state=0时才获得锁,释放后state就一直为0。所有线程调用await()都不会等待,所以CountDownLatch是一次性的,用完后如果再想用就只能重新创建一个;如果希望循环使用,推荐使用基于ReentrantLock实现的CyclicBarrier。Semaphore与CountDownLatch略有不同,同样也定义了资源总量state=permits,当state>1时就能获得锁,并将state减1,当state=0时只能等待其他线程释放锁,当释放锁时state加1,其他线程又能获得这个锁。当Semphore的permits定义为1时,就是互斥锁,当permits>1时,就是共享锁。
JDK提出了一个新的锁:StampedLock,改进了读写锁ReentrantReadWriterLock。这些新增的锁相关类不断丰富了JUC包的内容,降低了并发编程的难度,提高了锁的性能和安全性。
2、利用同步代码块
同步代码块一般使用Java的synchronized关键字来实现,有两种方式对方法进行加锁操作:第一,在方法签名处加synchronized关键字;第二,使用synchronized(对象或类)进行同步。这里的原则是锁的范围尽可能小,锁的时间尽可能短,即能锁对象,就不要锁类;能锁住代码块,就不要锁方法。
Synchronized锁特性由JVM负责实现。在JDK的不断优化迭代中,synchronized锁的性能得到极大提升,特别偏向锁的实现,使得synchronized已经不是昔日那个低性能且笨重的锁了。JVM底层是通过监视锁来实现synchronized同步的。监视所即monitor,是每个对象与生俱来的一个隐藏字段。使用synchronized时,JVM会根据synchronized的当前使用环境,找到对应对象的monitor,再根据monitor的状态进行加、解锁的判断。例如,线程在进入同步方法或代码块时,会获取该方法或代码块所属对象的monitor,进行加锁判断。如果成功加锁就成为该monitor的唯一持有者。Monitor在被释放前,不能再被其他线程获取。下面通过字节码学习synchronized锁时如何实现的:
方法元信息中会使用ACC_SYNCHRONIZED标识该方法是一个同步方法。同步代码块中会使用monitorenter及monitorexit两个字节码指令获取和释放monitor。如果使用monitorenter进入时monitor为0,表示该线程可以持有monitor后续代码。并将monitor加1;如果当前线程已经持有了monitor,那么monitor继续加1;如果monitor非0,其他线程就会进入阻塞转态。JVM对synchronized的优化主要在于对monitor的加锁、解锁上。JDK6后不断优化使得synchronized提供三种锁的实现,包括偏向锁、轻量级锁、重量级锁,还提供自动的升级和降级机制。JVM就是利用CAS在对象头上设置线程ID,表示这个对象偏向于当前线程,这就是偏向锁。
偏向锁是为了在资源没有被多线程竞争的情况下尽量减少锁带来的性能开销。在锁对象的对象头中有一个ThreadId字段,当第一个线程访问锁时,如果该锁没有被其他线程访问过,即ThreadId字段为空,那么JVM让其持有偏向锁,并将ThreadId字段的值设置为线程的ID。当下一次获取锁时,会判断当前线程的ID是否与锁对象的ThreadId一致。如果一致,那么该线程不会再重复获取锁,从而提高了程序的运行效率。如果出现锁的竞争情况,那么偏向锁会被撤销并升级为轻量级锁。如果资源的竞争非常激烈,会升级为重量级锁。偏向锁可以降低无竞争开销,它不是互斥锁,不存在线程竞争情况,省去再次同步判断的步骤,提升了性能。