Java锁相关系列——synchronized从使用到原理
什么是Lock接口
Lock: Lock不是Java中的关键字,而是J.U.C包( java.util.concurrent)下locks下的一个接口。在使用的过程中需要显式的获取(lock.lock())和释放锁(lock.unlock)。
Lock接口与synchronized的优劣对比
synchronized的优劣
在Java
多线程
编程中,开发者经常使用synchronized关键字来实现同步
,控制多线程对变量的访问,来避免并发问题
。但是在有些时候,synchronized关键字会比较沉重,不灵活。synchronized 方法或语句的使用提供了对与每个对象相关的隐式监视器锁的访问,但却强制所有锁获取和释放均要出现在一个块结构中:当获取了多个锁时,它们必须以相反的顺序释放
,且必须在与所有锁被获取时相同的词法范围内释放所有锁
。
Lock接口的优劣
lock接口提供了与synchronized关键字类似的同步功能,需要在使用的过程中需要显示的获取和释放锁,这个条件也是lock接口的缺点。lock接口拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。缺点则是缺少隐式获取释放锁的便捷性。
Lock接口的基本API
以下代码时常用的四个API方法实例Demo,帮助各位理解上面API表格的描述,Condition后文继续解读
package lock;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 潇兮
* @date 2019/9/21 21:41
**/
public class Demo4 {
//公平锁
// static Lock lock=new ReentrantLock(true);
//非公平锁
static Lock lock=new ReentrantLock();
public static void main(String[] args) throws InterruptedException{
//主线程获取锁
lock.lock();
Thread th=new Thread(new Runnable() {
@Override
public void run() {
//子线程获取
// 尝试获取,浅尝
boolean result=lock.tryLock();
System.out.println("尝试获取: 是否获得锁:" + result);
//过时不候
boolean result2= false;
try {
System.out.println("过时不候:开始尝试获得锁2:");
result2 = lock.tryLock(3, TimeUnit.SECONDS);
System.out.println("过时不候: 3s是否获得锁2:" + result2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//听话,有人通知就不获取并抛出异常 InterruptedException
try {
System.out.println("听话: 是否获得锁3:");
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("有人告诉我不要去获得锁3");
}
//不死不休 只有当15秒后锁释放,此时才能获取到锁打印出来,锁不释放,则一直处于等待队列中
System.out.println("不死不休: 是否获得锁4:");
lock.lock();
System.out.println("不死不休: 获得锁4:");
}
});
th.start();
Thread.sleep(6000L);
//中断
th.interrupt();
System.out.println("线程中断");
Thread.sleep(12000L);
lock.unlock();
}
}
Condition
Object中的wait()、notify()、notifyAll()只能和synchronized配合使用,可以唤醒一个线程或者全部(单个等待集);Condition需要与Lock配合使用的,提供了多个等待集合,更加精确的控制 。
Condition的使用—代码示例
可以从打印的结果看到直接运行时结果成功输出,当打开注释掉的代码,会出现死锁。
package lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 潇兮
* @date 2019/9/21 22:26
**/
public class Demo4_Condition {
private static Lock lock=new ReentrantLock();
private static Condition condition=lock.newCondition();
public static void main(String[] args) throws InterruptedException {
Thread th=new Thread(){
@Override
public void run(){
// try {
// Thread.sleep(4000L);
// System.out.println("3s后");
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
lock.lock();
try {
System.out.println("condition的挂起...");
//释放锁
condition.await();
System.out.println("condition的使用成功");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
};
th.start();
Thread.sleep(3000L);
//System.out.println("3s后");
//condition的方法都要搭配lock方法,不加锁会报错
lock.lock();
//唤醒
condition.signal();
System.out.println("2s后condition的唤醒");
lock.unlock();
}
}
成功输出打印结果
打开注释死锁的打印结果:
可重入锁—ReentrantLock
ReentrantLock的可重入性表现在
同一个线程
可以多次获得锁,而不同线程依然不可多次获得锁,加锁lock.lock几次需要对应释放几次的锁lock.unlock。ReentrantLock中的等待池是Condition,可以有多个Condition,waites为ReentrantLock的等待队列,当有线程T2抢锁的时候,若锁被占用,则进入等待队列中。
ReentrantLock的加锁逻辑
假设有 t1, t2, t3, t4这四个线程(图一),若t1,t2对ReentrantLock lock的执行CAS
(JMM内存模型—手撕CAS)操作抢锁,发现owner
值为null,并且count
为0,此时只有一个线程(CAS机制)能成功,若T2抢锁成功,将 count 值改为 1 并将owner的引用改为 t2,t1 进入 waiters
,状态变为 waiting
;若此时 t3 过来抢锁,会对比 owner 值,发现不指向 t3,则进入等待队列 waiters, t4 也是如此(图二)。假如 t2 在此时没有执行 释放锁的前提下又执行 lock( ) 操作抢锁,会对比owner
值,发现相等,通过CAS操作将owner值加一
变为 2。当 t2 具体如下脑图
图一
图二
手写ReentrantLock实现
package lock;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
/**
* @author 潇兮
* @date 2019/9/22 0:03
* 类说明: 手写实现ReentrantLock 逻辑
**/
public class Demo4_ReentrantLock implements Lock {
//创建一个锁拥有者
AtomicReference<Thread> owner=new AtomicReference<>();
//记录重入次数
AtomicInteger count=new AtomicInteger(0);
//等待队列,实际上的ReentrantLock采用AbstractQueuedSynchronizer(AQS)
private LinkedBlockingQueue<Thread> waiters=new LinkedBlockingQueue<>();
@Override
public boolean tryLock() {
//获取AtomicInteger值
int ct=count.get();
//判断count值是否为0,不为0,锁被占用
if (ct!=0){
//判断是否当前线程占用
if (Thread.currentThread()!= owner.get()){
//当前线程占用,重入
count.set(ct+1);
return true;
}
// else {
// //不是当前线程进入等待队列
// return false;
// }
}else {
// count值为0,锁未占用,CAS抢锁
if (count.compareAndSet(ct,ct+1)){
owner.set(Thread.currentThread());
return true;
}
// else {
// //CAS失败,进入等待队列
// return false;
// }
}
return false;
}
@Override
public void lock() {
//判断锁是否被占用
if (!tryLock()){
//加入等待队列
waiters.offer(Thread.currentThread());
for (;;){
//若线程是队列头部尝试加锁
Thread head = waiters.peek();
if (head == Thread.currentThread()){
if (!tryLock()){
//失败,挂起线程
LockSupport.park();
}else {
//移出队列
waiters.poll();
return;
}
}else {
//不是线程头部
LockSupport.park();
}
}
}
}
public boolean tryUnlock() {
//判断当前线程是否占有锁
if (owner.get() !=Thread.currentThread()){
throw new IllegalMonitorStateException();
}else {
//unlock
int ct=count.get();
int nextCt=ct-1;
count.set(nextCt);
//判断count是否为0,为0则释放锁成功,返回true,不为0则释放锁失败返回false
if (nextCt==0){
owner.compareAndSet(Thread.currentThread(),null);
return true;
}else {
return false;
}
}
}
@Override
public void unlock() {
if (tryUnlock()){
Thread head=waiters.peek();
if (head!=null){
//唤醒
LockSupport.unpark(head);
}
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public Condition newCondition() {
return null;
}
}
读写锁—ReentrantReadWriteLock
什么是ReentrantReadWriteLock
维护一对关联锁,一个只用于读操作,一个只用于写操作;读锁可以有多个线程共同持有,是一种共享锁;写锁是排他的,是一种互斥锁。同一时间内,两把锁不能被不同线程持有。
ReentrantReadWriteLock基本原理—丧心病狂版
假设有两个线程 t1 , t2。(实际上源码中使用state单个字段存储读写数量,所以不需要采用cas操作保证原子性,这里是为了方便理解)
- 现在假如 t1 要去抢写锁,会先判断 readCount 是否为 0,如果 readCount 为0,则读锁未被占用,才能去抢写锁,如图一;
- t1 开始抢写锁,判断 writeCount 是否为 0,若果 writeCount 为 0,则对它进行 CAS 操作,从 0 变为 1,owner 由 null 变为 t1,如图二;
- 此时若 t1 线程又去抢写锁,会先判断 readCount 是否为 0, readCount 不为 0,则抢写锁 ,判断写锁 writeCount 是否为 0,writeCount 不为 0 则继续判断 owner 的值是否为当前线程。如果是做重入操作若不是则进入waiters,当下指向 t1 线程,则 writeCount 的值 +1 变为 2 ,如图三;
- 此时 t2线程进入抢锁,判断 readCount 为 0,去争抢写锁,判断写锁 writeCount 是否为 0,writeCount 等于2不为 0,则继续判断 owner 的值是否为当前线程,不是指向 t2,进入等待队列,状态waiting;如图四;
- 此时 t1 释放锁,writeCount 由2变为1,再释放一次变为0,owner值变为null,如图五;
- 此时waiters队列的头部线程唤醒即 t2 ,假设t2抢锁成功,writeCount变为1,owner变为 t2(难受不画图了,脑补吧);
- 假设此时有 t3 线程进入抢读锁,会先判断 writerCount 是否为0,writerCount 不为0进入等待队列,类推,t4,t5 都进入等待队列,状态waiting,如图六;
- 此时t2释放掉锁,writerCount变为0,owner变为null;唤醒了头部队列 t3,t3进行抢锁,会先判断 writerCount 是否为0,writerCount为0,说明写锁未被占用,才能去抢读锁,采用CAS操作将 0 改为 1,t4 ,t5类推,readCount为3,如图七;
- 此时 t1 抢写锁,先判断readCount是否为0,不为0,进入等待队列,此时t5,t4,t3释放锁(采用CAS方式),readCount变为0,唤醒头部队列 t1重新抢锁。
完美,这就是ReentrantReadWriteLock的基本原理,,,,,,,,,,,,
图一
图二
图三
图四
图五
图六
图七