当前位置:网站首页>synchronized 解决共享带来的问题
synchronized 解决共享带来的问题
2022-07-06 08:10:00 【兀坐晴窗独饮茶】
共享带来的问题
小案例
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
package cn.knightzz.example;
import lombok.extern.slf4j.Slf4j;
@SuppressWarnings("all")
@Slf4j(topic = "c.TestShareValue")
public class TestShareValue {
static int value = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
value++;
log.debug("value + 1 = {}", value);
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
value--;
log.debug("value - 1 = {}", value);
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("value : {} ", value);
}
}
问题分析
以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理
解,必须从字节码来进行分析
例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
而对应 i-- 也是类似:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i
而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:
出现负数的情况 :
临界资源
一个程序运行多个线程本身是没有问题的
- 问题出在多个线程访问共享资源
多个线程读共享资源其实也没有问题
- 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
static int counter = 0;
public void increment(){
// 临界区
counter++;
}
public void decrement(){
counter--;
}
竞态条件
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
synchronized 解决方案
互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的 :
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
synchronized对象锁 , 采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程再想获取这个对象锁时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
synchronized(对象) // 线程1, 线程2(blocked)
{
临界区
}
小案例
package cn.knightzz.test;
import lombok.extern.slf4j.Slf4j;
@SuppressWarnings("all")
@Slf4j(topic = "c.TestSynchronized")
public class TestSynchronized {
private static int counter = 0;
public static void main(String[] args) throws InterruptedException {
method2();
}
private static void method1() throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
log.debug("counter add : {} ", counter);
counter++;
}
log.debug("t1 thread end ... ");
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
log.debug("counter sub : {} ", counter);
counter--;
}
log.debug("t2 thread end ... ");
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// 最终结果可能是 正数, 负数或者 0
log.debug("method1 : counter = {}", counter);
// 重置counter
counter = 0;
}
private static void method2() throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 500000; i++) {
synchronized (TestSynchronized.class) {
// 使用静态类对象作为 锁
log.debug("counter add : {} ", counter);
counter++;
}
}
log.debug("t1 thread end ... ");
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 500000; i++) {
synchronized (TestSynchronized.class) {
log.debug("counter sub : {} ", counter);
counter--;
}
}
log.debug("t2 thread end ... ");
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// 最终结果可能是 正数, 负数或者 0
log.debug("method2 : counter = {}", counter);
// 重置counter
counter = 0;
}
}
synchronized 可以被认为是一个房间的钥匙, 临界区可以被认为是一个房间
- 当线程拿到锁以后, 会进入房间然后关上门, 此时其他线程无法进入临界区执行临界区的代码
- 如上面的图, 线程2到了门口, 会发现门在关着, 并且线程2无法获取锁, 所以只能被阻塞, 等待线程1执行结束释放锁
- 另外需要注意的是 : 并不是拿到锁就可以一直执行下去, CPU分配给线程1的时间片结束后, 线程1就会被踢出临界区, 然后门重新被锁住.
- 此时线程仍然拿着锁, 等到线程1再次获得CPU时间片后, 执行完临界区的代码, 释放锁后, 线程2才可以获取锁进入临界区去执行临界区的代码
图解如下 :
原子性的理解
for (int i = 0; i < 50000; i++) {
synchronized (TestSynchronized.class) {
// 使用静态类对象作为 锁
log.debug("counter add : {} ", counter);
counter++;
}
}
如上面的代码, 此时 synchronized
包裹的是 counter++;
这行代码 , 操作的是 counter
这个临界资源
counter++
在jvm里面有四步指令 :
- get static i 获取静态变量i
- iconst 准备常量
- iadd 自增
- putstatic 将i的值写入到 静态变量
原子性就是要保证, 这四步是完整不可分割的整体, 在这四步执行完以前, 任何线程都无法拿到锁 , 一旦这四步执行结束, 那么锁就会被释放, 所以 t1线程和t2线程可以交替混合打印
思考与总结
synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切
换所打断
思考下面的问题 : ?
- 如果把 synchronized(obj) 放在 for 循环的外面,如何理解?-- 原子性
- 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?-- 锁对象
- 如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?-- 锁对象
问题1 :
- 如果把 synchronized(obj) 放在 for 循环的外面,如何理解?-- 原子性
上图需要注意的是 : 当一次counter++
或者counter--
执行结束以后, 对象锁就会被释放, 所以才会出现两个线程交替打印, 如果放在for循环外的话, 那么for循环就是一个整体, 具有原子性, 在执行结束前, 别的线程拿不到锁, 所以 此时是先后打印, 打印完线程1以后, 才会打印线程2
我们可以把存放临界区代码的地方看做一个房子, 这个房子只有一道门, 房子里面存放不同的临界区代码资源
// t1线程
synchronized (TestSynchronized.class) {
// 使用静态类对象作为 锁
log.debug("counter add : {} ", counter);
counter++;
}
// t2 线程
synchronized (TestSynchronized.class) {
log.debug("counter sub : {} ", counter);
counter--;
}
可以看到上面的代码, 使用的都是 TestSynchronized.class
这个类对象, TestSynchronized.class
是锁对象, 这个锁对象对应着一个房间(临界区) , 房间内存放着synchronized
包裹的临界区代码, 每当对应线程想要执行对应的代码的时候, 都要先获取锁.
回到问题 , 如果synchronized
在for
循环外包裹, 则
for
循环是临界代码, 当线程获取锁后, 会直接把for循环执行完, 锁才会释放- 整个for循环会被视为一个整体, 单个线程的for循环执行结束之前, 是不会受到其他线程的干扰
- 直观可以看到的就是 程序会先打印t1或者t2的输出, 等到t1或者t2执行完以后, 才会执行另外的线程的代码
假如是包裹在counter++
上 :
synchronized (TestSynchronized.class) {
log.debug("counter sub : {} ", counter);
counter--;
}
getstatic i // 获取i变量
iconst_1 // 准备常量1
iadd // i 自增1
putstatic i // 将1写入到i
因为加了 synchronized
, 上面的四行JVM指令是一个整体 , 不存在这四行指令尚未执行完毕的时候被其他线程干扰, 比如 : 执行到**iadd**
后 准备执行**putstatic i**
时线程上下文切换, 指令中断! , 这四个指令执行完以前, 不会被任何线程打断
可以看到 t1 和 t2 实际上是混合交替运行的
如果是包裹在 for 循环外的话 :
synchronized (TestSynchronized.class) {
for (int i = 0; i < 5000; i++) {
// 使用静态类对象作为 锁
log.debug("counter add : {} ", counter);
counter++;
}
}
如上面的代码, 相当于 for 循环被视为一个整体, 具有原子性,那么, 在for循环执行结束前, 不会受到其他线程的干扰
直观的感受, t1执行完毕以后, t2才获得锁对象开始执行
问题2 :
- 如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?-- 锁对象
如果两个锁对象不同, 但是操作的同一个临界资源的话, 就相当于没有锁不会产生互斥 :
如上图所示 : 相当于有两个房间, 因为锁不同, 一把锁可以理解对应一个房间 , 所以当线程1获取 testQuestion1
的锁时, 并不会和线程2出现互斥, 不影响线程2获取 testQuestion2
锁, 所以最终结果就和都不加锁是一样的
也可以理解为另外开了一个门, 在同一时间, 两个线程都可以进入房间去修改临界资源
问题3
- 如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?-- 锁对象
如果一个加锁, 另外一个不加, 也相当于都没加, 因为 t2线程可以随意的执行, 不需要获得锁就可以执行去修改临界资源的值
思考
其实如果想要保证原子性 , 就必须互斥 , 要保证, 所有的临界区代码都在一个房间, 也就是说要都使用同一把锁才可以
否则, 只要有一个临界区不在这个房间, 那么就无法保证原子性, 因为这个临界区的代码执行不受限制, 可以随意修改临界资源的
面向对象改进
把需要保护的共享变量放入一个类
package cn.knightzz.improve;
import lombok.extern.slf4j.Slf4j;
@SuppressWarnings("all")
@Slf4j(topic = "c.Room")
public class Room {
private int counter = 0;
public void increment() {
// 使用当前对象作为锁
synchronized(this) {
log.debug("increment counter : {} " , counter);
counter++;
}
}
public void decrement() {
// 使用当前对象作为锁
synchronized(this) {
log.debug("decrement counter : {} " , counter);
counter--;
}
}
public int getCounter() {
return counter;
}
}
然后使用时直接调用 Room
提供的方法即可
package cn.knightzz.improve;
import cn.knightzz.test.TestSynchronized;
import lombok.extern.slf4j.Slf4j;
/** * @author 王天赐 * @title: TestObjectImprove * @projectName hm-juc-codes * @description: 面向对象改进 * @website <a href="http://knightzz.cn/">http://knightzz.cn/</a> * @github <a href="https://github.com/knightzz1998">https://github.com/knightzz1998</a> * @create: 2022-07-03 21:09 */
@SuppressWarnings("all")
@Slf4j(topic = "c.Room")
public class TestObjectImprove {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
// 在 for 循环执行的过程中, 不会受到其他线程的干扰
for (int i = 0; i < 5000; i++) {
room.increment();
}
log.debug("t1 thread end ... ");
}, "t1");
Thread t2 = new Thread(() -> {
// 在 for 循环执行的过程中, 不会受到其他线程的干扰
for (int i = 0; i < 5000; i++) {
room.decrement();
}
log.debug("t2 thread end ... ");
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("counter : {} ... ", room.getCounter());
}
}
边栏推荐
- IP lab, the first weekly recheck
- Notes on software development
- Migrate data from a tidb cluster to another tidb cluster
- CAD ARX gets the current viewport settings
- Image fusion -- challenges, opportunities and Countermeasures
- Remote storage access authorization
- 使用 TiUP 升级 TiDB
- 2.10transfrom attribute
- What is the use of entering the critical point? How to realize STM32 single chip microcomputer?
- Migrate data from SQL files to tidb
猜你喜欢
Analysis of pointer and array written test questions
ESP系列引脚说明图汇总
File upload of DVWA range
[count] [combined number] value series
It's hard to find a job when the industry is in recession
hcip--mpls
matplotlib. Widgets are easy to use
"Designer universe" Guangdong responds to the opinions of the national development and Reform Commission. Primary school students incarnate as small community designers | national economic and Informa
A Closer Look at How Fine-tuning Changes BERT
Nacos Development Manual
随机推荐
Migrate data from a tidb cluster to another tidb cluster
Yu Xia looks at win system kernel -- message mechanism
灰度升级 TiDB Operator
NFT smart contract release, blind box, public offering technology practice -- contract
Data governance: Data Governance under microservice architecture
Flash return file download
The ECU of 21 Audi q5l 45tfsi brushes is upgraded to master special adjustment, and the horsepower is safely and stably increased to 305 horsepower
IP lab, the first weekly recheck
Transformer principle and code elaboration
Analysis of Top1 accuracy and top5 accuracy examples
49. Sound card driven article collection
Artcube information of "designer universe": Guangzhou implements the community designer system to achieve "great improvement" of urban quality | national economic and Information Center
指针和数组笔试题解析
Artcube information of "designer universe": Guangzhou implements the community designer system to achieve "great improvement" of urban quality | national economic and Information Center
Document 2 Feb 12 16:54
"Friendship and righteousness" of the center for national economy and information technology: China's friendship wine - the "unparalleled loyalty and righteousness" of the solidarity group released th
Wireshark grabs packets to understand its word TCP segment
ESP系列引脚说明图汇总
MFC 给列表控件发送左键单击、双击、以及右键单击消息
ROS learning (IX): referencing custom message types in header files