Java 多线程基础【线程安全】

心已赠人 2022-05-17 09:15 418阅读 0赞

线程安全问题在理想状态下,不容易出现问题,但一旦出现对软件的影响是非常大的。

可能导致线程安全出问题的原因:
1:是否是多线程环境
2:是否有共享数据
3:是否有多条语句操作共享数据

下面是我遇到的问题:

假设有100张票需要卖出,同时我有两个窗口。那么同一张票肯定不能重复卖出,两个窗口也是同时开始卖票的,这几需要多线程来解决。下面是代码:

  1. package threads;
  2. public class Demo {
  3. public static void main(String[] args) {
  4. PrimeThread p = new PrimeThread();
  5. Thread my = new Thread(p, "窗口1");
  6. Thread m = new Thread(p, "窗口2");
  7. my.start();
  8. m.start();
  9. }
  10. }
  11. package threads;
  12. public class PrimeThread implements Runnable{
  13. private int ans = 100;//共享数据
  14. @Override
  15. public void run() {
  16. while(ans > 0) {
  17. try {
  18. Thread.sleep(100);//这里延迟100毫秒,突出以下问题
  19. } catch (InterruptedException e) {
  20. // TODO Auto-generated catch block
  21. e.printStackTrace();
  22. }
  23. System.out.println(Thread.currentThread().getName()+":"+(ans--));//多条语句操作共享数据
  24. }
  25. }
  26. }

按以上代码的输出结果应该是100到1,但是其结果却是:

  1. 窗口2:100
  2. 窗口1:100
  3. 窗口1:99
  4. 窗口2:98
  5. 窗口1:97
  6. 窗口2:96
  7. 窗口1:95
  8. 窗口2:94
  9. 窗口1:93
  10. 窗口2:92
  11. 窗口2:91
  12. 窗口1:90
  13. 窗口1:89
  14. 窗口2:88
  15. 窗口1:87
  16. 窗口2:86
  17. 窗口1:85
  18. 窗口2:84
  19. 窗口1:83
  20. 窗口2:82
  21. 窗口1:81
  22. 窗口2:81
  23. 窗口1:80
  24. 窗口2:79
  25. 窗口2:78
  26. 窗口1:78
  27. 窗口1:77
  28. 窗口2:76
  29. 窗口2:75
  30. 窗口1:74
  31. 窗口1:73
  32. 窗口2:72
  33. 窗口2:71
  34. 窗口1:71
  35. 窗口2:70
  36. 窗口1:70
  37. 窗口1:69
  38. 窗口2:68
  39. 窗口2:66
  40. 窗口1:67
  41. 窗口1:65
  42. 窗口2:64
  43. 窗口1:63
  44. 窗口2:62
  45. 窗口1:61
  46. 窗口2:60
  47. 窗口1:59
  48. 窗口2:58
  49. 窗口1:57
  50. 窗口2:56
  51. 窗口2:55
  52. 窗口1:55
  53. 窗口1:54
  54. 窗口2:53
  55. 窗口1:52
  56. 窗口2:51
  57. 窗口1:50
  58. 窗口2:49
  59. 窗口1:48
  60. 窗口2:48
  61. 窗口1:46
  62. 窗口2:47
  63. 窗口1:45
  64. 窗口2:44
  65. 窗口2:42
  66. 窗口1:43
  67. 窗口2:40
  68. 窗口1:41
  69. 窗口2:39
  70. 窗口1:38
  71. 窗口2:37
  72. 窗口1:36
  73. 窗口2:35
  74. 窗口1:34
  75. 窗口2:33
  76. 窗口1:32
  77. 窗口2:31
  78. 窗口1:30
  79. 窗口2:29
  80. 窗口1:28
  81. 窗口2:27
  82. 窗口1:26
  83. 窗口2:25
  84. 窗口1:24
  85. 窗口2:23
  86. 窗口1:22
  87. 窗口2:21
  88. 窗口1:20
  89. 窗口2:19
  90. 窗口1:18
  91. 窗口2:17
  92. 窗口1:16
  93. 窗口2:15
  94. 窗口1:14
  95. 窗口2:13
  96. 窗口1:12
  97. 窗口2:11
  98. 窗口1:10
  99. 窗口2:9
  100. 窗口1:8
  101. 窗口2:7
  102. 窗口1:6
  103. 窗口2:5
  104. 窗口1:4
  105. 窗口2:3
  106. 窗口1:2
  107. 窗口2:1
  108. 窗口1:0

同一张票被卖了多次,这与我们预期中每张票只输出一次完全不一样。而且虽然我们限定了ans值大于0,但是还有可能出现0,当线程再多的话还可能出现负值。

这就是线程安全出了问题,以为它符合了我们前边说的导致线程安全出问题的全部三种原因,而其中一种就有可能导致线程安全出现问题。

出现重复值的问题:
导致上述问题中同一张票出现多次与CPU的一次操作必须是原子性的特点有关。
在我们刚才的代码中有这样一个语句。

System.out.println(Thread.currentThread().getName()+”:”+(ans–));

这个语句其实是先把ans当前的值输出,然后再执行ans = ans-1。
先假设窗口1这个线程先到达这一句并且操作系统正在执行输出操作,但还没有执行减一操作,这时窗口2这个线程也到达该语句,因为线程具有随机性,所以CPU先执行了两次输出操作,所以就出现了重复值的问题。

出现0和负值的问题:
假设此时ans = 1,窗口1线程进入循环,因为CPU的原子性,后边的语句还没有被执行,但就在这时,窗口2的线程因为ans的值还没有变为0,所以也紧随其后进入了循环,而执行后边的语句是正好是两个线程分先后顺序地执行完了

System.out.println(Thread.currentThread().getName()+”:”+(ans–));

语句,所以就出现了0和负值的情况。

那么既然出现了问题就要解决问题,而之前我们也知道了导致线程不安全的三种原因
1:是否是多线程环境
2:是否有共享数据
3:是否有多条语句操作共享数据

但是第一条和第二条又是必须的,那么就只能从第三条上解决。而解决第三条就需要当一个线程在调用操作共享数据的语句时,其他线程就不能调用。这就需要线程同步。

可以利用Java中的synchronize关键字建一个同步代码块将操作语句锁起来

  1. package threads;
  2. public class PrimeThread implements Runnable{
  3. private int ans = 100;
  4. private Object obj = new Object();
  5. @Override
  6. public void run() {
  7. while(ans > 0) {
  8. synchronized(obj) {
  9. //将操作共享数据的代码锁起来
  10. try {
  11. Thread.sleep(100);//这里延迟100毫秒,突出一下问题
  12. } catch (InterruptedException e) {
  13. // TODO Auto-generated catch block
  14. e.printStackTrace();
  15. }
  16. System.out.println(Thread.currentThread().getName()+":"+(ans--));
  17. }
  18. }
  19. }
  20. }

这样就解决了线程不同步的问题。

同步的特点:

  • 前提:是需要多个线程,在解决问题时需要注意,多个线程使用的是同一个锁对象。
  • 好处:同步的出现解决了多线程的安全问题
  • 弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,只是很耗费资源的,无形中会降低程序的运行效率。

上边说了同步代码块,格式为:synchronize(对象) {}
同步带么快的所对象是任意对象,所以这里的对象就可以是任意的;

同步方法的锁对象是 this
静态方法的锁对象是 类的字节码文件对象

下面进行一个对比:
同步方法:

  1. package threads;
  2. import java.lang.reflect.Method;
  3. public class PrimeThread implements Runnable{
  4. private int ans = 100;
  5. int x = 0;
  6. private Object obj = new Object();
  7. @Override
  8. public void run() {
  9. while(ans > 0) {
  10. if(x%2 == 0) {
  11. synchronized (this) {
  12. //对象
  13. try {
  14. Thread.sleep(100);
  15. } catch (InterruptedException e) {
  16. // TODO Auto-generated catch block
  17. e.printStackTrace();
  18. }
  19. System.out.println(Thread.currentThread().getName()+":"+(ans--));
  20. }
  21. }else {
  22. md();
  23. }
  24. }
  25. }
  26. private synchronized void md() {
  27. try {
  28. Thread.sleep(100);
  29. } catch (InterruptedException e) {
  30. // TODO Auto-generated catch block
  31. e.printStackTrace();
  32. }
  33. System.out.println(Thread.currentThread().getName()+":"+(ans--));
  34. }
  35. }

静态同步方法:

  1. package threads;
  2. import java.lang.reflect.Method;
  3. public class PrimeThread implements Runnable{
  4. private static int ans = 100;
  5. int x = 0;
  6. private Object obj = new Object();
  7. @Override
  8. public void run() {
  9. while(ans > 0) {
  10. if(x%2 == 0) {
  11. synchronized (PrimeThread.class) {
  12. //对象
  13. try {
  14. Thread.sleep(100);
  15. } catch (InterruptedException e) {
  16. // TODO Auto-generated catch block
  17. e.printStackTrace();
  18. }
  19. System.out.println(Thread.currentThread().getName()+":"+(ans--));
  20. }
  21. }else {
  22. md();
  23. }
  24. }
  25. }
  26. private static synchronized void md() {
  27. try {
  28. Thread.sleep(100);
  29. } catch (InterruptedException e) {
  30. // TODO Auto-generated catch block
  31. e.printStackTrace();
  32. }
  33. System.out.println(Thread.currentThread().getName()+":"+(ans--));
  34. }
  35. }

因为静态方法是随着类的建立而建立的,所以对象要是类才行。

刚才的三种同步方法最后都会输出一个0,这是因为ans的判断没有放在同步中,如果你细心的话就会发现
这里写图片描述
输出0和1的窗口永远是不一样的,这种现象在解释为什么会出现0和负数时已经解释过了,只需再同步代码中加一个判断就可以解决。

发表评论

表情:
评论列表 (有 0 条评论,418人围观)

还没有评论,来说两句吧...

相关阅读