并发编程–显示锁
1.预备概念
- 显示锁和内部锁区别?
最大区别在于是否需要手动地去获取、释放锁,手动获取和释放锁的方法就叫显示锁,否则就是内部锁。ReentrantLock 属于显示锁,synchronzied 属于内部锁。 - 为什么需要显示锁?
内部锁拿锁和取锁的过程比较固定,没有办法灵活的获取和释放锁,显示锁就是手动获取和释放锁,提供超时拿锁、尝试拿锁的功能,这些功能是内部锁不具备的。
2.显示锁
1.分类
1.读写锁
2. 重入锁
2.常见操作方法
lock():获取锁,拿不到锁就一直处于自旋的状态。
unlock():释放锁。
tryLock():尝试获取锁。
技巧:调用unlock()时,放入finally块中进行释放锁。
add(){
lock.lock()
try{
dosomething();
}finally{
lock.unlock();
}
}
可重入锁:一个方法内部重复获取锁,防止在递归的时候产生死锁。
公平锁:对获取锁的线程进行排序,先竞争锁的线程先拿到锁,效率比非公平锁低。
3.显示锁线程协作
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyMessageQueue {
private static ReentrantLock reentrantLock = new ReentrantLock();
private static Condition condition = reentrantLock.newCondition();
private static LinkedList<String> list = new LinkedList<>();
public MyMessageQueue(){}
// 放
public void put(String message){
reentrantLock.lock();
try {
list.addLast(message);
} catch (Exception e) {
e.printStackTrace();
}finally {
condition.signalAll();
reentrantLock.unlock();
}
}
// 取
public String poll(){
//如果为空则等待
String message ;
reentrantLock.lock();
try {
while (list.isEmpty()) {
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
message = list.removeFirst();
}finally {
reentrantLock.unlock();
}
return message;
}
}
4.读写锁实践
读写锁:(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();
}
读写锁实践:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReentrantRwLock implements GoodsService {
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock readLock = readWriteLock.readLock();
private Lock writeLock = readWriteLock.writeLock();
private GoodsInfo goodsInfo ;
public ReentrantRwLock(GoodsInfo goodsInfo) {
this.goodsInfo = goodsInfo;
}
@Override
public GoodsInfo getNum() {
readLock.lock();
try {
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
return goodsInfo;
}finally {
readLock.unlock();
}
}
@Override
public void setNum(int number) {
writeLock.lock();
try {
goodsInfo.changeNumber(200);
}finally {
writeLock.unlock();
}
}
}
测试代码:
import cn.enjoyedu.tools.SleepTools;
import java.util.Random;
/**
*类说明:对商品进行业务的应用
*/
public class BusiApp {
static final int readWriteRatio = 10;//读写线程的比例
static final int minthreadCount = 3;//最少线程数
//读操作
private static class GetThread implements Runnable{
private GoodsService goodsService;
public GetThread(GoodsService goodsService) {
this.goodsService = goodsService;
}
@Override
public void run() {
long start = System.currentTimeMillis();
for(int i=0;i<100;i++){//操作100次
goodsService.getNum();
}
System.out.println(Thread.currentThread().getName()+"读取商品数据耗时:"
+(System.currentTimeMillis()-start)+"ms");
}
}
//写操做
private static class SetThread implements Runnable{
private GoodsService goodsService;
public SetThread(GoodsService goodsService) {
this.goodsService = goodsService;
}
@Override
public void run() {
long start = System.currentTimeMillis();
Random r = new Random();
for(int i=0;i<10;i++){//操作10次
SleepTools.ms(50);
goodsService.setNum(r.nextInt(10));
}
System.out.println(Thread.currentThread().getName()
+"写商品数据耗时:"+(System.currentTimeMillis()-start)+"ms---------");
}
}
public static void main(String[] args) throws InterruptedException {
GoodsInfo goodsInfo = new GoodsInfo("Cup",100000,10000);
GoodsService goodsService = new ReentrantRwLock(goodsInfo);
for(int i = 0;i<minthreadCount;i++){
Thread setT = new Thread(new SetThread(goodsService));
for(int j=0;j<readWriteRatio;j++) {
Thread getT = new Thread(new GetThread(goodsService));
getT.start();
}
SleepTools.ms(100);
setT.start();
}
}
}
运行结果:
Thread-0写商品数据耗时:558ms---------
Thread-11写商品数据耗时:553ms---------
Thread-22写商品数据耗时:548ms---------
Thread-9读取商品数据耗时:1082ms
Thread-2读取商品数据耗时:1082ms
Thread-4读取商品数据耗时:1082ms
5. ReentrantLock实现
LockSupport:提供了操作线程的基本方法,park() 阻塞当前线程 unpark() 唤醒当前线程,parkNanos()超时阻塞。
CLH队列锁:队列锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程 仅仅在本地变量上自旋,它不断轮询前驱的状态,假设发现前驱释放了锁就结束 自旋。
- 创建一个的QNode,将其中的locked设置为true表示需要获取锁,myPred 表示对其前驱结点的引用
2.线程 A 对 tail 域调用 getAndSet 方法,使自己成为队列的尾部,同时获取 一个指向其前驱结点的引用 myPred
3.线程就在前驱结点的 locked 字段上旋转,直到前驱结点释放锁(前驱节点 的锁值 locked == false)
4.当一个线程需要释放锁时,将当前结点的 locked 域设置为 false,同时回收 前驱结点
**AbstractQueuedSynchronizer:**抽象队列同步器,基于模板方法模式,springboot中的各种template就是基于模板方法模式,什么时模板方法模式这里就不介绍了。
既然基于模板方法模式,只要按照步骤操作就可以写一个自己的同步锁,
开干。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class MyReentrantLock implements Lock {
private final Sync sync = new Sync();
static class Sync extends AbstractQueuedSynchronizer{
@Override
protected boolean tryAcquire(int arg) {
if(compareAndSetState(0,arg)){
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if (getState() == 0){
throw new IllegalMonitorStateException("状态错误");
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
@Override
public void lock() {
System.out.println(Thread.currentThread().getName()+"will get lock");
sync.acquire(1);
System.out.println(Thread.currentThread().getName()+"already get lock");
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
System.out.println(Thread.currentThread().getName()+"will release lock");
sync.release(1);
System.out.println(Thread.currentThread().getName()+"already release lock");
}
public boolean isLock(){
return sync.isHeldExclusively();
}
@Override
public Condition newCondition() {
return null;
}
}
运行结果:
Thread-0will get lock
Thread-2will get lock
Thread-1will get lock
Thread-3will get lock
Thread-0already get lock
Thread-0
Thread-0will release lock
Thread-0already release lock
Thread-2already get lock
Thread-2
Thread-2will release lock
Thread-2already release lock
Thread-1already get lock
Thread-1
Thread-1will release lock
Thread-1already release lock
Thread-3already get lock
Thread-3
Thread-3will release lock
Thread-3already release lock
6.源码深入
每个请求锁的线程如果在lock()方法中没有拿到锁,则会把每个线程打包成一个Node,然后把Node对象设置到队列的尾节点。
node定义了线程的等待模式:
- SHARED:表示线程以共享的模式等待锁(如 ReadLock)
- EXCLUSIVE:表示线程以互斥的模式等待锁(如 ReetrantLock),互斥就是一 把锁只能由一个线程持有,不能同时存在多个线程使用同一个锁
node定义了线程的状态枚举:
- CANCELLED:值为 1,表示线程的获锁请求已经“取消”
- SIGNAL:值为-1,表示该线程一切都准备好了,就等待锁空闲出来给我
- CONDITION:值为-2,表示线程等待某一个条件(Condition)被满足
- PROPAGATE:值为-3,当线程处在“SHARED”模式时,该字段才会被使用 上
多个线程获取锁的流程分析:
入队分析:
public void lock() {
System.out.println(Thread.currentThread().getName()+"will get lock");
sync.acquire(1);
System.out.println(Thread.currentThread().getName()+"already get lock");
}
-------
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//如果线程通过cas拿到锁 !tryAcquire(args)返回false,没有拿到锁时进入addWaiter(Node.EXCLUSIVE),
-------
private Node addWaiter(Node mode) { //首先快速尝试一次添加到同步队列中,并返回打包后的节点
Node node = new Node(Thread.currentThread(), mode);// 把当前线程打包成一个node节点
// 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)) { // 通过CAS操作把当前队列设置成队列的尾节点
pred.next = node; // 前驱节点的后置节点设置成当前节点
return node;
}
}
enq(node); //如果添加队列失败,
return node;
}
------------
private Node enq(final Node node) {
for (;;) { // 自旋
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) { // 自旋设置尾节点,直到成功。
t.next = node;
return t;
}
}
}
}
自旋分析:(自旋的目的是为了拿到锁)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 入队并进入自旋
selfInterrupt();
}
---
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { // 自旋获取锁
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) { // 再试着拿锁,并且当前的前驱节点是队列的头节点
setHead(node); // 把当前节点设置成头节点
p.next = null; // help GC //并且把之前的头节点只想的后驱节点设置成空
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) && // 获取锁失败之后,是否应该阻塞当前线程
parkAndCheckInterrupt()) // 调用LockSupp的park方法阻塞当前线程
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
-- ----
// 既然方法阻塞了,怎么让线程感知外界线程情况。
//当前面的线程调用unlock时 ,会通知后面的节点
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); // 解锁park方法,让上面的自旋继续操作,
}