Java 多线程详解(三)------线程的同步
介绍完如何创建进程以及线程了,那么我们接着来看一个实例:
利用多线程模拟 3 个窗口卖票
第一种方法:继承 Thread 类
创建窗口类 TicketSell
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 | package com.ys.thread; public class TicketSell extends Thread{ //定义一共有 50 张票,注意声明为 static,表示几个窗口共享 private static int num = 50 ; //调用父类构造方法,给线程命名 public TicketSell(String string) { super (string); } @Override public void run() { //票分 50 次卖完 for ( int i = 0 ; i < 50 ;i ++){ if (num > 0 ){ try { sleep( 10 ); //模拟卖票需要一定的时间 } catch (InterruptedException e) { // 由于父类的 run()方法没有抛出任何异常,根据继承的原则,子类抛出的异常不能大于父类, 故我们这里也不能抛出异常 e.printStackTrace(); } System.out.println( this .currentThread().getName()+ "卖出一张票,剩余" +(--num)+ "张" ); } } } } |
创建主线程测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | package com.ys.thread; public class TestTicket { public static void main(String[] args) { //创建 3 个窗口 TicketSell t1 = new TicketSell( "A窗口" ); TicketSell t2 = new TicketSell( "B窗口" ); TicketSell t3 = new TicketSell( "C窗口" ); //启动 3 个窗口进行买票 t1.start(); t2.start(); t3.start(); } } |
结果:这里我们省略了一些,根据电脑配置,结果会随机出现不同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | B窗口卖出一张票,剩余 48 张 A窗口卖出一张票,剩余 47 张 C窗口卖出一张票,剩余 49 张 C窗口卖出一张票,剩余 46 张 B窗口卖出一张票,剩余 44 张 A窗口卖出一张票,剩余 45 张 A窗口卖出一张票,剩余 43 张 ... C窗口卖出一张票,剩余 5 张 A窗口卖出一张票,剩余 4 张 B窗口卖出一张票,剩余 3 张 A窗口卖出一张票,剩余 2 张 C窗口卖出一张票,剩余 3 张 B窗口卖出一张票,剩余 1 张 C窗口卖出一张票,剩余 0 张 A窗口卖出一张票,剩余- 1 张 |
第二种方法:实现 Runnable 接口
创建窗口类 TicketSellRunnable
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.ys.thread; public class TicketSellRunnable implements Runnable{ //定义一共有 50 张票,继承机制开启线程,资源是共享的,所以不用加 static private int num = 50 ; @Override public void run() { //票分 50 次卖完 for ( int i = 0 ; i < 50 ;i ++){ if (num > 0 ){ try { //模拟卖一次票所需时间 Thread.sleep( 10 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+ "卖出一张票,剩余" +(--num)+ "张" ); } } } } |
创建主线程测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | package com.ys.thread; public class TicketSellRunnableTest { public static void main(String[] args) { TicketSellRunnable t = new TicketSellRunnable(); Thread t1 = new Thread(t, "A窗口" ); Thread t2 = new Thread(t, "B窗口" ); Thread t3 = new Thread(t, "C窗口" ); t1.start(); t2.start(); t3.start(); } } |
结果:同理为了篇幅我们也省略了中间的一些结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | B窗口卖出一张票,剩余 49 张 C窗口卖出一张票,剩余 48 张 A窗口卖出一张票,剩余 49 张 B窗口卖出一张票,剩余 47 张 A窗口卖出一张票,剩余 45 张 ...... A窗口卖出一张票,剩余 4 张 C窗口卖出一张票,剩余 5 张 A窗口卖出一张票,剩余 3 张 B窗口卖出一张票,剩余 2 张 C窗口卖出一张票,剩余 1 张 B窗口卖出一张票,剩余 0 张 A窗口卖出一张票,剩余- 2 张 C窗口卖出一张票,剩余- 1 张 |
结果分析:这里出现了票数为 负数的情况,这在现实生活中肯定是不存在的,那么为什么会出现这样的情况呢?
解决办法分析:即我们不能同时让超过两个以上的线程进入到 if(num>0)的代码块中,不然就会出现上述的错误。我们可以通过以下三个办法来解决:
1、使用 同步代码块
2、使用 同步方法
3、使用 锁机制
①、使用同步代码块
1 2 3 4 5 6 7 8 9 | 语法: synchronized (同步锁) { //需要同步操作的代码 } 同步锁:为了保证每个线程都能正常的执行原子操作,Java 线程引进了同步机制;同步锁也叫同步监听对象、同步监听器、互斥锁; Java程序运行使用的任何对象都可以作为同步监听对象,但是一般我们把当前并发访问的共同资源作为同步监听对象 注意:同步锁一定要保证是确定的,不能相对于线程是变化的对象;任何时候,最多允许一个线程拿到同步锁,谁拿到锁谁进入代码块,而其他的线程只能在外面等着 |
实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public void run() { //票分 50 次卖完 for ( int i = 0 ; i < 50 ;i ++){ //这里我们使用当前对象的字节码对象作为同步锁 synchronized ( this .getClass()) { if (num > 0 ){ try { //模拟卖一次票所需时间 Thread.sleep( 10 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+ "卖出一张票,剩余" +(--num)+ "张" ); } } } } |
②、使用 同步方法
语法:即用 synchronized 关键字修饰方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @Override public void run() { //票分 50 次卖完 for ( int i = 0 ; i < 50 ;i ++){ sell(); } } private synchronized void sell(){ if (num > 0 ){ try { //模拟卖一次票所需时间 Thread.sleep( 10 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+ "卖出一张票,剩余" +(--num)+ "张" ); } } |
注意:不能直接用 synchronized 来修饰 run() 方法,因为如果这样做,那么就会总是第一个线程进入其中,而这个线程执行完所有操作,即卖完所有票了才会出来。
③、使用 锁机制
1 | public interface Lock |
主要方法:
常用实现类:
1 2 3 | public class ReentrantLock extends Object implements Lock, Serializable<br> //一个可重入互斥Lock具有与使用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 | package com.ys.thread; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class TicketSellRunnable implements Runnable{ //定义一共有 50 张票,继承机制开启线程,资源是共享的,所以不用加 static private int num = 50 ; //创建一个锁对象 Lock l = new ReentrantLock(); @Override public void run() { //票分 50 次卖完 for ( int i = 0 ; i < 50 ;i ++){ //获取锁 l.lock(); try { if (num > 0 ){ //模拟卖一次票所需时间 Thread.sleep( 10 ); System.out.println(Thread.currentThread().getName()+ "卖出一张票,剩余" +(--num)+ "张" ); } } catch (Exception e) { e.printStackTrace(); } finally { //释放锁 l.unlock(); } } } private void sell(){ } } |
分类:
Java SE
标签:
Java 多线程详解系列
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?