目录
线程按顺序执行
面试题:两个线程,一个线程打印1-52,另一个打印字母A-Z打印顺序为12A34B...5152Z,要求用线程间通信
1、简化问题:两个线程操作一个初始值为0的变量,实现一个线程对变量增加1,一个线程对变量减少1,交替10轮。
2、实现方案-线程间通信模型:
生产者+消费者
通知等待唤醒机制 (wait、notify)
3、多线程编程模板中:线程 操作 资源类;判断、干活、通知;高内聚低耦合
- // 实现一个线程对该变量加1,一个线程对该变量减1, 交替 10 轮!
- class ShareDataOne{
- private Integer number = 0;
- //加一
- public synchronized void incre() {
- try {
- //1.判断
- if (number!=0){
- this.wait();
- }
- //2.干活
- number++;
- System.out.println(Thread.currentThread().getName()+":"+number);
- //3.通知
- this.notifyAll();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- //减一
- public synchronized void decre(){
- try {
- //1.判断
- if (number!=1){
- this.wait();
- }
- //2.干活
- number--;
- System.out.println(Thread.currentThread().getName()+":"+number);
- //3.通知
- this.notifyAll();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
- /*
- * 现在两个线程,
- * 可以操作初始值为零的一个变量,
- * 实现一个线程对该变量加1,一个线程对该变量减1,
- * 交替,来10轮。
- *
- * 笔记:Java里面如何进行工程级别的多线程编写
- * 1 多线程变成模板(套路)-----上
- * 1.1 线程 操作 资源类
- * 1.2 高内聚 低耦合
- * 2 多线程变成模板(套路)-----中
- * 2.1 判断
- * 2.2 干活
- * 2.3 通知
- */
- public class NotifyWaitDemo {
- public static void main(String[] args) {
- ShareDataOne shareDataOne = new ShareDataOne();
- new Thread(()->{
- for (int i = 0; i < 10; i++) {
- shareDataOne.incre();
- }
- },"AAA").start();
-
- new Thread(()->{
- for (int i = 0; i < 10; i++) {
- shareDataOne.decre();
- }
- },"BBB").start();
- }
- }
换成4个线程会导致错误,虚假唤醒问题

原因:在java多线程判断时,不能用if,程序出事出在了判断上面。
注意,消费者被唤醒后是从wait()方法(被阻塞的地方)后面执行,而不是重新从同步块开头,没有进行条件判断。




对标实现


- class ShareDataOne{
- private Integer number = 0;
- Lock lock = new ReentrantLock(); // 初始化lock锁
- Condition cd = lock.newCondition(); // 初始化condition对象
- //加一
- public void incre() {
- lock.lock();
- try {
- //1.判断
- while (number!=0){
- //this.wait();
- cd.await(); //睡眠
- }
- //2.干活
- number++;
- System.out.println(Thread.currentThread().getName() + ":" + number);
- //3.通知
- //this.notifyAll();
- cd.signalAll(); //唤醒
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- lock.unlock();
- }
- }
- //减一
- public void decre(){
- lock.lock();
- try {
- //1.判断
- while (number!=1){
- //this.wait();
- cd.await(); //睡眠
- }
- //2.干活
- number--;
- System.out.println(Thread.currentThread().getName() + ":" + number);
- //3.通知
- //this.notifyAll();
- cd.signalAll(); //唤醒
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- lock.unlock();
- }
- }
- }
案例:
多线程之间按顺序调用,实现A->B->C。三个线程启动,要求如下:
AA打印5次,BB打印10次,CC打印15次;
接着
AA打印5次,BB打印10次,CC打印15次;
。。。打印10轮
分析实现方式:
- 有一个锁Lock,3把钥匙Condition
- 有顺序通知(切换线程),需要有标识位
- 判断标志位
- 输出线程名 + 内容
- 修改标识符,通知下一个
- /*多线程之间按顺序调用,实现A->B->C。三个线程启动,要求如下:
- AA打印5次,BB打印10次,CC打印15次;
- 接着
- AA打印5次,BB打印10次,CC打印15次;
- 。。。打印10轮*/
- // 资源类:
- class ShareDataTwo{
- // 声明一些变量
- private int flag = 1; // 【flag=1 AA flag=2 BB flag=3 CC】
- private Lock lock = new ReentrantLock();
- private Condition c1 = lock.newCondition();
- private Condition c2 = lock.newCondition();
- private Condition c3 = lock.newCondition();
-
- // 定义打印方法!
- public void print5(int total){
- lock.lock(); //上锁
- try {
- while (flag!=1){ //判断
- c1.await();
- }
- //干活:循环打印
- for (int i = 0; i <= 5; i++) {
- System.out.println(Thread.currentThread().getName() +
- "\t"+i+"\t第"+total+"轮");
- }
- //通知,修改标识位
- flag=2;
- c2.signal();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- lock.unlock(); //解锁
- }
- }
-
- public void print10(int total){
- lock.lock(); //上锁
- try {
- while (flag!=2){ //判断
- c2.await();
- }
- //干活:循环打印
- for (int i = 0; i <= 10; i++) {
- System.out.println(Thread.currentThread().getName() +
- "\t"+i+"\t第"+total+"轮");
- }
- //通知,修改标识位
- flag=3;
- c3.signal();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- lock.unlock(); //解锁
- }
- }
-
- public void print15(int total){
- lock.lock(); //上锁
- try {
- while (flag!=3){ //判断
- c3.await();
- }
- //干活:循环打印
- for (int i = 0; i <= 15; i++) {
- System.out.println(Thread.currentThread().getName() +
- "\t"+i+"\t第"+total+"轮");
- }
- //通知,修改标识位
- flag=1;
- c1.signal();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- lock.unlock();//解锁
- }
- }
- }
- public class ThreadOrderAccess {
- public static void main(String[] args) {
- // 线程操作资源类
- ShareDataTwo shareDataTwo = new ShareDataTwo();
- // 线程:
- new Thread(()->{
- for (int i = 0; i < 10; i++) {
- shareDataTwo.print5(i+1);
- }
- },"AA").start();
-
- new Thread(()->{
- for (int i = 0; i < 10; i++) {
- shareDataTwo.print10(i+1);
- }
- },"BB").start();
-
- new Thread(()->{
- for (int i = 0; i < 10; i++) {
- shareDataTwo.print15(i+1);
- }
- },"CC").start();
- }
- }
面试题:
- //面试题:两个线程,一个线程打印1-52,另一个打印字母A-Z打印顺序为12A34B...5152Z,要求用线程间通信
- class ShareDataThree{
-
- private int flag = 1;
- private Integer number = 1;
- private char letter = 'A';
- private Lock lock = new ReentrantLock();
- Condition c1 = lock.newCondition();
- Condition c2 = lock.newCondition();
-
- public void printNum(){
- lock.lock(); //上锁
- try {
- //判断
- while (flag!=1){
- c1.await(); //睡眠
- }
- //干活
- System.out.print(number++);
- System.out.print(number++);
- //通知 修改标识,唤醒
- flag = 2;
- c2.signal();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- lock.unlock(); //解锁
- }
- }
-
- public void printLetter(){
- lock.lock(); //上锁
- try {
- //判断
- while (flag!=2){
- c2.await(); //睡眠
- }
- //干活
- System.out.print(letter++ + " ");
- //通知 修改标识,唤醒
- flag = 1;
- c1.signal();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } finally {
- lock.unlock(); //解锁
- }
- }
- }
-
- public class Test {
- public static void main(String[] args) {
- ShareDataThree shareDataThree = new ShareDataThree();
- new Thread(()->{
- for (int i = 0; i < 26; i++) {
- shareDataThree.printNum();
- }
- },"num").start();
- new Thread(()->{
- for (int i = 0; i < 26; i++) {
- shareDataThree.printLetter();
- }
- },"let").start();
- }
- }
面试题:请举例说明集合类是不安全的。
方法一:代码验证
public class NotSafeDemo { public static void main(String[] args) { //List //new Vector(); 效率太低 //Collections.synchronizedList(new ArrayList<>()); 效率太低 List for (int i = 0; i < 20; i++) { new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,8)); System.out.println(list);//读取遍历数据时数据被修改,迭代器报错 },String.valueOf(i)).start(); } } }出现并发修改异常
方法二:看源码
Redis 和 Memcached 区别?
所有读的线程可以并发去读(共享锁),所有写的线程可以单独去写(独占锁)。
CopyOnWrite容器(简称COW容器)即写时拷贝的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
CopyOnWriteArraySet、CopyOnWriteArrayList; ConcurrentHashMap
优点:保证高效的读取(共享),安全的写入(独占) (用于读多写少的并发场景:搜索框黑名单)
缺点:占用内存难问题,只能保证最终一致;
源码片段:
扩展:HashSet是HashMap 的Kay部分
JUC的多线程辅助类非常多,这里我们介绍三个:
CountDownLatch是一个非常实用的多线程控制工具类,应用非常广泛。
例如:在手机上安装一个应用程序,假如需要5个子进程检查服务授权,那么主进程会维护一个计数器,初始计数就是5。用户每同意一个授权该计数器减1,当计数减为0时,主进程才启动,否则就只有阻塞等待了。
CountDownLatch中count down是倒数的意思,latch则是门闩的含义。整体含义可以理解为倒数的门栓,似乎有一点“三二一,芝麻开门”的感觉。CountDownLatch的作用也是如此。
- new CountDownLatch(int count) //实例化一个倒计数器,count指定初始计数
- countDown() // 每调用一次,计数减一
- await() //等待,当计数减到0时,阻塞线程(可以是一个,也可以是多个)并行执行
案例:6个同学陆续离开教室后值班同学才可以关门。
- //案例:6个同学陆续离开教室后班长才可以关门。
- public class CountDownLatchDemo {
- public static void main(String[] args) throws InterruptedException {
-
- CountDownLatch cd = new CountDownLatch(6);
-
- for (int i = 1; i <= 6; i++) {
- new Thread(()->{
- try {
- // 每个同学墨迹几秒钟
- TimeUnit.SECONDS.sleep(new Random().nextInt(5));
- System.out.println(Thread.currentThread().getName() + " 同学出门了");
- //计数器减一
- cd.countDown();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- },String.valueOf(i)).start();
- }
- //计数器不为0一致阻塞
- cd.await();
- System.out.println("班长锁门了");
- }
- }
面试:CountDownLatch 与 join 方法的区别
调用一个子线程的 join()方法后,该线程会一直被阻塞直到该线程运行完毕。而 CountDownLatch 则使用计数器允许子线程运行完毕或者运行中时候递减计数,也就是 CountDownLatch 可以在子线程运行任何时候让 await 方法返回而不一定必须等到线程结束;另外使用线程池来管理线程时候一般都是直接添加 Runnable 到线程池这时候就没有办法在调用线程的 join 方法了,countDownLatch 相比 Join 方法让我们对线程同步有更灵活的控制。
练习:秦灭六国,一统华夏。(模仿课堂案例,练习枚举类的使用)
CyclicBarrier 的中文意思是“循环栅栏”,可循环利用的屏障。该命令只在每个屏障点运行一次。若在所有参与线程之前更新共享状态,此屏障操作很有用
常用方法:
注意:所有的"过关了"都是由最后到达await方法的线程执行打印的
- public class CyclicBarrierDemo {
- //集齐七颗龙珠召唤神龙
- private static void test() {
- //完成的线程数,要做的事
- CyclicBarrier cb = new CyclicBarrier(7,()->{
- System.out.println("集齐七颗龙珠召唤神龙");
- });
- for (int i = 1; i <= 7; i++) {
- new Thread(()->{
- try {
- System.out.println(Thread.currentThread().getName()+"星龙珠被收集");
- //完成任务阻塞
- cb.await();
- } catch (InterruptedException e) {
- e.printStackTrace();
- } catch (BrokenBarrierException e) {
- e.printStackTrace();
- }
- },String.valueOf(i)).start();
- }
- }
-
- public static void main(String[] args) {
- //test();
- //组队打Boss
- CyclicBarrier cyclicBarrier = new CyclicBarrier(3, () -> {
-
- System.out.println(Thread.currentThread().getName() + " 过关了");
- });
-
- for (int i = 0; i < 3; i++) {
- new Thread(()->{
- try {
- System.out.println(Thread.currentThread().getName() + " 开始第一关");
- TimeUnit.SECONDS.sleep(new Random().nextInt(4));
- System.out.println(Thread.currentThread().getName() + " 开始打boss");
- cyclicBarrier.await();
-
- System.out.println(Thread.currentThread().getName() + " 开始第二关");
- TimeUnit.SECONDS.sleep(new Random().nextInt(4));
- System.out.println(Thread.currentThread().getName() + " 开始打boss");
- cyclicBarrier.await();
-
- System.out.println(Thread.currentThread().getName() + " 开始第三关");
- TimeUnit.SECONDS.sleep(new Random().nextInt(4));
- System.out.println(Thread.currentThread().getName() + " 开始打boss");
- cyclicBarrier.await();
-
- } catch (Exception e) {
- e.printStackTrace();
- }
- }, String.valueOf(i)).start();
- }
-
- }
- }
面试:CyclicBarrier和CountDownLatch的区别?
CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置,可以使用多次,所以CyclicBarrier能够处理更为复杂的场景;CountDownLatch允许一个或多个线程等待一组事件的产生,而CyclicBarrier用于等待其他线程运行到栅栏位置。
CyclicBarrier可以处理更复杂的业务。
Semaphore翻译成字面意思为 信号量,Semaphore可以控制同时访问的线程个数。非常适合需求量大,而资源又很紧张的情况。比如给定一个资源数目有限的资源池,假设资源数目为N,每一个线程均可获取一个资源,但是当资源分配完毕时,后来线程需要阻塞等待,直到前面已持有资源的线程释放资源之后才能继续。
信号量主要用于两个目的:
1. 多个共享资源的互斥使用。
2. 用于并发线程数的控制。保护一个关键部分不要一次输入超过N个线程。
类比场景:停车场、YY软件
- public Semaphore(int permits) // 构造方法,permits指资源数目(信号量)
- public void acquire() throws InterruptedException // 占用资源,当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时。
- public void release() // (释放)实际上会将信号量的值加1,然后唤醒等待的线程。
6辆汽车抢占3个停车位:
- //6辆汽车抢占3个停车位
- public class SemaphoreDemo {
-
- public static void main(String[] args) {
- // 初始化信号量,3个车位
- Semaphore sp = new Semaphore(3);
-
- // 6个线程,模拟6辆车
- for (int i = 0; i < 6; i++) {
- new Thread(()->{
- try {
- // 抢占一个停车位
- sp.acquire();
- System.out.println(Thread.currentThread().getName() + " 抢到了一个停车位!!");
- // 停一会儿车
- TimeUnit.SECONDS.sleep(new Random().nextInt(10));
- System.out.println(Thread.currentThread().getName() + " 离开停车位!!");
- // 开走,释放一个停车位
- sp.release();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }, String.valueOf(i)).start();
- }
- }
- }
Callable 是 Runnable 的增强
Thread类、Runnable接口使得多线程编程简单直接。
从java5开始,提供了Callable接口,是Runable接口的增强版。用Call()方法作为线程的执行体,增强了之前的run()方法。因为call方法可以有返回值,也可以声明抛出异常。
Callable 和 Runnable 区别:1.有返回值(泛型); 2.可以抛异常; 3.方法名不同
- //Callable 和 Runnable 区别
- public class MyRunnable implements Runnable{
- @Override
- public void run() {
- System.out.println("MyRunnable");
- }
- }
- //1.有返回值(泛型) 2.可以抛异常 3.方法名不同
- class MyCallable implements Callable
{ - @Override
- public Integer call() throws Exception {
- System.out.println(Thread.currentThread().getName()+"Callable");
- return null;
- }
- }
使用Callable (查看API文档)
java1.5之后才有的juc,所以Thread方法并无法兼容

- public class CallableDemo{
- public static void main(String[] args) {
- //1.直接替换 不行
- //new Thread(new MyCallable(),"zhang3").start();
- //2.通过中间类 FutureTask
- FutureTask futureTask1 = new FutureTask<>(new MyCallable());
- new Thread(futureTask1).start();
-
- }
- }
FutureTask:未来的任务,用它就干一件事,异步调用。通常用它解决耗时任务,挂起堵塞问题。 在主线流程最后获取结果。
建议:1. 为了防止主线业务阻塞,在整个主线业务流程之后去get结果。
2. 只计算一次,FutureTask会复用之前计算过的结果
去API文档:查找中间类
注意:
V | get()如有必要,等待计算完成,然后获取其结果。 |
V | get(long timeout, TimeUnit unit)如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。 |
boolean | isDone()如果任务已完成,则返回 true。 |
boolean | cancel(boolean mayInterruptIfRunning)试图取消对此任务的执行。 |
实例:
- public class CallableDemo{
- public static void main(String[] args) throws ExecutionException, InterruptedException {
- //1.直接替换 不行
- //new Thread(new MyCallable(),"zhang3").start();
- //2.通过中间类 FutureTask未来任务
- FutureTask
futureTask1 = new FutureTask<>(new MyCallable()); - FutureTask
futureTask2 = new FutureTask<>(()->{ - System.out.println(Thread.currentThread().getName()+"Callable");
- return 1024;
- });
- new Thread(futureTask1,"zhang3").start();
- //new Thread(futureTask2,"li4").start();
-
- //isDone: 判断任务是否完成
- while (!futureTask1.isDone()) {
- System.out.println("wait...");
- }
-
- //为了防止主线程阻塞,建议get方法放到最后
- System.out.println(futureTask1.get());
- //System.out.println(futureTask2.get());
- }
- }
-
面试题:callable接口与runnable接口的区别?
相同点:都是接口,都可以编写多线程程序,都采用Thread.start()启动线程
不同点:
- 具体方法不同:一个是run,一个是call
- Runnable没有返回值;Callable可以返回执行结果,是个泛型
- Callable接口的call()方法允许抛出异常;Runnable的run()方法异常只能在内部消化,不能往上继续抛
- 它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果 (FutureTask)。
面试题:获得多线程的方法几种?
传统的是继承thread类和实现runnable接口
java5以后又有实现callable接口和java的线程池获得