ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] > 多线程的同步问题指的是多个线程同时修改一个数据的时候,可能导致的问题 > 多线程的问题,又叫Concurrency 问题 # 演示同步问题 假设盖伦有10000滴血,并且在基地里,同时又被对方多个英雄攻击 就是 **有多个线程在减少盖伦的hp** 同时又有 **多个线程在恢复盖伦的hp** 假设线程的数量是一样的,并且每次改变的值都是1,那么所有线程结束后,盖伦应该还是10000滴血。 但是。。。 > 注意: 不是每一次运行都会看到错误的数据产生,多运行几次,或者增加运行的次数 ``` package multiplethread; import charactor.Hero4; public class TestThread8 { public static void main(String[] args) { Hero4 garen = new Hero4(); garen.name = "盖伦"; garen.hp = 10000; System.out.printf("盖伦的初始血量是 %.0f%n", garen.hp); // 多线程同步问题指的是多个线程同时修改一个数据的时候,导致的问题 // 假设盖伦有10000滴血,并且在基地里,同时又被对方多个英雄攻击 // 用JAVA代码来表示,就是有多个线程在减少盖伦的hp // 同时又有多个线程在恢复盖伦的hp // n个线程增加盖伦的hp int n = 10000; Thread[] addThreads = new Thread[n]; Thread[] reduceThreads = new Thread[n]; for (int i = 0; i < n; i++) { Thread t = new Thread() { public void run() { garen.recover(); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; t.start(); addThreads[i] = t; } // n个线程减少盖伦的hp for (int i = 0; i < n; i++) { Thread t = new Thread() { public void run() { garen.hurt(); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; t.start(); reduceThreads[i] = t; } // 等待所有增加线程结束 for (Thread t : addThreads) { try { t.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } // 等待所有减少线程结束 for (Thread t : reduceThreads) { try { t.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } // 代码执行到这里,所有增加和减少线程都结束了 // 增加和减少线程的数量是一样的,每次都增加,减少1. // 那么所有线程都结束后,盖伦的hp应该还是初始值 // 但是事实上观察到的是: System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量变成了 %.0f%n", n, n, garen.hp); } } ``` # 分析同步问题产生的原因 1. 假设**增加线程**先进入,得到的hp是10000 2. 进行增加运算 3. 正在做增加运算的时候,**还没有来得及修改hp的值,减少线程**来了 4. 减少线程得到的hp的值也是10000 5. 减少线程进行减少运算 6. 增加线程运算结束,得到值10001,并把这个值赋予hp 7. 减少线程也运算结束,得到值9999,并把这个值赋予hp hp,最后的值就是9999 虽然经历了两个线程各自增减了一次,本来期望还是原值10000,但是却得到了一个9999 这个时候的值9999是一个错误的值,在业务上又叫做**脏数据** ![](../images/787.png) # synchronized 同步对象概念 解决上述问题之前,先理解 **synchronized**关键字的意义 如下代码: ``` Object someObject =new Object(); synchronized (someObject){ //此处的代码只有占有了someObject后才可以执行 } ``` **synchronized表示当前线程,独占 对象 someObject** 当前线程**独占** 了对象someObject,如果有**其他线程试图占有对象**someObject,**就会等待**,直到当前线程释放对someObject的占用。 someObject 又叫同步对象,所有的对象,都可以作为同步对象 为了达到同步的效果,必须使用同一个同步对象 **释放同步对象**的方式: synchronized 块自然结束,或者有异常抛出 ![](../images/789.png) ``` package multiplethread; import java.text.SimpleDateFormat; import java.util.Date; public class TestThread10 { public static String now() { return new SimpleDateFormat("HH:mm:ss").format(new Date()); } public static void main(String[] args) { final Object someObject = new Object(); Thread t1 = new Thread() { public void run() { try { System.out.println(now() + " t1 线程已经运行"); System.out.println(now() + this.getName() + " 试图占有对象:someObject"); synchronized (someObject) { System.out.println(now() + this.getName() + " 占有对象:someObject"); Thread.sleep(5000); System.out.println(now() + this.getName() + " 释放对象:someObject"); } System.out.println(now() + " t1 线程结束"); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; t1.setName(" t1"); t1.start(); Thread t2 = new Thread() { public void run() { try { System.out.println(now() + " t2 线程已经运行"); System.out.println(now() + this.getName() + " 试图占有对象:someObject"); synchronized (someObject) { System.out.println(now() + this.getName() + " 占有对象:someObject"); Thread.sleep(5000); System.out.println(now() + this.getName() + " 释放对象:someObject"); } System.out.println(now() + " t2 线程结束"); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; t2.setName(" t2"); t2.start(); } } ``` # 使用synchronized 解决同步问题 所有需要修改hp的地方,有要**建立在占有someObject的基础上**。 而对象 someObject在同一时间,只能被一个线程占有。 间接地,**导致同一时间,hp只能被一个线程修改**。 ``` package multiplethread; import charactor.Hero4; public class TestThread11 { public static void main(String[] args) { Object someObject = new Object(); Hero4 garen = new Hero4(); garen.name = "盖伦"; garen.hp = 10000; System.out.printf("盖伦的初始血量是 %.0f%n", garen.hp); int n = 10000; Thread[] addThreads = new Thread[n]; Thread[] reduceThreads = new Thread[n]; for (int i = 0; i < n; i++) { Thread t = new Thread() { public void run() { // 任何线程要修改hp的值,必须先占用someObject synchronized (someObject) { garen.recover(); } try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; t.start(); addThreads[i] = t; } for (int i = 0; i < n; i++) { Thread t = new Thread() { public void run() { // 任何线程要修改hp的值,必须先占用someObject synchronized (someObject) { garen.hurt(); } try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; t.start(); reduceThreads[i] = t; } for (Thread t : addThreads) { try { t.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } for (Thread t : reduceThreads) { try { t.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量是 %.0f%n", n, n, garen.hp); } } ``` # 使用hero对象作为同步对象 既然任意对象都可以用来作为同步对象,而所有的线程访问的都是同一个hero对象,**索性就使用garen来作为同步对象** 进一步的,对于Hero的hurt方法,加上: ``` synchronized (this) { } ``` 表示当期对象为同步对象,即也是garen为同步对象 ``` package multiplethread; import charactor.Hero5; public class TestThread12 { public static void main(String[] args) { Hero5 garen = new Hero5(); garen.name = "盖伦"; garen.hp = 10000; System.out.printf("盖伦的初始血量是 %.0f%n", garen.hp); int n = 10000; Thread[] addThreads = new Thread[n]; Thread[] reduceThreads = new Thread[n]; for (int i = 0; i < n; i++) { Thread t = new Thread() { public void run() { // 使用garen作为synchronized synchronized (garen) { garen.recover(); } try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; t.start(); addThreads[i] = t; } for (int i = 0; i < n; i++) { Thread t = new Thread() { public void run() { // 使用garen作为synchronized // 在方法hurt中有synchronized(this) garen.hurt(); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; t.start(); reduceThreads[i] = t; } for (Thread t : addThreads) { try { t.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } for (Thread t : reduceThreads) { try { t.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量是 %.0f%n", n, n, garen.hp); } } ``` # 在方法前,加上修饰符synchronized 在recover前,直接加上synchronized ,其所对应的同步对象,就是this 和hurt方法达到的效果是一样 外部线程访问garen的方法,就不需要额外使用synchronized 了 ``` package multiplethread; import charactor.Hero6; public class TestThread13 { public static void main(String[] args) { Hero6 garen = new Hero6(); garen.name = "盖伦"; garen.hp = 10000; System.out.printf("盖伦的初始血量是 %.0f%n", garen.hp); int n = 10000; Thread[] addThreads = new Thread[n]; Thread[] reduceThreads = new Thread[n]; for (int i = 0; i < n; i++) { Thread t = new Thread() { public void run() { // recover自带synchronized garen.recover(); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; t.start(); addThreads[i] = t; } for (int i = 0; i < n; i++) { Thread t = new Thread() { public void run() { // hurt自带synchronized garen.hurt(); try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }; t.start(); reduceThreads[i] = t; } for (Thread t : addThreads) { try { t.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } for (Thread t : reduceThreads) { try { t.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量是 %.0f%n", n, n, garen.hp); } } ``` # 线程安全的类 如果一个类,其**方法都是有synchronized修饰的**,那么该类就叫做**线程安全的类 ** 同一时间,只有一个线程能够进入 **这种类的一个实例** 的去修改数据,进而保证了这个实例中的数据的安全(不会同时被多线程修改而变成脏数据) 比如StringBuffer和StringBuilder的区别 StringBuffer的方法都是有synchronized修饰的,StringBuffer就叫做线程安全的类 而StringBuilder就不是线程安全的类 ``` ```