李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
09.并发编程之共享问题
Leefs
2022-10-10 PM
559℃
0条
[TOC] ### 一、共享带来的问题 **通过下方示例来演示共享变量产生的问题** > 两个线程对初始值为0的静态变量一个做自增,一个做自减,各做5000次 ```java @Slf4j(topic = "c.Test01") public class Test01 { static int counter = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i=0; i < 5000; i++){ counter ++; } },"t1"); Thread t2 = new Thread(() -> { for (int i=0; i< 5000; i++){ counter --; } },"t2"); t1.start(); t2.start(); t1.join(); t2.join(); log.debug("{}",counter); } } ``` **运行结果1** ``` 10:13:47.774 c.Test01 [main] - -474 ``` **运行结果2** ``` 10:14:24.914 c.Test01 [main] - 765 ``` **问题分析** + 以上的结果可能是正数、负数、零。为什么呢? 因为 **Java 中对静态变量的自增,自减并不是原子操作** + 要彻底理解,必须从字节码来进行分析 例如对于 `i++` 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令 ```less getstatic i //获取静态变量i的值 iconst_1 //准备常量1 iadd //自增 putstatic i //将修改后的值存入静态变量i ``` 而对应 `i--` 也是类似 ```less getstatic i //获取静态变量i的值 iconst_1 //java准备常量1 isub //自减 putstatic i //将修改后的值存入静态变量i ``` 而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换 ![09.并发编程之共享问题01.png](https://lilinchao.com/usr/uploads/2022/10/1697996521.png) + 如果是单线程执行,以上八行指令顺序执行,不会产生指令交错,运行不会产生问题 ![09.并发编程之共享问题02.png](https://lilinchao.com/usr/uploads/2022/10/477730753.png) + 在多线程情况下,会产生指令交错,所以运行结果可能会存在负数或正数的情况 **出现负数的情况** ![09.并发编程之共享问题03.png](https://lilinchao.com/usr/uploads/2022/10/4077734625.png) + **出现正数的情况** ![09.并发编程之共享问题04.png](https://lilinchao.com/usr/uploads/2022/10/1411837744.png) **通过出现正数的情况对运行过程进行分析** + 当 CPU 时间片分给 t1 线程时,t1 线程去读取变量值为 0 并且执行 ++ 的操作,如上在字节码自增操作中; + 当 t1 执行完自增,还没来得急将修改后的值存入静态变量时,假如线程的时间片用完了; + 此时CPU 将时间片分配给 t2 线程,t2 线程拿到时间片执行自减操作,并且将修改后的值存入静态变量,此时 count 的值为 -1; + 当 CPU 将时间片再次分给经历了上下文切换的 t1 线程时,t1 将修改后的值存入静态变量,此时 counter 的值为 1,覆盖了 t2 线程执行的结果,出现了丢失更新。 *以上就是多线对共享资源读取的问题。* ### 二、临界区和竞争条件 #### 2.1 临界区(Critical Section) - 一个程序运行多个线程本身是没有问题的 - 问题出在多个线程访问**共享资源** - 多个线程读**共享资源**其实也没有问题 - 在多个线程对**共享资源**读写操作时发生指令交错,就会出现问题 - 一段代码块内如果存在对**共享资源**的多线程读写操作,称这段代码块为**临界区** **临界区代码示例** ```java static int counter = 0; static void increment() //临界区 { counter++; } static void decrement() //临界区 { counter--; } ``` #### 2.2 竞态条件 Race Condition 多个线程在临界区内执行,由于代码的**执行序列不同**而导致结果无法预测,称之为发生了**竞态条件**。 ### 三、synchronize解决方案 #### 3.1 应用之互斥 为了避免临界区的竞态条件发生,有多种手段可以达到目的。 - 阻塞式的解决方案:`synchronized`,`Lock` - 非阻塞式的解决方案:原子变量。 本次使用synchronized来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获得这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全地执行临界区内的代码,不用担心线程上下文切换。 > 注意: > > 虽然Java中互斥和同步都可以采用synchronized关键字来完成,但它们还是有区别的。 > > - 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码; > - 同步是由于线程执行的先后、顺序不同,需要一个线程等待其他线程运行到某个点。 #### 3.2 synchronized语法 ```java synchronized(对象) { // 线程1, 线程2(blocked) 临界区 } ``` synchronized实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,**不会被线程切换所打断**。 ##### 方法上的synchronized + **加在成员方法上,锁的是this对象** ```java class Test{ public synchronized void test() { } } //等价于 class Test{ public void test() { synchronized(this) { } } } ``` + **加在静态方法上,锁的是类对象** ```java class Test{ public synchronized static void test() { } } //等价于 class Test{ public static void test() { synchronized(Test.class) { } } } ``` **不加 synchronized 的方法** 不加 `synchronzied` 的方法就好比不遵守规则的人,不去老实排队(好比翻窗户进去的) #### 3.3 共享问题解决方法 + **方案一** ```java @Slf4j(topic = "c.Test02") public class Test02 { static int counter = 0; //创建一个共享对象 static final Object lock = new Object(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i=0; i < 5000; i++){ synchronized (lock){ counter ++; } } },"t1"); Thread t2 = new Thread(() -> { for (int i=0; i< 5000; i++){ synchronized (lock){ counter --; } } },"t2"); t1.start(); t2.start(); t1.join(); t2.join(); log.debug("{}",counter); } } ``` **运行结果** ``` 10:57:40.880 c.Test02 [main] - 0 ``` **分析** 上面方案,直接创建一个共享Object对象,在临界区增加synchronized对象锁,可以保证++和--操作不会产生指令错乱。 + **方案二** 方案二是对方案一的改进,把需要保护的共享变量放入一个类,由面向过程改为面向对象 ```java @Slf4j(topic = "c.Test03") public class Test03 { public static void main(String[] args) throws InterruptedException { Room room = new Room(); Thread t1 = new Thread(() -> { for (int i=0; i < 5000; i++){ room.increment(); } },"t1"); Thread t2 = new Thread(() -> { for (int i=0; i< 5000; i++){ room.decrement(); } },"t2"); t1.start(); t2.start(); t1.join(); t2.join(); log.debug("{}",room.getCounter()); } } class Room{ private int counter = 0; public void increment(){ synchronized (this){ counter ++; } } public void decrement(){ synchronized (this){ counter --; } } public int getCounter() { synchronized (this){ return counter; } } } ``` **运行结果** ``` 11:06:18.639 c.Test03 [main] - 0 ``` *附参考原文链接* *《黑马程序员并发编程》*
标签:
并发编程
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://lilinchao.com/archives/2472.html
上一篇
08.并发编程之线程状态
下一篇
10.并发编程之线程八锁
取消回复
评论啦~
提交评论
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
标签云
二叉树
DataX
Hbase
DataWarehouse
数学
Java阻塞队列
Spark Core
Netty
数据结构
Spark
VUE
GET和POST
Scala
锁
Map
人工智能
Spark SQL
Jquery
队列
Livy
ClickHouse
机器学习
查找
线程池
Docker
Kibana
Redis
MySQL
Java编程思想
链表
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞