【Java 并发编程】线程操作原子性问题 ( 问题业务场景分析 | 使用 synchronized 解决线程原子性问题 )

简介: 【Java 并发编程】线程操作原子性问题 ( 问题业务场景分析 | 使用 synchronized 解决线程原子性问题 )

文章目录

总结

一、原子性问题示例

二、线程操作原子性问题分析

三、使用 synchronized 解决线程原子性问题

总结


原子操作问题 : 线程中 , 对变量副本 count 进行自增操作 , 不是原子操作 , 首先 从工作内存中读取变量副本到执行引擎 ( 操作数栈 ) 中 , 然后 再 进行自增运算 , 最后 写回到线程工作内存中 , 这是 3 33 个操作 , 如果变量 在这 3 33 个操作的空档时间进行了修改 , 那么就会产生无法预知的效果 ;



总结一下 : 线程 A 的变量副本入操作数栈的时刻 , 该共享变量被线程 B 修改并且同步更新 , 此时入栈的这个变量自增是无效的 , 但是也算自增了 1 11 次 , 因此这里就丢失了 1 11 次计算机会 ;






一、原子性问题示例


开启 20 2020 个线程 , 对某个线程共享 int 类型变量进行自增 , 每个线程自增 10000 1000010000 次 , 那么按照正常执行 , 20 2020 个线程执行完毕后的变量值应该是 200000 200000200000 ;



代码示例 :


public class Main {
    private volatile static int count = 0;
    private static void increase() {
        count++;
    }
    public static void main(String[] args) {
        for (int i = 0; i < 20; i ++) {
            new Thread(){
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i ++) {
                        increase();
                        System.out.println(count);
                    }
                }
            }.start();
        }
    }
}


执行结果 : 多运行几次 , 有的时候会出现结果不是 200000 的情况 , 这就是出现问题的情景 ;



image.png





二、线程操作原子性问题分析


上述程序中 , 将变量 int count 设置成 volatile 类型的 , 只能保证其 可见性 和 有序性 , 无法保证 线程操作的 原子性 ;


在线程中对 int count = 0 进行累加操作 , 首先将变量 int count = 0 加载到线程工作内存的变量副本中 , 这里创建了 20 2020 个线程 , 就会有 20 2020 个线程对应的工作内存空间 , 需要将 count 变量拷贝 20 2020 份到相应的线程工作内存中 ;



有这样一种极端情况 , 当某个线程 A , 将 变量副本 加载到 线程执行引擎 时 , 就是 线程栈 中的 栈帧 的的 操作数栈 中 , 此时将要开始执行相关操作 , 在线程执行引擎没有执行之前 ,


与此同时 , 线程 B 修改了 count 副本变量 , 并进行了同步 , 主内存 , 包括 线程 A 的副本变量也已经更新了最新的值 ,


当前 线程栈中的栈帧中的操作数栈 中 , 还压着一个副本变量 , 虽然 该变量已经过时 , 该 count++ 操作无效 , 这样就 丢失了 1 11 次 count 变量自增的操作 , 导致 最终输出的值是 19999 1999919999 ;



原子操作问题 : 线程中 , 对变量副本 count 进行自增操作 , 不是原子操作 , 首先 从工作内存中读取变量副本到执行引擎 ( 操作数栈 ) 中 , 然后 再 进行自增运算 , 最后 写回到线程工作内存中 , 这是 3 33 个操作 , 如果变量 在这 3 33 个操作的空档时间进行了修改 , 那么就会产生无法预知的效果 ;



总结一下 : 线程 A 的变量副本入操作数栈的时刻 , 该共享变量被线程 B 修改并且同步更新 , 此时入栈的这个变量自增是无效的 , 但是也算自增了 1 11 次 , 因此这里就丢失了 1 11 次计算机会 ;






三、使用 synchronized 解决线程原子性问题


使用 synchronized 修饰 increase 方法 ;


 

private static void increase() {
        count++;
    }


方法 , 相当于在方法体重添加了 synchronized 代码块 ;


private static void increase() {
        synchronized (Main.class) {
            count++;
        }
    }


一旦某个线程执行 synchronized 方法或代码块中的代码 , 则当前线程持有互斥锁 , 只能由当前线程访问 count 变量 ;



代码示例 :


public class Main {
    private volatile static int count = 0;
    private synchronized static void increase() {
        count++;
    }
    public static void main(String[] args) {
        for (int i = 0; i < 20; i ++) {
            new Thread(){
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i ++) {
                        increase();
                        System.out.println(count);
                    }
                }
            }.start();
        }
    }
}


执行结果 :


image.png

目录
相关文章
|
21小时前
|
Java API
Java操作elasticsearch
Java操作elasticsearch
7 0
|
21小时前
|
NoSQL Java Redis
在Java中操作Redis
在Java中操作Redis
5 0
|
23小时前
|
SQL Java 关系型数据库
【JAVA基础篇教学】第十六篇:Java连接和操作MySQL数据库
【JAVA基础篇教学】第十六篇:Java连接和操作MySQL数据库
|
1天前
|
Oracle 关系型数据库 Java
java操作多数据源将oracle数据同步达梦数据库
java操作多数据源将oracle数据同步达梦数据库
|
1天前
|
缓存 Java 数据库
Java并发编程学习11-任务执行演示
【5月更文挑战第4天】本篇将结合任务执行和 Executor 框架的基础知识,演示一些不同版本的任务执行Demo,并且每个版本都实现了不同程度的并发性。
18 4
Java并发编程学习11-任务执行演示
|
1天前
|
Java Android开发
java利用xml-rpc协议操作wordpress博客
java利用xml-rpc协议操作wordpress博客
|
1天前
|
存储 安全 Java
12条通用编程原则✨全面提升Java编码规范性、可读性及性能表现
12条通用编程原则✨全面提升Java编码规范性、可读性及性能表现
|
1天前
|
缓存 Java 程序员
关于创建、销毁对象⭐Java程序员需要掌握的8个编程好习惯
关于创建、销毁对象⭐Java程序员需要掌握的8个编程好习惯
关于创建、销毁对象⭐Java程序员需要掌握的8个编程好习惯
|
2天前
|
安全 Java 程序员
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
【Java多线程】面试常考——锁策略、synchronized的锁升级优化过程以及CAS(Compare and swap)
5 0
|
2天前
|
Java
【Java多线程】分析线程加锁导致的死锁问题以及解决方案
【Java多线程】分析线程加锁导致的死锁问题以及解决方案
9 1