小言_互联网的博客

多线程之间的通讯(仿真生产与消费)

312人阅读  评论(0)

源于蚂蚁课堂的学习,点击这里查看(老余很给力)

java内存模型(jmm)


  
  1. java内存模型分为两大类型即,主内存和本地内存
  2. 1.主内存
  3. 也就是主进程所占用的内存
  4. 2.本地内存
  5. 线程中开辟的属于线程自己的内存,其中存放着全局变量的副本数据
  6. 也就是全局共享的数据实在主内存中的,线程中复制一份放入自己的本地内存,线程执行结束后将其变动刷新至主内存。
  7. 这也就是为什么多线程会有线程安全的问题所在。
  8. 多个线程同时做了修改,都去刷新主内存,会造成结果和实际不一致。

 volatile关键字


  
  1. 其功能为多线程可见。
  2. 怎么说呢?实际上就是当线程在本地内存中修改了被 volatile修改的变量后,会立刻将其值刷新至主内存中,其他线程
  3. 也会马上得到这个修改的结果
  4. 实际看上去就像线程在直接修改主线程中的这个变量一样。
  5. 还有就是防止指令重新排序
  6. 代码自上而下执行,这是常识,但是,计算机执行时,可能会将,没有依赖关系的代码指令的执行顺序打乱,但一般不会
  7. 影响结果。
  8. 如: int a= 1; int b= 1;这时无论怎么变化这两条指令,都没有影响。
  9. 如果再来一条 int c=a+b;就会相互之间有了依赖关系,计算机就不会将其重新排序
  10. 而对于那些比较隐晦的,指令重拍可能会引发问题得数据, volatile关键字可以指定其对应的代码不会参与重新排序

 案例


  
  1. package live.yanxiaohui;
  2. /**
  3. * @Description 仿真消息的生产和消费(多线程之间的通讯),即来一个消息,消费一条消息
  4. * @CSDN https://blog.csdn.net/yxh13521338301
  5. * @Author: yanxh<br>
  6. * @Date 2020-05-15 11:08<br>
  7. * @Version 1.0<br>
  8. */
  9. public class Test2 {
  10. public static void main(String[] args) {
  11. Money money = new Money();
  12. new Task1(money).start();
  13. new Task2(money).start();
  14. }
  15. }
  16. class Task1 extends Thread{
  17. private Money money;
  18. private int count;
  19. public Task1(Money money) {
  20. this.money = money;
  21. }
  22. @Override
  23. public void run() {
  24. while ( true){
  25. if(count % 2 == 0){
  26. money.name = "马云";
  27. money.componet = "阿里";
  28. } else {
  29. money.name = "马化腾";
  30. money.componet = "腾讯";
  31. }
  32. count++;
  33. }
  34. }
  35. }
  36. class Task2 extends Thread{
  37. private Money money;
  38. public Task2(Money money) {
  39. this.money = money;
  40. }
  41. @Override
  42. public void run() {
  43. while ( true){
  44. System.out.println(money.name + "," + money.componet);
  45. }
  46. }
  47. }
  48. class Money{
  49. public String name;
  50. public String componet;
  51. }

首先我们运行程序,会发现数据错乱的现象


  
  1. 这是由于多个线程访问全局共享数据造成的线程安全问题。
  2. 说人话:
  3. 写入的线程做了修改,将本地内存改动的数据要同步至主内存,刚好同步一半,这时被挂起,
  4. 读线程从主内存读取数据就是一个错误的数据。

 那么加锁如何?


  
  1. 对共享的money对象加锁。关键代码如下
  2. 写入操作:
  3. synchronized (money){
  4. if(count % 2 == 0){
  5. money.name = "马云";
  6. money.componet = "阿里";
  7. } else {
  8. money.name = "马化腾";
  9. money.componet = "腾讯";
  10. }
  11. }
  12. 读操作:
  13. synchronized (money){
  14. System.out.println(money.name + "," + money.componet);
  15. }

 一定会有读者有疑问:读为什么还要加锁?


  
  1. 此处对数据的修改和读取是两个变量,这两个变量需要保证其原子性,否则依旧是可能出现线程安全的
  2. 比如:如果读不加锁,那么其读到第一个属性值后,被挂起,写入的线程做了修改,读线程继续执行后
  3. 就会出现第二个属性和第一个属性不同步的现象

 运行程序

 

数据是保证一致性了,但依然不满足我们消费的机制,所以需要再优化下 

 


  
  1. 我们可以使用wait和notify配合进行多个线程之间对同一共享的全局数据进行通讯
  2. package live.yanxiaohui;
  3. /**
  4. * @Description 仿真消息的生产和消费(多线程之间的通讯),即来一个消息,消费一条消息
  5. * @CSDN https://blog.csdn.net/yxh13521338301
  6. * @Author: yanxh<br>
  7. * @Date 2020-05-15 11:08<br>
  8. * @Version 1.0<br>
  9. */
  10. public class Test2 {
  11. public static void main(String[] args) {
  12. Money money = new Money();
  13. new Task1(money).start();
  14. new Task2(money).start();
  15. }
  16. }
  17. class Task1 extends Thread{
  18. private Money money;
  19. private int count;
  20. public Task1(Money money) {
  21. this.money = money;
  22. }
  23. @Override
  24. public void run() {
  25. try{
  26. while ( true){
  27. synchronized (money){
  28. // 方便查看效果,设置阻塞时间
  29. Thread.sleep( 100);
  30. if(!money.success){
  31. // 交出对象的使用权,等待消息消费
  32. money.wait();
  33. }
  34. if(count % 2 == 0){
  35. money.name = "马云";
  36. money.componet = "阿里";
  37. } else {
  38. money.name = "马化腾";
  39. money.componet = "腾讯";
  40. }
  41. money.success = false;
  42. // 唤醒此对象所有等待的线程
  43. money.notify();
  44. }
  45. count++;
  46. }
  47. } catch (Exception e){
  48. e.printStackTrace();
  49. }
  50. }
  51. }
  52. class Task2 extends Thread{
  53. private Money money;
  54. public Task2(Money money) {
  55. this.money = money;
  56. }
  57. @Override
  58. public void run() {
  59. try {
  60. while ( true){
  61. synchronized (money){
  62. if(money.success){
  63. // 交出对象的使用权,等待消息生产
  64. money.wait();
  65. }
  66. System.out.println(money.name + "," + money.componet);
  67. money.success = true;
  68. // 唤醒此对象所有等待的线程
  69. money.notify();
  70. }
  71. }
  72. } catch (InterruptedException e) {
  73. e.printStackTrace();
  74. }
  75. }
  76. }
  77. class Money{
  78. public String name;
  79. public String componet;
  80. // true表示可以写,false表示可以读
  81. public boolean success;
  82. }

 

 由此可以看出,线程执行是抢夺CPU分配权的,和主线程代码的执行顺序没太大关系

 

我们可以使用lock锁,去替代sync 

 


  
  1. package live.yanxiaohui;
  2. import java.util.concurrent.locks.Condition;
  3. import java.util.concurrent.locks.Lock;
  4. import java.util.concurrent.locks.ReentrantLock;
  5. /**
  6. * @Description 仿真消息的生产和消费(多线程之间的通讯),即来一个消息,消费一条消息
  7. * @CSDN https://blog.csdn.net/yxh13521338301
  8. * @Author: yanxh<br>
  9. * @Date 2020-05-15 11:08<br>
  10. * @Version 1.0<br>
  11. */
  12. public class Test3 {
  13. public static void main(String[] args) {
  14. Money money = new Money();
  15. new Task1(money).start();
  16. new Task2(money).start();
  17. }
  18. }
  19. class Task1 extends Thread {
  20. private Money money;
  21. private int count;
  22. public Task1(Money money) {
  23. this.money = money;
  24. }
  25. @Override
  26. public void run() {
  27. while ( true) {
  28. try {
  29. // 对象上锁
  30. money.lock.lock();
  31. // 方便查看效果,设置阻塞时间
  32. Thread.sleep( 100);
  33. if (!money.success) {
  34. // 交出对象的使用权,等待消息消费
  35. money.condition.await();
  36. }
  37. if (count % 2 == 0) {
  38. money.name = "马云";
  39. money.componet = "阿里";
  40. } else {
  41. money.name = "马化腾";
  42. money.componet = "腾讯";
  43. }
  44. money.success = false;
  45. // 唤醒此对象所有等待的线程
  46. money.condition.signal();
  47. count++;
  48. } catch (Exception e) {
  49. e.printStackTrace();
  50. } finally {
  51. // 释放锁
  52. money.lock.unlock();
  53. }
  54. }
  55. }
  56. }
  57. class Task2 extends Thread {
  58. private Money money;
  59. public Task2(Money money) {
  60. this.money = money;
  61. }
  62. @Override
  63. public void run() {
  64. try {
  65. while ( true) {
  66. // 对象上锁
  67. money.lock.lock();
  68. if (money.success) {
  69. // 交出对象的使用权,等待消息生产
  70. money.condition.await();
  71. }
  72. System.out.println(money.name + "," + money.componet);
  73. money.success = true;
  74. // 唤醒此对象所有等待的线程
  75. money.condition.signal();
  76. }
  77. } catch (InterruptedException e) {
  78. e.printStackTrace();
  79. } finally {
  80. // 释放锁
  81. money.lock.unlock();
  82. }
  83. }
  84. }
  85. class Money {
  86. public String name;
  87. public String componet;
  88. // true表示可以写,false表示可以读
  89. public boolean success;
  90. public Lock lock = new ReentrantLock();
  91. public Condition condition = lock.newCondition();
  92. }

运行结果也是相同

 

总结


  
  1. 1.多线程之间实现通讯基于对全局共享数据的锁定,即多个线程都必须去争夺锁,获取锁之后依照业务进行对应的等待或运行。
  2. 2.wait和notify必须要配合 synchronized使用, 且必须要在同步代码块中执行
  3. 3.lock锁因为是手动添加的,所以它可控,实际开发中可用到的最多
  4. 4.wait底层是将对象的锁释放掉,自身线程进入阻塞等待状态,直到被对象的notify唤醒
  5. 5.sleep只是线程的定时阻塞,不会释放锁
  6. 6. volatile指定全局共享变量在内存模型中线程间数据的可见性
  7. 7. volatile还可以防止指令重新排序

 


转载:https://blog.csdn.net/yxh13521338301/article/details/106137976
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场