Java中的线程安全与线程同步

1.为什么需要线程同步

什么是线程安全:指在 被多个线程访问时,程序可以 持续进行正确的处理。

1.1.线程安全问题

案例:通过抢优惠例子说明线程安全问题

Java中的线程安全与线程同步
public class Demo1 {

    public static void main(String[] args) {
        // 简单模拟20人抢优惠
        for(int i=0;i

执行结果:出现的问题

同一个优惠号码可能被多次获取到;
优惠号码可能获取到0 和负数,类似超买超卖

并发访问线程不安全的共享变量时,会出现如上的常见问题。

避免问题的产生

共享变量设计为不可变的(final)
编程过程不修改共享变量(不修改)
将对象设计为无状态的(无状态)
在修改共享变量时使用线程同步(通过锁实现)

2.怎么实现线程同步

线程同步是指程序中用于控制不同线程间操作发生相对顺序的机制。

2.1.使用volatile关键字

volatile是轻量级的synchronized,对于 共享变量可以通过volatile关键字来实现线程同步。

共享变量处理情况:总线先从主内存拷贝共享变量到线程私有的工作内存,再交由处理器进行处理,处理完成后再从工作内存将运算结果回写主内存。工作内存起到临时缓存数据和指令的作用、并且线程私有,这就会存在缓存不一致问题。

实现原理:有volatile变量修饰的共享变量进行 写操作的时候会多出一行lock 前缀指令的汇编代码,lock前缀指令会直接锁缓存行,起到内存屏障的效果,并使处理器立即执行缓存回写到主内存的操作,并且导致其他处理器的缓存失效,需要重新从主内存获取最新值。

附上一张图便于理解

Java中的线程安全与线程同步

Java线程需要由操作系统内核线程调度器进行调度,并不是直接访问处理器资源,图中仅展示关键的几个点。

怎么输出汇编指令

windows:下载hsdis-amd64.dll(下载链接在文末),并放在目录 jdk1.8.0_181\jre\bin\server

在idea 工具中配置VM: -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*TestVolatile.main

// 测试代码public class TestVolatile {
    private static volatile int num = 0;

    public static void main(String[] args) {
        num--;
        System.out.println("结果为:" + num);
    }
}

执行结果:lock add dword ptr [rsp]

0x000000000320606d: lock add dword ptr [rsp],0h ;*putstatic num ; – com.yty.concurrent.synchronizeddemo.TestVolatile::main@5 (line 7)

通过缓存锁定和缓存一致性协议实现 可见性,确保多线程程序读写共享变量的时候,每个CPU看到的都是最新值。

MESI 高速缓存一致性协议,处理器使用嗅探技术保证缓存、主内存和其他处理器缓存的数据一致。还有其他的缓存一致性协议,比如:AMD的MOESI协议、Intel的MOSIF协议。

MESI 分别表示:

  • M(Modify):修改
  • E(Exclusive):独占、互斥
  • S(Shared):共享
  • I(Invalid):无效

volatile 关键字的另一个作用是 禁止编译器或处理器对指令进行 重排序优化

什么是重排序:重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。说白了,如果在编译或运行期间发生重排序,那么程序就可能不按照编写的顺序执行,会出现意料之外的执行结果。

编译器和处理器不会对存在 数据依赖关系的操作做重排序,一般重排序可能发生在没有数据依赖关系的指令之中。就算数据之间没有依赖关系,在多线程场景中若发生指令重排序优化,依然存在影响到最终执行结果的情况。当多线程场景下存在这种情况时,就需要使用volatile关键字等其他手段去禁止编译器和处理器对指令重排序优化。

实现原理是在编译器在生成字节码时,会在指令序列中插入内存屏障禁止在内存屏障前后的指令执行重排序优化。

回来到一开始的案例问题,给线程类的共享变量 private static Integer num = 10; 加上volatile关键字能实现线程安全吗?

private static volatile Integer num = 10;

答案是:不能

原因是:指令【num–】看似一条指令,其实分成三步执行: 先获取、再计算、后保存,所以自减和自增都不是 原子性操作,而volatile 无法确保其原子性操作,所以使用volatile关键字无法确保该情况下的线程安全,需要使用锁来实现原子性操作。

2.2.使用synchronized关键字

synchronized块是Java提供的一种原子性内置锁,Java中的每个对象都可以把它当作一个同步锁来使用,也称为监视器锁。synchronized 同步代码块中的操作被看为原子性操作。同步锁是一种排它锁、独占锁、可重入锁,当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁,并支持锁释放后可以再次获取锁。

使用方式

private Integer num = 1;
private static Integer num2 = 1;

public synchronized void test1(){
    System.out.println("普通方法" + num++);
}
public static synchronized void test2(){
    System.out.println("静态方法" + num2++);
}

public void test1_1(){
    synchronized (this){
        System.out.println("锁当前对象"+ num++);
    }
    synchronized (TestSynchronized.class){
        System.out.println("锁当前类" + num++);
    }
    synchronized (num){
        System.out.println("锁指定变量" + num++);
    }
}

synchronized 关键字加在普通方法时,是给当前实例对象上锁;

synchronized 关键字加在静态方法时,是给当前Class类对象上锁;

synchronized 关键字加在同步代码块时,是给括号里配置的对象上锁。

注意:这个Class类对象指的是每个类在类加载过程中生成的Class类。

原理简析

在synchronized的同步代码块中,入口处执行了monitorenter,在其出口执行了monitorexit,虚拟机中的每个Object实例都有一个monitor(监视器锁);而同步方法通过字节码flags 标记该方法为ACC_SYNCHRONIZED,表明执行该方法时需要获取到监视器锁才可以执行。两者的本质都是对一个对象的监视器(monitor)的获取和释放。

可以使用 javap -c -v xxx.class 查看字节码信息

同步代码块

Java中的线程安全与线程同步

同步方法

Java中的线程安全与线程同步

关于一开始的例子,可以写成👇

public class Demo1 {

    public static void main(String[] args) {
        // 简单模拟20人抢优惠
        for(int i=0;i

或者改为

注意:num 变量是static变量,是属于类的变量,需要锁住变量对象或Class类,单独或组合使用synchronized (this) 和volatile都无法确保其线程安全,这个可自行验证。

案例:对象单例实现

public class SingletonDemo {
    private static volatile SingletonDemo singletonDemo;
    public SingletonDemo(){
    }
    public static SingletonDemo getInstance(){
        if(singletonDemo==null)
            synchronized (SingletonDemo.class){
                if (singletonDemo==null)
                    singletonDemo = new SingletonDemo();
            }
        return singletonDemo;
    }
}
// 测试class Demo{
    public static void main(String[] args) {
        Demo demo = new Demo();
        for(int i=0;i{
            SingletonDemo instance = SingletonDemo.getInstance();
            System.out.println(Thread.currentThread().getName()+"="+instance);
        }).start();
    }
}

说明:volatile 和 synchronized实现双重锁校验。synchronized 起到指令执行的原子性和同一时间只能单个线程执行;synchronized和volatile都起到内存可见性保证;volatile起到禁止指令重排序,指令重排序导致线程不安全的可能性较小,但存在可能发生。

除了简单易用的synchronized 同步锁之外,还有其他更灵活的锁🔒。

篇幅原因,将在下一篇讲述关于Java中的锁:

Java中的线程安全与线程同步

hsdis-amd64.dll 的下载链接:https://pan.baidu.com/s/1LZqJb7WaUlh9VVGelfAgcw?pwd=yyds

参考资料:

《Java并发编程的艺术》
《Java并发编程之美》

Java中的线程安全与线程同步

Java线程状态(生命周期)–一篇入魂

自己编写平滑加权轮询算法,实现反向代理集群服务的平滑分配

Java实现平滑加权轮询算法–降权和提权

Java实现负载均衡算法–轮询和加权轮询

Java全栈学习路线、学习资源和面试题一条龙

更多优质文章,请关注WX公众号:Java全栈布道师

Java中的线程安全与线程同步

Original: https://www.cnblogs.com/dennyLee2025/p/16404975.html
Author: 渊渟岳
Title: Java中的线程安全与线程同步

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/576526/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 组管理和权限管理

    组管理和权限管理 在 linux 中的每个用户必须属于一个组,不能独立于组外。在 linux 中每个文件有所有者、所在组、其它组的概念。 文件所有者,谁创建了这个文件就是这个文件的…

    数据库 2023年6月16日
    0204
  • 【MySQL】MySQL的安装、卸载、配置、登陆和退出

    1 MySQL安装 安装环境:Win10 64位软件版本:MySQL 5.7.24 解压版 1.1 下载 https://downloads.mysql.com/archives/…

    数据库 2023年6月16日
    080
  • SQL与数据库编程学习笔记-day2

    SQL与数据库编程学习笔记-day2 修改数据库密码; – 登出数据库(修改数据库密码必须在数据库外执行命令); * – Ps:登出命令:quit* 操作完…

    数据库 2023年5月24日
    057
  • 前端开发:如何正确地跨端

    导读:面对多种多样的跨端诉求,有哪些跨端方案?跨端的本质是什么?作为业务技术开发者,应该怎么做?本文分享阿里巴巴ICBU技术部在跨端开发上的一些思考,介绍了当前主流的跨端方案,以及…

    数据库 2023年6月14日
    083
  • 01-MySQL连接查询、聚合函数

    1、连接查询 1.1、左连接 以左表为基准进行查询,左表数据回全部显示出来 右表中如果匹配连接条件的数据则显示相应字段的数据,如果不匹配,则显示为NULL 1.2、右连接 以右表为…

    数据库 2023年6月16日
    095
  • 事务的四大特性

    事务的四大特性 原子性(Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状…

    数据库 2023年6月11日
    080
  • 关于Mysql索引的数据结构

    索引的数据结构 1、为什么使用索引 概念: 索引是存储索引用于快速找到数据记录的一种数据结构,就好比一本书的目录部分,通过目录中对应的文…

    数据库 2023年5月24日
    086
  • Spring源码分析-BeanFactoryPostProcessor

    Spring源码分析-BeanFactoryPostProcessor 博主技术有限,本文难免有错误的地方,如果您发现了欢迎评论私信指出,谢谢JAVA技术交流群:737698533…

    数据库 2023年6月16日
    089
  • windows环境下nacos集群启动报错-无法启动内嵌的tomcat

    解决办法:使用64位jdk切记不要使用32位。切记不要使用32位。切记不要使用32位。 Original: https://www.cnblogs.com/journeyhch/p…

    数据库 2023年6月11日
    075
  • ATM系统开发(Java版)

    ATM系统Java模拟开发总结 ATM系统开发 技术点分析 1.面向对象编程 每个用户的账户都是一个对象,所以需要设计账户类Accent用于创建账户对象封装账户信息。 2.使用集合…

    数据库 2023年6月16日
    064
  • mybatis缓存

    加上flushCache=”true”后,再次运行结果如下 2.二级缓存 mybatis的二级缓存默认开启,但真正使用需要在mapper文件中添加相应的缓存…

    数据库 2023年6月16日
    079
  • MySQL进阶系列:一文详解explain

    explain有何用处呢: 为了知道优化SQL语句的执行,需要查看SQL语句的具体执行过程,以加快SQL语句的执行效率。 ​ 可以使用explain+SQL语句来模拟优化器执行SQ…

    数据库 2023年5月24日
    0107
  • Mybatis第三方PageHelper分页插件原理

    欢迎关注公号:BiggerBoy,看更多文章 往期精品 此时commentAnalyses为Page对象(PageHelper插件包内定义的) 而Page对象继承自JDK中的Arr…

    数据库 2023年6月11日
    076
  • Dubbo源码(八)-负载均衡

    前言 本文基于Dubbo2.6.x版本,中文注释版源码已上传github:xiaoguyu/dubbo 负载均衡,英文名称为Load Balance,其含义就是指将负载(工作任务)…

    数据库 2023年6月11日
    070
  • http状态码总结

    表示临时响应并需要请求者继续执行操作的状态代码。 100 (继续) 请求者应当继续提出请求。 服务器返回此代码表示已收到请求的第一部分,正在等待其余部分。101 (切换协议) 请求…

    数据库 2023年6月6日
    056
  • 三种云计算服务模式XaaS简单随笔

    SaaS的云计算服务随笔 马上队伍要组为解决方案团队了,得先理一理咱所处的解决方案SaaS团队的建设目标,其实就是给用户提供集成的软件解决方案,对物联网设备上云数据可视化管理等。 …

    数据库 2023年6月6日
    062
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球