一、演示多线程同步问题 多线程的同步问题是指多个线程同时修改一个数据的时候,可能导致的问题,多线程的问题,也叫并发(Concurrency)问题。
假设盖伦有 10000 滴血,并且在基地里,同时又被对方多个英雄攻击,相当于:
有多个线程在减少盖伦的 hp,同时又有多个线程在恢复盖伦的 hp
假设线程的数量是一样的,并且每次改变的值都是 1,那么所有线程结束后,盖伦的 hp 应该还是 10000。
但实际结果并非如此
注意: 不是每一次运行都会看到错误的数据产生,多运行几次,或者增加运行的次数
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 105 106 107 108 109 110 111 112 113 class Hero { public String name; public float hp; public int damage; public void recover () { hp = hp + 1 ; } public void hurt () { hp = hp - 1 ; } public void attackHero (Hero h) { h.hp -= damage; System.out.format("%s 正在攻击 %s, %s的血变成了 %.0f%n" , name, h.name, h.name, h.hp); if (h.isDead()) System.out.println(h.name + "死了!" ); } public boolean isDead () { return 0 >= hp; } }public class TestThread { public static void main (String[] args) { final Hero gareen = new Hero (); gareen.name = "盖伦" ; gareen.hp = 10000 ; System.out.printf("盖伦的初始血量是 %.0f%n" , gareen.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 () { gareen.recover(); try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } } }; t.start(); addThreads[i] = t; } for (int i = 0 ; i < n; i++) { Thread t = new Thread () { public void run () { gareen.hurt(); try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } } }; t.start(); reduceThreads[i] = t; } for (Thread t : addThreads) { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } } for (Thread t : reduceThreads) { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.printf("%d个加血线程和%d个减线程结束后%n盖伦的血量变成了 %.0f%n" , n, n, gareen.hp); } } 盖伦的初始血量是 10000 10000 个加血线程和10000 个减血线程结束后 盖伦的血量变成了 9999
二、分析同步问题产生的原因 如下图:
1、假设加血线程先进入,得到的 hp 是 10000,进行增加运算
2、正在做增加运算的时候,还没有来得及修改 hp 的值,减血线程来了
3、减血线程得到的 hp 也是 10000,减血线程进行减少运算
4、加血线程运算结束,得到值 10001,并把这个值赋予 hp
5、减血线程运算结束,得到值 9999,并把这个值赋予 hp,那么 hp 最终的值就是 9999
虽然经历了两个线程各自增减了一次,本来期望还是原值 10000,但是却得到了 9999,这个时候的值是一个错误的值,在业务上又叫做脏数据
三、解决思路 总体解决思路是:在加血线程访问 hp 期间,其他线程不可以访问 hp
如下图:
1、加血线程获得 hp 并进行运算,在运算期间,减血线程试图来获取 hp ,但不被允许
2、加血线程运算结束,并成功修改 hp 的值为 10001
3、减血线程在加血线程做完后,才能访问 hp 的值即 10001
4、减血线程做减少运算,并得到新的值 10000
四、synchronized 同步对象概念 解决上述问题之前,先理解 synchronized 关键字的意义,如下代码:
1 2 3 4 Object someObject = new Object ();synchronized (someObject){ }
1)、synchronized 表示当前线程,独占对象 someObject,当前对象独占了 someObject,如果有其他线程试图占有 someObject ,就会等待,直到当前线程释放 someObject 的占用。
2)、someObject 又叫同步对象,所有的对象都可以作为同步对象
3)、为了达到同步的效果,必须使用同一个同步对象
4)、释放同步对象的方式: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 49 50 51 52 53 54 55 56 57 58 59 60 61 public class SynchronizedTest { 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 () { @Override public void run () { try { System.out.println(now() + " " + getName() + " 试图占有对象:someObject" ); synchronized (someObject) { System.out.println(now() + " " + getName() + " 占有对象:someObject" ); Thread.sleep(5000 ); System.out.println(now() + " " + getName() + " 释放对象:someObject" ); } System.out.println(now() + " " + getName() + " 线程结束" ); } catch (InterruptedException e) { e.printStackTrace(); } } }; t1.setName("t1" ); t1.start(); Thread t2 = new Thread () { @Override public void run () { try { System.out.println(now() + " " + getName() + " 试图占有对象:someObject" ); synchronized (someObject) { System.out.println(now() + " " + getName() + " 占有对象:someObject" ); Thread.sleep(5000 ); System.out.println(now() + " " + getName() + " 释放对象:someObject" ); } System.out.println(now() + " " + getName() + " 线程结束" ); } catch (InterruptedException e) { e.printStackTrace(); } } }; t2.setName("t2" ); t2.start(); } }10 :42 :42 t2 试图占有对象:someObject10 :42 :42 t1 试图占有对象:someObject10 :42 :42 t2 占有对象:someObject10 :42 :47 t2 释放对象:someObject10 :42 :47 t2 线程结束10 :42 :47 t1 占有对象:someObject10 :42 :52 t1 释放对象:someObject10 :42 :52 t1 线程结束
根据打印结果我们可以知道:只有当 t2 释放对象之后,t1 才能占有对象
五、使用 synchronized 解决同步问题 所有需要修改 hp 的地方,是建立在占有 someObject 的基础上,someObject 在同一时间,只能被一个线程占有。间接地,导致同一时间, hp 只能被一个线程修改
代码实现:
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 public class TestThread { public static void main (String[] args) { final Object someObject = new Object (); final Hero gareen = new Hero (); gareen.name = "盖伦" ; gareen.hp = 10000 ; 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 () { synchronized (someObject) { gareen.recover(); } try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } } }; t.start(); addThreads[i] = t; } for (int i = 0 ; i < n; i++) { Thread t = new Thread () { public void run () { synchronized (someObject) { gareen.hurt(); } try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } } }; t.start(); reduceThreads[i] = t; } for (Thread t : addThreads) { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } } for (Thread t : reduceThreads) { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量是 %.0f%n" , n, n, gareen.hp); } }10000 个增加线程和10000 个减少线程结束后 盖伦的血量是 10000
六、使用 hero 对象作为同步对象 既然任意对象都可以用来作为同步对象,而所有的线程访问的都是同一个 hero 对象,索性就使用 gareen 来作为同步对象 ,进一步的,对于 Hero 的 hurt 方法,加上:
1 2 3 synchronized (this ) { }
表示当前对象为同步对象,即也是 gareen 为同步对象。
代码实现:
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 class Hero { public void hurt () { synchronized (this ){ hp = hp - 1 ; } } }public class TestThread { public static void main (String[] args) { final Hero gareen = new Hero (); gareen.name = "盖伦" ; gareen.hp = 10000 ; 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 () { synchronized (gareen) { gareen.recover(); } try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } } }; t.start(); addThreads[i] = t; } for (int i = 0 ; i < n; i++) { Thread t = new Thread () { public void run () { gareen.hurt(); try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } } }; t.start(); reduceThreads[i] = t; } for (Thread t : addThreads) { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } } for (Thread t : reduceThreads) { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.printf("%d个增加线程和%d个减少线程结束后%n盖伦的血量是 %.0f%n" , n, n, gareen.hp); } }10000 个增加线程和10000 个减少线程结束后 盖伦的血量是 10000
七、在方法前,加上修饰符 synchronized 在 recover 前,直接加上 synchronized ,其所对应的同步对象,就是 this,和 hurt 方法达到的效果是一样,外部线程访问 gareen 的方法,就不需要额外使用 synchronized 了。
代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Hero { public synchronized void recover () { hp=hp+1 ; } public void hurt () { synchronized (this ) { hp=hp-1 ; } } }
八、总结 本篇文章我们通过演示多线程同步问题,分析产生的原因,解决思路,最后引出 synchronized 修饰符,并通过 synchronized 修饰符解决了同步问题
好了,本篇文章到这里就结束了,感谢你的阅读🤝