一、Lock概要介绍
先来一张锁的框架图
- Lock接口
JUC包中的 Lock 接口支持那些语义不同(重入、公平等)的锁规则。所谓语义不同,是指锁可是有"公平机制的锁"、"非公平机制的锁"、"可重入的锁"等等。"公平机制"是指"不同线程获取锁的机制是公平的",而"非公平机制"则是指"不同线程获取锁的机制是非公平的","可重入的锁"是指同一个锁能够被一个线程多次获取。主要实现的类是ReentrantLock。
- ReadWriteLock接口
ReadWriteLock 接口以和Lock类似的方式定义了一些读取者可以共享而写入者独占的锁。JUC包只有一个类实现了该接口,即 ReentrantReadWriteLock,因为它适用于大部分的标准用法上下文。但程序员可以创建自己的、适用于非标准要求的实现。
- Condition接口
Condition需要和Lock联合使用,它的作用是代替Object监视器方法,可以通过await(),signal()来休眠/唤醒线程。 Condition 接口描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关联。为了避免兼容性问题,Condition 方法的名称与对应的 Object 版本中的不同。
二、Lock用法
1、lock,unlock
void lock() ;获取锁
- 如果该锁没有被其他线程保持,则当前线程获取该锁并立即返回,将锁的保持计数设置为 1。
- 如果该锁被其他线程保持,在获得锁之前,该线程将一直处于休眠状态,此时锁保持计数被设置为 1。
void unlock(); 释放锁
- 对应于lock()、tryLock()、tryLock(xx)、lockInterruptibly()等操作,如果成功的话应该对应着一个unlock(),这样可以避免死锁或者资源浪费
lock unlock 一般这样使用
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}}
注意点
- lock unlock 一定要成对出现。获取锁,必须要释放锁,不然很有可能引起死锁
- unlock 一定要在finally里使用,不然业务代码如果发生异常,直接抛出异常时,锁没有得到释放,也是很有可能引起死锁
代码示例
package wang.conge.javasedemo.core.thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Model model = new Model();
Runnable numCountThread = (() -> {
lock.lock();
try {
model.addNum();
System.out.println(model.getNum());
} finally {
lock.unlock();
}
});
for (int i = 1; i <= 10; i++) {
new Thread(numCountThread).start();
}
}
static class Model {
private int num = 0;
public int getNum() {
return num;
}
public void addNum() {
num = num + 1;
}
}
}
代码解析
- ReentrantLock 是Lock独占锁的一种实现
- 多个线程执行Runnable时,先去获取锁,然后再执行具体的addNum操作
- 通过锁,线程可以安全的执行
2、tryLock
boolean tryLock();尝试获取锁
- 如果该锁没有被其他线程保持,则当前线程获取该锁并立即返回true,将锁的保持计数设置为 1。
- 如果该锁被其他线程保持,则立即返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;尝试获取锁
- 如果该锁没有被其他线程保持,则当前线程获取该锁并立即返回true,将锁的保持计数设置为 1。
- 如果该锁被其他线程保持,在time 毫秒内等待获取锁,
- 如果time时间内其他线程释放了锁,则当前线程获取该锁并返回true,将锁的保持计数设置为 1。
- 如果time时间内其他线程没有释放锁,则返回false
tryLock 一般这样使用
Lock lock = ...;
if (lock.tryLock()) {
try {
// manipulate protected state
} finally {
lock.unlock();
}
} else {
// perform alternative actions
}}
注意点
- 尝试获取锁,然后执行相关的业务,最后也一定要unlock
- 获取锁失败,执行其他的业务
代码示例
package wang.conge.javasedemo.core.thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockTest {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Model model = new Model();
Runnable numCountThread = (() -> {
if (lock.tryLock()) {
try {
model.addNum();
System.out.println(model.getNum());
} finally {
lock.unlock();
}
} else {
System.out.println("nolock:"+model.getNum());
}
});
for (int i = 1; i <= 10; i++) {
new Thread(numCountThread).start();
}
}
static class Model {
private int num = 0;
public int getNum() {
return num;
}
public void addNum() {
num = num + 1;
}
}
}
代码解析
- 线程执行addNum之前,尝试获取锁
- 获取锁成功,再去执行addNum
- 没有获取锁,只打印出当前num
- 这样做的好处是,不会阻塞当前线程,可以先去执行其他业务
3、lockInterruptibly
void lockInterruptibly() throws InterruptedException;可中断的获取锁
- 当前线程等待获取锁,如果被其他线程调用了该线程的interrupt,会停止等待获取锁,直接抛出InterruptedException异常
例如,两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
- 当前线程获取锁之后,是不会再被interrupt方法打断的。也就是说线程不能打断正在运行过程中的线程,只能打断阻塞过程中的线程(例如线程调用了wait(),join(),sleep())
- 而使用lock获取锁,或者synchronized,也是出于阻塞状态等待锁的过程,是无法被打断的,只会一直阻塞等待下去
几种获取锁大致区别
- 内部锁(synchronized) 优先响应锁获取再响应中断
- Lock.lock() 优先响应锁获取再响应中断
- Lock.tryLock() 判断锁的状态不可用后马上返回不等待
- tryLock(long time, TimeUnit unit) 优先响应中断再响应锁获取
- Lock.lockInterruptibly() 优先响应中断再响应锁获取
lockInterruptibly 一般这样用
由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
或者
public void method() {
Lock lock = ...;
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
// todo InterruptedException
}
try {
// .....
} finally {
lock.unlock();
}
}
注意点
- 一定要妥善处理InterruptedException
- 如果获取锁之后,一定要unlock
代码示例
package wang.conge.javasedemo.core.thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockInterruptiblyTest {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Model model = new Model();
Runnable run = (()->{
try{
lock.lockInterruptibly();
}catch(InterruptedException e){
System.out.println("Interrupt");
return;
}
try {
model.addNum();
System.out.println(model.getNum());
} finally {
lock.unlock();
}
}) ;
int threadNum = 100;
Thread [] threads = new Thread[threadNum];
for(int i=0;i<threadNum;i++){
threads[i] = new Thread(run);
threads[i].start();
}
for(int i=0;i<threadNum;i++){
if(i%10==0){
threads[i].interrupt();
}
}
}
static class Model {
private int num = 0;
public int getNum() {
return num;
}
public void addNum() {
num = num + 1;
}
}
}
代码解析
- 每个线程都是采用lockInterruptibly方式去获取锁
- 如果被打断,输出打断信息,线程结束
- 如果已经获取锁,不被打断,直接继续运行结果
- 所以该程序运行的结果,是不确定的,threads[i],i%10==0,也有可能已经获取锁,不被中断,已经运行结果
4、Condition
通过lock获取Condition方法定义
package java.util.concurrent.locks;
public interface Lock {
...
Condition newCondition();
...
}
Condition接口定义的方法
package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;
import java.util.Date;
public interface Condition {
/**
* 等待
*/
void await() throws InterruptedException;
/**
* 不可中断的等待
*/
void awaitUninterruptibly();
/**
* 等待nanosTimeout时间
*/
long awaitNanos(long nanosTimeout) throws InterruptedException;
/**
* 等待time + nanosTimeout时间
*/
boolean await(long time, TimeUnit unit) throws InterruptedException;
/**
* 在deadline之前,一直等待
*/
boolean awaitUntil(Date deadline) throws InterruptedException;
/**
* 唤醒等待在此Condition上的随机一个线程
*/
void signal();
/**
* 唤醒等待在此Condition上的所有线程
*/
void signalAll();
}
Condition说明
- 在Object方法上定义了wait(),notify(),notifyAll()等方法,是用来协调基于object对象锁的线程,而Condition上定义的方法,正是和这些方法有着类似的功能
- Object的wait() 基于该对象锁的等待,让出锁,阻塞线程,等待其他线程唤醒
- Condition的await(),基于Condition对象的等待,让出Condition对应的lock锁, 阻塞当前线程,等待该其他线程通过该Condition唤醒
- Object的notify(),notifyAll(),唤醒等待在此object上的线程
- Condition的signal(),signalAll(),唤醒等待在此Condition上的线程
- 一个Object对象只有一个锁,也只能基于一个对象进行await,notify,nofifAll
- 一个Lock可以有多个Condition,通过不同的Condition切换,可以更精细的协调线程运行
代码示例
package wang.conge.javasedemo.core.thread;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockConditionTest {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();//1.主线程获取锁
System.out.println("main thread lock and run");//2.主线程业务执行
new Thread (()->{
lock.lock();//6.主线程阻塞让出lock锁,子线程获取锁
System.out.println("child thread lock and run");//7.子线程业务执行
condition.signalAll();//唤醒等待此condition下面的所有线程//8.子线程唤醒等待在condition下面的线程
System.out.println("child thread unlock");
lock.unlock();//9.子线程unlock,让出锁
}).start();//4.子线程start
try {
condition.await();//5.主线程执行condition.await(),让出condition对应的lock锁, 主线程进入阻塞状态
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main thread continue run");//10.因为被子线程唤醒,而且子线程让出锁,主线程重新获取锁,继续运行
System.out.println("main thread unlock");
lock.unlock();//11.主线程unlock,让出锁,结束整个流程
}
}
代码解析
- 代码里详细的说明每步执行的顺序
- 通过condition可以很好的协调线程
三、Lock与synchronized对比
相同点
- ReentrantLock提供了synchronized类似的功能和内存语义
不同点
- Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现
- synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁
- Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到
- Lock可以提高多个线程进行读操作的效率
- ReentrantLock功能性方面更全面,比如时间锁等候,可中断锁等候,锁投票等,因此更有扩展性。在多个条件变量和高度竞争锁的地方,用ReentrantLock更合适,ReentrantLock还提供了Condition,对线程的等待和唤醒等操作更加灵活,一个ReentrantLock可以有多个Condition实例,所以更有扩展性
- ReentrantLock 的性能比synchronized好
- ReentrantLock提供了可轮询的锁请求,他可以尝试的去取得锁,如果取得成功则继续处理,取得不成功,可以等下次运行的时候处理,所以不容易产生死锁,而synchronized则一旦进入锁请求要么成功,要么一直阻塞,所以更容易产生死锁
- 一个Lock可以通过多个Condition协调不同条件下的线程
四、可重入锁
ReentrantLock 是Lock接口的一个实现,事实上上面所有的示例都是基于ReentrantLock去实现,而该ReentrantLock也是一个可重入锁,But,什么是可重入锁呢?
- 如果锁具备可重入性,则称作为可重入锁。
- 像synchronized和ReentrantLock都是可重入锁,可重入性在我看来实际上表明了锁的分配机制
- 基于线程的分配,而不是基于方法调用的分配。
- 举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
代码示例
package wang.conge.javasedemo.core.thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantTest {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Model model = new Model();
Runnable numCountThread = (() -> {
lock.lock();
try {
model.addNum();
System.out.println("num:"+ model.getNum());
lock.lock();
try{
model.addFlg();
System.out.println("flg:"+ model.getFlg());
}finally{
lock.unlock();
}
} finally {
lock.unlock();
}
});
for (int i = 1; i <= 10; i++) {
new Thread(numCountThread).start();
}
}
static class Model {
private int num = 0;
private int flg = 0;
public int getNum() {
return num;
}
public void addNum() {
num = num + 1;
}
public int getFlg() {
return flg;
}
public void addFlg() {
flg = flg + 1;
}
}
}
代码解析
- 当前线程再已经获取锁的情况下,如果执行的业务代码中还需要再次获取锁,这个时候锁直接分配给了当前线程
- 可重入锁,基于线程分配锁
- synchronized和ReentrantLock都是可重入锁
还有一些其他锁分类
- 可中断锁,比如lock 的lockInterruptibly
- 公平锁,非公平锁,见下文
- 读写锁,见下文
- 自旋锁,即是利用CAS原子操作,CPU不断轮询不断试错获取锁
- 还有更多的锁。。。
五、公平锁,非公平锁
先来一张ReentrantLock图:
ReentrantLock实现Lock接口时,依赖了一个变量
private final Sync sync;
而该变量有两种内部实现
static final class NonfairSync extends Sync {
...
}
static final class FairSync extends Sync {
...
}
- FairSync是公平锁,是按照通过CLH等待线程按照先来先得的规则,公平的获取锁
- NonfairSync是非公平锁,则当线程要获取锁时,它会无视CLH等待队列而直接获取锁
- ReentrantLock 默认是非公平锁
- public ReentrantLock(boolean fair);通过此方法创建公平锁
什么是CLH等待队列
- Craig, Landin, and Hagersten lock queue
- CLH队列是AQS中“等待锁”的线程队列。在多线程中,为了保护竞争资源不被多个线程同时操作而起来错误,我们常常需要通过锁来保护这些资源。在独占锁中,竞争资源在一个时间点只能被一个线程锁访问;而其它线程则需要等待。CLH就是管理这些“等待锁”的线程的队列。
- CLH是一个非阻塞的 FIFO 队列。也就是说往里面插入或移除一个节点的时候,在并发条件下不会阻塞,而是通过自旋锁和 CAS 保证节点插入和移除的原子性。
CLH大致原理
- CLH lock queue其实就是一个FIFO的队列,队列中的每个Node就是每个线程
- 如果队列中没有线程等待,那么该线程设为占有锁的线程,返回成功
- 如果队列中已经有线程占有锁,把获取锁的线程排队到队尾
- 然后设置当前线程为阻塞状态,等待其他线程释放锁
- 随着其他线程unlock,删除队列中的线程,这样整个流程就串通了
公平锁和非公平锁的区别
- 在获取锁的机制上的区别,表现在,在尝试获取锁时
- 公平锁,只有在当前线程是CLH等待队列的表头时,才获取锁
- 非公平锁,只要当前锁处于空闲状态,则直接获取锁,而不管CLH等待队列中的顺序。只有当非公平锁尝试获取锁失败的时候,它才会像公平锁一样,进入CLH等待队列排序等待。
- unlock流程都一样
六、读写锁
ReadWriteLock接口定义极其简单
package java.util.concurrent.locks;
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*/
Lock readLock();
/**
* Returns the lock used for writing.
*/
Lock writeLock();
}
- 返回两种锁,很好理解,一个是读锁,一个是写锁
- 读锁是共享锁,能同时被多个线程获取
- 写入锁用于写入操作,它是独占锁,写入锁只能被一个线程锁获取。
- 读锁与读锁不会冲突
- 读锁与写锁会冲突
- 写锁与写锁会冲突
- ReentrantReadWriteLock 类实现了ReadWriteLock接口
ReentrantReadWriteLock的UML类图如下:
代码示例
package wang.conge.javasedemo.core.thread;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ThreadSafeArrayList<E> {
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock readLock = readWriteLock.readLock();
private final Lock writeLock = readWriteLock.writeLock();
private final List<E> list = new ArrayList<>();
public void set(E o) {
writeLock.lock();
try {
list.add(o);
System.out.println("Adding element by thread" + Thread.currentThread().getName());
} finally {
writeLock.unlock();
}
}
public E get(int i) {
readLock.lock();
try {
System.out.println("Printing elements by thread" + Thread.currentThread().getName());
return list.get(i);
} finally {
readLock.unlock();
}
}
public static void main(String[] args) {
ThreadSafeArrayList<String> threadSafeArrayList = new ThreadSafeArrayList<>();
threadSafeArrayList.set("1");
threadSafeArrayList.set("2");
threadSafeArrayList.set("3");
System.out.println("Printing the First Element : " + threadSafeArrayList.get(1));
}
}
代码解析
- 在ThreadSafeArrayList的set 写操作时,使用writeLock ,确保写的过程中是独占锁
- 在ThreadSafeArrayList的get 读操作时,使用readLock,多个读操作不会冲突,但写操作不能操作
引用文章
- JUC锁之框架
- Condition条件
- Java并发编程:Lock
- ReentrantLock与Condition
- 可重入锁
- 线程间协作的两种方式:wait、notify、notifyAll和Condition
- 自旋锁
- 公平锁(一)
- 公平锁(二)
- JAVA并发编程学习笔记之CLH队列锁
- 共享锁和ReentrantReadWriteLock