功能:Java多线程
Java多线程
一、介绍#
在了解线程之前,还需要简单了解进程的概念。简单的来说就是一心多用
在生活之中,我们常常可以一心多用。我可以一边打游戏,一边放着音乐听听歌,甚至可以再泡个脚。没错,这也可以理解成我的多线程生活。
而在计算机之中,也有以上同时进行的任务,这就可以叫做多线程,例如
- 进程:比如说电脑上开着游戏,音乐等其他多款应用。这每一个应用姑且可以算作一个进程
- 线程:往往一款游戏,有伤害计算,有数据上传,图像音乐等等的步骤,这每个执行的细项也可以理解成一个线程
所以总结来看,进程是一个应用运行的过程,可以包含多个线程运行,但至少必须要有一个线程,这样才能撑得起这是个进程。
线程是cpu对某个资源的调度计算的通道,这条通道下,cpu可以执行某些任务的调度。
在java中,我们从Main方法运行,所以称其为主线程
除了主线程外,java还有一个后台线程在默默地工作着,这就是GC线程,也就是垃圾回收所处的线程
二、Java线程的实现#
1)继承Thread
类#
package com.banmoon.mode; /** * 实现多线程方式 * 1、继承类Thread * 2、实现其run方法 * 3、创建该对象,调用start方法 */ 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
接口#
package com.banmoon.mode; /** * 实现多线程方式 * 1、实现接口Runnable * 2、构造Thread对象,将Runnable实现对象作为参数 * 3、调用Thread对象的start方法 */ 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
的简写方式
package com.banmoon.mode; /** * 实现多线程方式 * 1、实现接口Runnable,Lambda简写方式 */ 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
接口#
package com.banmoon.mode; import java.util.concurrent.*; /** * 实现多线程方式 * 1、实现Callable接口,了解 * 2、需要定义返回值类型 * 3、创建执行服务线程池,来进行执行 */ 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#
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#
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#
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#
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,则退出
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再重新进行调度,看谁运气好能获取到这一次的锁。
在这个等待锁的线程状态,也被称为同步阻塞状态。
package com.banmoon.sync; public class SyncTicketServer implements Runnable{ private int ticketNum = 10; /** * 取票 */ @Override public void run() { // 获取当前线程名 String name = Thread.currentThread().getName(); while (true){ // 注意,锁住的是this对象哦,也就是32行创建出来的ticketServer 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可以解决同步问题,但他的缺点需要了解
- 效率:线程处于同步阻塞中,效率上不去,但这是没有办法的
- 死锁:当代码考虑不周时,将会出现死锁问题
下列示例代码展示了死锁,有两个线程,手中持有自己的一把锁,这又想获取对方手中的锁时,两个线程相持不下,都处于同步阻塞阶段,导致出现的死锁。
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时#
package com.banmoon.sync; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; /** * sync修饰对象时 */ 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(); // System.out.println(syncDemo1);// 指的就是31行创建出来的对象 } }
如上,因为只创建一个实例,所以他们锁定的只是this
如果不信,可以再new SyncDemo1()
,让线程B去调用这个实例对象的printTwo()
,保证你看到不同的结果
2、修饰XXX.class时#
package com.banmoon.sync; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; /** * 当修饰class对象时 */ 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(); // System.out.println(SyncDemo2.class);// class对象具有唯一性 } }
这个结果与上面一致,因为锁住的都是同一个对象
3、修饰成员方法时#
在这里我做了对比,判断修饰this和修饰成员方法时,锁住的对象是否是同一个
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"){ // synchronized (this){// 测试锁住的是否是this 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对象时,获取的是什么对象的锁
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){ // synchronized (SyncDemo4.class){// 判断获取的是否是SyncDemo4.class对象 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)异常释放锁#
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)简单示例#
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(); // 等待3秒 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)生产者消费者模式#
通过一个中间容器,来设置该容器的最大容量作为生产者线程的结束
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
来进行通信。
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{ // 标志位 true:生成,false:消费 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)虚假唤醒问题#
在线程通信中,如果使用不当,将会出现虚假唤醒的问题,运行下列代码来进行查看
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
package com.banmoon.question; /** * 使用线程循环输出ABC */ 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);// 保证初始ABC的启动顺序 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(); } } } } }
个人博客
欢迎来登录我的个人博客
作者: 半月无霜
出处:https://www.cnblogs.com/banmoon/p/15726850.html
本站使用「CC BY 4.0」创作共享协议,转载请在文章明显位置注明作者及出处。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!