Java 多线程编程4---同步与死锁 r囧r小猫 2022-05-20 07:04 195阅读 0赞 一个多线程的程序如果是通过实现Runable接口实现的,实现类中的属性可以被多个线程共享,这样就造成一个问题,如果这个多线程程序操作同一资源时就有可能出现资源同步的问题。例如之前的买票程序,如果多个线程同时操作时,就有可能出现卖出的票为负数的问题。 ## 问题的引出 ## 下面通过Runable接口实现多线程,并产生3个线程对象,同时卖掉这5张票。 **实例:有问题的买票程序** package my.thread.sync; class MyThread implements Runnable { //有5张票 private int ticket = 5; public void run() { for (int i = 0; i < 100; i++) { if (ticket > 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"卖票掉一张,余票" + ticket--); } } } } public class SyncDemo01 { public static void main(String args[]) { MyThread mt = new MyThread(); Thread t1 = new Thread(mt,"售票员A"); Thread t2 = new Thread(mt,"售票员B"); Thread t3 = new Thread(mt,"售票员C"); //启动3个线程进行买票 t1.start(); t2.start(); t3.start(); } } **一种运行结果:** 售票员B卖票掉一张,余票5 售票员C卖票掉一张,余票4 售票员A卖票掉一张,余票4 售票员B卖票掉一张,余票3 售票员C卖票掉一张,余票2 售票员A卖票掉一张,余票3 售票员B卖票掉一张,余票1 售票员C卖票掉一张,余票0 售票员A卖票掉一张,余票-1 从运行结果中看一共有5张票,但是却卖掉了9次,而且结果出现了余票为负数的情况。下面来分析查产生这样我问题的原因。 上面卖票的操作步骤: (1)判断票数是否大于0,如果票数大于0,则表示还有票可以卖。 (2)如果可以卖票,就把余票减一 但是,我们在上面的代码中,加入了延迟操作,那么一个线程有可能还没来得及把票数减1,其他线程就已经把票数减1了,这样就有可能出现票数为负数的情况。 ## 使用同步解决问题 ## 这里有两种方式可以结局资源的同步问题,一种是使用同步代码块完成,一种是使用同步方法完成。 ### 使用同步代码块 ### 所谓代码块就是使用`{}`括起来的一段代码,根据其位置和声明的不同,可以分为普通代码块,构造块,静态代码块3中,如果在代码块前面加上synchronized关键字,则称该代码块为同步代码块。同步代码块的格式如下 synchronized(同步对象) { 需要同步的代码; } 从上面可以同步代码块的格式,可以看出,在使用同步代码块的时候必须指定一个需要同步的对象,一般都将当前对象this设置成同步对象。 **实例:使用同步代码块解决上述买票问题** 使用同步代码块把上面买票的if语句包裹起来即可,完整代码如下。 package my.thread.sync; class MyThread implements Runnable { //有5张票 private int ticket = 5; public void run() { for (int i = 0; i < 100; i++) { //使用同步代码块,同步对象设置为当前对象 synchronized (this) { if (ticket > 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"卖票掉一张,余票" + ticket--); } } } } } public class SyncDemo01 { public static void main(String args[]) { MyThread mt = new MyThread(); Thread t1 = new Thread(mt,"售票员A"); Thread t2 = new Thread(mt,"售票员B"); Thread t3 = new Thread(mt,"售票员C"); //启动3个线程进行买票 t1.start(); t2.start(); t3.start(); } } **运行结果:** 售票员A卖票掉一张,余票5 售票员A卖票掉一张,余票4 售票员A卖票掉一张,余票3 售票员C卖票掉一张,余票2 售票员C卖票掉一张,余票1 多次运行,不管你怎么运行,结果都是只卖掉5张票。 上面的代码将判断余票`if (ticket > 0)`和修改票数`ticket--`这两个操作进行了同步,所以不会出现多次卖票的情况。 这里一定要注意,同步代码块中一定要包括取值和修改值两个操作,如果单独同步一个操作,将不是同步,错误的代码如下: package my.thread.sync; class MyThread implements Runnable { // 有5张票 private int ticket = 5; public void run() { for (int i = 0; i < 100; i++) { // 使用同步代码块,同步对象设置为当前对象 if (ticket > 0) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (this) { System.out.println(Thread.currentThread().getName() + "卖票掉一张,余票" + ticket--); } } } } } public class SyncDemo01 { public static void main(String args[]) { MyThread mt = new MyThread(); Thread t1 = new Thread(mt, "售票员A"); Thread t2 = new Thread(mt, "售票员B"); Thread t3 = new Thread(mt, "售票员C"); // 启动3个线程进行买票 t1.start(); t2.start(); t3.start(); } } **运行结果:** 售票员C卖票掉一张,余票5 售票员B卖票掉一张,余票4 售票员A卖票掉一张,余票3 售票员C卖票掉一张,余票2 售票员B卖票掉一张,余票1 售票员A卖票掉一张,余票0 售票员C卖票掉一张,余票-1 上面的同步代码块只同步了`对票数减1`的操作,而不同步`票数判断`的操作,所以达不到同步的效果。使用时**一定要在把`判断操作`和`修改操作`成对放入到同步代码块中**个,不然达不到同步的效果。 ### 使用同步方法 ### 除了可以将需要的代码设置成同步代码块之外,还可使用`synchronized`关键字将一个方法声明成同步方法。声明同步方法的格式如下。 synchronized 方法返回值 方法名称(参数列表) { //方法体 } **实例:使用同步方法实现卖票的同步操作** package my.thread.sync; class MyThread1 implements Runnable { // 有5张票 private int ticket = 5; public void run() { for (int i = 0; i < 100; i++) { saleTicket(); } } public synchronized void saleTicket() { if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println( Thread.currentThread().getName() + "卖票掉一张,余票" + ticket--); } } } public class SyncDemo2 { MyThread1 mt=new MyThread1(); Thread th1=new Thread(mt,"售票员A"); Thread th2=new Thread(mt,"售票员B"); Thread th3=new Thread(mt,"售票员C"); } **一次运行结果:** 售票员A卖票掉一张,余票5 售票员C卖票掉一张,余票4 售票员C卖票掉一张,余票3 售票员C卖票掉一张,余票2 售票员B卖票掉一张,余票1 从程序的运行结果中可以发现,上面的代码完成了与之前同步代码块同样的功能。 ## 使用同步代码块还是使用同步方法 ## ### 同步代码块,同步方法,静态同步方法使用的锁 ### * 同步代码块使用的锁是任意的对象。 * 同步方法使用的锁是当前对象this * 使用static关键字修饰的静态同步方法使用的是该类所在的字节码文件对象,格式为类名.class。 ### 同步代码块和同步方法的区别 ### * 同步方法默认用this或者当前类class对象作为锁; * 同步代码块可以选择加锁的对象; * 同步代码块比同步方法要更细颗粒度,我们可以选择**只同步会发生同步问题的关键代码而不是整个方法**; ### 使用同步代码块还是使用同步方法比较好 ### 同步方法直接在方法上加synchronized实现加锁,同步代码块则在方法内部加锁,很明显,同步方法锁的范围比较大,而同步代码块范围要小点。 而且同步是一个开销很大的操作,因此尽量减小同步的区域。所以通常没有必要同步整个方法,使用同步代码块同发生同步问题的关键代码即可。 **所以考虑性能,最好使用同步代码块从而减少锁定范围以提高并发效率。** ## 死锁 ## 同步可以保证资源共享操作的正确性,但是过多同步也会产生问题,例如会产生死锁。所谓死锁,就是指两个线程都在等待彼此先完成,造成程序的卡着无法往下运行。一般死锁都是在程序运行时出现的,发生在两个线程相互持有对方正在等待的东西(实际是两个线程共享的东西)。只要有两个线程和两个对象就可能产生死锁。 **实例:死锁例子** package my.thread.deadlock; public class DeadLock implements Runnable { public String name; public boolean flag; // 静态对象是类的所有对象共享的 private static Object object1 = new Object(); private static Object object2 = new Object(); @Override public void run() { System.out.println("flag=" + flag); if (flag) { // 同步代码块1 synchronized (object1) { System.out.println(Thread.currentThread().getName() + "成功持有object1对象的锁,成功进入同步代码块1中"); try { System.out.println( Thread.currentThread().getName() + "睡眠500毫秒"); Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "睡眠结束,正在获取object2对象的锁..."); // 同步代码块2 synchronized (object2) { System.out.println(Thread.currentThread().getName() + "成功持有object2对象的锁,成功进入同步代码块2中"); System.out.println("1"); } } } if (!flag) { // 同步代码块3 synchronized (object2) { System.out.println(Thread.currentThread().getName() + "成功持有object2对象的锁,成功进入同步代码块3"); try { System.out.println( Thread.currentThread().getName() + "睡眠500毫秒"); Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "睡眠结束,正在获取object1对象的锁..."); // 同步代码块4 synchronized (object1) { System.out.println(Thread.currentThread().getName() + "成功获取object1对象的锁,成功进入同步代码块4中"); System.out.println("0"); } } } } public static void main(String[] args) { DeadLock A = new DeadLock(); DeadLock B = new DeadLock(); A.flag = true; B.flag = false; new Thread(A, "线程A").start(); new Thread(B, "线程B").start(); } } **运行结果:** ![死锁运行结果][70] **分析** 1. 线程A启动,由于A对象的flag为true,且此时`object1对象的锁`还没有任何被线程持有,所以线程A就马上持有`object1对象的锁`,然后进入`同步代码块1`中去执行里面的代码,然后线程A睡眠500毫秒。 2. 然后线程B启动,由于B对象的flag为false,且此时`object2对象的锁`还没有被任何线程持有,所以线程B很愉快的持有`object2对象的锁`,然后进入`同步代码块3`中去执行里面的代码,然后线程B睡眠500毫秒。 3. 线程A睡眠结束后,就需要进入`同步代码块2`中去执行,此时就需要持有`object2对象的锁`,但是由于线程B还没走出`同步代码块3`中,也就是说**object2对象的锁还被线程B持有**。只有等到线程B执行执行完毕`同步代码块3`中的代码,线程B才会释放object2对象的锁。所以,此时线程A无法获取到object2对象的锁,**线程A要等待线程B执行完同步代码块3中的所有代码,然后把object2的锁释给线程A。** 4. 线程B睡眠结束后,想要进入`同步代码块4`中去执行,此时就需要持有`object1对象的锁`,但是此时线程A还在同步代码块中等待线程B释放object2给它,所以线程A没有执行完`同步代码块1`中的内容,线程A将继续占有**object1对象的锁**。所以**线程B需要等待线程A执行完同步块1中的所有代码,然后吧object1对象的锁释放给线程B.** 5. 好的,现在的解说是,线程A等着线程B执行完毕释放object2对象的锁,而线程B也在等待线程A执行完毕释放object2对象的锁。线程A等待线程B,线程B等待线程A。**线程A和线程B相互等待,这样造成了死锁。** ### 产生死锁的四个必要条件 ### 虽然进程(线程)在运行过程中,可能发生死锁,但死锁的发生也必须具备一定的条件,**死锁的发生必须具备以下四个必要条件。** **1.互斥条件** 指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程(线程)占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。 **2.请求和保持条件** 指进程(线程)已经持有至少一个资源,但又提出了新的资源请求,而该资源已被其它进程(线程)占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。 **3.不可剥夺条件** 指进程(线程)已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。 **4.循环等待条件** 指在发生死锁时,必然存在一个相互等待的环形链,即进程集合\{P0,P1,P2,···,Pn\}中的P0正在等待一个被P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源,也就是若干进程之间形成一种头尾相接的循环等待资源关系。 分析上述的代码,使用使用了同步代码块,就满足了1.互斥条件,2.请求与和保持条件,3.不可剥夺条件。此时在使用两个相互嵌套的同步代码块, 第一个嵌套的同步代码块的锁对象由外到内的顺序是:object1,object2. 第二个嵌套的同步代码块的锁对象由外到内的顺序是:object2,object1. 进入第一个嵌套的同步代码块中的线程A必然持有object1对象的锁了,之后他想要持有object2对象的锁。 而进入第二个嵌套的同步代码块的线程B必然持有object2对象的锁了,之后他又想要持有object1对象的锁。 这样就造成了 线程A等待线程B,线程B等待线程A 这就产生死锁的第四个条件:循环等待,所以上面程序出现了死锁。 ### 死锁的避免 ### 上面四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。 **参考博客**:[https://blog.csdn.net/silence723/article/details/52036609][https_blog.csdn.net_silence723_article_details_52036609] [70]: /images/20220520/5bd6c7796f22443891c15a10ab0cc9df.png [https_blog.csdn.net_silence723_article_details_52036609]: https://blog.csdn.net/silence723/article/details/52036609
相关 并发编程:Java多线程同步与死锁示例 Java多线程编程时,经常会遇到同步问题和死锁。这里我将简要解释这两种情况,并提供一个简单的示例。 1. **同步问题**: - 当多个线程访问共享资源,而这些资源的访 红太狼/ 2024年09月18日 00:54/ 0 赞/ 15 阅读
相关 Java多线程编程:同步锁和死锁案例 Java多线程编程中,同步锁主要有两种方式:synchronized关键字和Lock接口。 1. `synchronized`关键字: ```java // 假设有一个共享 迷南。/ 2024年09月17日 14:57/ 0 赞/ 13 阅读
相关 Java多线程问题:线程同步与死锁 在Java多线程编程中,线程同步和死锁是两个关键概念。 1. **线程同步**: - 同步是为了保证多个线程访问共享资源(如内存、文件等)时的顺序性和一致性。 - 曾经终败给现在/ 2024年09月16日 06:03/ 0 赞/ 17 阅读
相关 Java多线程问题:线程同步与死锁 在Java多线程编程中,线程同步和死锁是两个关键概念。 1. 线程同步: - **synchronized关键字**:这是Java中最常用的同步机制。在一个方法或者代码 太过爱你忘了你带给我的痛/ 2024年09月10日 16:45/ 0 赞/ 28 阅读
相关 Java并发编程:线程同步与死锁实例 在Java并发编程中,线程同步和死锁是两个重要的概念。下面我们将通过实例来详细讲解。 1. **线程同步**: - **synchronized关键字**:用于修饰方法 本是古典 何须时尚/ 2024年09月06日 01:42/ 0 赞/ 18 阅读
相关 Java多线程编程:同步机制与死锁问题 在Java多线程编程中,同步机制是控制多个线程对共享资源进行访问的关键。以下是几种常用的同步机制: 1. **synchronized关键字**:可以用于方法或代码块上,保证 ゝ一纸荒年。/ 2024年09月04日 05:39/ 0 赞/ 23 阅读
相关 Java多线程的同步与死锁 知识要点: 了解线程同步的作用 了解同步代码块以及同步方法的使用 了解死锁的产生 在多线程开发中,同步与死锁是至关重要的需要掌握以下几点: 1、哪里需要同步 2、 绝地灬酷狼/ 2022年09月21日 05:36/ 0 赞/ 204 阅读
相关 Java 多线程编程4---同步与死锁 一个多线程的程序如果是通过实现Runable接口实现的,实现类中的属性可以被多个线程共享,这样就造成一个问题,如果这个多线程程序操作同一资源时就有可能出现资源同步的问题。例如之 r囧r小猫/ 2022年05月20日 07:04/ 0 赞/ 196 阅读
相关 java高级编程——多线程(二)之同步与死锁 文章目录 线程的操作状态 线程常见操作方法 线程的命名和获取 线程的休眠 线程的优先级 线程的同步与死锁 ╰+哭是因爲堅強的太久メ/ 2022年01月27日 19:27/ 0 赞/ 217 阅读
还没有评论,来说两句吧...