1. 锁的意义——线程同步
2. 案例——基于ReentrantLock来实现同步
public class A {
public static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> {
testSync();
}, "t1").start();
new Thread(() -> {
testSync();
}, "t2").start();
}
public static void testSync(){
reentrantLock.lock();
try {
System.out.println(Thread.currentThread().getName());
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}
}
3. ReentrantLock跟Synchronized的区别?
ReentrantLock 1.6之后才出现
1. 其中while + CAS就是自旋 —— 同时会空转,耗费cpu
2. 针对第一点可以尝试添加yield ,主动让出cpu,但是具体还是内部调度器自身实现的
3. 针对第一点可以尝试添加sleep,但是主要问题在于没法确定休眠时间,过多过少都有问题
4. 使用park+自旋【park是由unsafe提供的,unsafe可以申请堆外内存, park方法可以让当前线程立即睡眠】【其中需要注意:park也要调用osrere】
public class A { public static ReentrantLock reentrantLock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { testSync(); }, "t1"); t1.start(); Thread.sleep(1000); System.out.println("main-1"); LockSupport.unpark(t1); } public static void testSync(){ System.out.println("t1-1"); LockSupport.park(); System.out.println("t1-2"); } }
4. ReentrantLock理解?
1. 有两种场景,公平锁和非公平锁。属于哪一种取决于具体参数
2. 调用lock
3. acquire 【注意:tryAcquire是取非】
4. tryAcquire
——获取当前线程
——getState 获取一个volatile 类型State的值,默认是0
——c == 0,本质上判断的是锁是否是自由状态
——接续看:在一个线程占有锁的情况下,其他线程来了,那么会进入队列,如:t1占有锁,t2、3、4在队列里面;
——t1执行完毕,那么需要:释放锁(state=0,unpark,维护队列XXX)
——那么问题来了,假如state=0的时候,t5来了,能直接cas吗?对t2、3、4公平吗?
——如果直接cas,那么这种情况下就叫做非公平锁
——所以:公平与非公平就只有这一个区别(!hasQueuedPredecessors —— 判断自己是否需要排队)
5. AQS
Node里面有当前线程ID、有pre、next
AbstractQueuedSynchronizer里面有head、tail
6. hasQueuePredecessors
——在刚开始的时候,h、t都是null。所以直接返回false
7. 基础属性
private transient volatile Node head; private transient Thread exclusiveOwnerThread; //当前持有锁的线程 private transient volatile Node tail; private volatile int state; //锁的状态所以,这里返回为true,回到Acquire。整体结束
所以说:同步就是为了让lock方法正常返回 reentrantLock.lock();
多线程t1没有执行完的情况下【对于t1而言:获取到锁的自由状态,然后判断是否需要排队】,t2来了【也就是修改锁的计数器】
也就是tryAcquire返回false。
因为刚开始初始化的时候tail为null。所以只想enq 【初始化】
务必关注:AQS队列里面的队头结点的thread永远为null
注意:所谓的入队就是维护好链表关系
来一个真实场景【t1获取锁,t2抢锁失败,进入队列,进入后需要自旋,因为t1有可能结束】
首先:队列里面第一个不是排队的,第二个才是排队的【如:去火车站买票,第一个人是排队吗?肯定不是啊,正在受理业务,只有第二个人才叫排队】
只有第二个才有自旋的资格,相当于运行过程中,定期询问第一个是否办理完成
但是后续的,如:第三个就没有资格询问,因为前面的人都没有弄完 【当然:这里原则不变,也就是AQS里面,队头的thread永远为null,说明第二个人永远是第一个排队的 】
重点来说:如果前一个是头部的话,那么当前就是第二个结点,会进行tryAcqquire,也就是自旋一次 。
tryAcquire 如果获取不到锁,那么就返回false
volatile int waitStatus; //这个是Node的状态假如加锁失败:那么进入到shouldParkAfterFailedAcquire。调整当前结点状态,将ws置为-1,同时返回false
因为返回false。所以acquireQueued里面重新自旋,假如还没有获取到锁shouldParkAfterFailedAcquire 会返回true
这里会park当前线程
所以:这里总结下,总共需要自旋两次【t2】。这里也牵扯到Node.SIGNAL 0和-1的变化,为什么需要这一次呢?因为尽量在提升效率的时候,避免park
但是:t3不进行自旋,因为没有资格啊
根据shouldParkAfterFailedAcquire,也就是后一个线程会把前一个线程Node状态修改为-1
-1表示在进行休眠
为什么需要下一个结点来进行调整呢?
因为当前线程休眠后没法修改状态。或者修改状态后,不一定会正常进入休眠
类比:我自己睡着了,这个是别人看到的
因为这个时候,队列还没有初始化
5. 核心
关于AQS或者ReentrantLock
1. 自旋
2. CAS
3. park-unpark
同时:单线程或者多线程交替执行其实和队列无关 —— jdk级别解决并发问题