开发中有时synchronized关键字并不能满足我们线程同步的具体场景(具体什么场景不满足见第4节),ReentrantLock类就是JUC包下的又一个保证线程同步的常用手段,功能上要比synchronized锁更丰富。ReentrantLock类是Lock接口的一个实现类,底层实现是基于AQS类(下一篇文章介绍)。本文主要介绍ReentrantLock类提供的常见方法和如何使用,对读写锁ReentrantReadWriteLock的使用做简单介绍,最后对比一下synchronized关键字和Lock接口。
1、Lock接口
前面讲的synchronized关键字是java内置语言实现同步的一种方法,Lock接口是JDK 1.5的JUC包下实现同步的另一种方式,Lock接口如下:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
对接口中的方法说明:
(1)void lock() && void unlock()
加锁的方法,与 unclock()搭配使用,如下:
Lock lock = new ReentrantLock();
lock.lock();
try
{
要同步的代码块
}
catch (Exception e)
{
处理异常
}
finally
{
lock.unlock();
}
与try - catch -finally搭配使用,synchronized关键字不需要显式地释放锁,lock必须在finally中显式释放锁。
(2) boolean tryLock()
tryLock()方法是有boolean返回值的,它表示用来尝试获取锁,如果成功获取锁则返回 true,获取失败返回false,tryLock在这上述两种情况下会立即返回结果,不会因为获取锁失败而一直等待 ,使用方式如下:
Lock lock = new ReentrantLock();
if (lock.tryLock())
{
try
{
要同步的代码块
}
catch (Exception e)
{
处理异常
}
finally
{
lock.unlcok();
}
}
else
{
获取锁失败后的处理
}
boolean tryLock(long time, TimeUnit unit) throws InterruptedException
方法传入了一个时间参数,当获取锁失败时该方法会等待一段时间,当时间超过time时返回false,一开始就获取到了锁或者在time时间内获取到了锁都返回true。
(3)void lockInterruptibly() throws InterruptedException
之前介绍的使用synchronized关键字达到线程同步的效果,当线程未获取到锁而处于等待阻塞状态时,此时调用线程的interrupt方法时不能中断线程,即synchronized锁线程无法响应中断;而当线程通过 lockInterruptibly() 方法获取锁时,如果该线程因没有获取到锁而处于等待阻塞状态时,该线程可以响应中断,且此时需要处理InterruptedException异常,如果当前线程未被中断可以获取锁。举个例子,线程A和线程B都在通过lock.lockInterruptibly()方法尝试获取一个对象锁,假设线程A获取到了,则线程B处于阻塞状态,此时调用threadB.interrupt()能够中断线程B的阻塞状态,且会抛出InterruptedException异常。
使用demo如下:
try {
lock.lockInterruptibly();
同步代码块...
} catch (InterruptedException e) {
处理因中断抛出的异常
} finally {
lock.unlock();
}
}
(4)Condition newCondition()
Lock接口中实现线程间通信协作的方法,这一部分在之前wait()、notify()、notifyAll()的文章中介绍。
2、ReentrantLock实现类
2.1 ReentrantLock常用方法
上面提到Lock是一个接口,ReenTrantLock类是唯一一个实现了Lock接口的类,ReentrantLock类不仅实现了Lock接口里的方法,还新增了一些其他的方法,ReenTrantLock提供的方法如下:
// 创建一个 ReentrantLock ,默认是“非公平锁”
ReentrantLock()
// 创建策略是fair的 ReentrantLock。fair为true表示是公平锁,fair为false表示是非公平锁
ReentrantLock(boolean fair)
// 查询当前线程保持此锁的次数
int getHoldCount()
// 返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null
protected Thread getOwner()
// 返回一个collection,它包含可能正等待获取此锁的线程。
protected Collection<Thread> getQueuedThreads()
// 返回正等待获取此锁的线程估计数
int getQueueLength()
// 返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程。
protected Collection<Thread> getWaitingThreads(Condition condition)
// 返回等待与此锁相关的给定条件的线程估计数
int getWaitQueueLength(Condition condition)
// 查询给定线程是否正在等待获取此锁
boolean hasQueuedThread(Thread thread)
// 查询是否有些线程正在等待获取此锁。
boolean hasQueuedThreads()
// 查询是否有些线程正在等待与此锁有关的给定条件。
boolean hasWaiters(Condition condition)
// 如果是“公平锁”返回true,否则返回false
boolean isFair()
// 查询当前线程是否保持此锁。
boolean isHeldByCurrentThread()
// 查询此锁是否由任意线程保持。
boolean isLocked()
// 获取锁。
void lock()
// 如果当前线程未被中断,则获取锁。
void lockInterruptibly()
// 返回用来与此 Lock 实例一起使用的 Condition 实例。
Condition newCondition()
// 尝试获取锁,仅当锁没有被其他线程获取时才能获取到锁
boolean tryLock()
// 如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。
boolean tryLock(long timeout, TimeUnit unit)
// 试图释放此锁。
void unlock()
2.2 ReentrantLock使用案例
下面介绍一下ReenTrantLock类常用的方法的使用案例。
(1)void lock()
package Lock;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @ClassName LocklMain
* @Description TODO
* @Auther Jerry
* @Date 2020/3/11 - 22:32
* @Version 1.0
*/
public class LocklMain {
// 公共写的数据
private List<Integer> list = new ArrayList<>();
private Lock lock= new ReentrantLock();
public static void main(String[] args) {
final LocklMain locklMain = new LocklMain();
new Thread(locklMain::insert).start();
new Thread(locklMain::insert).start();
}
private void insert()
{
lock.lock();
try{
System.out.println(Thread.currentThread().getName() + " 开始添加元素");
for (int i =0; i < 5; ++i)
{
list.add(i);
System.out.println(Thread.currentThread().getName() + " 添加了元素 " + String.valueOf(i));
}
System.out.println(Thread.currentThread().getName() + " 线程添加元素完毕");
}
finally {
System.out.println(Thread.currentThread().getName() + " 释放了锁");
lock.unlock();
}
}
}
运行结果如下:
Thread-0 开始添加元素
Thread-0 添加了元素 0
Thread-0 添加了元素 1
Thread-0 添加了元素 2
Thread-0 添加了元素 3
Thread-0 添加了元素 4
Thread-0 线程添加元素完毕
Thread-0 释放了锁
Thread-1 开始添加元素
Thread-1 添加了元素 0
Thread-1 添加了元素 1
Thread-1 添加了元素 2
Thread-1 添加了元素 3
Thread-1 添加了元素 4
Thread-1 线程添加元素完毕
Thread-1 释放了锁
例子中在main方法里初始化一个final实例和一个公共写的list,起两个线程,每个线程里都调用这个final实例的insert方法向list里add元素。由于实例方法insert()是需要同步的方法,之前可用synchronized关键字直接声明insert方法,这里在insert方法里通过调用lock.lock()和lock.unlock()对insert()方法里需要同步的部分进行加锁,从结果看达到了同步的目的:Thread-0先调用lock.lock()上锁,向list add元素,add完成后调用lock.unlock()方法释放锁,接着Thread-1再调用lock.lock()上锁,向list add元素,添加完后再释放锁。
注意private Lock lock=newReentrantLock();这个语句,不应该在insert方法里声明,而是全局声明(可以使用单例模式创建一个全局唯一的锁对象),不然两个线程调用insert()方法时,都会new一个lock出来,两个lock各自锁各自调用的insert()方法,到不到同步的目的。
(2) boolean tryLock()
package Lock;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @ClassName TryLockMain
* @Description TODO
* @Auther Jerry
* @Date 2020/3/11 - 23:03
* @Version 1.0
*/
public class TryLockMain {
// 公共写的数据
private List<Integer> list = new ArrayList<>();
private Lock lock= new ReentrantLock();
public static void main(String[] args) {
final TryLockMain tryLocklMain = new TryLockMain();
new Thread(tryLocklMain::insert).start();
new Thread(tryLocklMain::insert).start();
}
private void insert()
{
if (lock.tryLock())
{
System.out.println(Thread.currentThread().getName() + " 获得锁成功");
try{
System.out.println(Thread.currentThread().getName() + " 开始添加元素");
for (int i =0; i < 5; ++i)
{
list.add(i);
System.out.println(Thread.currentThread().getName() + " 添加了元素 " + String.valueOf(i));
}
System.out.println(Thread.currentThread().getName() + " 线程添加元素完毕");
}
finally {
System.out.println(Thread.currentThread().getName() + " 释放了锁");
lock.unlock();
}
}
else {
System.out.println(Thread.currentThread().getName() + " 获取锁失败");
}
}
}
运行结果如下:
Thread-0 获得锁成功
Thread-0 开始添加元素
Thread-1 获取锁失败
Thread-0 添加了元素 0
Thread-0 添加了元素 1
Thread-0 添加了元素 2
Thread-0 添加了元素 3
Thread-0 添加了元素 4
Thread-0 线程添加元素完毕
Thread-0 释放了锁
跟上面例子很相似,不同的是在需要同步的insert()方法里用tryLock()而不是lock(),同样起两个工作线程,调用final实例对象tryLocklMain的insert()方法向list里add元素。Thread-0先获得lock锁,进行for循环打印,此时Thread-1也调用tryLock()方法尝试获取lock锁,由于Thread-0此时持有该锁,Thread-1获取锁失败,无需等待直接返回false,走else语句打印“获取锁失败”。
(3)void lockInterruptibly() throws InterruptedException
package Lock;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @ClassName LockInInterruptiblyMain
* @Description TODO
* @Auther Jerry
* @Date 2020/3/11 - 23:11
* @Version 1.0
*/
public class LockInInterruptiblyMain {
// 公共写的数据
private List<Integer> list = new ArrayList<>();
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
final LockInInterruptiblyMain lockInInterruptiblyMain = new LockInInterruptiblyMain();
ThreadImpl t1 = new ThreadImpl(lockInInterruptiblyMain);
ThreadImpl t2 = new ThreadImpl(lockInInterruptiblyMain);
t1.start();
t2.start();
try {
Thread.sleep(4000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
t2.interrupt();
}
private void insert() throws InterruptedException {
lock.lockInterruptibly();
try {
System.out.println(Thread.currentThread().getName() + " 开始添加元素");
for (int i = 0; i < 5; ++i) {
list.add(i);
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 添加了元素 " + String.valueOf(i));
}
System.out.println(Thread.currentThread().getName() + " 线程添加元素完毕");
}
finally {
System.out.println(Thread.currentThread().getName() + " 释放了锁");
lock.unlock();
}
}
static class ThreadImpl extends Thread{
private LockInInterruptiblyMain lockInInterruptiblyMain;
private ThreadImpl(LockInInterruptiblyMain lockInInterruptiblyMain)
{
this.lockInInterruptiblyMain = lockInInterruptiblyMain;
}
@Override
public void run() {
try {
this.lockInInterruptiblyMain.insert();
}
catch (InterruptedException e)
{
System.out.println(Thread.currentThread().getName() + " 被中断");
}
}
}
}
运行结果会有两个情况:
情况1:Thread-0先获取锁,Thread-1阻塞,4秒后调用thead1.interrupt()令Thread-1响应阻塞,Thread-0不受影响成功打印完五次,如下:
Thread-0 开始添加元素 Thread-0 添加了元素 0 Thread-0 添加了元素 1 Thread-0 添加了元素 2 Thread-1 被中断 Thread-0 添加了元素 3 Thread-0 添加了元素 4 Thread-0 线程添加元素完毕 Thread-0 释放了锁
情况2:Thread-1先获取锁,Thread-0阻塞,4秒后调用thead1.interrupt()令Thread-1响应阻塞,此时Thread-1刚要打印“Thread-1 添加了元素 3”还没有打印,被中断,先执行insert()方法里finally块中的语句,释放lock锁,结束insert()方法后再执行run()方法里异常处理的语句,此时由于Thread-1释放了lock锁,Thread-0由阻塞状态(同步blocked)变为就绪状态(runnable),获得CPU时间片后重新变为运行状态(running)执行run()方法中的语句,从0-4添加元素。
Thread-1 开始添加元素 Thread-1 添加了元素 0 Thread-1 添加了元素 1 Thread-1 添加了元素 2 Thread-1 释放了锁 Thread-1 被中断 Thread-0 开始添加元素 Thread-0 添加了元素 0 Thread-0 添加了元素 1 Thread-0 添加了元素 2 Thread-0 添加了元素 3 Thread-0 添加了元素 4 Thread-0 线程添加元素完毕 Thread-0 释放了锁
如果是synchronized关键字修饰同步方法的话,当一个线程处于阻塞状态时,在其他线程里调用这个线程的interrupt()方法时,该线程不会响应中断,还是会等到占用了对象锁的线程释放掉锁后,获取该对象锁执行同步方法,举个例子:
main方法:package ConcurrentOne; /** * @ClassName ConcurrentOne.TestMain * @Description TODO * @Auther Jerry * @Date 2020/3/1 - 18:19 * @Version 1.0 */ public class TestMain { public static void main(String[] args) { final ClassTest classTest = new ClassTest(); MyThread t1 = new MyThread(classTest); MyThread t2 = new MyThread(classTest); t1.start(); t2.start(); try{ Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } t2.interrupt(); } }
继承Thread类:
package ConcurrentOne; /** * @ClassName ExecuteClass * @Description TODO * @Auther Jerry * @Date 2020/2/26 - 23:31 * @Version 1.0 */ public class MyThread extends Thread { private ClassTest classTest; public MyThread(ClassTest classTest) { this.classTest = classTest; } @Override public void run() { classTest.fun(); } }
同步方法对应的实例:
package ConcurrentOne; /** * @ClassName ClassTest * @Description TODO * @Auther Jerry * @Date 2020/3/11 - 23:59 * @Version 1.0 */ public class ClassTest { public synchronized void fun() { for (int i = 0; i < 5; ++i) { System.out.println(Thread.currentThread().getName() + " 正在打印 " + String.valueOf(i)); try{ Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
运行结果:
Thread-0 正在打印 0 Thread-0 正在打印 1 Thread-0 正在打印 2 Thread-0 正在打印 3 Thread-0 正在打印 4 Thread-1 正在打印 0 java.lang.InterruptedException: sleep interrupted at java.base/java.lang.Thread.sleep(Native Method) at ConcurrentOne.ClassTest.fun(ClassTest.java:19) at ConcurrentOne.MyThread.run(MyThread.java:20) Thread-1 正在打印 1 Thread-1 正在打印 2 Thread-1 正在打印 3 Thread-1 正在打印 4
在main方法里对t2线程(Thread-1)调用了interrupt()方法,可是Thread-1并没有响应该中断,而是继续等待Thread-0完成run()方法后释放对象锁,Thread-1再获得对象锁进行run()方法,这点与lock接口的lockInterruptibly()截然相反。
3、经典的锁概念
这一节本来题目叫锁的常见分类,但是并没有介绍是根据什么标准对JUC下的锁进行分类,分类的标准或者维度不同,自然分类的结果不同。本节仅是罗列一个锁的经典叫法,因为在很多文章或者博客中下面的这些名词的出现频率较高。
3.1 可重入锁
锁具备可重入性,成为可重入锁,synchronized锁和lock锁都是可重入锁。所谓锁的可重入性,举个例子:
class MyClass { public synchronized void method1() { method2(); } public synchronized void method2() { 同步代码块... } }
当一个线程执行到synchronized修饰的method1方法时,该方法会调用同被synchronized修饰的method2,如果该线程已经获取到锁对象了,此时该线程不必再重新申请锁,可以直接进入到method2,该特性就是锁的可重入性。
3.2 公平锁
3.2.1 公平锁的定义 && ReentrantLock公平锁的使用
公平锁尽量以请求锁的顺序来获取锁,比如多个线程在等待一个锁,该锁释放后,等待时间最长的线程(即最先尝试获取锁失败而进入阻塞状态的线程)获取锁。非公平锁无法保证锁的获取是按照线程等待顺序来的,会导致某些线程可能永远获取不到锁。synchronized锁就是非公平锁,一般非公平锁的效率要比公平锁高,因此如非特殊需要,锁一般是非公平锁。
ReentrantLock类可以通过构造函数传递一个boolean类型的值来指定创建的是公平锁还是非公平锁。设置公平锁的方法:ReentrantLock lock= new ReentrantLock(true);
ReentrantLock类设置非公平锁的方法(默认是非公平锁):
ReentrantLock lock= new ReentrantLock(false); 或者 ReentrantLock lock= new ReentrantLock();
ReentrantLock类判断当前锁是否为公平锁的方法(返回Boolean类型的结果):
lock.isFair();
3.2.2 ReentrantLock如何实现公平锁
ReentrantLock公平锁与非公平锁的实现原理区别就是抽象方法tryAcquire的实现不同,具体的是NonfairSync类和FairSync类中tryAcquire方法的实现不同。
NonfairSync类中tryAcquire方法:
//公平锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 需要判断当前节点是否有前驱节点,前驱节点对应等待时间更久的阻塞线程,已经没有前驱节点,则自己能获取到锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
FairSync类中tryAcquire方法:
// 非公平锁
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 与公平锁相比较,不再关心其是否有前驱节点
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
公平锁与非公平锁相比,唯一不同的是判断条件多了一个hasQueuedPredecessors方法,该方法是判断当前阻塞线程对应的Node是否有前驱节点,如果该方法返回true,则表示有前驱节点,即表示有线程比当前线程更早地尝试获取锁失败而阻塞,需要等待当前节点的前驱节点对应的线程释放掉锁后,当前节点对应的线程才能继续尝试获取锁。
3.3 读写锁
读写锁就是读写分离的锁,分为读锁和写锁,针对不同的场景(比如读多写少或者读少写多)选取读锁或者写锁。
3.3.1 为什么需要读写分离?
多个读线程同时访问共享资源,读取资源,此时是不会有问题的;多个线程同时访问共享资源,只要有一个线程在进行写操作,都会引发竞争。设想一下这样的场景:写操作没有那么频繁,读操作很频繁,此时如果用synchronized关键字或者lock锁去进行同步,虽然能保证同一时刻只有一个线程在执行读操作或写操作,但对于读操作这种可以多个线程同时访问资源的场景效率就太低了。因此对于读操作比较频繁,即同一时刻尝试获取锁的线程都是读操作,没有写操作,此时完全没必要加锁,可以使用读锁;同一时刻众多线程中如果有一个线程是写操作,那就必须加锁让线程同步,使用写锁。
3.3.2 ReentrantReadWriteLock类说明
JUC下的ReadWriteLock接口提供了读写锁的样式,该接口仅有两个方法:readLock()获取读锁,writeLock()获取写锁。ReadWriteLock接口与Lock接口没有任何关系,ReadWriteLock接口源码如下:
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading.
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing.
*/
Lock writeLock();
}
ReentrantReadWriteLock是ReadWriteLock接口的实现类,提供的方法如下:
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
// 读锁
private final ReentrantReadWriteLock.ReadLock readerLock;
// 写锁
private final ReentrantReadWriteLock.WriteLock writerLock;
// 基于AQS实现的同步器,ReenTrantLock类的底层也是它实现的
final Sync sync;
// 构造函数,默认是非公平锁
public ReentrantReadWriteLock() {
this(false);
}
// 支持通过boolean类型的入参的构造函数指定是否为公平锁
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; }
// 基于AQS实现的同步器
abstract static class Sync extends AbstractQueuedSynchronizer {}
// 内部类,基于同步器实现的非公平同步器
static final class NonfairSync extends Sync {}
// 内部类,基于同步器实现的公平同步器,这三个类在ReentrantLock类中也有
static final class FairSync extends Sync {}
// 内部类,
public static class ReadLock implements Lock, java.io.Serializable {
...
// 读锁的加锁方法,底层是基于AQS同步器的acquireShared方法实现的,这一块和ReentrantLock类里的lock方法的底层实现就不一样,
// ReentrantLock类里的lock方法是AQS同步器的acquire方法实现,AQS同步器的acquireShared方法和acquire方法实现底层有类似的地方(比如自旋)
public void lock() {
sync.acquireShared(1);
}
}
public static class WriteLock implements Lock, java.io.Serializable {
...
// 写锁的加锁方法,底层与ReentrantLock类里的lock方法是一致的
public void lock() {
sync.acquire(1);
}
}
}
ReentrantReadWriteLock实现类在readLock()和writeLock()的基础上又新增了很多方法,但重要的还是获取读锁和获取写锁这两个方法。下面说明一下线程获取读锁和写锁的前提条件:
- 线程获取读锁的前提条件:
- 允许其他线程获取读锁;
- 没有其他线程获取写锁;
- 线程可以先获取写锁,然后同一个线程可以继续获取读锁。
- 线程获取写锁的前提条件:
- 没有其他线程获取读锁;
- 没有其他线程获取写锁。
3.3.3 ReentrantReadWriteLock类使用
(1)两个异步线程都执行读操作
结果如下:package Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * @ClassName ReadWriteLockMain * @Description TODO * @Auther Jerry * @Date 2020/3/16 - 22:49 * @Version 1.0 */ public class ReadWriteLockMain { // 初始化一个读写锁 private ReadWriteLock rwlock = new ReentrantReadWriteLock(); public static void main(String args[]) { ReadWriteLockMain readWriteLockMain = new ReadWriteLockMain(); new Thread(readWriteLockMain::readMethod).start(); new Thread(readWriteLockMain::readMethod).start(); } private void readMethod() { rwlock.readLock().lock(); System.out.println(Thread.currentThread().getName() + " 获得了读锁"); try { System.out.println(Thread.currentThread().getName() + " 正在执行读操作"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } finally { rwlock.readLock().unlock(); System.out.println(Thread.currentThread().getName() + " 释放了读锁"); } } private void writeMethod() { rwlock.writeLock().lock(); System.out.println(Thread.currentThread().getName() + " 获得了写锁"); try { System.out.println(Thread.currentThread().getName() + " 正在执行写操作"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } finally { rwlock.writeLock().unlock(); System.out.println(Thread.currentThread().getName() + " 释放了写锁"); } } }
从结果可以看出,Thread-0和Thread-1都同时获取了读锁,分别进行完读操作后再释放了读锁,说明多个线程可以同时拥有一个读锁。Thread-0 获得了读锁 Thread-1 获得了读锁 Thread-0 正在执行读操作 Thread-1 正在执行读操作 Thread-0 释放了读锁 Thread-1 释放了读锁
(2)两个异步线程都执行写操作
在main方法里改一下即可,这里不赘述,结果如下:
从结果可以看出,Thread-0和Thread-1同一时刻只能有一个线程获取写锁,另一个线程阻塞,直到获取了写锁的线程执行完写操作释放了写锁,另一个线程才能获取写锁进行写操作,实现了线程同步。Thread-0 获得了写锁 Thread-0 正在执行写操作 Thread-0 释放了写锁 Thread-1 获得了写锁 Thread-1 正在执行写操作 Thread-1 释放了写锁
(3)两个异步线程,一个执行写操作,另一个执行读操作
在main方法里改一下即可,这里不赘述,结果有两种情况,先执行写操作和先执行读操作,这里以先执行写操作说明,如下:
从结果可以看出,当一个线程获取到了写锁,另一个线程只能等待直到写锁被释放才能获取读锁;同样,当一个线程获取到了读锁,另一个线程只能等待直到读锁被释放才能获取写锁,也就是说当两个线程分别获取读锁和写锁时,实现了线程同步。Thread-1 获得了写锁 Thread-1 正在执行写操作 Thread-1 释放了写锁 Thread-0 获得了读锁 Thread-0 正在执行读操作 Thread-0 释放了读锁
3.3.4 锁降级
(1)什么是锁降级?
对存在写操作,且业务强依赖与写操作后的值的业务,使用读写锁时,先加写锁,再加读锁,写操作完成后释放写锁,待依赖写操作后值的业务执行完后再释放读锁,这个完整的过程叫锁降级。由于释放完写锁后,只剩读锁了,写锁 -> 读锁这个过程可以理解为“降级”。核心是:通过再加一道读锁,保证在释放了写锁后,强依赖与写操作的业务在执行过程中仍有当前线程的读锁保护,防止其他写操作的线程获取到锁。
一个线程执行写操作,先获取写锁,再获取读锁,完成写操作后先释放写锁,接下来的程序里可能要依赖写操作后的变量值,待程序全部执行完后再释放读锁。先释放了写锁,只剩下了读锁,称之为“锁降级”。
(2)为什么需要锁降级?
一句话:为了保证数据可见性。假设线程A修改了数据,释放了写锁,这个时候线程B获得了写锁,修改了数据,然后也释放了写锁,线程A读取数据的时候,读到的是线程B修改的,并不是线程A自己修改的,那么在使用修改后的数据时,就会忽略线程A之前的修改结果。因此通过锁降级来保证数据每次修改后的可见性。
(3)举例
不采用锁降级,仅用写锁:
结果如下:package Lock; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * @ClassName LockDegradeMain * @Description TODO * @Auther Jerry * @Date 2020/3/16 - 23:22 * @Version 1.0 */ public class LockDegradeMain { private int i = 0; ReadWriteLock rwlock = new ReentrantReadWriteLock(); Lock readLock = rwlock.readLock(); Lock wirteLock = rwlock.writeLock(); public static void main(String[] args) { LockDegradeMain lockDegradeMain = new LockDegradeMain(); new Thread(lockDegradeMain::execute).start(); new Thread(lockDegradeMain::execute).start(); } private void execute() { wirteLock.lock(); System.out.println(Thread.currentThread().getName() + " 获取了写锁"); try { ++i; } finally { wirteLock.unlock(); System.out.println(Thread.currentThread().getName() + " 释放了写锁"); } try { // 模拟其他操作 Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } // 这里在上面模拟了其他操作后,要使用++i后的i值了,即模拟依赖写操作后值的业务 System.out.println(Thread.currentThread().getName() + ":i is " + String.valueOf(i)); } }
说明:在execute方法里仅用写锁进行++i,Thread-0线程仅想将i由0变成1,但是Thread-0在释放了写锁后,Thread-1立马获取写锁,执行++i,将i的值由1变成2,此时Thread-0再想用变量i时,已不再是Thread-0计划的i=1,而是i=2,这就是为什么要用锁降级。Thread-0 获取了写锁 Thread-0 释放了写锁 Thread-1 获取了写锁 Thread-1 释放了写锁 Thread-0:i is 2 Thread-1:i is 2
采用了锁降级的例子如下:
package Lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @ClassName LockDegradeMain
* @Description TODO
* @Auther Jerry
* @Date 2020/3/16 - 23:22
* @Version 1.0
*/
public class LockDegradeMain {
private int i = 0;
ReadWriteLock rwlock = new ReentrantReadWriteLock();
Lock readLock = rwlock.readLock();
Lock wirteLock = rwlock.writeLock();
public static void main(String[] args) {
LockDegradeMain lockDegradeMain = new LockDegradeMain();
new Thread(lockDegradeMain::execute).start();
new Thread(lockDegradeMain::execute).start();
}
private void execute()
{
wirteLock.lock();
System.out.println(Thread.currentThread().getName() + " 获取了写锁");
try
{
readLock.lock();
System.out.println(Thread.currentThread().getName() + " 获得了读锁");
++i;
}
finally {
wirteLock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放了写锁");
}
try
{
// 模拟其他操作
Thread.sleep(3000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
// 这里在上面模拟了其他操作后,要使用++i后的i值了,即模拟依赖写操作后值的业务
System.out.println(Thread.currentThread().getName() + ":i is " + String.valueOf(i));
readLock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放了读锁");
}
}
结果如下:
Thread-0 获取了写锁
Thread-0 获得了读锁
Thread-0 释放了写锁
Thread-0:i is 1
Thread-0 释放了读锁
Thread-1 获取了写锁
Thread-1 获得了读锁
Thread-1 释放了写锁
Thread-1:i is 2
Thread-1 释放了读锁
Thread-0释放了写锁后,由于Thread-0还拥有着读锁,Thread-1并不能获取写锁篡改i的值,保证了i的值修改后的可见性。
3.4 乐观锁、悲观锁
可以认为JUC包下的锁大致可以分为两类:悲观锁和乐观锁,二者大致概念如下:
- 悲观锁:总是假设最坏的情况,每次去读取数据时总认为别人会修改,因此每次读取数据时都会上锁,其他线程想访问该数据时只能阻塞,直到这个线程释放了锁。JUC包下的synchronized和ReentrantLock就是悲观锁思想的实现。
- 乐观锁:总是假设最好的情况,每次去读取数据时总认为别人不会修改,因此每次读取数据时不会上锁,但是在做写操作时会判断一下从读取这个数据到真正执行写操作前有没有其他线程去更新这个数据。CAS机制就是乐观锁的机制,JUC包下的Atomic原子类就是用CAS机制实现的。
可以看出悲观锁和乐观锁的一个很重要的区别在于读操作时是否会加锁,因为多个线程仅是读取而不做修改,其实是不用加锁的,因此在读操作多的情况下乐观锁的效率要高于悲观锁,但是悲观锁对读多写少的场景也有读写锁的解决方案。有关乐观锁的详细内容在后续文章中会介绍。
3.5 死锁
有时程序设计不当就会产生死锁, 这方面我在实际工作中确实没遇到过,但是分析JVM中是否有大量线程处于同步阻塞状态,或者想要确定是否产生了死锁,可以通过分析jstack日志来确认,有关死锁的理论知识在后续文章中会介绍。
4、ReentrantLock 与synchronized关键字的比较
(1)底层实现
synchronized是Java中的关键字,是JVM层面实现的锁;ReentrantLock 是JUC包下的类,是JDK层面实现的锁。
(2)是否需要手动释放锁
synchronized 不需要手动释放锁,在发生异常时,会自动释放锁,因此不会导致死锁现象发生;ReentrantLock 在发生异常时,如果没有主动通过 unLock() 去释放锁,很可能会造成死锁现象,因此使用 ReentrantLock 时需要在 finally 块中释放锁。
(3)锁的公平性
synchronized 是非公平锁;ReentrantLock 默认是非公平锁,但是可以通过参数设置成公平锁。
(4)是否可中断
synchronized 是不可被中断的;ReentrantLock 则可以被中断,通过lockInterruptibly()方法获取锁可响应中断,但需处理异常。
(5)灵活性
使用 synchronized 时,等待的线程会一直等待下去,直到获取到锁。ReentrantLock 的使用更加灵活,可以通过tryLock方法尝试获取锁,如果不成功可以去干别的事情;可以通过给lock方法或者tryLock方法传入一个timeout值设置获取锁的超时时间,超过锁超时时间还没获取到锁则去干别的事情而不是一直阻塞;可以通过lockInterruptibly方法区响应中断;可以通过读写锁来提高程序性能。总之ReentrantLock的方法可以不用在获取不到锁的时候一直阻塞。
(6)性能
随着近些年 synchronized 的不断优化(锁升级),ReentrantLock 和 synchronized 在性能上已经没有很明显的差距了,所以性能不应该成为我们选择两者的主要原因,选择两者的原因还应个业务场景出发。一般而言,还是选择ReentrantLock,毕竟提供的能力集更丰富,使用起来也更加灵活,可以覆盖大多数业务场景。
5、ReentrantLock类源码浅析
参考
Java并发编程:Lock - Matrix海子 - 博客园 www.cnblogs.com
Java多线程之ReentrantLock与Condition - 平凡希 - 博客园 www.cnblogs.com
ReentrantReadWriteLock中锁降级的理解 - JayInnn - 博客园 www.cnblogs.com