ReentrantReadWriteLock和ReentrantLock大差不差,只是前者多了一个S锁和X锁的兼容性| Synchronized | Lock |
|---|---|
| 内置的Java关键字 | Java类,包括如下三个常用的可重入锁:ReentrantLock 、 ReentrantReadWriteLock![]() |
| 无法判断获取锁的状态 | 可以判断是否获取到了锁 reentrantllock.isHeldByCurrentThread |
| 会自动释放锁 可重入 | lock 必须要手动释放锁,否则会死锁。 且由于可重入机制,lock()的次数要等于unlock() |
| 遇到阻塞就一直阻塞 | 可以使用tryLock(long timeout, TimeUnit unit)设置超时时间 |
| 非公平锁 | 可以设置是否公平,构造器ReentrantLock(boolean fair) |
| 适合锁少量的代码同步问题,如:单例模式检锁 | 自由度高,适合锁大量代码 |
| 普通方法对当前对象this监视 static方法对唯一Class对象监视 | 监视的是new出来的ReentrantLock对象 |
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();
输出:


在这个&&的前半块,在tryAcquire 中,公平锁要先判断是不是队列队头,而非公平锁是直接cas抢锁(当然因为没有循环,只会获取一次,如果没抢到就算了)

关于排队hasQueuedPredecessors,详细内容如下(考虑了并发问题)
在这里面公平锁与非公平锁都一样,都是使用了:
自旋锁 + 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操作的节点隶属于最外层AQS
而await signal操作的节点是AQS的内部类ConditionObject中的
因此形成了这种情况:
lock unlock操作的是同步队列
await signal操作的是等待队列
关于生产者消费者问题,只需要满足以下结构即可: