3、线程-锁 2022-02-15 19:29 > 以下基于JDK1.8,此文以后可能有更新 前面的章节中我们使用了synchronized关键字,他是一种锁,锁这个字眼很形象的解释了他的用途,就好比一个房间,我进去之后上了锁,这样其他人就不能进去了,等到我出来之后,把锁解开,其他人才能再进去。 日常开发时,用到的锁主要有: 1、synchronized 2、Lock 而Lock又有ReentrantLock,ReentrantReadWriteLock(内部包括ReadLock和WriteLock) Reentrant意思是“可重入”,代表ReentrantLock是可重入锁,其实synchronized也是可重入锁。可重入锁就是可以重新进入,可以循环拿锁。 ### synchronized synchronized是Java中的关键字,也即是JNI级别的东西,Java中每一个对象都可以作为一个锁,具体体现为一下3点: 1、对于同步方法块,锁的是synchronized括号里配置的对象。 2、对于普通同步方法,锁的是当前实例对象。 3、对于静态同步方法,锁的是当前类的Class对象。 示例: ```java package com.example.demo.core.Lock; /** * @author: HanXu * on 2022/2/9 * Class description: synchronized的三种方式 */ public class SyncDemo { public static void main(String[] args) { //1、同步方法块,锁的是monitor这个对象 Object monitor = new Object(); synchronized (monitor) { System.out.println("同步方法块"); } //2、普通同步方法,锁的是syncDemo对象 SyncDemo syncDemo = new SyncDemo(); syncDemo.test2(); //3、静态同步方法,锁的是SyncDemo这个类的Class对象 SyncDemo.test3(); } public synchronized void test2() { System.out.println("普通同步方法"); } public static synchronized void test3() { System.out.println("静态同步方法"); } } ``` #### 子父类之间的传递 需要注意的是,synchronized不属于方法定义的一部分,因此不能被继承。也就是说如果父类的某个方法上有synchronized关键字,子类覆盖了这个方法,那么子类中的这个方法默认是没有被synchronized修饰的,不属于同步方法。子类必须显示的在自己的方法上加上synchronized。 子类也可以不覆盖父类的方法,直接调用父类的方法,这样子类方法虽然不是同步的,但是调用了父类的同步方法,也相同于同步。 示例:两个线程对同一个变量分别自加10000次,最后查看结果。并观察在子父类之间synchronized的传递关系。 ```java package com.example.demo.core.Lock; import lombok.Getter; /** * @author: HanXu * on 2022/2/9 * Class description: synchronized在子父类之间 */ public class SyncExtendsDemo { public static void main(String[] args) throws InterruptedException { // Parent parent = new Parent(); // Parent parent = new Son1();//12467 // Parent parent = new Son2();//20000 Parent parent = new Son3();//20000 Thread t1 = new Thread(() -> { for (int i = 0; i < 10000; i++) { parent.test(); } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 10000; i++) { parent.test(); } }); t1.start(); t2.start(); //main线程等待两个子线程执行完再输出i t1.join(); t2.join(); System.out.println(parent.getI()); } } @Getter class Parent { protected int i; public synchronized void test() { i++; // System.out.println("父类同步方法"); } } class Son1 extends Parent { @Override public void test() { i++; // System.out.println("子类非同步方法"); } } class Son2 extends Parent { @Override public synchronized void test() { i++; // System.out.println("子类显示加上了synchronized的同步方法"); } } class Son3 extends Parent { @Override public void test() { //子类直接调用父类,也相当于同步方法 super.test(); } } ``` ##### 锁粗化 上述例子中,如果细心点就会发现,打开与不打开System.out.println的注释,执行结果是不一样的。对于`Parent parent = new Son1();`这组测试用例来说,理论上他是有线程安全问题的,但是如果把`System.out.println("子类非同步方法");`这个注释放开,就会发现,执行结果总是20000,是对的。但是他应该要错的呀,为什么呢?我们点开`System.out.println`的`println`看一下源码: ```java public void println(int x) { synchronized (this) { print(x); newLine(); } } ``` 原来内部添加了synchronized锁,在输出的时候获取当前对象的锁。那为什么加锁会对我们的结果有影响呢? 查阅资料发现,JVM虚拟机对锁有多项优化原则,其中之一就是“锁粗化”,指的是如果一系列连续操作都对同一个对象反复加锁和解锁,或者加锁操作就是出现在循环体中的(就像本例中,循环里使用println),那即使没有线程竞争,频繁的进行互斥同步操作也会导致不必要的性能损耗。所以JVM干脆给你直接在循环外部加锁! 我们这里在循环中使用`System.out.println`,而`println`方法内部又是要加锁的,所以可以理解为循环里面不断的加锁解锁,所以JVM会对其进行优化,优化后我们的代码就变成了类似这样:【针对`Parent parent = new Son1();`这个用例】 ```java synchronized (this) { for (int i = 0; i < 10000; i++) { //parent.test(); i++; print("子类非同步方法"); newLine(); } } ``` 所以使用`System.out.println`进行输出时,才会导致结果是正确的。 ### Lock Lock是一个接口,也就是说这种锁我们终于可以看到他的源码了!Lock在JUC(java.util.concurrent包)下。 简单使用: ```java package com.example.demo.core.Lock.create; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author: HanXu * on 2022/2/9 * Class description: Lock入门 */ public class LockDemo { public static void main(String[] args) { Lock lock = new ReentrantLock(); lock.lock(); try { System.out.println("这里就是加锁区域了,相当于synchronized代码块中的内容"); } finally { lock.unlock(); } } } ``` 上述就是一个简单的创建并使用Lock锁的例子,需要注意的点是:Lock锁不像synchronized那样会自动释放,所以必须手动释放。在写法上要求严格按照这样的形式写:这也是阿里规约规定的写法。 ```java lock.lock(); try { //to do something } finally { lock.unlock(); } ``` #### Lock接口源码 ```java package java.util.concurrent.locks; import java.util.concurrent.TimeUnit; public interface Lock { //加锁,若锁被其他线程占用,会一直阻塞 void lock(); void lockInterruptibly() throws InterruptedException; //尝试加锁,成功或失败会立即返回true/false boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //解锁 void unlock(); //获取一个锁上的条件 Condition newCondition(); } ``` 可以看到Lock接口还是很简单的,加锁共有4个方法,解锁有1个方法,还有一个是获取条件的方法。 4个加锁方法中,前2个是阻塞方式加锁,后2个是非阻塞方式加锁。 阻塞方式加锁的2个方法中,一个响应中断;一个不响应。非阻塞方式加锁的两个方法中,一个带尝试时间;一个不带尝试时间,立即返回。 #### ReentrantLock 日常我们使用Lock时,都是使用ReentrantLock这个实现类,至于他内部的细节,是和AQS(AbstractQueuedSynchronizer)有很大关系的,这个类有很大的说头,篇幅较长,我们后面单独开一章来讲。 ##### 阻塞方式加锁 我们试着让一个线程拿到锁后,故意停留,让另一个线程迟迟获取不到锁。 ```java package com.example.demo.core.Lock.create; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author: HanXu * on 2022/2/9 * Class description: Lock入门 */ public class LockDemo { public static void main(String[] args) throws InterruptedException { Lock lock = new ReentrantLock(); Thread t1 = new Thread(() -> { System.out.println("t1已开始执行" + System.currentTimeMillis()); lock.lock(); try { System.out.println("t1已拿到锁" + System.currentTimeMillis()); //拿到锁后故意睡眠5秒,让另一个线程等待 Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }); Thread t2 = new Thread(() -> { System.out.println("t2已开始执行" + System.currentTimeMillis()); lock.lock(); try { System.out.println("t2已拿到锁" + System.currentTimeMillis()); } finally { lock.unlock(); } }); t1.start(); //t1执行后主线程睡眠50ms,确保t1先于t2执行 Thread.sleep(50); t2.start(); } } ``` 看到执行结果: ``` t1已开始执行1644377106844 t1已拿到锁1644377106844 t2已开始执行1644377106894 t2已拿到锁1644377111845 ``` t1开始执行并拿到锁,t2开始执行后,一直卡着,卡在`lock.lock();`这行代码上,获取不到锁就阻塞在这行代码,等到t1过了5秒释放了锁后,t2才能获取到锁并执行。这就是所谓的阻塞方式加锁。 ##### 非阻塞方式加锁 依旧是t1先抢占到锁后故意等待5秒,t2使用非阻塞式加锁,查看执行效果: ```java package com.example.demo.core.Lock.create; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author: HanXu * on 2022/2/9 * Class description: Lock入门 */ public class LockDemo { public static void main(String[] args) throws InterruptedException { Lock lock = new ReentrantLock(); Thread t1 = new Thread(() -> { System.out.println("t1已开始执行" + System.currentTimeMillis()); lock.lock(); try { System.out.println("t1已拿到锁" + System.currentTimeMillis()); //拿到锁后故意睡眠5秒,让另一个线程等待 Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } }); Thread t2 = new Thread(() -> { System.out.println("t2已开始执行" + System.currentTimeMillis()); boolean b = lock.tryLock(); if (b) { try { System.out.println("t2已拿到锁" + System.currentTimeMillis()); } finally { lock.unlock(); } } else { System.out.println("t2没有获取到锁,已经准备结束" + System.currentTimeMillis()); } }); t1.start(); //t1执行后主线程睡眠50ms,确保t1先于t2执行 Thread.sleep(50); t2.start(); } } ``` 执行结果: ``` t1已开始执行1644385902467 t1已拿到锁1644385902467 t2已开始执行1644385902517 t2没有获取到锁,已经准备结束1644385902517 ``` 全部语句输出后,等待5秒钟,程序结束。 可以看到这次t2加锁时使用`tryLock()`,直接知道了当前无法抢占到锁,于是直接返回false,执行后续输出并结束。 #### 条件Condition 使用synchronized时,可以配合锁对象的wait()、notify()实现等待、唤醒效果,达到了线程间通信的目的,那Lock有没有呢? 当然有,Lock的源码中还有一个方法是`Condition newCondition();`这个方法就是用来获取一个`Condition`条件对象,顾名思义,使用这个条件对象就能在满足某些条件是等待与唤醒。 ##### Condition源码 `Condition`也是一个接口,让我们看下他的源码: ```java package java.util.concurrent.locks; import java.util.concurrent.TimeUnit; import java.util.Date; public interface Condition { //等待 void await() throws InterruptedException; void awaitUninterruptibly(); long awaitNanos(long nanosTimeout) throws InterruptedException; boolean await(long time, TimeUnit unit) throws InterruptedException; boolean awaitUntil(Date deadline) throws InterruptedException; //唤醒 void signal(); void signalAll(); } ``` 熟悉的模式,5个等待方法,2个唤醒方法。 5个等待方法中有一直等待的,有等待指定时间的,这个时间有相对时间,有绝对时间。2个唤醒,一个随机唤醒一个的,一个全部唤醒的。 ##### 等待/唤醒示例 循环10次,thread1和thread2交替打印奇偶数,thread1打印奇数,thread2打印偶数 ```java package com.example.demo.core.Lock.create; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author: HanXu * on 2022/2/9 * Class description: Lock入门 */ public class LockDemo { static volatile int i = 0; static final int count = 10; private static final Logger log = LoggerFactory.getLogger(LockDemo.class); public static void main(String[] args) throws InterruptedException { Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); Thread t1 = new Thread(() -> { while (i < count) { lock.lock(); try { //不是奇数 while ((i & 1) != 1) { condition.await(); } if (i < count) { log.info("{}", i); i++; condition.signalAll(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }); Thread t2 = new Thread(() -> { while (i < count) { lock.lock(); try { while ((i & 1) == 1) { condition.await(); } if (i < count) { log.info("{}", i); i++; condition.signalAll(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }); t1.start(); t2.start(); } } ``` 执行结果: ``` 14:41:08.904 [thread2] INFO com.example.demo.core.Lock.create.LockDemo - 0 14:41:08.907 [thread1] INFO com.example.demo.core.Lock.create.LockDemo - 1 14:41:08.908 [thread2] INFO com.example.demo.core.Lock.create.LockDemo - 2 14:41:08.908 [thread1] INFO com.example.demo.core.Lock.create.LockDemo - 3 14:41:08.908 [thread2] INFO com.example.demo.core.Lock.create.LockDemo - 4 14:41:08.908 [thread1] INFO com.example.demo.core.Lock.create.LockDemo - 5 14:41:08.909 [thread2] INFO com.example.demo.core.Lock.create.LockDemo - 6 14:41:08.909 [thread1] INFO com.example.demo.core.Lock.create.LockDemo - 7 14:41:08.909 [thread2] INFO com.example.demo.core.Lock.create.LockDemo - 8 14:41:08.909 [thread1] INFO com.example.demo.core.Lock.create.LockDemo - 9 ``` 注意:一个Lock可以生出多个Condition,多个Condition之间是没有关系的。多个Condition代表多个条件,他们的等待/唤醒是独立的。 例如 ```java Condition condition1 = lock.newCondition(); Condition condition2 = lock.newCondition(); ``` 同一个lock生成了两个Condition,就代表我的业务中有两个等待唤醒条件,当`condition1.signalAll();`时,对`condition1`是没有影响的。 #### ReadWriteLock读写锁 ReadWriteLock是读写锁,读写锁就是把读和写操作分开,单独加锁,这样就能提高读操作的效率。如果你没有接触过这个,可能看到这里会很懵,没事,接着向下看一个例子,然后总结一下就能够明白读写锁的“功效”了。 ##### ReadWriteLock源码 ReadWriteLock也是一个接口,内部只有两个抽象方法定义: ```java package java.util.concurrent.locks; public interface ReadWriteLock { //获取一个读锁 Lock readLock(); //获取一个写锁 Lock writeLock(); } ``` ##### ReentrantReadWriteLock源码 在日常使用时,经常用到的读写锁是ReentrantReadWriteLock,它本身不是Lock的子类,但是我们用的时候真正使用的读写锁`ReadLock`,`WriteLock`却是Lock的子类。 ```java import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.AbstractQueuedSynchronizer; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable { private static final long serialVersionUID = -6992448646407690164L; private final ReentrantReadWriteLock.ReadLock readerLock; private final ReentrantReadWriteLock.WriteLock writerLock; final Sync sync; public ReentrantReadWriteLock() { this(false); } public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); } public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; } public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; } abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = 6317671515068378041L; //省略... public static class ReadLock implements Lock, java.io.Serializable { private static final long serialVersionUID = -5992448646407690164L; private final Sync sync; protected ReadLock(ReentrantReadWriteLock lock) { sync = lock.sync; } public void lock() { sync.acquireShared(1); } public void lockInterruptibly() throws InterruptedException { sync.acquireSharedInterruptibly(1); } public boolean tryLock() { return sync.tryReadLock(); } public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)); } public void unlock() { sync.releaseShared(1); } public Condition newCondition() { throw new UnsupportedOperationException(); } //省略... } public static class WriteLock implements Lock, java.io.Serializable { private static final long serialVersionUID = -4992448646407690164L; private final Sync sync; protected WriteLock(ReentrantReadWriteLock lock) { sync = lock.sync; } public void lock() { sync.acquire(1); } public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } public boolean tryLock( ) { return sync.tryWriteLock(); } public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); } public void unlock() { sync.release(1); } public Condition newCondition() { return sync.newCondition(); } //省略... } //省略... } } ``` 继承关系: ![](http://minio.riun.xyz/riun1/2022-02-09_24i0KuGeFYGZomDsG8.jpg) 下面是简单使用: ##### 读读共享 创建n个线程,同时获取读锁,查看执行情况: ```java package com.example.demo.core.Lock.rwLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * @author: HanXu * on 2022/2/9 * Class description: 读写锁案例 */ public class RWLockDemo { private static final Logger log = LoggerFactory.getLogger(RWLockDemo.class); private static final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private static final Lock readLock = readWriteLock.readLock(); private static final Lock writeLock = readWriteLock.writeLock(); public static void main(String[] args) { RWLockDemo rwLockDemo = new RWLockDemo(); //创建5个线程,"同时"进行读操作 for (int i = 0; i < 5; i++) { new Thread(() -> { rwLockDemo.readOption(Thread.currentThread()); }, "thread" + i).start(); } } public void readOption(Thread thread) { readLock.lock(); try { //模拟某个线程读2ms数据 long startTime = System.currentTimeMillis(); while (System.currentTimeMillis() - startTime < 2) { log.debug("{} 正在读数据", thread.getName()); } log.debug("{} 读操作即将完毕", thread.getName()); } finally { readLock.unlock(); } } public void writeOption(Thread thread) { writeLock.lock(); try { //模拟某个线程写2ms数据 long startTime = System.currentTimeMillis(); while (System.currentTimeMillis() - startTime < 2) { log.debug("{} 正在写数据", thread.getName()); } log.debug("{} 写操作即将完毕", thread.getName()); } finally { writeLock.unlock(); } } } ``` 执行结果: ``` 15:16:06.268 [thread1] DEBUG com.example.demo.core.Lock.rwLock.RWLockDemo - thread1 正在读数据 15:16:06.268 [thread2] DEBUG com.example.demo.core.Lock.rwLock.RWLockDemo - thread2 正在读数据 15:16:06.271 [thread1] DEBUG com.example.demo.core.Lock.rwLock.RWLockDemo - thread1 读操作即将完毕 15:16:06.268 [thread0] DEBUG com.example.demo.core.Lock.rwLock.RWLockDemo - thread0 正在读数据 15:16:06.268 [thread3] DEBUG com.example.demo.core.Lock.rwLock.RWLockDemo - thread3 正在读数据 15:16:06.272 [thread0] DEBUG com.example.demo.core.Lock.rwLock.RWLockDemo - thread0 读操作即将完毕 15:16:06.272 [thread2] DEBUG com.example.demo.core.Lock.rwLock.RWLockDemo - thread2 读操作即将完毕 15:16:06.272 [thread3] DEBUG com.example.demo.core.Lock.rwLock.RWLockDemo - thread3 读操作即将完毕 15:16:06.268 [thread4] DEBUG com.example.demo.core.Lock.rwLock.RWLockDemo - thread4 正在读数据 15:16:06.276 [thread4] DEBUG com.example.demo.core.Lock.rwLock.RWLockDemo - thread4 读操作即将完毕 ``` 可以看到,有多个线程同时在进行读操作,能够反映出来,他们共同进入了读锁。试想下,如果这里换成ReentrantLock,那么他们还能共同抢占到读锁吗?显然是不能的。不信你可以将`Lock readLock = readWriteLock.readLock();`改成`Lock readLock = new ReentrantLock();`试试,就能看到执行结果是每个线程自己独立执行,一个线程执行完了,另一个线程才能执行。 到这里,大概能明白读写锁的威力了吧? ##### 写写互斥 要测试`WriteLock`写锁,只需要将上述代码中的for循环里的`readOption`方法调用改为`writeOption`即可。 ```java //创建5个线程,"同时"进行读/写操作 for (int i = 0; i < 5; i++) { new Thread(() -> { //读读共享 //rwLockDemo.readOption(Thread.currentThread()); //写写互斥 rwLockDemo.writeOption(Thread.currentThread()); }, "thread" + i).start(); } ``` 测试结果就是每个线程独立执行,一个结束了,另一个才能获取到锁并执行。和ReentrantLock执行结果基本一致。 ##### 读写互斥 依旧是上述代码,修改for循环为如下代码:创建多组线程分别进行读写操作,可以看到读写是互斥的。 ```java for (int i = 0; i < 3; i++) { new Thread(() -> { rwLockDemo.readOption(Thread.currentThread()); }, "threadRead" + i).start(); new Thread(() -> { rwLockDemo.writeOption(Thread.currentThread()); }, "threadWrite" + i).start(); } ``` 结果太多就不贴了,总体来说就是读完,才能写,写完,才能读。 ![](http://minio.riun.xyz/riun1/2022-02-09_24ivIkHikNUVRd3IdA.jpg) 从这个实验结果中我们也能得到一个信息:**想要获取读锁,则不能有其他线程获取到写锁**。(因为在读写混合的操作中,各个读锁依然是独立执行的,这就是因为其中存在其他线程获取了写锁。) ##### 锁升级与降级 升级与降级看字面意思就能理解,升级就是指从读锁升级到写锁(前者要求低,后者要求高,所以是升级),降级就是从写锁降级到读锁。 ReentrantReadWriteLock这种读写锁支持锁降级,不支持升级。(想想也能理解,我从要求高的,降级为要求低的是可行的,但是从要求低的升级为要求高的是不可行的。这就像JDK高版本兼容低版本,低版本却不兼容高版本差不多) 下面示例依旧沿用上述代码框架。 不支持升级: ```java readLock.lock(); try { System.out.println("已经获取到读锁"); writeLock.lock(); System.out.println("已经获取到写锁"); } finally { writeLock.unlock(); readLock.unlock(); } ``` 执行结果:已经获取到读锁,然后就卡在那。说明获取了读锁后无法完成升级,即无法再获取写锁。 从这个实验里面也得出一个信息:**若要获取写锁,则不能有其他线程获取读锁和写锁**。 支持降级: ```java writeLock.lock(); try { System.out.println("已经获取到写锁"); readLock.lock(); System.out.println("已经获取到读锁"); } finally { //这里只要释放了写锁,则读锁就自动释放了,真想写上手动释放读锁也可以 writeLock.unlock(); } new Thread(() -> { readLock.lock(); try { System.out.println("子线程也获取到了读锁"); } finally { readLock.unlock(); } }).start(); ``` 执行结果: ``` 已经获取到写锁 已经获取到读锁 子线程也获取到了读锁 ``` 从这个实验里面也能得出一个信息,那就是“读写互斥”小节中获取到的信息中关于“其他”两个字的证明。那个小节中我们得到的信息是:**想要获取读锁,则不能有其他线程获取到写锁**,为什么要强调**其他**呢?通过这里我们可以发现,**如果是自身线程获取到了写锁,则也能获取到读锁**。 ##### 总结 读写锁的三个特性: 1、公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。 2、可重入:读锁和写锁都支持线程重进入。 3、锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。 获取读写锁的条件: 1、获取读锁的条件:没有其他线程获取写锁。 2、获取写锁的条件:没有其他线程获取读锁或写锁。 锁可降级的原因: 当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁; 而对于获得写锁的线程,它一定**独占**了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。 ##### 小疑问 有人可能会说,读读不互斥,看起来好像不加锁也可以。 其实不是这样的,锁的目的是保证连续读在逻辑上是一致的。 还记得我们最开始举的例子吗?锁就像一个真实世界中的锁,把门一锁,其他人就进不来了。而读锁就好比告诉人们:我只是进来参观参观,不碰任何东西。如果有人是这样的,那这些人都可以进入这个由“读锁”锁着的屋子,他们可以共同在屋子里看,共同在屋子里的人看到的内容一定是相同的。而如果此时有要修改内容的人进来,他就要获取“写锁”,则他必须等屋子里持有“读锁”的人都出来,才能进去修改内容。而你改完的内容,再有其他读锁进来,看到的就是你修改之后的内容了。无论是前面那波读锁还是后面那波读锁,他们看到的内容在逻辑上是连续的。 而如果没有加读锁呢?房门打开,有可能你正在看,然后有人进来修改了内容,这样你看到的内容在逻辑上就不连续了。 举个编程例子: ```java int x = obj.x; // 这里线程可能中断 int y = obj.y; ``` 假设obj的x,y是[0,1],某个写线程修改成[2,3]。那理论上来说,你读到的要么是[0,1],要么是[2,3],这两种结果在对应的时间上都是正确的。但是如果没有锁,你读到的可能是[0,3],那就出现了逻辑上的错误,这是由于连续性被中断引起的。 ### Lock与synchronized比较 相同之处:二者都是悲观锁,都有基本的互斥,同步,锁重入功能 不同之处: 1. 接口和关键字:Lock 是一个接口,源码由jdk提供;而 synchronized 是 Java中的关键字,源码在jvm中,由C++实现; 2. 是否自动释放锁:synchronized 在发生异常时,会自动释放线程占有的锁;而 Lock 则不会,因此必须在 finally 块中手动释放锁; 3. 功能层面:Lock提供了许多synchronized 不具备的功能,如获取等待状态、公平锁、可打断、可超时、多条件变量等。 - 获取等待状态: Lock 可以显示的知道有没有成功获取锁(tryLock),而 synchronized 却无法办到; - 公平锁:创建Lock时可传入true、false代表创建公平与非公平锁,默认非公平; - 是否响应中断:Lock 可以让等待锁的线程响应中断;而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去; - ... 4. 提升读操作效率:Lock有读写锁子类,可以使用读锁提升读操作效率;而 synchronized 不能; 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的(synchronized做了很多优化,如偏向锁,轻量级锁);而当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于synchronized。 ### synchronized的其他优化 以下优化仅在server模式下起作用。可在启动命令中添加`-server`参数,开启server模式。如果不指定模式,JVM会根据当前机器CPU核数,可用内存等一系列指标判断是否为服务器,如果是,则会自动开启server模式。如果启动的是GUI程序,则默认开启client模式。 #### 1、锁粗化 原则上,锁的粒度要尽量小,因为这样可以提高并发度。但是假如一系列的连环操作都是对同一个对象反复加锁,解锁,比如把锁加载在循环体里,单次同步操作的时间也许很短,但是高频反复的锁请求、同步和释放,也会对系统资源造成一定消耗,可能还不如加一把大锁。 锁粗化就是增大锁的作用域,把很多次锁的请求合并成一个请求,以此来降低短时间内大量锁请求、同步、释放带来的性能损耗。 ```java public void doSomethingMethod(){ synchronized(lock){ //do some thing } //这是还有一些代码,做其它不需要同步的工作,但能很快执行完毕 synchronized(lock){ //do other thing } } //将上述代码,优化成下面这样 public void doSomethingMethod(){ //进行锁粗化:整合成一次锁请求、同步、释放 synchronized(lock){ //do some thing //做其它不需要同步但能很快执行完的工作 //do other thing } } ``` 另一种例子是: ```java for (int i = 0; i < size; i++) { synchronized (lock) { } } //优化成这样 synchronized (lock) { for (int i = 0; i < size; i++) { } } ``` #### 2、锁消除 Lock Elision Java虚拟机的即时编译器(JIT)在运行时,会对一些代码上要求是同步的,但被检测到其实不可能存在共享数据竞争的锁进行消除,主要是依据逃逸分析(看变量的生命周期是否可能逃出该方法)。 > 锁消除优化所依赖的逃逸分析技术自Java SE 6u23起默认是开启的,但是锁消除优化是在Java 7开始引入的。 ```java package com.example.demo.core.Test; /** * @author: HanXu * on 2022/2/10 * Class description: 锁优化 */ public class SyncOptimization { public static void main(String[] args) { long start = System.currentTimeMillis(); int size = 1000000; for (int i = 0; i < size; i++) { createStringBuffer("111", "222"); } long timeCost = System.currentTimeMillis() - start; System.out.println("createStringBuffer:" + timeCost + " ms"); } public static String createStringBuffer(String str1, String str2) { StringBuffer sBuf = new StringBuffer(); //append()方法是同步操作,方法上加了synchronized sBuf.append(str1); sBuf.append(str2); return sBuf.toString(); } } //没有参数 //createStringBuffer:71 ms //-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks (开启逃逸分析,开启锁消除) //createStringBuffer:70 ms //-server -XX:+DoEscapeAnalysis -XX:-EliminateLocks (开启逃逸分析,关闭锁消除) //createStringBuffer:149 ms ``` 上述代码中,createStringBuffer()内部调用了StringBuffer的append()方法,此方法是加锁的,但是经过分析可以发现,整个StringBuffer对象,生命周期都是在createStringBuffer()方法内部的,根本不会出这个方法,被外部其他变量引用。最后返回的sBuf.toString(),是重新new了一个String对象返回,跟内部的StringBuffer对象无关。生命周期在方法内部的对象实例,根本不会发生线程安全问题,因为每个线程内部都有自己的本地方法栈,线程间是互相独立的。所以这里append()方法虽然加了synchronized,但其实根本不需要。那么JIT就会将此处的锁进行消除处理。 可以看到开启和关闭锁消除,耗时区别还是很大的。从JDK1.6开始默认开启锁消除。 #### 3、偏向锁 #### 4、适应性锁 #### 注意 上述优化并不是在告诉我们要省略锁,锁消除是JIT帮我们做的,也就是说我们在该使用锁的时候还是要使用锁,开发人员应该在代码逻辑上思考是否需要锁,至于执行层面的优化由JIT去做。 --END--
发表评论