1.相关概念
- CAS:比较和交换(Conmpare And Swap)用于实现多线程同步的原子指令。 它将内存位置的内容与给定值进行比较,只有在相同的情况下,将该内存位置的内容修改为新的给定值。 这是作为单个原子操作完成的。 原子性保证新值基于最新信息计算; 如果该值在同一时间被另一个线程更新,则写入将失败。 操作结果必须说明是否进行替换; 这可以通过一个简单的布尔响应(这个变体通常称为比较和设置),或通过返回从内存位置读取的值来完成
JAVA中的CAS操作是通过
sun.misc.Unsafe
类的一系列compareAndSwap...()
函数实现的。这些都是native
函数。最终是由JVM利用CPU级的支持来实现。
- 自旋锁:循环使用CAS机制对目标进行修改,直到修改成功。
- 悲观锁/乐观锁:悲观锁认为一定发生冲突,将所有操作排队串行执行。乐观锁则假定数据没有变化,在修改时如果发生变化,读取最新的数据作为更新条件,然后重新尝试修改。
- 独享锁/共享锁:独享锁主要用在写操作,由单个线程独享。也叫“单写”。共享锁则主要用于读操作,多个线程可以同时加锁,但是只能加读锁,不能加写锁。也叫“多读”。
- 可重入锁/不可重入锁:当获得外围锁之后,则可访问这把锁内部所有的锁的内容则为可重入锁,反之则为不可重入锁。
- JIT:当Java虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为“Hot Spot Code”(热点代码),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,完成这项任务的正是JIT编译器
2. Java锁实现
2.1 synchronized
最基本的线程同步机制, 基于对象监视器实现。java中的每一个对象都有一个与之关联的监视器,可以由线程来锁定或者解锁。
2.1.1 锁的范围
synchronized
关键字修饰不同的对象,其所表达的范围有所不同。
类锁
//1,修饰静态函数时
public synchronized static void print()...
//2,直接指定类的class对象。
synchronized(Demo1.class){
//do somethings...
}
对象锁
//1,修饰动态函数时
public synchronized void print()...
//2,指定this关键字。
synchronized(this){
//do somethings...
}
//3,直接指定某个对象
Object obj = new Object();
synchronized(obj){
//do somethings...
}
2.1.2 实现原理
java中每一个对象都有与之关联的monitor,当monitor被占用时就会处于锁定状态。而synchronized在执行阶段是依靠monitorenter指令(获取对象锁),monitorexit指令(释放对象锁)来实现的。
- monitorenter:尝试获取monitor的所有权,如果成功则对象monitor进入数加1,否则线程将被阻塞。
- monitorexit:释放monitor所有权,当前线程释放对象锁,monitor进入数减1,其他被阻塞的线程可以尝试获取该对象monitor的所有权。
通过javap 查看以下代码class文件:
public class LockDemo{
public void print(){
synchronized (this){
System.out.println("hello");
}
}
}
class文件即可以看到指令在相应位置出现:
被synchronized修饰的方法在执行时是通过检查
ACC_SYNCHRONIZED
标记来实现的
当方法调用时,会先检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
2.1.3 synchronized的优化
从JDK 6 开始,synchronized的实现机制进行了较大调整,加入了CAS自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等优化策略,使得synchronized的性能有了较大的提升。
自旋锁
当线程尝试获取锁时,如果已被其他线程占用,就会一直采用玄幻监测是否被释放,而不是进入线程挂起或睡眠状态。
从JDK 6 开始,自旋默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整。
自旋锁适用于缩范围较小的情况,这样锁占用的时间很短。此时通过自旋避免线程切换带来的开销。但是,自旋等待不能替代阻塞,自旋等待期间一直占用CPU时间。如果缩范围较大,会使自旋等待时间较长,这样就会浪费资源。
锁消除
JIT编译器会根据代码上下文来判断一些加锁操作是否必要,并会对一些明显不需要的锁进行消除以提高效率。例如:
public void print(){
StringBuffer sb = new StringBuffer();
sb.append("hello").append("world");
System.out.println(sb.toString());
}
我们知道StringBuffer
的append()
函数是一个线程安全的方法(由synchronized
关键字修饰),但是当其作为函数的局部变量时,它的生命周期始终都是处于线程安全环境中,那么这个加锁的动作完全是没有必要的。在这种情况下,JIT就会将该加锁动作进行消除来提高效率。
锁粗化
JIT编译器在执行动态编译时,若发现前后相邻的synchronized块使用的是同一个锁对象,那么它就会把这几个synchronized块给合并为一个较大的同步块,这样就无需频繁申请与释放锁了通过一次锁操作,就可以执行全部代码块,从而提升了性能。
public class LockDemo{
private int count = 0;
public void print(){
synchronized (this){
count++;//同步代码1.
}
synchronized (this){
System.out.println(count);//同步代码2.
}
}
}
实际JIT在执行print()
函数时被优化一个同步块:
synchronized (this){
count++; //同步代码1.
System.out.println(count);//同步代码2.
}
偏向锁
偏向锁是在单线程执行同步代码块时所采用的策略,其本质就是没有锁。JVM在执行代码块时,如果发现该代码块只有单个线程访问,此时如果有获取锁的操作,会直接记录下当前线程ID而不获取锁资源。当有其他线程执行该代码块时,则会从偏向锁转向轻量级锁状态。以此来提高单线程下的代码块执行效率。
从JDK 6开始 默认开启偏向锁,可以使用参数-XX:-UseBiasedLocking 关闭偏向锁。
轻量级锁
轻量级锁的主要目的是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
轻量级锁的获取机制如下:
1,进入同步时
2, 将对象mark word复制到Lock record并使用CAS机制尝试更新对象mark word指向当前线程。
3,如果更新成功那么线程就获得了对象锁。
4,如果更新cas更新失败,有可能线程在之前已经获得过该对象的锁,此时可以直接金额如同步块执行。否则说明有其他线程与当前线程在竞争对象锁,此时会进入自旋等待重新尝试获取锁。如果自旋次数达到上限仍然没有获得对象锁,则会进入重量级锁模式。线程不再自旋等待而是释放CPU进入阻塞。
重量级锁
Synchronized是通过对象内部的监视器锁(Monitor)来实现的。其底层是通过操作系统的Mutex Lock来实现,成本非常之高;这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”
最后附上锁的升级过程(摘自open JDK文档)
2.2 j.u.c Lock API
j.u.c的Lock api与synchronized方式比较:
- synchronized是java底层就支持的,j.u.c Lock api,由具体的JDK实现。
- synchronized不需要用户去手动释放锁,当synchronized块执行完成后,jvm会自动让线程释放对象锁;而j.u.c Lock api则必须由用户手动释放锁(出现死锁的几率更高)。
2.2.1 Lock、ReadWriteLock
Lock接口定义了锁相关的操作,它定义了获取和释放锁相关方法:
void lock();
:当前线程以阻塞方式尝试获取锁,如果锁被占用线程会被阻塞直到成功获取锁void lockInterruptibly() throws InterruptedException;
:可中断获取锁,与lock()不同在于该方法会响应中断,即在锁的获取过程中可以中断当前线程boolean tryLock();
:尝试非阻塞的获取锁,调用该方法立即返回,true表示获取到锁boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
: 超时获取锁,以下情况会返回:时间内获取到了锁,时间内被中断,时间到了没有获取到锁void unlock();
: 释放锁
ReadWriteLock
是读写锁的顶层接口,读写锁维护了一对相关联的Lock,定义了返回读锁(共享锁)和写锁(独占锁)的方法:
Lock readLock()
:返回读锁(共享锁)。Lock writeLock()
:返回写锁(独占锁)。
接下来看下Lock接口和ReadWriteLock的具体实现类。
2.2.1.1 ReentrantLock 可重入锁
ReentrantLock
是支持重进入的锁即表示该锁能够支持一个线程对资源的重复加锁。该锁还支持获取锁时的公平与非公平的选择。
public class LockDemo{
//创建锁,lock对象的位置不同,锁影响的范围不同
private Lock lock = new ReentrantLock();
public static void main(String args[]){
final Demo demo = new LockDemo();
new Thread(public void run(){
demo.test();
}).start();
new Thread(public void run(){
demo.test();
}).start();
}
public void test(){
//考虑在此声明lock对象时,与成员变量的不同。
//Lock lock = new ReentrantLock();
try{
lock.lock();
System.out.println("获取到锁"+lock.getHoldCount());
lock.lock() //可重入
System.out.println("获取到锁"+lock.getHoldCount());
}finally{
//在finally块释放锁,避免因未释放导致死锁
System.out.println(Thread.currentThread().getName()+":释放锁");
lock.unlock();
//锁定几次,就必须释放几次,否则锁不会释放掉。
lock.unlock();
}
}
}
2.2.1.1 ReentrantReadWriteLock 读写锁
ReentrantReadWriteLock
是ReadWriteLock
的实现类,其内部维护一对关联的读写锁,一个用于只读操作,可以由多个读线程同时持有;一个用于写入操作,同时只能有1个线程操作。读写锁适合读取线程比写入线程多的场景,改进互斥锁的性能,例如:缓存组件、集合的并发线程安全性改造。
Demo1:线程安全的Cache
public class ConcurrentCache {
private static Map<String,Object> cache = new HashMap<>();
private static ReentrantReadWriteLock rwi = new ReentrantReadWriteLock();
//读取时,多个线程共享
public static Object get(String key){
rwi.readLock().lock();
try {
return cache.containsKey(key) ? cache.get(key) : null;
}finally {
rwi.readLock().unlock();
}
}
//写入时,单线程独占
public static void put(String key,Object obj){
rwi.writeLock().lock();
try {
cache.put(key,obj);
}finally {
rwi.writeLock().unlock();
}
}
}
Demo2:锁降级-缓存未命中时从DB读取
public class DBCache {
public static Map<String,Object> cache = new HashMap<>();
public static ReentrantReadWriteLock rwi = new ReentrantReadWriteLock();
//先从缓存读取,读取不到时,从数据库读取并更新缓存。
public static Object get(String key){
rwi.readLock().lock();
try {
if(!cache.containsKey(key)){
//缓存未命中,释放读锁,获取写锁后从数据库加载数据。
rwi.readLock().unlock();
rwi.writeLock().lock();
//双重判断避免有其他线程先行写入
if(cache.containsKey(key)){
Object obj = dbGet(key);
cache.put(key,obj);
}
//锁降级(持有写锁同时获取读锁,然后释放写锁)保证数据更新的可见。
rwi.readLock().lock();
rwi.writeLock().unlock();
}
return cache.get(key);
}finally {
rwi.readLock().unlock();
}
}
//模拟从数据库读取
public static Object dbGet(String key){
//从数据库读取
return "";
}
}
通过源码发现,不管是ReentrantLock
还是ReentrantReadWriteLock
其内部其实都是借助组合一个私有的内部类Sync
来实现最终的lock或者unlock操作。而Sync
则是继承自一个叫做AbstractQueuedSynchronizer
的东东。
这个就是Doug Lea大师创作的用来构建锁或者其他同步组件的基础框架类,简称AQS。除了ReentrantLock
和ReentrantReadWriteLock
,j.u.c中还有很多其他的并发工具类的实现都依赖于AQS,包括CountDownLatch
计数器、CyclicBarrier
线程屏障、Semaphore
信号量等。
后续通过分析AQS的源码和实现思路来理解j.u.c的内部原理。附上Doug Lea大师关于AQS的相关论文。