Lock
文章目录
1.Lock存在的意义
在JDK5之前是没有Lock的,那时一直用的synchronized来实现锁,在Lock出现后,Lock被广泛的使用,在JDK6时,synchronized得到了很大的优化,从此lock和synchronized各有各的特色;
那么是为什么lock没有销声匿迹呢?
synchronized产生死锁后就只能死等,而Lock有多种方案可以来解决死锁问题。
Lock(接口)-------------------解决死锁体系而产生,JDK5
死锁产生的四个必要条件:(必须同时满足下面四个条件才产生死锁)
- 互斥:共享资源X与Y只能被一个线程占用
- 占有且等待
- 不可抢占:其他线程不能强行抢占T1线程所持有的资源X
- 循环等待
2.Lock接口的常用方法:(⭐)
1.void lock()
- 阻塞式获取锁,跟synchronized的加锁解锁方式一样;
2.boolean tryLock()
- 非阻塞式获取锁,获取成功继续执行任务返回true,否则返回false线程直接退出;
3.void lockInterruptibly() throws InterruptedException
- 获取锁时响应中断;
4.boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
- 与2是方法重载;
- 获取锁时支持超时,在指定时间内未获取到锁,线程直接退出;
5.Condition newCondition()
- 获取绑定到该lock对象的等待队列,每当调用一次就产生一个新的等待队列;
3.Lock接口的实现类
Lock接口在JDK的并发包(juc)下有如下几个实现类:
- ReentrantLock(最常用)
- ReentrantReadWriteLock
Lock的一般用法:
Lock lock = new ReentrantLock();
lock.lock();
try {
//doSomeThing...
}finally {
lock.unlock();
}
Lock锁的简单实例:
class Task implements Runnable {
private Lock lock;
public Task(Lock lock) {
this.lock = lock;
}
@Override
public void run() {
try {
if (lock.tryLock(8,TimeUnit.SECONDS)) {
try {
System.out.println(Thread.currentThread().getName() +
"获取到锁");
TimeUnit.SECONDS.sleep(2);
}catch (InterruptedException e) {
e.printStackTrace();
}
finally {
lock.unlock();
System.out.println(Thread.currentThread().getName()+"已经释放锁");
}
}else {
System.out.println(Thread.currentThread().getName()
+"获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class LockTest {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
for (int i = 0;i < 5;i++) {
new Thread(new Task(lock)).start();
}
}
}
输出:
Thread-4获取到锁
Thread-4已经释放锁
Thread-2获取到锁
Thread-2已经释放锁
Thread-3获取到锁
Thread-3已经释放锁
Thread-1获取到锁
Thread-0获取锁失败
Thread-1已经释放锁Process finished with exit code 0
4.ReentrantLock源码阅读(⭐)
首先来看它的构造方法 :(2个)
public ReentrantLock() { //可以看出默认为非公平锁
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
这里的sync是个什么呢?
可以看到这是在ReentrantLock中定义的一个对象:
private final Sync sync;
这里我们把注意力转到Sync 这个类上,可以知道基本上所有的⽅法的实现实际上都是调⽤了其静态内存类Sync中的⽅法,而Sync类继承了AbstractQueuedSynchronizer(AQS)。可以看出要想理解 ReentrantLock 关键核心在于对队列同步器 AbstractQueuedSynchronizer (简称同步器)的理解;
下面来看这几个重要方法:
public void lock() {
sync.lock();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.release(1);
}
可以看到,这些方法的实现全部依靠这Sync这个静态内部类 ,我们来看看这个类的定义:
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock(); //具体实现还得看Sync的子类,如NonfairSync、FairSync都继承了它,从而覆 //写了lock方法;
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
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;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
// Methods relayed from outer class
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
final boolean isLocked() {
return getState() != 0;
}
/**
* Reconstitutes the instance from a stream (that is, deserializes it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
我们以非公平锁的实现来对源码进行一 一跟踪分析,当new 一个ReentrantLock对象时,构造方法中就new了一个Sync的子类对象(sync = new NonfairSync()),这个子类就是NonfairSync,这个类继承了Sync这个类;
当我们调用ReentrantLock对象的lock方法进行加锁时,可以从上面知道,lock方法调用了sync的lock方法,从而可以看到最终调用的是NonfairSync这个类中的lock方法 ,来看看这个方法:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1)) //以CAS方式尝试将AQS中的state从0更新为1
setExclusiveOwnerThread(Thread.currentThread());//获取锁成功则将当前线程标记为持有锁的线程,然后直接返回
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
首先尝试快速获取锁,以CAS(比较交换)的方式将state的值更新为1,只有当state的原值为0时更新才能成功,因为state在 ReentrantLock 的语境下等同于锁被线程重入的次数 ,这意味着只有当前锁未被任何线程持有时该动作才会返回成功。若获取锁成功,则将当前线程标记为持有锁的线程,然后整个加锁流程就结束了。若获取锁失败,则执行acquire方法 ;
acquire方法是Sync父类AQS类的一个方法:(AQS的讲解在下面一个大的知识点)
public final void acquire(int arg) {
if (!tryAcquire(arg) && //tryAcquire方法将再一次进行尝试获取锁,若失败,则才执行后面的两个方法,若获取锁成功,这里就会直接退出;
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
在这个方法里,主要来看if语句块中这三个方法:tryAcquire,addWaiter,acquireQueued
tryAcquire是AQS中定义的钩子方法,如下所示:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
该方法默认会抛出异常,强制同步组件通过扩展AQS来实现同步功能的时候必须重写该方法,ReentrantLock在公平和非公平模式下对此有不同实现,非公平模式的实现如下:
protected final boolean tryAcquire(int acquires) { //这个方法是在NonfairSync这个类中
return nonfairTryAcquire(acquires);
}
可以看到又调用了nonfairTryAcquire这个方法,点击这个方法,可以看到这是它的父类Sync类的一个方法:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();//获取当前线程实例
int c = getState();//获取state变量的值,即当前锁被重入的次数
if (c == 0) { //state为0,说明当前锁未被任何线程持有
if (compareAndSetState(0, acquires)) { //以cas方式获取锁
setExclusiveOwnerThread(current); //将当前线程标记为持有锁的线程
return true;//获取锁成功,非重入
}
}
else if (current == getExclusiveOwnerThread()) { //当前线程就是持有锁的线程,说明该锁被重入了
int nextc = c + acquires;//计算state变量要更新的值
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);//非同步方式更新state值
return true; //获取锁成功,重入
}
return false; //走到这里说明尝试获取锁失败
}
其实这里还可以思考一个问题:nonfairTryAcquire已经实现了一个囊括所有可能情况的尝试获取锁的方式,为何在刚进入lock方法时还要通过compareAndSetState(0, 1)去获取锁,毕竟前者只有在锁未被任何线程持有时才能执行成功,我们完全可以把compareAndSetState(0, 1)去掉,对最后的结果不会有任何影响。这种在进行通用逻辑处理之前针对某些特殊情况提前进行处理的方式在后面还会看到,一个直观的想法就是它能提升性能,而代价是牺牲一定的代码简洁性。
此时回到acquire方法中,若if语句块中若tryAcquire方法获取锁失败,则会继续执行&&的后半部分 ,即执行addWaiter 方法 ,这部分代码描述了当线程获取锁失败时如何安全的加入同步等待队列。这部分代码可以说是整个加锁流程源码的精华,充分体现了并发编程的艺术性:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);//首先创建一个新节点,并将当前线程实例封装在内部,mode这里为null
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) { //当前尾节点不为空
// 将当前线程以尾插的⽅式插⼊同步队列中
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 当前尾节点为空或CAS尾插失败
enq(node);//入队的逻辑这里都有
return node;
}
首先创建了一个新节点,并将当前线程实例封装在其内部,之后我们直接看enq(node)方法就可以了,中间这部分逻辑在enq(node)中都有,之所以加上这部分“重复代码”和尝试获取锁时的“重复代码”一样,对某些特殊情况 进行提前处理,牺牲一定的代码可读性换取性能提升。
来看enq方法:
private Node enq(final Node node) {
for (;;) {
Node t = tail;//t指向当前队列的最后一个节点,队列为空则为null
if (t == null) { // Must initialize //队列为空
if (compareAndSetHead(new Node())) //构造新结点,CAS方式设置为队列首元素,当head==null时更新成功
tail = head;//尾指针指向首结点
} else { //队列不为空
node.prev = t;
if (compareAndSetTail(t, node)) { //CAS将尾指针指向当前结点,当t(原来的尾指针)==tail(当前真实的尾指针)时执行成功
t.next = node; //原尾结点的next指针指向当前结点
return t;
}
}
}
}
对于enq方法,我们可以归纳为:
- 在当前线程是第⼀个加⼊同步队列时,调⽤compareAndSetHead(new Node())⽅法,完
成链式队列的头结点的初始化; - 若队列不为空,⾃旋不断尝试CAS尾插⼊节点直⾄成功为⽌;
上面已经完成了将获取锁失败的线程包装成节点并插入到同步队列中,下面自然而然得关心如何出队列,即如何获取到锁后出同步队列;
这个实现就交给了acquireQueued()方法 :
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//死循环,正常情况下线程只有获得锁才能跳出循环
for (;;) {
final Node p = node.predecessor();//获得当前线程所在结点的前驱结点
//第一个if分句
if (p == head && tryAcquire(arg)) {
setHead(node); //将当前结点设置为队列头结点
p.next = null; // help GC
failed = false;
return interrupted;//正常情况下死循环唯一的出口
}
//第二个if分句
if (shouldParkAfterFailedAcquire(p, node) && //判断是否要阻塞当前线程
parkAndCheckInterrupt()) //阻塞当前线程
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
线程在同步队列中会尝试获取锁,失败则被阻塞,被唤醒后会不停的重复这个过程,直到线程真正持有了锁,并将自身结点置于队列头部。
后续步骤就不再进行深究,有兴趣的可以去阅读这篇完整源码分析文章:
https://www.cnblogs.com/takumicx/p/9402021.html
总结:
将线程获取独占锁得整个流程大致如下:
4.AQS(同步包装器)
1.定义与作用
这是一个抽象类,在juc包中;
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable
里面定义了基于链表的一个队列 ,此队列包装了线程节点,所有获取锁失败的线程 都被包装为队列节点进入AQS中排队获取锁;
2.同步队列结构
同步队列结构如下:
该同步队列得结构特点可简单归纳如下:
- 1.同步队列是个先进先出(FIFO)队列,获取锁失败的线程将构造结点并加入队列的尾部,并阻塞自己;
- 2.队列首结点可以用来表示当前正获取锁的线程;
- 3.当前线程释放锁后将尝试唤醒后续结点中处于阻塞状态的线程;
3.Lock与AQS的关系
lock提供锁获取成功与否的状态给AQS,AQS根据此状态来确定是否将线程置入AQS队列中。
5.公平锁与非公平锁(lock默认采用非公平锁)
公平锁:等待时间最常的线程优先获取到锁,只有lock有;
6.读写锁
HashMap+读写锁 = 缓存(⭐,特别爱考缓存)
缓存:暂时在内存中存储;
范例:用读写锁实现缓存
public class MyCache<K,V> {
// 存放具体数据
private HashMap<K,V> hashMap;
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 并发读锁
private Lock readLock = lock.readLock();
// 互斥写锁
private Lock writeLock = lock.writeLock();
public V get(K k) {
readLock.lock();
try {
return hashMap.get(k);
}finally {
readLock.unlock();
}
}
public void put(K k,V v) {
writeLock.lock();
try {
hashMap.put(k,v);
}finally {
writeLock.unlock();
}
}
}
7.synchronized与Lock的关系与区别
- synchronized是JVM级别的锁,属于Java中的关键字,使用sychronized,加锁与解锁都是隐式的;
Lock是Java层面的锁,加锁与解锁都需要显示使用; - lock可以提供一些synchronized不具备的功能,如响应中断、超时获取、非阻塞式获取、公平锁、读写锁;
- synchronized的等待队列只有一个,而同一个Lock可以拥有多个等待队列(多个Condition对象)(节省开销,提高效率)
8.juc包提供的关于并发的工具
1.CountDownLatch----闭锁
为啥叫闭锁?
每个CountDownLatch对象的计数器在减值为0时不可恢复原值
构造方法:
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
await方法:
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
用法例子:
class CountDownTask implements Runnable {
private CountDownLatch countDownLatch;
public CountDownTask(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"已经到达终点");
countDownLatch.countDown();
}
}
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(3);
System.out.println("比赛开始!");
new Thread(new CountDownTask(countDownLatch),"运动员A").start();
TimeUnit.SECONDS.sleep(1);
new Thread(new CountDownTask(countDownLatch),"运动员B").start();
TimeUnit.SECONDS.sleep(1);
new Thread(new CountDownTask(countDownLatch),"运动员C").start();
countDownLatch.await();
System.out.println("所有运动员已经到达终点,比赛结束!");
}
}
比赛开始!
运动员A已经到达终点
运动员B已经到达终点
运动员C已经到达终点
所有运动员已经到达终点,比赛结束!Process finished with exit code 0
2.CyclicBarrier----循环栅栏
每个CyclicBarrier的对象可以重复使用;
构造方法:(2个)
public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)
第二个构造方法:所有调用await方法阻塞的子线程在计数器值减为0后,随机挑选一个线程执行barrierAction任务后,所有子线程恢复执行;
await方法:
(子线程调用await方法后,将计数器值-1并进入阻塞状态,直到计数器值减为0时,所有调用await方法阻塞的子线程再同时恢复执行)
public int await() throws InterruptedException, BrokenBarrierException
综合例子:
class CyclicTask implements Runnable {
private CyclicBarrier cyclicBarrier;
public CyclicTask(CyclicBarrier cyclicBarrier) {
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"正在写入数据....");
try {
TimeUnit.SECONDS.sleep(2);
// 相当于一堵墙
cyclicBarrier.await();
System.out.println("所有线程都已将数据写入完毕,同时恢复执行");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}
public class CyclicBarrierTest {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new Runnable() {
@Override
public void run() {
System.out.println("当前线程为"+Thread.currentThread().getName());
}
});
for (int i = 0;i < 3;i++) {
new Thread(new CyclicTask(cyclicBarrier),
"写入线程"+(i+1)).start();
}
}
}
写入线程1正在写入数据…
写入线程2正在写入数据…
写入线程3正在写入数据…
当前线程为写入线程2
所有线程都已将数据写入完毕,同时恢复执行
所有线程都已将数据写入完毕,同时恢复执行
所有线程都已将数据写入完毕,同时恢复执行Process finished with exit code 0
3.Exchanger----线程数据交换器
当只有一个线程调用exchange方法时,它会阻塞知道有新的线程进入Exchanger缓冲区,当有新的线程进入缓冲区后,就交换彼此的数据在同时恢复执行。
例子:
public class CountDownLatchTest {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>();
Thread gril = new Thread(new Runnable() {
@Override
public void run() {
try {
String str = exchanger.exchange("i am a girl...");
System.out.println("girl say:"+str);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
gril.start();
Thread boy = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("boy start......");
Thread.sleep(2000);
String str = exchanger.exchange("i am a boy..");
System.out.println("boy say:"+str);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
boy.start();
}
}
输出:
boy start… //这里等待两秒
boy say:i am a girl…
girl say:i am a boy…Process finished with exit code 0
4.Semaphore----信号量
Semaphore翻译成字⾯意思为 信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取
⼀个许可,如果没有就等待,⽽ release() 释放⼀个许可。
例子:
class SemaphoreTask implements Runnable {
private Semaphore semaphore;
private int num;
public SemaphoreTask(Semaphore semaphore, int num) {
this.semaphore = semaphore;
this.num = num;
}
@Override
public void run() {
// 尝试去申请设备
try {
semaphore.acquire(2); //每个工人占用2台
System.out.println("工人"+this.num+"占用一台设备生产");
TimeUnit.SECONDS.sleep(2);
System.out.println("工人"+this.num+"释放一台设备");
semaphore.release(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class SemaphoreTest {
public static void main(String[] args) {
// 5台设备
Semaphore semaphore = new Semaphore(8);
for (int i = 0;i < 8;i++) {
new Thread(new SemaphoreTask(semaphore,(i+1))).start();
}
}
}
工人1占用一台设备生产
工人2占用一台设备生产
工人3占用一台设备生产
工人4占用一台设备生产
工人3释放一台设备
工人4释放一台设备
工人2释放一台设备
工人7占用一台设备生产
工人1释放一台设备
工人6占用一台设备生产
工人5占用一台设备生产
工人8占用一台设备生产
工人6释放一台设备
工人8释放一台设备
工人5释放一台设备
工人7释放一台设备Process finished with exit code 0