李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
Java
正文
09.并发编程之共享问题
Leefs
2022-10-10 PM
966℃
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
NLP
4
标签云
DataX
Azkaban
Hive
Java
pytorch
Quartz
Http
算法
字符串
正则表达式
Yarn
Eclipse
Elasticsearch
JVM
Netty
稀疏数组
哈希表
gorm
容器深入研究
并发编程
Stream流
Flume
Docker
Scala
序列化和反序列化
设计模式
Jquery
MySQL
Filter
SpringCloud
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞
评论已关闭