Java并发编程中的JMM、3个基本属性、synchronized和volatile

news2024/11/17 21:25:24

1、Java内存模型JMM (Java Meemory Model)

在这里插入图片描述

  • JMM规定,所有变量均存储在主内存中
  • 每个线程都有自己的工作内存,保存了该线程中用到的变量的主内存副本拷贝
  • 线程对变量的所有操作,必须在自己的工作内存中,不可直接读写主内存
  • 不同线程无法直接访问对方工作内存中的变量
  • 线程间变量的传递,需要自己的工作内存和主内存之间进行数据同步

注:此处的主内存、工作内存,与JVM内存结构中的Java堆、栈、方法区不是同一个层次的内存划分,无法直接对比。若一定要勉强对应起来,那么,主内存主要对应JVM堆中的对象示例数据部分,工作内存主要对应JVM栈中的部分区域。

综上,JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行 等带来的问题。

补充:

  • Java提供了一系列和并发处理相关的关键字,如synchronized、volatile、final、concurren包

其中,Java提供两种内在同步机制:
在这里插入图片描述

2、并发编程三个基本属性

  • 原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
  • 可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
  • 有序性:即程序执行的顺序按照代码的先后顺序执行。

补充,锁提供的两种主要特性:
在这里插入图片描述

3、synchronized

3.1、代码示例

  • 锁方法
public class Test {

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            String threadName = "" + (i + 1);
            System.out.println("线程序号:" + threadName);
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    test(Thread.currentThread().getName());
                }
            }, threadName);
            thread.start();
        }
    }

    private static synchronized void test(String threadName) {
        for (int i = 0; i < 5; i++) {
            System.out.println("线程【" + threadName + "】 输出: " + (i + 1));
        }
    }
}

日志输出

线程序号:1
线程序号:2
线程序号:3
线程【1】 输出: 1
线程【1】 输出: 2
线程【1】 输出: 3
线程【1】 输出: 4
线程【1】 输出: 5
线程【3】 输出: 1
线程【3】 输出: 2
线程【3】 输出: 3
线程【3】 输出: 4
线程【3】 输出: 5
线程【2】 输出: 1
线程【2】 输出: 2
线程【2】 输出: 3
线程【2】 输出: 4
线程【2】 输出: 5

注:所有变量均存储在主内存,每个线程会把自己要用到的变量从主内存复制一份副本存储到自己的工作内存使用

  • 锁代码块
public class Test {
    
    public static void main(String[] args) {
        Object lock = new Object();
        for (int i = 0; i < 3; i++) {
            String threadName = "" + (i + 1);
            System.out.println("线程序号:" + threadName);
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (lock) {
                        for (int i = 0; i < 5; i++) {
                            System.out.println("线程【" + Thread.currentThread().getName() + "】 输出: " + (i + 1));
                        }
                    }
                }
            }, threadName);
            thread.start();
        }
    }
}

输出日志

线程序号:1
线程序号:2
线程序号:3
线程【1】 输出: 1
线程【1】 输出: 2
线程【1】 输出: 3
线程【1】 输出: 4
线程【1】 输出: 5
线程【3】 输出: 1
线程【3】 输出: 2
线程【3】 输出: 3
线程【3】 输出: 4
线程【3】 输出: 5
线程【2】 输出: 1
线程【2】 输出: 2
线程【2】 输出: 3
线程【2】 输出: 4
线程【2】 输出: 5

3.2、synchronized实现同步机制的原理

  • 对于同步方法,JVM采用ACC_SYNCHRONIZED标记符实现同步
  • 对于同步代码块,JVM采用monitorenter、monitorexit两个指令实现同步

在这里插入图片描述

无论是ACC_SYNCHRONIZED还是monitorenter、monitorexit都是基于Monitor实现的,在Java虚拟机(HotSpot)中,Monitor是基于C++实现的,由ObjectMonitor实现。

ObjectMonitor类中提供了几个方法,如enter、exit、wait、notify、notifyAll等。

sychronized加锁的时候,会调用objectMonitor的enter方法,解锁的时候会调用exit方法。

(1)synchronized与原子性

  • 在Java中,为了保证原子性,提供了两个高级的字节码指令monitorenter和monitorexit。前面中,介绍过,这两个字节码指令,在Java中对应的关键字就是synchronized。
  • 通过monitorenter和monitorexit指令,可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。因此,在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。
  • 线程1在执行monitorenter指令的时候,会对Monitor进行加锁,加锁后其他线程无法获得锁,除非线程1主动解锁。即使在执行过程中,由于某种原因,比如CPU时间片用完,线程1放弃了CPU,但是,他并没有进行解锁。而由于synchronized的锁是可重入的,下一个时间片还是只能被他自己获取到,还是会继续执行代码。直到所有代码执行完。这就保证了原子性。

(2)synchronized与可见性

  • 被synchronized修饰的代码,在开始执行时会加锁,执行完成后会进行解锁。
  • 而为了保证可见性,有一条规则是这样的:对一个变量解锁之前,必须先把此变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值。

(3)synchronized与有序性

  • 除了引入了时间片以外,由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,比如load->add->save 有可能被优化成load->save->add 。这就是可能存在有序性问题。
  • synchronized是无法禁止指令重排和处理器优化的。也就是说,synchronized无法避免上述提到的问题。
  • 那么,为什么还说synchronized也提供了有序性保证呢?
  • 这就要再把有序性的概念扩展一下了。Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有操作都是天然有序的。如果在一个线程中观察另一个线程,所有操作都是无序的。
  • 以上这句话也是《深入理解Java虚拟机》中的原句,但是怎么理解呢?周志明并没有详细的解释。这里我简单扩展一下,这其实和as-if-serial语义有关。
  • as-if-serial语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。编译器和处理器无论如何优化,都必须遵守as-if-serial语义。
  • 这里不对as-if-serial语义详细展开了,简单说就是,as-if-serial语义保证了单线程中,指令重排是有一定的限制的,而只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行的。当然,实际上还是有重排的,只不过我们无须关心这种重排的干扰。
  • 所以呢,由于synchronized修饰的代码,同一时间只能被同一线程访问。那么也就是单线程执行的。所以,可以保证其有序性。

4、volatile

  • volatile的用法比较简单,只需要在声明一个可能被多线程同时访问的变量时,使用volatile修饰就可以了

4.1、volatile 原理

  • 如果一个变量被volatile所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。
  • 缓存一致性协议:每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。

(1)volatile与可见性

  • 被volatile修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新

(2)volatile与有序性

  • volatile可以禁止指令重排,这就保证了代码的程序会严格按照代码的先后顺序执行

(3)volatile不保证原子性

  • 原子性:指一个操作是不可中断的,要全部执行完成,要不就都不执行
public class Test2 {

    public volatile int cnt = 0;

    public void cntIncrease(String threadName) {
        cnt++;
        System.out.println("线程【" + threadName + "】:" + cnt);
    }


    public static void main(String[] args) {

        final Test2 t2 = new Test2();
        for (int i = 0; i < 10; i++) {
            String threadName = "" + (i + 1);
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                        t2.cntIncrease(Thread.currentThread().getName());
                    }
                }
            }, threadName);
            thread.start();
        }


        //获取当前线程活跃数量
        int activeThreadNum = Thread.activeCount();
        System.out.println("当前线程活跃数量:" + activeThreadNum);
        //打印当前活跃线程
        Thread.currentThread().getThreadGroup().list();
        while (activeThreadNum > 2) {
            //yield()让掉当前 CPU 的调度权,等下次获取到再继续执行
            // 这样不但能完成自己的重要工作,也能给其他线程一些运行的机会
            // 避免一个线程长时间占有 CPU 资源。
            Thread.yield();
            //获取当前线程活跃数量
            activeThreadNum = Thread.activeCount();
            System.out.println("当前线程活跃数量:" + activeThreadNum);
            //打印当前活跃线程
            Thread.currentThread().getThreadGroup().list();
        }

        System.out.println("最终值:" + t2.cnt);
    }
}

日志输出(部分日志)

线程【4】:9970
线程【4】:9971
Thread[#31,9,5,]
线程【5】:9968
线程【5】:9973
线程【5】:9974
线程【5】:9975
线程【5】:9976
线程【5】:9977
线程【5】:9978
线程【4】:9972
线程【4】:9979
线程【4】:9980
    线程【4】:9981
线程【4】:9982
Thread[#32,10,5,]
线程【4】:9983
线程【4】:9984
线程【4】:9985
线程【4】:9986
当前线程活跃数量:3
线程【4】:9987
线程【4】:9988
线程【4】:9989
线程【4】:9990
线程【4】:9991
线程【4】:9992
线程【4】:9993
线程【4】:9994
线程【4】:9995
线程【4】:9996
线程【4】:9997
线程【4】:9998
线程【4】:9999
java.lang.ThreadGroup[name=main,maxpri=10]
    Thread[#1,main,5,main]
    Thread[#20,Monitor Ctrl-Break,5,main]
    Thread[#26,4,5,]
当前线程活跃数量:2
java.lang.ThreadGroup[name=main,maxpri=10]
    Thread[#1,main,5,main]
    Thread[#20,Monitor Ctrl-Break,5,main]
最终值:9999

我们希望最终值是1w,但实际小于1w。原因就是volatile不支持原子性,即cnt++不具有原子性。比如,由于volatile支持可见性,所以Thread【1】和Thread【2】都可以看到cnt的当前值(假设cnt=520),那么Thead【1】执行了520+1=521,Thread【2】也执行了520+1=521,那么就无法达到1w

4.2、补充点

(1)Thread.activeCount()

  • 用于返回当前线程的线程组中活动线程的数量
  • 返回的值只是一个估计值,因为当此方法遍历内部数据结构时,线程数可能会动态更改

(2)IntelliJ IDEA 编译器中,最终Thread.activeCount() == 2原因

当前线程活跃数量:2
java.lang.ThreadGroup[name=main,maxpri=10]
    Thread[#1,main,5,main]
    Thread[#20,Monitor Ctrl-Break,5,main]
  • 除了main以外,还多了一个预期外的 Monitor Ctrl-Break 线程
  • IntelliJ IDEA执行用户代码的时候,实际是通过反射方式去调用,而与此同时会创建一个Monitor Ctrl-Break 用于监控目的

(3)Thread.yield()

  • yield 方法可以很好的控制多线程,如执行某项复杂的任务时,如果担心占用资源过多,可以在完成某个重要的工作后使用 yield 方法让掉当前 CPU 的调度权,等下次获取到再继续执行,这样不但能完成自己的重要工作,也能给其他线程一些运行的机会,避免一个线程长时间占有 CPU 资源。

yield 和 sleep 的异同

  • yield, sleep 都能暂停当前线程,sleep 可以指定具体休眠的时间,而 yield 则依赖 CPU 的时间片划分
  • yield, sleep 两个在暂停过程中,如已经持有锁,则都不会释放锁资源
  • yield 不能被中断,而 sleep 则可以接受中断
public class Test3 {

    /**
     * 当是10的倍数时,当前线程便让出自己的CPU时间片
     */
    public static void main(String[] args) {
        Runnable runnable = () -> {
            for (int i = 0; i < 10000; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + (i + 1));
                if (i % 10 == 0) {
                    Thread.yield();
                }
            }
        };

        new Thread(runnable, "鸣人").start();
        new Thread(runnable, "佐助").start();
    }
}

最终有两种结果

  • 鸣人先跑完
佐助:9994
佐助:9995
佐助:9996
佐助:9997
佐助:9998
佐助:9999
佐助:10000
  • 佐助先跑完
鸣人:9996
鸣人:9997
佐助:9999
佐助:10000
鸣人:9998
鸣人:9999
鸣人:10000

如果给线程加上优先级,则结果也会不同

Thread threadNaruto = new Thread(runnable, "鸣人");
Thread threadSasuke = new Thread(runnable, "佐助");

threadNaruto.setPriority(Thread.MAX_PRIORITY);
threadSasuke.setPriority(Thread.MIN_PRIORITY);

threadSasuke.start();
threadNaruto.start();

给鸣人最大优先级,给佐助最小优先级,则即使佐助先跑,也大概率是鸣人先跑完

内存模型

synchronized

volatile 1

volatile 2

volatile 3

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/711485.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023-6-29-第十一式代理模式

&#x1f37f;*★,*:.☆(&#xffe3;▽&#xffe3;)/$:*.★* &#x1f37f; &#x1f4a5;&#x1f4a5;&#x1f4a5;欢迎来到&#x1f91e;汤姆&#x1f91e;的csdn博文&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f49f;&#x1f49f;喜欢的朋友可以关注一下&#xf…

C++primer(第五版)第八章(IO库)

8.1 IO库 上表中以w开头的类型和函数是C标准库为了支持使用宽字符的语言而定义的一组类型和对象来操纵wchar_t类型的数据.(然而我没有遇到过) 8.1.1 IO对象无拷贝或赋值 IO对象不能拷贝或赋值,通常用引用方式传递和返回流,由于读写一个IO对象回改变其状态,因此传递和返回的引…

Cetos7.x连接不上网络解决办法

Cetos7.x连接不上网络解决办法 Cetos7.x连接不上网络解决办法 在VM中设置网络连接为桥接&#xff0c;修改后仍无法连接网络 ##配置centos7中ens33&#xff0c;将默认的no修改为yes 启动CentOS系统&#xff0c;并打开一个连接终端会话&#xff0c;使用root登录&#xff1b;进…

tomcat多台应该怎么能设置

一个tomcat一般能处理5000-1000的并发量但是还是远远不够我们可以设置多台来满足我们的要求 首先进入tomcat目录 配置tomcat环境变量 vim /etc/profile.d/tomcat.sh 然后刷新 source /etc/profile.d/tomcat.sh 修改tomcat1里面的配置文件 然后进入tomcat1中的启动bin程序中…

Docker安装、常见命令、安装常见容器(Mysql、Redis等)

目录 一、Docker安装 二、Docker常见命令 2.1 镜像命令 2.2 容器命令 2.3 总结 2.4 容器挂载-容器卷技术 三、Docker安装mysql容器 3.1 下载镜像文件 3.2 创建实例并启动 3.3 MySQL 配置 3.4 进入容器文件系统 四、Docker安装Redis 一、Docker安装 官网安装指引&a…

SSM框架原理畅谈之SpringMVC

SpringMVC 一、Java SE Servlet标准1.1 Servlet 接口1.2 HttpServletRequest 接口1.3 HttpServletResponse 接口1.4 Cookie 对象1.5 Filter 接口1.6 HttpSession 接口 二、SpringMVC2.1 Spring MVC核心概念2.2 DispatcherServlet2.3 DispatcherServlet.init()2.4 DispatcherSer…

第三章 搜索与图论(一)——深搜,广搜,图的存储与拓扑序列

文章目录 深度优先搜索广度优先搜索树和图的存储图的深搜 拓扑序深搜练习题842. 排列数字843. n-皇后问题 广搜练习题844. 走迷宫845. 八数码 树和图的存储与遍历练习题846. 树的重心847. 图中点的层次 拓扑序练习题848. 有向图的拓扑序列 深度优先搜索 数据结构&#xff1a; …

常见排序算法详解

文章目录 前言1. 排序算法简介2 算法效率2.1 度量一个程序执行时间两种方法2.2 时间频度2.3 时间复杂度2.4 常见的时间复杂度2.5 平均和最坏时间复杂度 3. 常见排序算法详解3.1 基数排序 (Radix Sort)(1) 算法过程(2)代码实现 3.2 冒泡排序 (Bubble Sort)(1) 算法过程(2) 代码实…

2023年7月2日leetcode每日一题打卡——125.验证回文串

一、题目描述与要求 125. 验证回文串 - 力扣&#xff08;LeetCode&#xff09; 题目描述 如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后&#xff0c;短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。 字母和数字都属于字母数字字符。 给…

学习系统编程No.28【多线程概念实战】

引言&#xff1a; 北京时间&#xff1a;2023/6/29/15:33&#xff0c;刚刚更新完博客&#xff0c;目前没什么状态&#xff0c;不好趁热打铁&#xff0c;需要去睡一会会&#xff0c;昨天睡的有点迟&#xff0c;然后忘记把7点到8点30之间的4个闹钟关掉了&#xff0c;恶心了我自己…

C语言学习(三十)---枚举、位段、联合体

这几天在往实习的地方弄东西&#xff0c;比较累&#xff0c;因此没有更新&#xff0c;在几天前我们学习了内存操作函数&#xff0c;其与之前学习的字符串操作函数相比&#xff0c;适用范围更加广泛&#xff0c;大家要注意掌握学习&#xff0c;今天我们将学习枚举、位段和联合体…

闲置BROOKSTONE Rover间谍车重生记

22年春节在家&#xff0c;哪也去不了&#xff0c;收拾出来一个多年前的玩具&#xff0c;全名叫BROOKSTONE Rover revolution&#xff0c;长这个样子。 尽管是7年前的产品了&#xff0c;科技感依旧挺足 印象中能手机控制&#xff0c;并且能语音对讲。只是网上找到的安卓版应用已…

xenomai内核解析--xenomai实时线程创建流程

版权声明&#xff1a;本文为本文为博主原创文章&#xff0c;未经同意&#xff0c;禁止转载。如有错误&#xff0c;欢迎指正&#xff0c;博客地址&#xff1a;https://blog.csdn.net/qq_22654551?typeblog 文章目录 问题概述1 libCobalt中调用非实时POSIX接口2 阶段1 linux线程…

02_jQuery与Ajax

jquery jquery的作用 他是js的库 处理html,事件,实现动画效果,方便的为网站提供AJAX交互 命名格式 .ji:体积大,用于学习和debug使用 .min.js:压缩的文件,体积小,用于线上环境使用 使用方法 必须先在页面文件中进行引用 $就是jQuery 注意: jQuery是DOM的封装 jQuery和…

Spring Boot 中的服务网关是什么,原理,如何使用

Spring Boot 中的服务网关是什么&#xff0c;原理&#xff0c;如何使用 在微服务架构中&#xff0c;服务网关是一个非常重要的组件。它可以作为所有微服务的入口&#xff0c;负责路由、负载均衡、安全性和监控等方面的功能。Spring Boot 提供了一系列的服务网关工具&#xff0…

redis-哨兵安装

解决问题 自动故障修复 1.在主从模式的基础上,在主节点添加自己的认证密码即可 2.将代码客户端地址改为哨兵地址 ------------- 主节点配置 daemonize yes port 6379 bind 0.0.0.0 requirepass 123456 save 3600 1 300 100 60 10000dir /usr/local/redis dbfilename dump.r…

Java POI (4)—— Linux环境下文件解析过程出现OOM的问题

Excel文件在进行解析的时候&#xff0c;在Windows环境中&#xff0c;没用报错&#xff0c;但是在Linux环境中&#xff0c;出现了如下的报错&#xff1a; nested exception is javalang.OutofMemoryError: Java heap space &#xff08;OOM&#xff09; 一、内存溢出和栈溢出有什…

主流特征工程平台(一)

一. 目标 对于Feature Store的能力与边界&#xff0c;每家的定义略微不同&#xff0c;《Feature Stores - A Hierarchy of Needs》&#xff09;这篇文章做了很好的总结&#xff0c;大体分为如下几个层次&#xff1a; 特征管理&#xff1a;特征抽取、处理、存储、元数据管理&am…

群晖NAS 安装 MySQL 远程访问连接

目录 1. 安装Mysql 2. 安装phpMyAdmin 3. 修改User 表 4. 本地测试连接 5. 安装cpolar 6. 配置公网访问地址 7. 固定连接公网地址 [TOC] > **转载自cpolar极点云文章&#xff1a;[群晖NAS 安装 MySQL远程访问连接](https://www.cpolar.com/blog/install-mysql-remote-…

Spring 事务使用详解

前言 什么是事务&#xff1f;根据 维基百科事务 介绍&#xff0c;数据库事务&#xff08;简称&#xff1a;事务&#xff09;是数据库管理系统执行过程中的一个逻辑单位&#xff0c;由一个有限的数据库操作序列构成。简单来说&#xff0c;事务就是将一系列操作当成一个不可拆分…