java多线程与高并发:LockSupport、淘宝面试题与源码阅读方法论

愿天堂没有BUG

共 22764字,需浏览 46分钟

 ·

2022-06-08 15:43

前言

首先我们简单回顾一下前面三节课讲的内容,分别有线程的基本概念、synchronized、volatile、AtomicXXX、各种JUC同步框架(ReentrantLock、CountDownLatch、CyclicBarrier、Phaser、ReadWriteLock-StampedLock、Semaphore、Exchanger、LockSupport),其中synchornized重点讲了一下,包括有synchornized的底层实现原理、锁升级的概念(四种状态:无锁、偏向锁、轻量级锁、重量级锁),volatile我们讲了可见性和禁止指令重排序如何实现。

synchronized和ReentrantLock的不同?

synchronized:系统自带、系统自动加锁,自动解锁、不可以出现多个不同的等待队列、默认进行四种锁状态的升级。

ReentrantLock:需要手动枷锁,手动解锁、可以出现多个不同的等待队列、CIS的实现本章我们补一个小漏洞,它叫LockSupport,然后我们分析两道面试题,紧接着我会教大家阅读源码的技巧,源码层出不穷,生生不息,掌握了源码的阅读技巧,大家培养出了阅读源码兴趣的时候,之后好多代码,你需要自己去抠,抠出来才是你自己的,最后我们会分析AQS源码,以上是我们本章主讲的内容概述。

LockSupport

我们会以几个小程序为案例,展开对LockSupport的讲解,在以前我们要阻塞和唤醒某一个具体的线程有很多限制比如:

1、因为wait()方法需要释放锁,所以必须在synchronized中使用,否则会抛出异常
IllegalMonitorStateException

2、notify()方法也必须在synchronized中使用,并且应该指定对象

3、synchronized()、wait()、notify()对象必须一致,一个synchronized()代码块中只能有一个线程调用wait()或notify()以上诸多限制,体现出了很多的不足,所以LockSupport的好处就体现出来了。

在JDK1.6中的java.util.concurrent的子包locks中引了LockSupport这个API,LockSupport是一个比较底层的工具类,用来创建锁和其它同步工具类的基本线程阻塞原语。java锁和同步器框架的核心 AQS:


AbstractQueuedSynchronizer,就是通过调用 LockSupport .park()和 LockSupport .unpark()的方法,来实现线程的阻塞和唤醒的。我们先来看一个小程序:

public class T13_TestLockSupport {public static void main(String[] args) {//使用lombda表达式创建一个线程tThread t = new Thread(()->{for (int i = 0; i < 10; i++) {
System.out.println(i);if(i == 5) {//使用LockSupport的park()方法阻塞当前线程tLockSupport.park();
}try {//使当前线程t休眠1秒TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});//启动当前线程tt.start();
}
}

从以上的小程序中,我们不难看出LockSupport使用起来的是比较灵灵活的,没有了所谓的限制。我们来分析一下代码的执行过程,首先我们使用lombda表达式创建了线程对象 " t " ,然后通过 " t " 对象调用线程的启动方法start(),然后我们再看线程的内容,在for循环中,当 i 的值等于5的时候,我们调用了LockSupport的.park()方法使当前线程阻塞,注意看方法并没有加锁,就默认使当前线程阻塞了,由此可以看出LockSupprt.park()方法并没有加锁的限制。

我们再来看一个小程序:

public class T13_TestLockSupport {public static void main(String[] args) {//使用lombda表达式创建一个线程tThread t = new Thread(()->{for (int i = 0; i < 10; i++) {
System.out.println(i);if(i == 5) {//使用LockSupport的park()方法阻塞当前线程tLockSupport.park();
}try {//使当前线程t休眠1秒TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});//启动当前线程tt.start();//唤醒线程tLockSupport.unpark(t);
}
}

我们来分析一下以上小程序,我们只需要在第一个小程序的主线程中,调用LockSupport的unpark()方法,就可以唤醒某个具体的线程,这里我们指定了线程 " t " ,代码运行以后结果显而易见,线程并没有被阻塞,我们成功唤醒了线程 " t " ,在这里还有一点,需要我们来分析一下,在主线程中线程 " t " 调用了start()方法以后,因为紧接着执行了LockSupport的unpark()方法,所以也就是说,在线程 " t "还没有执行还没有被阻塞的时候,已经调用了LockSupport的unpark()方法来唤醒线程 " t " ,之后线程 " t"才启动调用了LockSupport的park()来使线程 " t " 阻塞,但是线程 " t " 并没有被阻塞,由此可以看出,LockSupport的unpark()方法可以先于LockSupport的park()方法执行。

我们再来看最后一个小程序:

public class T13_TestLockSupport {public static void main(String[] args) {//使用lombda表达式创建一个线程tThread t = new Thread(()->{for (int i = 0; i < 10; i++) {
System.out.println(i);if(i == 5) {//调用LockSupport的park()方法阻塞当前线程tLockSupport.park();
}if(i == 8){//调用LockSupport的park()方法阻塞当前线程tLockSupport.park();
}try {//使当前线程t休眠1秒TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});//启动当前线程tt.start();//唤醒线程tLockSupport.unpark(t);
}
}

我们来分析一下以上小程序,在第二个小程序的基础上又添加了一个if判断,在i等于8的时候再次调用LockSupport的park()方法来使线程 " t " 阻塞, 我们可以看到线程被阻塞了,原因是LockSupport的unpark()方法就像是获得了一个“令牌”,而LockSupport的park()方法就像是在识别“令牌”,当主线程调用了LockSupport.unpark(t)方法也就说明线程 " t " 已经获得了”令牌”,当线程 " t " 再调用LockSupport的park()方法时,线程 " t " 已经有令牌了,这样他就会马上再继续运行,也就不会被阻塞了,但是当i等于8的时候线程 " t " 再次调用了LockSupport的park()方法使线程再次进入阻塞状态,这个时候“令牌”已经被使用作废掉了,也就无法阻塞线程 " t " 了,而且如果主线程处于等待“令牌”状态时,线程 " t " 再次调用了LockSupport的park()方法,那么线程 " t "就会永远阻塞下去,即使调用unpark()方法也无法唤醒了。

由以上三个小程序我们可以总结得出以下几点:

1、LockSupport不需要synchornized加锁就可以实现线程的阻塞和唤醒

2、LockSupport.unpartk()可以先于LockSupport.park()执行,并且线程不会阻塞

3、如果一个线程处于等待状态,连续调用了两次park()方法,就会使该线程永远无法被唤醒

LockSupport中park()和unpark()方法的实现原理

park()和unpark()方法的实现是由Unsefa类提供的,而Unsefa类是由C和C++语言完成的,其实原理也是比较好理解的,它主要通过一个变量作为一个标识,变量值在0,1之间来回切换,当这个变量大于0的时候线程就获得了“令牌”,从这一点我们不难知道,其实park()和unpark()方法就是在改变这个变量的值,来达到线程的阻塞和唤醒的,具体实现不做赘述了。

淘宝面试题

面试题1

实现一个容器,提供两个方法add、size,写两个线程:

线程1,添加10个元素到容器中

线程2,实时监控元素个数,当个数到5个时,线程2给出提示并结束

我们根据小程序来剖析这个面试题,小程序1:

小程序1的执行流程:

通过内部的list来new一个ArrayList,在自定义的add方法直接调用list的add方法,在自定义的size方法直接调用list的size方法,想法很简单,首先小程序化了这个容器,接下来启动了t1线程,t1线程中做了一个循环,每次循环就添加一个对象,加一个就打印显示一下到第几个了,然后给了1秒的间隔,在t2线程中写了了一个while循环,实时监控着集合中对象数量的变化,如果数量达到5就结束t2线程。

小程序1的执行结果:

方法并没有按预期的执行,我们注意看t2线程中c.size()这个方法,当对象添加以后,ArraylList的size()方肯定是要更新的,我们分析一下,当t1线程中的size()方法要更新的时候,还没有更新t2线程就读了,这个时候t2线程读到的值就与实际当中加入的值不一致了,所以得出两结论,第一这个方案没有加同步,第二while(true)中的c.size()方法永远没有检测到,没有检测到的原因是线程与线程之间是不可见的

public class T01_WithoutVolatile {
List lists = new ArrayList();public void add(Object o) {
lists.add(o);
}public int size() {return lists.size();
}public static void main(String[] args) {
T01_WithoutVolatile c = new T01_WithoutVolatile();new Thread(() -> {for(int i=0; i<10; i++) {
c.add(new Object());
System.out.println("add " + i);try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();new Thread(() -> {while(true) {if(c.size() == 5) {javabreak;
}
}
System.out.println("t2 结束");
}, "t2").start();
}
}
小程序2public class T02_WithVolatile {volatile List lists = new LinkedList();public void add(Object o) {
lists.add(o);
}public int size() {return lists.size();
}public static void main(String[] args) {
T02_WithVolatile c = new T02_WithVolatile();new Thread(() -> {for(int i=0; i<10; i++) {
c.add(new Object());
System.out.println("add " + i);/*try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
}
}, "t1").start();new Thread(() -> {while(true) {if(c.size() == 5) {break;
}
}
System.out.println("t2 结束");
}, "t2").start();
}
}

小程序2是在小程序1的基础上做了一些改动,用volatile修饰了一下List集合,实现线程间信息的传递,但是还是有不足之处,程序还是无法运行成功,而且我们还得出,volatile一定要尽量去修饰普通的值,不要去修饰引用值,因为volatile修饰引用类型,这个引用对象指向的是另外一个new出来的对象对象,如果这个对象里边的成员变量的值改变了,是无法观察到的,所以小程序2也是不理想的。

小程序3:

public class T03_NotifyHoldingLock { //wait notify//添加volatile,使t2能够得到通知volatile List lists = new ArrayList();public void add(Object o) {
lists.add(o);
}public int size() {return lists.size();
}public static void main(String[] args) {
T03_NotifyHoldingLock c = new T03_NotifyHoldingLock();
final Object lock = new Object();//需要注意先启动t2再启动t1new Thread(() -> {
synchronized(lock) {
System.out.println("t2 启动");if(c.size() != 5) {try {lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 结束");
}
}, "t2").start();try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}new Thread(() -> {
System.out.println("t1 启动");
synchronized(lock) {for(int i=0; i<10; i++) {
c.add(new Object());
System.out.println("add " + i);if(c.size() == 5) {lock.notify();
}try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t1").start();
}
}

小程序3的执行流程:

小程序3用了锁的方式(利用wait()和notify()),通过给object对象枷锁然后调用wait()和notify()实现这道面试题,我们从头到尾分析一下,首先List集合实现的add和size方法不多做解释,我们把重点放在mian方法上,main方法里我们创建了object对象,然后写了两个线程t1和t2,t1用来增加对象,t2用来监控list集合添加的对象个数,在t2线程我们给object对象加锁,然后判断list集合对象的个数为5的时候,就调用wait()方法阻塞t2线程,并给出相应提示,t1线程里我们给object对象加锁,通过for循环来给list集合添加对象,当对象添加到5个的时候,唤醒t2线程来完成对象个数的监控,这里我们需要保证先启动的是第二个线程,让它直接进入监控状态,以完成实时监控。

小程序3的执行结果:

当试过了小程序3,我们会发现,这种写法也是行不通的,原因是notify()方法不释放锁,当t1线程调用了notify()方法后,并没有释放当前的锁,所以t1还是会执行下去,待到t1执行完毕,t2线程才会被唤醒接着执行,这个时候对象已经不只有5个了,所以这个方案也是行不通的。

小程序4public class T03_NotifyHoldingLock { //wait notify//添加volatile,使t2能够得到通知volatile List lists = new ArrayList();public void add(Object o) {
lists.add(o);
}public int size() {return lists.size();
}public static void main(String[] args) {
T03_NotifyHoldingLock c = new T03_NotifyHoldingLock();
final Object lock = new Object();//需要注意先启动t2再启动t1new Thread(() -> {
synchronized(lock) {
System.out.println("t2 启动");if(c.size() != 5) {try {lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 结束");
}//通知t1继续执行lock.notify()。
}, "t2").start();try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}new Thread(() -> {
System.out.println("t1 启动");
synchronized(lock) {for(int i=0; i<10; i++) {
c.add(new Object());
System.out.println("add " + i);if(c.size() == 5) {lock.notify();//释放锁,让t2得以执行try{lock.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t1").start();
}
}

小程序4是在小程序3的基础上做了一些小改动,我们来分析一下执行流程,首先t2线程执行,判断到list集合里的对象数量没有5个,t2线程被阻塞了,接下来t1线程开始执行,当循环添加了5个对象后,唤醒了t2线程,重点在于小程序3我们说过notify()方法是不会是释放锁的,所以在notify()以后,又紧接着调用了wait()方法阻塞了t1线程,实现了t2线程的实时监控,t2线程执行结束,打印出相应提示,最后调用notify()方法唤醒t1线程,让t1线程完成执行。看过执行结果,发现示例4完成了面试题的功能成功运行。

小程序5:

public class T05_CountDownLatch {//添加volatile,使t2能够得到通知volatile List lists = new ArrayList();public void add(Object o) {
lists.add(o);
}public int size() {return lists.size();
}public static void main(String[] args) {
T05_CountDownLatch c = new T05_CountDownLatch();//需要注意先启动t2再启动t1CountDownLatch latch = new CountDownLatch(1);new Thread(() -> {
System.out.println("t2 启动");if (c.size() != 5) {try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 结束");
}, "t2").start();try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}new Thread(() -> {
System.out.println("t1 启动");for (int i = 0; i < 10; i++) {
c.add(new Object());
System.out.println("add " + i);if (c.size() == 5) {// 暂停t1线程latch.countDown();
}/*try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
}
}, "t1").start();
}
}

小程序5我们用CountDownLatch ( 门闩 ) 来完成这一题的需求,我们来分析代码的执行流程,首先我们不难看出和小程序4的写法大同小异,同样是list集合实现add和size方法,两个线程t1和t2,t1线程里是循环添加对象,t2里是实时监控,不同点在于没有了锁,采用了await()方法替换了t2线程和t1线程中的wait()方法,执行流程是创建门闩对象latch,t2线程开始启动,判断到对象不等于5,调用await()方法阻塞t2线程,t1线程开始执行添加对象,当对象增加到5个时,打开门闩让t2继续执行。

执行结果看似没什么大问题,但是当我们把休眠1秒这段带代码,从t1线程里注释掉以后,会发现出错了,原因是在t1线程里,对象增加到5个时,t2线程的门闩确实被打开了,但是t1线程马上又会接着执行,之前是t1会休眠1秒,给t2线程执行时间,但当注释掉休眠1秒这段带代码,t2就没有机会去实时监控了,所以这种方案来使用门闩是不可行的。但是如果我们非得使用门闩,还要求在对象数量为5的时候把t2线程打印出来,如何实现呢?

小程序6:

public class T05_CountDownLatch {//添加volatile,使t2能够得到通知volatile List lists = new ArrayList();public void add(Object o) {
lists.add(o);
}public int size() {return lists.size();
}public static void main(String[] args) {
T05_CountDownLatch c = new T05_CountDownLatch();
CountDownLatch latch = new CountDownLatch(1);//需要注意先启动t2再启动t1new Thread(() -> {
System.out.println("t2 启动");if (c.size() != 5) {try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("t2 结束");
}, "t2").start();try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}new Thread(() -> {
System.out.println("t1 启动");for (int i = 0; i < 10; i++) {
c.add(new Object());System.out.println("add " + i);if (c.size() == 5) {//打开门闩,让t2得以执行latch.countDown();//给t1上门闩,让t2有机会执行try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
}
}

小程序6很容易理解,我们只需要在t1线程打开t2线程门闩的时候,让他再给自己加一个门闩就可以了。

小程序7:

public class T06_LockSupport {//添加volatile,使t2能够得到通知volatile List lists = new ArrayList();public void add(Object o) {
lists.add(o);
}public int size() {return lists.size();
}public static void main(String[] args) {
T06_LockSupport c = new T06_LockSupport();
CountDownLatch latch = new CountDownLatch(1);//需要注意先启动t2再启动t1Thread t2 = new Thread(() -> {
System.out.println("t2 启动");if (c.size() != 5) {
LockSupport.park();
}
System.out.println("t2 结束");
LockSupport.unpark(t1);
}, "t2");
t2.start();try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}new Thread(() -> {
System.out.println("t1 启动");for (int i = 0; i < 10; i++) {
c.add(new Object());
System.out.println("add " + i);if (c.size() == 5) {
LockSupport.unpark(t2);
LockSupport.park();
}/*try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
}
}, "t1").start();
}
}

小程序7采用了本章所学的LockSupport实现的,与之前的小程序也是大同小异,不同的只是改变了线程阻塞和唤醒所使用的方法,仔细观看小程序7就会明白,但是小程序7中其实也是有不足的,当注释掉t1线程中休眠1秒方法的时候,程序就出错了,原因是在t1线程调用unpark()方法唤醒t2线程的时候,t1线程并没有停止,就会造成t2线程无法及时的打印出提示信息,怎么解决呢?很简单,在t1线程调用unpark()方法唤醒t2线程的时候,紧接着调用park()方法使t1线程阻塞,然后在t2线程打印信息结束后调用unpark()方法唤醒t1线程,至此程序结束,具体实现,我们往下看。

小程序8

public class T07_LockSupport_WithoutSleep {// 添加volatile,使t2能够得到通知volatile List lists = new ArrayList();public void add(Object o) {
lists.add(o);
}public int size() {return lists.size();
}static Thread t1 = null, t2 = null;public static void main(String[] args) {
T06_LockSupport c = new T06_LockSupport();
CountDownLatch latch = new CountDownLatch(1);
t2 = new Thread(() -> {
System.out.println("t2 启动");if (c.size() != 5) {
LockSupport.park();
}
System.out.println("t2 结束");
LockSupport.unpark(t1);
}, "t2");try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
t1 = new Thread(() -> {
System.out.println("t1 启动");for (int i = 0; i < 10; i++) {
c.add(new Object());
System.out.println("add " + i);if (c.size() == 5) {
LockSupport.unpark(t2);
LockSupport.park();
}try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1");//需要注意先启动t2再启动t1t2.start();
t1.start();
}
}

我们来分析一下小程序8,首先看,我们在类的成员变量里定义了静态的线程对象t1和t2,然后在main方法里创建了t1线程和t2线程,t2线程中判断了list集合中对象的数量,然后t2线程阻塞,t1线程开始执行添加对象,对象达到5个时,打开t2线程阻塞t1线程,至此程序结束,运行成功。

小程序9:

public class T08_Semaphore {// 添加volatile,使t2能够得到通知volatile List lists = new ArrayList();public void add(Object o) {
lists.add(o);
}public int size() {return lists.size();
}static Thread t1 = null, t2 = null;public static void main(String[] args) {
T08_Semaphore c = new T08_Semaphore();
Semaphore s = new Semaphore(1);
t1 = new Thread(() -> {try {
s.acquire();for (int i = 0; i < 5; i++) {
c.add(new Object());
System.out.println("add " + i);
}
s.release();
} catch (InterruptedException e) {
e.printStackTrace();
}try {
t2.start();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}try {
s.acquire();for (int i = 5; i < 10; i++) {
c.add(new Object());
System.out.println("add"+i);
}
s.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1");
t2 = new Thread(() -> {try {
s.acquire();
System.out.println("t2 结束");
s.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2");
t1.start();
}
}

小程序9我们通过Semaphore来实现,大体的执行流程大体是这样的,创建一个Semaphore对象,设置只能有1一个线程可以运行,首先线程1开始启动,调用acquire()方法限制其他线程运行,在for循环添加了4个对象以后,调用s.release()表示其他线程可以运行,这个时候t1线程启动t2线程,调用join()把CPU的控制权交给t2线程,t2线程打印出提示信息,并继续输出后来的对象添加信息,当然了这个方案看起来很牵强,但是的确实现了这个效果,思路是好的,可以用做参考。

面试题1总结

面试题1中的9个小程序9种方案,5种技术分别是volatile、wait()和notify()、Semaphore、CountDownLatch、LockSupport,其中wait()和notify()这个方案建议牢牢掌握,其它的可以用作巩固技术。

面试题2

写一个固定容量同步容器,拥有put和get方法,以及getCount方法,能够支持2个生产者线程以及10个消费者线程的阻塞调用。

小程序1:

public class MyContainer1<T> {final private LinkedList<T> lists = new LinkedList<>();final private int MAX = 10; //最多10个元素private int count = 0;//生产者public synchronized void put(T t) {while(lists.size() == MAX) { //想想为什么用while而不是if?try {this.wait(); //effective java} catch (InterruptedException e) {
e.printStackTrace();
}
}
lists.add(t);
++count;this.notifyAll(); //֪ͨ

知消费者线程进行消费
}//消费者public synchronized T get() {
T t = null;while(lists.size() == 0) {try {this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
t = lists.removeFirst();
count --;this.notifyAll(); //通知生产者线程进行生产return t;
}public static void main(String[] args) {
MyContainer1<String> c = new MyContainer1<>();//启动消费者线程for(int i=0; i<10; i++) {new Thread(()->{for(int j=0; j<5; j++) System.out.println(c.get());
}, "c" + i).start();
}try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}//启动生产者线程for(int i=0; i<2; i++) {new Thread(()->{for(int j=0; j<25; j++) c.put(Thread.currentThread().getName() +" " + j);
}, "p" + i).start();
}
}
}

我们来说一下以上小程序的执行流程,我们创建了一个LinkedList集合用于保存 “馒头”,定义了MAX变量来限制馒头的总数,定义了count变量用来判断生产了几个 “馒头”和消费了几个 “馒头”,在put()方法中,首先判断 LinkedList集合中“馒头”是否是MAX变量的值,如果是启动所有消费者线程,反之开始生产 “馒头”,在get()方法中,首先判断是否还有 “馒头”,也就是MAX的值是否为0,如果为0通知所有生产者线程开始生产 “馒头”,反之不为0 “馒头”数就继续减少,需要注意的点是,我们为什么要加synchronized,因为我们++count我们生产了3个 “馒头”,当还没来得及加的时候,count值为2的时候,另外一个线程读到的值很可能是2,并不是3,所以不加锁就会出问题,我们接着来看main方法中通过for循环分别创建了2个生产者线程生产分别生产25 “馒头”,也就是50个馒头,10个消费者线程每个消费者消费5个 “馒头”,也就是50个 “馒头”,首先启动消费者线程,然后启动生产者线程,至此流程介绍完毕。

我们来分析一下这个小程序

为什么用while而不是用if?因为当LinkedList集合中“馒头”数等于最大值的时候,if在判断了集合的大小等于MAX的时候,调用了wait()方法以后,它不会再去判断一次,方法会继续往下运行,假如在你wait()以后,另一个方法又添加了一个“ 馒头”,你没有再次判断,就又添加了一次,造成数据错误,就会出问题,因此必须用while。

注意看我们用的是notifyAll()来唤醒线程的,notifyAll()方法会叫醒等待队列的所有方法,那么我们都知道,用了锁以后就只有一个线程在运行,其他线程都得wait(),不管你有多少个线程,这个时候被叫醒的线程有消费者的线程和生产者的线程,所有的线程都会争抢这把锁,比如说我们是生产者线程,生产满了,满了以后我们叫醒消费者线程,可是很不幸的是,它同样的也会叫醒另外一个生产者线程,假如这个生产者线程难道了这把锁刚才第一个生产者释放的这把锁,拿到了以后,它又wait()一遍,wait()完以后,又叫醒全部的线程,然后又开始争抢这把锁,其实从这个意义上来讲,生产者的线程wait的你是没有必要去叫醒别的生产者的,我们能不能只叫醒消费者线程,就是生产者线程只叫醒消费者线程,消费者线程只负责叫醒生产者线程,如果想达到这样一个程度的话用另外一个小程序。

public class MyContainer2<T> {final private LinkedList<T> lists = new LinkedList<>();final private int MAX = 10; //最多10个元素private int count = 0;private Lock lock = new ReentrantLock();private Condition producer = lock.newCondition();private Condition consumer = lock.newCondition();public void put(T t) {try {
lock.lock();while(lists.size() == MAX) { //想想为什么用while而不是用if?producer.await();
}
lists.add(t);
++count;
consumer.signalAll(); //通知消费者线程进行消费} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}public T get() {
T t = null;try {
lock.lock();while(lists.size() == 0) {
consumer.await();
}
t = lists.removeFirst();
count --;
producer.signalAll(); //通知生产者进行生产} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}return t;
}public static void main(String[] args) {
MyContainer2<String> c = new MyContainer2<>();//启动消费者线程for(int i=0; i<10; i++) {new Thread(()->{for(int j=0; j<5; j++) System.out.println(c.get());
}, "c" + i).start();
}try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}//启动生产者线程for(int i=0; i<2; i++) {new Thread(()->{for(int j=0; j<25; j++) c.put(Thread.currentThread().getName() +" " + j);
}, "p" + i).start();
}
}
}

上面这个小程序用了ReentrantLock,它与synchronized最大区别其实在这个面试题里已经体现出来了,ReentrantLock它可以有两种Condition条件,在put()方法里是我们的生产者线程,生产者线程lock()最后unlock()不多说,一旦MAX达到峰值的时候是producer.await(),最后是consumer.signalAll(),就是说我在producer的情况下阻塞的,我叫醒的时候只叫醒consumer,在get()方法里是我们的消费者线程,一旦集合的size空了,我是consumer.await(),然后我只叫醒producer,这就是ReentrantLock的含义,它能够精确的指定哪些线程被叫醒,注意是哪些不是哪个,我们来说一下Lock和Condition的本质是什么,它的本质是在synchronized里调用wait()和notify()的时候,它只有一个等待队列,如果lock.newnewCondition()的时候,就变成了多个等待队列,Condition的本质就是等待队列个数,以前只有一个等待队列,现在我new了两个Condition,一个叫producer一个等待队列出来了,另一个叫consumer第二个的等待队列出来了,当我们使用producer.await();的时候,指的是当前线程进入producer的等待队列,使用producer.signalAll()指的是唤醒producer这个等待队列的线程,consumner也是如此,所以上面的小程序就很容易理解了,我在生产者线程里叫醒consumer等待队列的线程也就是消费者线程,在消费者线程里叫醒producer待队列的线程也就是生产者线程,这个小程序的思路就是这样了。

源码阅读技巧和AQS源码结构解析

我们会通过学习ReentrantLock来学习如何阅读源码,在之前的小程序,我们已经了解过ReentrantLock了,就是创建ReentrantLock对象,调用对象的方法lock()和unlock()方法来加锁解锁,那么我们就来了解一下,在源码中是如何实现这些的,首先,要承认一点,读源码很难,因为你必须要理解别的思路,你要了解理解自己思路,或者理解跟你类似的人的思路,这件事是比较容易的,想法都是一样的,都是住在农村,从小在小河边光着屁股长大,我们聊河边那些柳树的问题,聊那些小虫子的问题,他一聊我就能理解,可惜很不幸的是那边是一白富美,人住在天宫,每天都用各种的口红,用一些我们没用过的,他一聊这些色号的时候,我就跟她聊不到一起去了,现在写程序的这些人就像住在天宫的人,而你就是小河边光屁股的小孩,你想理解的她的思路,你得先让你的思维达到她那个高度再说,读源码不管你读什么源码,基本上第一个要有一定的数据结构基础 ,第二个要有设计模式基础,其次是不要吹毛求疵,阅读源码贵在读懂别人的思路就行,没必要边边角角的都要去看,当你达到手中无剑,心中有剑的境界的时候,你会发现这些东西都是互通的,概念是人类发明出来帮助大家理解问题的,如果死抠概念,这样做是不对的,理解就好

阅读源码的原则

1、跑不起来的不读

跑不起来的源码不要读,看也看不懂,很难看懂,事倍功半,读起来还费劲,什么时候这个源码必须得跑起来,跑起来有什么好处就是,你可以用debug一条线跟进去,举个例子,比如ReentrantLock的lock()方法,没有跑起来的时候,静态的来读源码你会怎么读,按ctrl鼠标单击lock()方法,进入这个方法,会看到这个方法调用了别的方法,你又会按ctrl鼠标单击进入它调用的这个方法,一层层往下,你会发现没法读了,所以如果这个东西能跑起来就不一样了,你会发现与之前鼠标单击跟进的结果不一样了,原因是因为多态的实现,如果一个方法有很多子类的实现,但是你不知道跟着这条线,它会去实现那个方法,所以你就得全部看一遍。

2、解决问题就好 — 目的性

在实际中解决问题就好,读源码一方面可以解决问题,一方面可以应对面试,什么意思呢,如果你接手了一个别人改过6手的代码,现在你的老板说这个代码有些问题,你往里边加一些功能或者修改一些bug,你解决问题就好,你不要从头到尾去读去改这个代码,你读你能累死你,目的性要强,解决问题就好。

3、一条线索到底

读源码的时候要一条线索到底,不要只读表面,我们知道一个程序跑起来以后,可能这个程序非常大,一个main方法有很多的put、get、size各种各样其他的方法,每一个方法你调进去,这个方法很有可能又去调别的方法,你不要每个方法先看一遍表面,然后再去里边找,要一条线索到底,就读一个方法,由浅到深看一遍。

4、无关细节略过

有那些边界性的东西,在你读第一遍的时候,没必要的时候,你可以先把它略过。

读源码的时候我建议大家自己去画图比如:


这种图叫甬道图,这是UML图的一种叫甬道,这个图诠释了哪个类里的哪个方法又去调用了哪个类里的的哪个方法,你读源码的时候,你一定要把它画成图给画出来,其次是授人以鱼不如授人以渔,把源码从头到尾带着你那样进步不会太大 ,我把方法教给你,远比带你要好的多。

从上面的甬道图可以看出,ReentrantLock调用它lock()方法的时候,它会调用acquire(1)方法,谁的acquire?是Sync的acquire,这很容易理解,在我们的lock方法里它调用了内部的一个类,这个类叫NonfairSync,NonfairSync它里边有个方法叫acquire(1),当你读到这里的时候,实际上你要开始分支了,你要知道这个Sync是个什么样的类,大概扫一眼,前面说过要顺着线读进去,然后你再看一下这个acquire(1)方法它内部是怎么调用的,这个Sync是谁呢?它是一个NonfairSync,我们找到这个NonfairSync这个时候你再画一张图


这个图是说这个NonfairSync的父类是Sync,因为你看到NonfairSync子类里方法的时候,它有可能用到父类的方法,所以你要去父类里读才可以,这个图一定要画好,NonfairSync的父类是Sync,好这个时候再回到Sync,这个时候我们看一下,这个Sync的父类又是谁?是AQS(
AbstarctQueuedSynchronizer),这个时候说一下AQS是所有锁的核心,我们继续lock调用了

acquire(1)这个方法,是谁的acquire(1)?是NonfairSync的acquire(1),NonfairSync又是谁?他是

Sync的子类,Sync又是谁,Sync是AQS的子类,所以调用了acquire(1)我们再跟进去,这个时候就调用了AQS的acquire(1)了,AQS里面调用的是什么呢?是tryAcquire(1),再跟进去你会发现这次调用的是NonfairSync里的tryAcquire(1),刚才我们读的时候已经知道AQS里有一个tryAcquire(1),但是它里面是抛出了一个异常,所以很容易理解,是NonfairSync里边重写了AQS里的tryAcquire(1),所以AQS里的acquire(1)调用了NonfairSync里的tryAcquire(1),我们再来看NonfairSync里的tryAcquire(1)又调用了nonfairTryAcquire(acquires),我们再跟进这个时候读到这里,我们就必须要了解AQS了,如果不懂,就没办法继续进行下去了,我们看nonfairTryAcquire(acquires)方法实现,开始它得到了当前线程,跟线程有关系了,这个时候出现了一个方法getState(),我们来看一下这个方法,我们跟进你会发现,这个方法又进到了AQS类里,这个getState()方法返回了一个state,这个state是什么呢?按着ctrl点过去,你会发现这个state就是一个volatile修饰的int类型的数,这个时候就牵扯到AQS的结构了。

AQS源码解析


AQS队列又可以称为CLH队列,AQS的核心是什么?

就是这个state,这个state所代表的意思随你定,随子类来定,我们现在讲的是ReentrantLock,刚才state的值是0,当你获得了之后它会变成1,就表示当前线程得到了这把锁,什么时候你释放完了,state又会从1变回0,说明当前线程释放了这把锁,所以这个state0和1就代表了加锁和解锁,所以这个state的值是根据你子类不同的实现取不同的意义,这个state的值的基础之上,它的下面跟着一个队列,这个队列是AQS自己内部所维护的队列,这个队列里边每一个所维护的都是node一个节点,它在哪里呢?

它在AQS这个类里属于AQS的内部类,在这个node里最重要的一项是他里面保留了一个Thread一个线程,所以这个队列是个线程队列,而且还有两个prev和next分别是前面的节点和后面的节点,所以AQS里边的队列是这样子的,一个一个的node,node里装的是线程Thread,这个node它可以指向前面的这一个,也可以指向后面的这一个,所以叫双向列表,所以AQS的核心是一个state,以及监控这个state的双向列表,每个列表里面有个节点,这个节点里边装的是线程,那么那个线程得到了state这把锁,那个线程要等待,都要进入这个队列里边,当我们其中一个node得到了state这把锁,就说明这个node里得线程持有这把锁,所以当我们acquire(1)上来以后看到这个state的值是0,那我就直接拿到state这个把锁,现在是非公平上来就抢,抢不着就进队列里acquireQueued(),怎么是抢到呢?先得到当前线程,然后获取state的值,如果state的值等于0,用compareAndSetState(0,acquire)方法尝试把state的值改为1,假如改成了setExclusiveOwnerThread()把当前线程设置为独占statie这个把锁的状态,说明我已经得到这个把锁,而且这个把锁是互斥的,我得到以后,别人是得不到的,因为别人再来的时候这个state的值已经变成1了,如果说当前线程已经是独占state这把锁了,就往后加个1就表示可重入了。

什么叫模板方法?

模板方法是我在父类里有一个方法,就是这个tryAcquire(1),它调用了我子类的一些方法,这些子类的方法没有实现,我调用自己的方法事先写好,但是由于这些方法就是给子类用来去重写实现的,所以我就像一个模板一样,我要打造一辆汽车,我要造地盘,造发动机,造车身,最后组装,造地盘这件事子类去实现,哪个车厂去实现是哪个车厂的事,奥迪是奥迪的事,奥托是奥托的事,车身也是发动机也是,最后反正这个流程是一样的,就像一个模板一样,我的模板是固定的,里边的方法是由具体的子类去实现的,当我们开始造车的时候,就会调用具体子类去实现的函数,所以叫为钩子函数,我勾着谁呢?勾着子类的实现,这个就叫模板方法。

什么叫非公平的去获得(nonfairTryAcquire)?

什么叫公平什么叫非公平,当我们获得一把锁的时候,有一些等待队列,如果说新来了一个线程要获得这个锁的时候,先去检查等待队列有没有人,如果有后面排队,这个叫公平,上来二话不说,我才不管你的队列有没有人在等着,我上来就抢,抢着就算我的,插队非公平。

最后我们来总结一下阅读源码的原则,跑不起来的不读、解决问题就好、一条线索到底、无关细节略过,在你阅读的过程中,我希望你画两种图,第一种方法之间的调用图,哪个方法调用了哪个方法,第二种类之间的类图,读源码重点在于你自己去读,所以强烈建议,边读边画图,这样会更深刻。

今天给大家分享的内容是LockSupport、淘宝面试题与源码阅读方法论,
明天给大家分享AQS源码阅读与强软弱虚4种引用以及ThreadLocal原理与源码的内容。

喜欢的朋友可以转发关注一下~~~


本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。

浏览 29
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报