ReentrantReadWriteLock
和ReentrantLock
大差不差,只是前者多了一个S锁和X锁的兼容性 synchronized (this) {
。。。 System.out.println(Thread.currentThread().getName()+"售票一张,余票:"+ --ticket);
}
public synchronized void sale(){
。。。System.out.println(Thread.currentThread().getName()+"售票一张,余票:"+ --ticket);
}
Lock lock = new ReentrantLock( true );
//公平锁创建读写锁:ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
加读锁:readWriteLock.readLock().lock();
开读锁:readWriteLock.readLock().unlock();
主要是使用这两个方法,其内部也用到了上述双端队列,并且在大题执行逻辑上
LockSupport.unpark(node.thread);
LockSupport.park(this);
Condition
类,在阻塞、唤醒的操作上比传统多线程提高了很多灵活性notify()
、notifyAll()
、wait()
来唤醒其他线程 和 阻塞自己并进入等待队列,等待唤醒后进入同步队列,在JUC编程中使用await()
替换了Object类中的wait,signal()
替换Object中的notify其中doSIgnal()底层调用的就是LockSupport.unpark(node.thread);
着重需要注意的是:调用condition.await()很容易写成condition.wait()从而报错
这个方法与unlock调用的都是tryRelease()
,底层也都是unpark()
因为Condition类的存在,await()变得异常灵活,我们可以在不同的线程用同一个condition调用await(),其等待队列维护在这个condition对象中,可以被signal()给唤醒
关于同步队列、等待队列的内容在第6号标题
这里把结论提前放这里:
reentrantLock.newCondition()
创建)offset
偏移量来实现原子性操作,一般配合while()或者for(;;)实现自旋锁,如果只是想尝试一次获取锁,那么就不需要循环,只需要保证原子性即可 reentrantLock.lock()
和condition.await()
底层都同时做了两件事:将节点放入队列、在for(;;)
自旋锁中调用LockSupport.park()
来阻塞线程,暂停自旋,防止消耗cpu资源,有且仅有同步队列的第二个节点在自旋 reentrantLock.unlock()
和condition.signal()
底层都同时做了两件事:从自己维护的队列中取出头节点(即便是非公平锁也是取头节点)、底层调用LockSupport.park()
来唤醒对应的线程LockSupport.park()
,底层就是阻塞线程,lock失败、awiat都用到了他。同理unpark在unlock和signal中被使用,用于唤醒指定线程(唤醒等待队列的头节点)lock和unlock操作的是同步队列
(即使源码注释只提到了等待队列),而awati和signal分别是“将同步队列节点移到等待队列
”,“将等待列队节点移到同步队列
”————在第6点中细讲while(true) for(;;)
自旋 + park()
阻塞,等待别人unpark()
唤醒后继续自旋state
的值在CAS锁机制下使用的参数是stateOffset
偏移量,效果相同,例如unsafe.objectFieldOffset
获取偏移量,然后usafe.compareAndSwapInt
执行原子指令while( !unsafe.compareAndSwapInt(this, stateOffset, 0 ,1 ))
来实现自旋锁,直到加到锁,其中原子性是由CAS(一种乐观锁实现)来保证的public class AQSTest {
public static void main(String[] args) {
AQSTest aqsTest = new AQSTest();
aqsTest.test();
}
public void test(){
System.out.println(state);//0
Unsafe unsafe = getUnsafe();
boolean b = unsafe.compareAndSwapInt(this, stateOffset, 0, 1);
System.out.println(state);//1
}
private volatile int state = 0;//状态0则没加锁,volatile防止指令重排
private static final Unsafe unsafe = getUnsafe();//import sun.misc.Unsafe;
//偏移量,即在计算机中定位到state的位置,以便于原子操作
private static Long stateOffset;
//用静态代码块捕获异常,如果直接定义private static Long stateOffset =
// unsafe.objectFieldOffset(AQSTest.class.getDeclaredField("state"));
//那么则会在空参构造上抛出异常
static {
try {
stateOffset = unsafe.objectFieldOffset(AQSTest.class.getDeclaredField("state"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
//获取unsafe对象
private static Unsafe getUnsafe(){
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}
加锁解锁方法:
public void lock(){
Unsafe unsafe = getUnsafe();
while ( !unsafe.compareAndSwapInt(this,stateOffset,0,1) ){
System.out.println(Thread.currentThread().getName() + "尝试获取锁");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "获取锁成功");
}
public void unlock(){
Unsafe unsafe = getUnsafe();
boolean flag = unsafe.compareAndSwapInt(this, stateOffset, 1, 0);
if(flag){
System.out.println("解锁成功");
}
}
main中开启两个线程:
AQSTest t = new AQSTest();
new Thread(()->{
System.out.println("线程1开始,上锁");
t.lock();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//3秒后释放锁
t.unlock();
},"线程1").start();
new Thread(()->{
System.out.println("线程2开始");
t.lock();
t.unlock();
},"线程2").start();
输出:
在这里面公平锁与非公平锁都一样,都是使用了:
自旋锁 + park阻塞 + unpark唤醒 + 同步队列FIFO 的策略
(这里源码中的等待队列其实本质是同步队列,相关内容在第6号标题中)
那么非公平锁是否有维护这个队列呢?在&&的后半块:我们在addWaiter()
的方法打个断点,分别测试公平锁和非公平锁,发现都会进来,就证明其实二者都维护了同步队列(只有公平锁加锁时用hasQueuedPredecessors
判断是否是队列头)
debug后发现,对于addWaiter同步队列
,二者的流程一致。不得不说这个addWaiter设计十分精妙,enq(Node node)方法的自旋锁完全考虑了队列为空、创建队列时被插入、新增节点时可能遇到的所有并发问题
刚刚分析到了加锁失败后,这里形参是刚刚生成的节点,这里最重要的是这个打框的地方,shouldParkAfterFailedAcquire(pre,node)
是一个should开头的疑问句,作用是判断当前节点是不是下一个执行节点,如果是的话则执行parkAndCheckInterrupt()
阻塞
而在for(;;)中是自旋的,这里阻塞了可以减少循环消耗cpu资源,也能被上一个节点成功唤醒
因为被parkAndCheckInterrupt()
阻塞了,停止了循环,当上一个节点唤醒当前节点后解除阻塞,继续循环,尝试加锁(非公平锁仍有可能抢不过————存在虽然被唤醒但竞争失败的情况)
关于LockSupport.park方法,这里参考参考链接,park是一个native方法,可以实现精准唤醒(配合队列可以指定唤醒某一个节点),其中公平锁非公平锁都用了相同逻辑的同步队列
unlock调的都是同一个release()
方法
这里调用unpark去唤醒下一个节点,下一个节点那边接触阻塞
这个案例主要是探究await()阻塞、
同步队列
,如果阻塞也是阻塞在同步队列同步队列
剔除,释放锁,并通知下一个节点(LockSupport.unpark()
)线程的执行顺序由同步队列决定,等待队列仅仅起到一个保存节点的作用
public class AwaitTest {
public static void main(String[] args) {
final ReentrantLock lock = new ReentrantLock();
final Condition condition = lock.newCondition();
new Thread(()->{
lock.lock();
System.out.println("线程000000开始");
System.out.println("线程000000await()阻塞,进入等待队列");
try {
condition.await();//线程0进入等待队列
} catch (InterruptedException e) {
}
System.out.println("线程0被唤醒");
lock.unlock();//线程0释放锁,锁交给同步队列的下一个节点
}).start();
new Thread(()->{
lock.lock();
System.out.println("线程111111开始");
System.out.println("线程111111await()阻塞,进入等待队列,此时等待队列有线程0和1");
try {
//线程1从同步队列移除,进入condition等待队列,此时的condition等待队列有两个元素
condition.await();
} catch (InterruptedException e) {
}
System.out.println("线程1被唤醒");
lock.unlock();//线程1释放锁,锁交给同步队列的下一个节点
}).start();
new Thread(()->{
try {
lock.lock();
System.out.println("此时同步队列队首为线程2(线程2获取锁),线程222222开始");
System.out.println("唤醒0线程————线程0从等待队列进入同步队列,当线程2 unlock后执行");
condition.signal();//唤醒0线程
System.out.println("唤醒1线程————线程1从等待队列进入同步队列,当线程0 unlock后执行");
condition.signal();//唤醒1线程
System.out.println("此时同步队列队首的线程2调用unlock释放锁,执行其他同步队列节点");
lock.unlock();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
本质其实就是这个Node节点的结构问题。Node节点是在AQS抽象类中的,且被AQS内部类创建,因此一个ReentrantLock可以有多个Node节点(1个同步队列,多个newCondition创建的等待队列)
因此形成了这种情况:
lock unlock操作的是同步队列
await signal操作的是等待队列
关于生产者消费者问题,只需要满足以下结构即可: