Java多线程
一、介绍
在了解线程之前,还需要简单了解进程的概念。简单的来说就是一心多用
在生活之中,我们常常可以一心多用。我可以一边打游戏,一边放着音乐听听歌,甚至可以再泡个脚。没错,这也可以理解成我的多线程生活。
而在计算机之中,也有以上同时进行的任务,这就可以叫做多线程,例如
所以总结来看,进程是一个应用运行的过程,可以包含多个线程运行,但至少必须要有一个线程,这样才能撑得起这是个进程。
线程是cpu对某个资源的调度计算的通道,这条通道下,cpu可以执行某些任务的调度。
在java中,我们从Main方法运行,所以称其为主线程
除了主线程外,java还有一个后台线程在默默地工作着,这就是GC线程,也就是垃圾回收所处的线程
二、Java线程的实现
1)继承Thread
类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| package com.banmoon.mode;
public class ExtendsMode{
public static void main(String[] args) { ExtendsModeA modeA = new ExtendsModeA(); ExtendsModeB modeB = new ExtendsModeB(); modeA.start(); modeB.start(); for (int i = 0; i < 1000; i++) System.out.println("=========== 主线程 ==========="); } }
class ExtendsModeA extends Thread{ @Override public void run() { for (int i = 0; i < 1000; i++) { System.out.println("=========== 线程A ==========="); } } }
class ExtendsModeB extends Thread{ @Override public void run() { for (int i = 0; i < 1000; i++) { System.out.println("=========== 线程B ==========="); } } }
|
启动后就会发现,原本应该最后打印的主线程,居然夹杂在线程A和线程B之中了。
这也就是说这几条线程是交替执行的,计算机实际上不能做到真正的并发,但它的线程之间的切换人为感知不出来,所以就给人一种并发的错觉。
那一条线程优先执行,这和CPU的调度有关,后续会讲到。
2)实现Runnable
接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| package com.banmoon.mode;
public class RunnableMode {
public static void main(String[] args) { Thread modeA = new Thread(new RunnableModeA()); Thread modeB = new Thread(new RunnableModeB()); modeA.start(); modeB.start(); for (int i = 0; i < 1000; i++) System.out.println("=========== 主线程 ==========="); } }
class RunnableModeA implements Runnable{ @Override public void run() { for (int i = 0; i < 1000; i++) { System.out.println("=========== 线程A ==========="); } } }
class RunnableModeB implements Runnable{ @Override public void run() { for (int i = 0; i < 1000; i++) { System.out.println("=========== 线程B ==========="); } } }
|
执行后效果与上方一致,打印的信息都是穿插打印的
由于Java只支持单继承,为了使得线程实现更具有灵活性,推荐使用Runnable接口方式
此外,Runnable还有Lanmbda
的简写方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package com.banmoon.mode;
public class RunnableModeByLambda {
public static void main(String[] args) { new Thread(() -> { for (int i = 0; i < 1000; i++) System.out.println("=========== 线程A ==========="); }).start(); new Thread(() -> { for (int i = 0; i < 1000; i++) System.out.println("=========== 线程B ==========="); }).start(); for (int i = 0; i < 1000; i++) System.out.println("=========== 主线程 ==========="); } }
|
3)实现Callable
接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| package com.banmoon.mode;
import java.util.concurrent.*;
public class CallableMode {
public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService service = Executors.newFixedThreadPool(2); Future<String> resultA = service.submit(new CallableModeA()); Future<String> resultB = service.submit(new CallableModeB()); System.out.println("结果A:" + resultA.get()); System.out.println("结果B:" + resultB.get()); for (int i = 0; i < 1000; i++) System.out.println("=========== 主线程 ==========="); service.shutdown(); }
}
class CallableModeA implements Callable<String>{ @Override public String call() throws Exception { String str = "=========== 线程A ==========="; for (int i = 0; i < 1000; i++) System.out.println(str); return str; } }
class CallableModeB implements Callable<String>{ @Override public String call() throws Exception { String str = "=========== 线程B ==========="; for (int i = 0; i < 1000; i++) System.out.println(str); return str; } }
|
这里使用到了一个执行服务工具类Executors
,它可以创建线程池,后续会讲到
三、线程状态及方法
1)状态
其实,jdk中还有一个线程状态的枚举Thread.State
和上图有些不同,但不影响理解,只是少了个就绪的状态
2)方法
Thread方法 |
是否静态 |
说明 |
setPriority |
否 |
设置线程的优先级,优先级高的更有机会优先被CPU调度,但这个不是绝对 |
sleep |
是 |
让当前所处的线程进行休眠,可以用来模拟网络延迟,放大同步问题 |
join |
否 |
插队,等待正在运行的线程终止 |
yield |
是 |
暂停当前的线程,执行其他的线程,让CPU选择再次进行选择调度 |
1、setPriority
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| package com.banmoon.status;
public class ThreadPriorityMethods {
public static void main(String[] args) { Thread threadA = new Thread(new MyThread(), "线程A"); Thread threadB = new Thread(new MyThread(), "线程B"); Thread threadC = new Thread(new MyThread(), "线程C"); threadA.setPriority(9); threadB.setPriority(5); threadC.setPriority(1); threadC.start(); threadB.start(); threadA.start(); }
}
class MyThread implements Runnable{ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println("============" + Thread.currentThread().getName() + "============"); } } }
|
2、sleep
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package com.banmoon.status;
import java.text.SimpleDateFormat; import java.util.Date;
public class ThreadSleepMethods {
public static void main(String[] args) { new Thread(() -> { int i = 0; SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); while (i++<10){ String dateStr = sdf.format(new Date()); System.out.println(dateStr + "============ 线程A ============"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } }
|
3、join
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package com.banmoon.status;
public class ThreadJoinMethods {
public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { for (int i = 0; i < 200; i++) { System.out.println("============= 线程A"+ i +" ============="); } }); thread.start(); for (int i = 0; i < 1000; i++) { if(i==100) thread.join(); System.out.println("============= 主线程"+ i +" ============="); } }
}
|
4、yield
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package com.banmoon.status;
public class ThreadYieldMethods {
public static void main(String[] args) { Thread thread = new Thread(() -> { for (int i = 0; i < 1000; i++) { if(i==200) Thread.yield(); System.out.println("============= 线程A"+ i +" ============="); } }); thread.start(); for (int i = 0; i < 1000; i++) { if(i==500) Thread.yield(); System.out.println("============= 主线程"+ i +" ============="); } } }
|
四、synchronized
关键字
1)并发数据问题
看下列代码,总共有10张票,创建3个线程,每个线程都去取票,直到票数小于0,则退出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| package com.banmoon.sync;
public class TicketServer implements Runnable{
private int ticketNum = 10;
@Override public void run() {
String name = Thread.currentThread().getName(); while (true){ if(ticketNum<=0) return; System.out.println(name + ":取到了第" + ticketNum + "张票"); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } ticketNum--; } }
public static void main(String[] args) { TicketServer ticketServer = new TicketServer(); new Thread(ticketServer, "A").start(); new Thread(ticketServer, "B").start(); new Thread(ticketServer, "C").start(); } }
|
预期将会是,10,9,8,7…直到取完票,但真实的结果是
我执行了很多遍,结果远远没有出现我预期的模样。
这是因为cpu调度线程太快了,当取票完成,但票数还没有减一的时候,其他的线程读取了没有减票前的票数,所以导致出现的问题,此类问题被称为并发问题,也称线程安全问题
2)synchronized介绍
此关键字保证了访问同个资源时出现的并发问题。
他的工作原理是对指定对象进行加锁,对此锁表示占有。导致其他线程进不来,可以查看下面示例的代码
用synchronized
对取票检票进行限制,这把锁就是ticketServer
。当A线程获取锁,进入代码执行时,其他线程必须进行等待,直到A线程完成逻辑释放锁后,CPU再重新进行调度,看谁运气好能获取到这一次的锁。
在这个等待锁的线程状态,也被称为同步阻塞状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| package com.banmoon.sync;
public class SyncTicketServer implements Runnable{
private int ticketNum = 10;
@Override public void run() { String name = Thread.currentThread().getName(); while (true){ synchronized (this){ if(ticketNum<=0) return; System.out.println(name + ":取到了第" + ticketNum + "张票"); try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } ticketNum--; } } }
public static void main(String[] args) { SyncTicketServer ticketServer = new SyncTicketServer(); new Thread(ticketServer, "A").start(); new Thread(ticketServer, "B").start(); new Thread(ticketServer, "C").start(); } }
|
执行结果
3)死锁
诚然,synchronized可以解决同步问题,但他的缺点需要了解
下列示例代码展示了死锁,有两个线程,手中持有自己的一把锁,这又想获取对方手中的锁时,两个线程相持不下,都处于同步阻塞阶段,导致出现的死锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| package com.banmoon.sync;
public class DeadLock {
public static void main(String[] args) { Thread threadA = new Thread(new ThreadA(), "ThreadA"); Thread threadB = new Thread(new ThreadB(), "ThreadB"); threadA.start(); threadB.start(); }
}
class ThreadA implements Runnable{
@Override public void run() { synchronized (ThreadA.class){ System.out.println(Thread.currentThread().getName() + "已持有锁:ThreadA"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (ThreadB.class){ System.out.println(Thread.currentThread().getName() + "已持有锁:ThreadA"); } } } }
class ThreadB implements Runnable{
@Override public void run() { synchronized (ThreadB.class){ System.out.println(Thread.currentThread().getName() + "已持有锁:ThreadB"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (ThreadA.class){ System.out.println(Thread.currentThread().getName() + "已持有锁:ThreadA"); } } } }
|
真实的情况远比上述要复杂的多,但死锁的基本概念就是如此。
4)不同的修饰位置
我们现在知道,synchronized锁住的是对象,也就是获取到了对象的锁,但处于不同的修饰位置,获取哪个对象的锁也是不一致的。
简单可以分为下面几个修饰位置
1、修饰this时
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| package com.banmoon.sync;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil;
public class SyncDemo1{
private void printOne(){ synchronized (this){ try { Thread.sleep(2000); String format = StrUtil.format("{}:当前this对象:{},时间:{}", Thread.currentThread().getName(), this, DateUtil.now()); System.out.println(format); } catch (InterruptedException e) { e.printStackTrace(); } } }
private void printTwo(){ synchronized (this){ String format = StrUtil.format("{}:当前this对象:{},时间:{}", Thread.currentThread().getName(), this, DateUtil.now()); System.out.println(format); } }
public static void main(String[] args) { SyncDemo1 syncDemo1 = new SyncDemo1(); new Thread(() -> { syncDemo1.printOne(); }, "线程A").start(); new Thread(() -> { syncDemo1.printTwo(); }, "线程B").start();
}
}
|
如上,因为只创建一个实例,所以他们锁定的只是this
如果不信,可以再new SyncDemo1()
,让线程B去调用这个实例对象的printTwo()
,保证你看到不同的结果
2、修饰XXX.class时
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| package com.banmoon.sync;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil;
public class SyncDemo2 {
private void printOne(){ synchronized (SyncDemo2.class){ try { Thread.sleep(2000); String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now()); System.out.println(format); } catch (InterruptedException e) { e.printStackTrace(); } } }
private void printTwo(){ synchronized (SyncDemo2.class){ String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now()); System.out.println(format); } }
public static void main(String[] args) { SyncDemo2 syncDemo2 = new SyncDemo2(); new Thread(() -> { syncDemo2.printOne(); }, "线程A").start(); new Thread(() -> { syncDemo2.printTwo(); }, "线程B").start();
} }
|
这个结果与上面一致,因为锁住的都是同一个对象
3、修饰成员方法时
在这里我做了对比,判断修饰this和修饰成员方法时,锁住的对象是否是同一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| package com.banmoon.sync;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil;
public class SyncDemo3 {
private synchronized void printOne(){ try { Thread.sleep(2000); String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now()); System.out.println(format); } catch (InterruptedException e) { e.printStackTrace(); } }
private void printTwo(){ synchronized ("Don't write that"){
String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now()); System.out.println(format); } }
public static void main(String[] args) { SyncDemo3 syncDemo3 = new SyncDemo3(); new Thread(() -> { syncDemo3.printOne(); }, "线程A").start(); new Thread(() -> { syncDemo3.printTwo(); }, "线程B").start(); }
}
|
当23行放开进行使用,会发现线程B会被线程A卡住,说明修饰成员方法时,获取的就是当前对象的锁
4、修饰静态方法时
在这里进行了对比,判断修饰静态方法、修饰this、修饰当前class对象时,获取的是什么对象的锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| package com.banmoon.sync;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil;
public class SyncDemo4 {
private static synchronized void printOne(){ try { Thread.sleep(2000); String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now()); System.out.println(format); } catch (InterruptedException e) { e.printStackTrace(); } }
private void printTwo(){ synchronized (this){
String format = StrUtil.format("{}:时间:{}", Thread.currentThread().getName(), DateUtil.now()); System.out.println(format); } }
public static void main(String[] args) { SyncDemo4 syncDemo4 = new SyncDemo4(); new Thread(() -> { syncDemo4.printOne(); }, "线程A").start(); new Thread(() -> { syncDemo4.printTwo(); }, "线程B").start(); } }
|
如果线程B马上打印,那说明获取的不是同一把锁。要是线程B被线程A卡住了,那说明确实是一把锁了
总结
- 修饰代码块时:锁住的是括号中的对象
- this:指向的是当前对象,也就是实现重写run方法的类实例化出来的对象
- XXX.class:就是这个class对象,我比较喜欢使用,因为class对象具有唯一性
- 修饰方法时:
- 成员方法:成员方法所在的类所创建出来的对象,也就是谁调用了这个方法,获取的就是谁的锁
- 静态方法:当前方法所在类的class对象,XXX.class
注意:不要修饰什么乱七八糟的对象,比如字符串对象,就像上面写的Don't write that
。有时候,自己都分不清两个是不是同一个对象,还敢乱写在代码中。
5)异常释放锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package com.banmoon.sync;
public class SyncException {
public static void main(String[] args) throws InterruptedException { new Thread(() -> { synchronized (SyncException.class){ for (int i = 0; i < 20; i++) { if(i==10) i = 1/0; System.out.println("线程A:" + i); } } }).start();
Thread.sleep(3000); synchronized (SyncException.class){ for (int i = 0; i < 10; i++) { System.out.println("主线程"); } } }
}
|
如下运行结果,线程中出现异常时,当前持有的锁会立即释放。所以一定要准确的捕获异常,可以试试将异常捕获,保证线程的安全。
五、线程通信
在上述synchronized
的代码案例中,线程获取了锁后,都是一条路走到黑的,除了异常没捕获的那次。
线程通信,主要是线程在获取锁后,主动将锁放弃,让其他线程也来喝喝汤,cpu大哥觉得你很懂事,cpu很欣慰。
1)主要方法
由于synchronized
获取的是对象的锁,所以有关线程之间的阻塞唤醒,都来自Object
类
方法名 |
功能 |
public final void wait() |
释放当前锁,本线程进入睡眠,从运行状态进入阻塞状态 需要等待其他线程唤醒 |
public final native void wait(long timeout) |
释放当前锁,本线程进入睡眠,从运行状态进入阻塞状态, 一段时间后本线程自动醒来 |
public final native void notify() |
唤醒其他任意一个线程,将它从阻塞状态拉回到就绪状态 |
public final native void notifyAll() |
唤醒其他所有线程,将它们从阻塞状态拉回到就绪状态 |
2)简单示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| package com.banmoon.wait;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil;
public class Demo1 {
public static void main(String[] args) throws InterruptedException { new Thread(() -> { synchronized (Demo1.class){ for (int i = 1; i <= 10; i++) { try { System.out.println(StrUtil.format("线程A:{},时间:{}", i, DateUtil.now())); if(i==5){ System.out.println("睡一会,释放锁"); Demo1.class.wait(); } Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start();
Thread.sleep(5000);
System.out.println("主线程:唤醒"); Demo1.class.notify(); synchronized (Demo1.class){ System.out.println("主线程:唤醒"); Demo1.class.notify(); } }
}
|
执行结果,主线程唤醒后,线程A继续走他未走完的路
大家可以放开上述代码的第29,30行,运行后惊奇的发现居然出了异常
这个异常是什么原因呢,在执行wait()、notify()、notifyAll()方法时,必须要持有锁。而且唤醒还一定要持有相同对象的锁,也就是使用synchronized
获取同样的对象的锁,并使用该对象进行唤醒其实很好理解,一个持有锁的线程,怎么可能会被没有持有同样锁的线程唤醒呢
3)生产者消费者模式
通过一个中间容器,来设置该容器的最大容量作为生产者线程的结束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
| package com.banmoon.wait;
import cn.hutool.core.util.StrUtil;
import java.util.LinkedList; import java.util.Queue;
public class Demo2 {
public static void main(String[] args) throws InterruptedException { Container container = new Container(); new Thread(new Consumer(container)).start(); Thread.sleep(1000); new Thread(new Producer(container)).start(); }
}
class Producer implements Runnable{ private Container container;
public Producer(Container container) { this.container = container; }
@Override public void run() { int i = 0; while (true){ try { container.put(i); System.out.println(StrUtil.format("生产者:生产了{},当前数量:{}", i, container.queue.size())); i++; Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } } }
class Consumer implements Runnable{ private Container container;
public Consumer(Container container) { this.container = container; }
@Override public void run() { while (true){ try { Integer i = container.get(); System.out.println("消费者:消费了" + i); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }
class Container{ public Queue<Integer> queue; public int MAX_SIZE = 10;
public Container() { this.queue = new LinkedList<>(); }
public synchronized Integer get(){ try { if(queue.size()==0){ notifyAll(); wait(); } return this.queue.poll(); } catch (InterruptedException e) { e.printStackTrace(); } return null; }
public synchronized void put(Integer integer){ try { if(queue.size()>=MAX_SIZE){ notifyAll(); wait(); } this.queue.add(integer); } catch (InterruptedException e) { e.printStackTrace(); } } }
|
4)信号灯法
设置一个标志位flag,来控制线程之间的通信状态。简单改造上面的生产消费者,使通过flag
来进行通信。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| package com.banmoon.wait;
import cn.hutool.core.date.DateUtil;
public class Demo3 {
public static void main(String[] args) { Demo3Flag flag = new Demo3Flag(); new Thread(new Demo3Producer(flag)).start(); new Thread(new Demo3Consumer(flag)).start(); } }
class Demo3Producer implements Runnable{
private Demo3Flag flag;
public Demo3Producer(Demo3Flag flag) { this.flag = flag; }
@Override public void run() { for (int i = 0; i < 100; i++){ flag.production(); } } }
class Demo3Consumer implements Runnable{
private Demo3Flag flag;
public Demo3Consumer(Demo3Flag flag) { this.flag = flag; }
@Override public void run() { for (int i = 0; i < 100; i++){ flag.consumption(); } } }
class Demo3Flag{
private boolean flag;
public Demo3Flag() { this.flag = true; }
public void setFlag(boolean flag) { this.flag = flag; }
public synchronized void production(){ if(!this.flag){ try { wait(); } catch (InterruptedException interruptedException) { interruptedException.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + ":正在生产..." + DateUtil.now()); this.flag = false; notifyAll(); }
public synchronized void consumption(){ if(this.flag){ try { wait(); } catch (InterruptedException interruptedException) { interruptedException.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + ":正在消费..." + DateUtil.now()); this.flag = true; notifyAll(); } }
|
5)虚假唤醒问题
在线程通信中,如果使用不当,将会出现虚假唤醒的问题,运行下列代码来进行查看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| package com.banmoon.wait;
public class Demo4 {
public static void main(String[] args) { MyDemo4 myDemo4 = new MyDemo4(); new Thread(() -> { for(int i = 0; i < 10; i++) myDemo4.increment(); }, "线程A").start(); new Thread(() -> { for(int i = 0; i < 10; i++) myDemo4.decrement(); }, "线程B").start(); new Thread(() -> { for(int i = 0; i < 10; i++) myDemo4.increment(); }, "线程C").start(); new Thread(() -> { for(int i = 0; i < 10; i++) myDemo4.decrement(); }, "线程D").start();
}
}
class MyDemo4{
private int number = 0;
public synchronized void increment(){ try { if(number==1) wait(); number++; System.out.println(Thread.currentThread().getName() + ":" + number); notifyAll(); } catch (InterruptedException e) { e.printStackTrace(); } }
public synchronized void decrement(){ try { if(number==0) wait(); number--; System.out.println(Thread.currentThread().getName() + ":" + number); notifyAll(); } catch (InterruptedException e) { e.printStackTrace(); } }
}
|
正常预想的结果,ABCD四个线程使得number在0和1之间反复横跳,但实际上的结果,却大大出乎我所料
这个原因很好解释,因为wait
方法有个特性,在摔倒就在哪里爬起来,然后继续向前走。简单的描述下出现问题的步骤,
-
A线程,number+1
-
C线程,判断后进行wait
-
A线程,判断后进行wait
-
B线程,number-1,唤醒其他线程,此时A,C被唤醒
-
A线程,number+1
-
C线程,number+1
-
一直持续下去…
好的,到第6步就已经出现问题了,记住上面说的,在哪里摔倒就在哪里爬起来。
第二步时,C线程在判断完成后进入等待,直到第六步被CPU调度,因为判断已经完成,所以直接进入了number+1的逻辑。
像上述这种现象被称为虚假唤醒
解决虚假唤醒
既然是由于被唤醒后没有判断导致,所以我们这里只需要将if
改为while
,让线程唤醒后的第一件事就是判断条件
修改为while
后的执行结果
六、经典笔试题
思考一下如何用3条线程循环输出ABC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| package com.banmoon.question;
public class Question1 {
public static void main(String[] args) throws InterruptedException { Object a = new Object(); Object b = new Object(); Object c = new Object(); ABC A = new ABC("A", c, a); ABC B = new ABC("B", a, b); ABC C = new ABC("C", b, c);
new Thread(A).start(); Thread.sleep(10); new Thread(B).start(); Thread.sleep(10); new Thread(C).start(); Thread.sleep(10); }
}
class ABC implements Runnable{
private String name; private Object prev; private Object self;
public ABC(String name, Object prev, Object self) { this.name = name; this.prev = prev; this.self = self; }
@Override public void run() { while (true){ synchronized (prev) { synchronized (self) { System.out.print(name); self.notifyAll(); } try { Thread.sleep(50); prev.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
|
七、最后
多线程使用不是难点,而多线程的锁是面试的常客,synchronized
关键字锁住的对象这一知识点必须要掌握。
关于其他什么轻量级锁,总量级锁,读写锁,重入锁等等的概念,后续会出个单章来进行理解。
关于本文出现的代码示例,已提交至码云,只看文章不懂时,一定要敲代码进行理解。
我是半月,祝你幸福!