Java中的锁的实现分synchronized和Lock两种
synchronized
synchronized是一种重量级锁,表现形式有三种
- 对于普通同步方法,锁是当前的实例对象
- 对于静态同步方法,锁是类的Class对象
- 对于同步方法块,锁是synchronized括号里配置的对象
每个对象都有一个minitor与之关联,当一个minitor被持有后,它将处于锁定状态,synchronized的实现原理即是基于进入Minitor对象来实现方法同步和代码块同步。
synchronized用的锁存在Java对象头里,对象头里的MarkWord默认存储对象的hashcode、对象分带年龄、锁标志位(00轻量级,01偏向锁,10重量级锁)
Lock
Lock
接口的实现为ReentrantLock
、ReadWriteLock
,基于AQS实现
ReentrantLock
重入锁,支持一个线程对资源的重复加锁,同时还支持获取锁时的公平和非公平性选择。
synchronized隐式的支持重入。
线程对锁的重复获取会进行计数,当计数为0时最终释放,其他线程才能获取。
公平锁的获取与线程的等待时间有关,等待时间最长的线程下次一定获取到锁,公平锁能够减少线程饥饿,某些线程长时间无法获取锁,非公平锁能够减少大量的线程切换,增大吞吐量。
ReadWriteLock
读写锁可以允许同一个时间多个读线程访问,但写线程访问时,其他线程均被阻塞。
java提供了ReentrantReadWriteLock
实现读写锁,支持公平与非公平(默认),具备重入和锁降级(获取写锁—获取读锁—释放写锁)特性。
在获取写锁时,先检测是否存在读锁,若存在则无法获取,等待。因为写锁是排他锁,若存在读锁,读线程也无法感知写线程进行的操作,造成数据不一致。
锁降级的必要:若先释放写锁再获取读锁,在这之间被其他线程获取了写锁,则读锁无法获取;若使用锁降级,则其他写锁会因为存在读锁而无法获取,等待读锁释放才获取。
lock比synchronized更灵活除了尝试获取(带时间、中断)等外,也包括:使用后者如果事物失败将抛出异常,使用前者可以结合try-finally语句块,在finally中维护系统使系统处于一个正常的状态。
队列同步器
队列同步器(AbstractQueueSynchronizer,AQS)是构建锁或其他同步组件的基础框架,通过继承的方式,子类继承并实现父类的抽象方法管理同步状态。
同步队列
通过内部的一个同步队列(FIFO双向队列)管理同步状态,当前线程获取同步状态失败时,同步器将当前线程即等待状态等信息构造成一个节点加入到同步队列中,同时阻塞当前线程;当同步状态释放时,会把首节点中的线程唤醒尝试获取同步状态。
同步器提供CAS操作将新节点加入到队尾,队列的首节点是获取到同步状态的节点,当其释放同步状态时会唤醒后继节点,后继节点获取同步状态成功后会将自己设为首节点
锁升级
- 偏向锁
当锁对象第一次被获取时,对象头里的MarkWord会将锁标志置为偏向锁标志,并将该线程的ID记录在MarkWord中,以后该线程进出同步块,无需进行同步操作;当有其他线程开始竞争这把锁时,偏向锁模式结束,升级为轻量级锁。 - 轻量级锁
当有其他线程对锁进行竞争时,偏向锁升级为轻量级锁,若竞争失败,自旋等待锁。竞争的线程不会阻塞,但若一直获取不到锁会消耗CPU,适用于追求响应速度且同步块执行速度非常快的场景。
偏向锁使用CAS机制,原理是在线程栈中创建一个新的空间(锁记录)复制存储锁对象的MarkWord,然后使用CAS机制将锁对象的MarkWord更新为指向栈锁记录的指针,如果更新成功则成功获取到锁;退出锁时将锁记录中的MarkWord复制会锁对象的MarkWord - 若自旋一定时间仍未获取锁,线程被阻塞,轻量级锁升级为重量级锁。
线程不会自旋,不会消耗CPU但响应时间慢,适用追求吞吐量,同步块执行时间长的场景
锁消除
在即时编译过程中,根据逃逸分析如果判断堆对象不会逃逸,即不会被其他线程获取到,则认为它们是线程私有的,同步加锁无需进行
锁粗化
如果一系列连续操作都是对同一个对象进行反复加锁解锁,例如一个循环中循环加锁或StringBuffer
中的append()
,则虚拟机会将加锁操作粗化到整个操作序列的外部,如循环append()
的加锁放在循环之外
独占锁的获取与释放
首先尝试获取同步状态,若失败则构建节点加入到同步队列中的队尾,并自旋循环获取同步状态,若获取不到则阻塞,当前驱结点出队或阻塞线程被中断,阻塞线程被唤醒。
共享式锁的获取与释放
共享锁与独占锁的区别是同一时刻能否有多个线程同时获取到同步状态。写操作要求对资源的独占式访问,读操作可以共享式访问。
同步器调用tryAcquireShare(int arg)
来尝试获取同步状态,当方法返回值大于等于0时代表可以获取同步状态,在自旋中成功获取同步状态后将退出自旋状态。由于同步状态由多个线程持有,在释放时需要保证安全,一般通过循环和CAS。
等待/通知机制的实现
wait/notify
线程在获取锁是基于获取对象的Minitor实现的,该过程是排他的,若没有获得Minitor,线程将进入同步队列;若持有锁的线程调用了wait()
,线程或被移到等待队列中,当线程调用notify()
或notifyAll()
时,线程才有机会从等待队列移到同步队列中,当线程释放锁后,处于同步队列中的线程进行锁的竞争。
locksupport
与Object类的wait/notify机制相比,park/unpark有两个优点:
- 以thread为操作对象更符合阻塞线程的直观定义;
- 操作更精准,可以准确地唤醒某一个线程(notify随机唤醒一个线程,notifyAll唤醒所有等待的线程),增加了灵活性。
- wait/notify前提是synchronize获取锁
Thread t1 = currentThread();
LockSupport.park(t1);//lock
LockSupport.unpark(t1);//unlock
Condition
任何对象都有一组监视器方法(基于Object的wait()
,notify()
等),与synchronized配合实现等待通知模式,Condition接口也提供了类似的监视器方法,与Lock接口配合实现等待通知机制。
Condition是同步器的内部类,每个Condition都包含一个等待队列。
队列中的每个节点都包含了一个线程引用,该线程是在Condition对象上等待的线程,若一个线程调用了Condition.await()
方法,该线程会释放锁,构造成节点加入等待队列进入等待状态,加入队列过程没有使用CAS,因为这些线程必定是获取了锁的线程。当线程从await()
返回时,当前线程一定获取了Condition相关联的锁。
Condition.signal()
唤醒等待队列中等待时间最长的节点,唤醒之前将节点移到同步队列中,具体操作为:
调用该方法的前提条件为当前线程获取了锁,然后获取等待队列的首节点,将其移动到同步队列中并使用LockSupport唤醒节点中的线程。被唤醒后的线程将从await()
中的while()
退出,进而调用同步器的acquireQueued()
方法加入到获取同步状态的竞争中。成功获取同步状态后,被唤醒的线程将从先前调用的await()
方法返回,此时该现场已经获取到了锁。