前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >多线程基础(三):synchronized关键字及java内存模型简介

多线程基础(三):synchronized关键字及java内存模型简介

作者头像
冬天里的懒猫
发布于 2020-09-08 13:10:33
发布于 2020-09-08 13:10:33
56700
代码可运行
举报
运行总次数:0
代码可运行

1.线程安全问题

在前面了解过一些java多线程基础之后,现在,我们用多线程来解决一个实际问题。 假定每个线程可以将一个数字加到100000,现在我们用十个线程,同时相加,看看结果是不是1000000?,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ackage com.dhb.concurrent.test;

import java.util.concurrent.CountDownLatch;

public class SyncDemo implements Runnable{

	private static int count = 0;
	static CountDownLatch countDownLatch = new CountDownLatch(10);

	public static void main(String[] args) throws InterruptedException{
		for(int i=0;i<10; i++) {
			Thread t = new Thread(new SyncDemo());
			t.start();
		}
		countDownLatch.await();
		System.out.println(count);

	}

	 private void add(){
		for (int i = 0; i < 100000; i++) {
			count++;
		}
		countDownLatch.countDown();
	}

	@Override
	public void run() {
		add();
	}
}

在上述代码中,分别启动了10个线程,现在看看输出结果是不是1000000?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
361177

结果跟预想的并不一样,我们再执行一次看看?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
294781

每次还不一样。 这就说明,一定遇到了线程安全的问题。即我们定义的count这个成员变量,在10个线程并发访问的过程中,可能出现了脏读,即一个线程还没有写入完成,另外一个线程就读到了这个没写完的结果,这样就导致了最终的结果不为1000000。为此,我们解决这个问题,不得不想到一个关键字,synchronized来解决。

2.synchronized 的使用说明

并发问题,通常需要解决两类问题,一个是互斥,即资源只能同时由一个线程来访问,当这个线程在访问的过程中,其他线程不能访问这个变量。这就是互斥。另外一个问题就是同步,同步主要是解决线程间通信的问题。即线程由于获取不到访问这个变量之前需要的锁资源,就会进入阻塞状态,让出CPU执行权限。那么何时能够重新执行呢,就需要访问资源的线程在执行完成之后进行通知,wait和notify方法就是很好的线程同步方法。 实际上synchronized的英文就是同步的意思,但是比较有意思的是,synchronized主要是解决的互斥问题。即加锁。 我们将代码修改为如下方式:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 private void add(){
	synchronized (SyncDemo.class) {
		for (int i = 0; i < 100000; i++) {
			count++;
		}
		countDownLatch.countDown();
	}
}

再次查看执行结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
1000000

果然,输出的就是想要的结果了。但是,如果我们将sunchronized的代码块修改为如下呢?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 private void add(){
	synchronized (this) {
		for (int i = 0; i < 100000; i++) {
			count++;
		}
		countDownLatch.countDown();
	}
}

结果如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
205975

又不能满足了。这说明,synchronized代码块,括号中锁定的对象,是有讲究的,前面的SyncDemo.class,由于SyncDemo.class是个特殊的对象,只有一个对象。因此多线程访问的时候就会形成互斥。而改成this之后,由于这个类在使用的时候通过new,导致了多个实例,实例与实例之间加索就不能构成互斥关系。 另外,上述代码块也可以与如下情况等价:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 private synchronized void add(){
	for (int i = 0; i < 100000; i++) {
		count++;
	}
	countDownLatch.countDown();
}

如果方法中除了代码块没有任何内容,那么这种方式与前面的synchronized(this)等价。 此外synchronized(SyncDemo.class)也与如下等价:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 private static synchronized void add(){
	for (int i = 0; i < 100000; i++) {
		count++;
	}
	countDownLatch.countDown();
}

对,就是将方法改为静态方法,这样锁住的就是类了。我们总结一下:

分类

详细分类

被锁的对象

代码示例

方法

实例方法

类的实例对象

public synchronized void method(){ … … }

方法

实例方法

类对象

public static synchronized void method() { … … }

代码块

实例对象

类的实例对象

synchronized(this) { … … }

代码块

class对象

类对象

synchronized(SyncDemo.class){ … … }

代码块

任意实例对象Object

实例对象Object

Object lock = new Object(); synchronized(lock){ … … }

理论上来说,synchronized()的括号中可以是任意对象。但是,需要注意的是, 一般我们最好不要用String和包装类做为被锁定的对象。 这是因为,在jvm中,对这些类进行特殊处理,String类,尤其是G1中要是开启了字符串去重,那么全部jvm中都只有这一个对象。这样会导致许多系统其他的功能受到影响。包装类由于有常量池,也会导致同样的问题,这样你会莫名其妙的感觉系统卡顿。

3.java的内存模型JMM

在前面学习伪共享的时候了解过,操作系统中,实际上CPU与主内存之间存在多级缓存架构。而这些多级高速缓存的速度远远高于主内存的读取速度。其结构如下:

高速缓存和主内存以及CPU的同步关系,需要通过缓存一致性协议来确保数据的一致性。如MESI、MSI等协议。通过这些协议,才能保证各内存高速缓存与主内存的数据一致性。这个模型如下图所示

除了高速缓存之外,为了使CPU运算单元尽可能的充分利用,还会对输入的代码进行优化,其先后顺序会被改变。因此,在实际代码的执行过程中,其先后顺序不一定按照代码顺序来执行。这就是指令的重排序。关于这一点的细节再后续volatile关键字部分进行详细介绍。 那么JAVA实际上也是与这个模型类似,再java虚拟机中,虚拟机做为最外层的容器,其执行的逻辑与这个模型也非常相似。实际上,线程是CPU的最小执行单位,Java的内存模型实际上是对这个模型的抽象。在java中,也分为主内存和工作内存:

  • 主内存:java虚拟机规定,所有变量必须在主内存上产生,主内存也等价于是堆区。与前面的模型相比,这里的主内存可能是前面内存的一部分。
  • 工作内存:java虚拟机中的每个线程都有自己的工作内存,也就是线程的栈区。与前面的高速缓存相比,线程的工作过程中需要使用高速缓存。线程的工作内存实际上大部分内容在内存中,分配到CPU执行的时候,就会将需要执行的部分放入高速缓存。 那么java主内存和工作内存之间,也需要通过jvm的一些规则来保证数据的一致性。 需要说明的是,这两个模型只用于对比记忆,实际上二者并无直接关系。因为中间还有操作系统层的映射。而对于操作系统是如何在这两个模型之间转换的,还有很多内容本文并未涉及。 java内存模型如下:

在java中,工作内存与主内存的交互,主要通过如下8种活动来进行,每个活动都是原子性的。

  • lock(锁定):作用于主内存的变量,一个变量在同一时间只能一个线程锁定,该操作表示这条线成独占这个变量
  • unlock(解锁):作用于主内存的变量,表示这个变量的状态由处于锁定状态被释放,这样其他线程才能对该变量进行锁定
  • read(读取):作用于主内存变量,表示把一个主内存变量的值传输到线程的工作内存,以便随后的load操作使用
  • load(载入):作用于线程的工作内存的变量,表示把read操作从主内存中读取的变量的值放到工作内存的变量副本中(副本是相对于主内存的变量而言的)
  • use(使用):作用于线程的工作内存中的变量,表示把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时就会执行该操作
  • assign(赋值):作用于线程的工作内存的变量,表示把执行引擎返回的结果赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的字节码指令时就会执行该操作
  • store(存储):作用于线程的工作内存中的变量,把工作内存中的一个变量的值传递给主内存,以便随后的write操作使用
  • write(写入):作用于主内存的变量,把store操作从工作内存中得到的变量的值放入主内存的变量中

可以看到,上述图种绿色部分就是在工作内存种执行的活动。其他活动则是在主内存种执行。其过程详细如下图:

在每个线程中,其执行的时候的变量,实际上是其主内存中变量的副本。那么如果采用了synchronized,则会用lock操作锁定该变量,之后其他线程并无法访问。之后再进行read、load过程,之后使用或者赋值。 对于两个线程,分别从主内存中读取变量a和b的值,并不一样要read a; load a; read b; load b; 也会出现如下执行顺序:read a; read b; load b; load a; volatile修饰的变量则除外。 上述这些操作,JSR133规定,需要满足如下规则:

  • 1.不允许read和load、store和write操作之一单独出现。这两组操作必须成对出现。read之后就必须load,store之后就必须write,不允许读取之后工作内存不接收,或者store之后主内存不接收的情况。
  • 不允许一个线程丢弃最近的assign操作。其修改的值,必须同步回主内存。
  • 变量只能在主内存中产生。在工作内存中不允许直接使用一个未被初始化的变量。执行use和assign操作之前必须执行load。
  • 一个变量在同一时刻只能被一个线程对其进行lock操作,这就实现了互斥性。也是我们使用synchronized的本质。
  • 不允许对没有lock的变量执行unlock操作,如果一个线程没有执行lock,那么肯定不允许执行unlock,当然,也不允许对其他的线程lock的变量执行unlock。
  • 对一个变量执行unlock之前,必须先把变量同步回主内存中。也就是执行write之后。

4.总结

本文从线程安全问题引出了synchronized的用法。以及java内存模型的简单介绍。当然,synchronized还有可重入,以及底层具体实践和优化的知识也是非常重要的部分。后续对此详细介绍。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020/09/06 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Java内存模型
Java 内存模型试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。
Java架构师必看
2021/07/14
3440
深入理解Java内存模型JMM
Java 内存模型(JMM)是一种抽象的概念,并不真实存在,它描述了一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。试图屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。
美的让人心动
2019/05/19
1.1K0
Java内存模型
多任务处理在现代计算机操作系统中几乎已经是一项必备的功能了。计算机cpu的运算速度与它的存储和通信子系统速度的差距太大,大量的时间都花费在磁盘I/O、网络通信或数据库访问上。如果不希望处理器在大部分时间里都处于等待其他资源的状态,那么并发的处理多项任务是最容易想到、也是非常有效的“压榨”处理器运算能力的一种手段。 服务端是java语言最擅长的领域之一。如果写好并发应用程序是服务端程序开发的难点之一,java语言和虚拟机提供了许多工具来帮助程序员降低门槛,并且各种中间件服务器、各类框架都努力的替程序员处理更多的并发希捷,使得程序员在编码过程中更关注业务逻辑。但无论语言、中间件和框架多么先进,都不能独立的完成所有并发处理的事情,所以了解并发的内幕也是一个高级程序员不可缺少的课程。 高效并发是本教程的最后一部分,主要讲解虚拟机如何实现多线程、多线程之间由于共享和竞争数据而导致的一系列问题及解决方案。
栋先生
2018/09/29
8190
Java内存模型
java内存模型_简述java内存模型
  JMM即为JAVA 内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问逻辑有一定的差异,结果就是当你的代码在某个系统环境下运行良好,并且线程安全,但是换了个系统就出现各种问题。Java内存模型,就是为了屏蔽系统和硬件的差异,让一套代码在不同平台下能到达相同的访问结果。JMM从java 5开始的JSR-133发布后,已经成熟和完善起来。
全栈程序员站长
2022/10/03
1.2K0
java内存模型_简述java内存模型
Java内存模型
目前计算机上会有多个CPU核心,我们可以创建多个线程,操作系统会将线程分配各不同的CPU去执行,如果只有一个线程,那么只会有一个CPU去工作,其他的CPU将会被浪费。
shysh95
2021/03/16
3960
Java内存模型
Java底层-01-Java内存模型
Java内存模型是一种抽象的规则或规范,定义了程序中存在竞争现象的对象(包括实例字段、静态字段和数组对象,不包括局部变量,形式参数;后者是线程私有,不存在竞争问题)的访问方式。
devi
2021/08/18
5220
java内存模型JMM「建议收藏」
jmm(java memory model)规范,他规范了java虚拟机与计算机内存如何协调工作 ,他规定了一个线程如何及何时看到其他线程修改过的变量的值,以及在必须时,如何同步的访问共享变量。
全栈程序员站长
2022/09/02
5820
java内存模型JMM「建议收藏」
Java 内存模型
物理机遇到的并发问题与虚拟机中的情况有不少相似之处,物理机对并发的处理方案对于虚拟机的实现也有相当大的参考意义。
静默虚空
2019/12/30
9030
Java 内存模型
彻底理解Java并发:Java内存模型
进程是静态的概念,进程是资源(CPU、内存等)分配和调度的基本单位,它拥有自己的资源空间,每启动一个进程,系统就会为它分配地址空间;
栗筝i
2022/12/01
3680
彻底理解Java并发:Java内存模型
Java内存模型(JMM)
JMM本身只是一个抽象的概念,并不真实存在,它描述的是一种规则或规范;通过这组规范,定义了程序中对各种变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。需要每个JVM的实现都要遵守这样的规范;有了JMM规范的保障后,并发程序运行在不同虚拟机上时,得到的程序结果才是安全可靠可信赖的,如果没有JMM内存模型来规范,那经过不同JVM翻译之后,就可能出现,运行结果不相同或不正确。
科技新语
2024/08/27
970
Java内存模型(JMM)
Java内存模型(JMM)详解
在Java JVM系列文章中有朋友问为什么要JVM,Java虚拟机不是已经帮我们处理好了么?同样,学习Java内存模型也有同样的问题,为什么要学习Java内存模型。它们的答案是一致的:能够让我们更好的理解底层原理,写出更高效的代码。
程序新视界
2019/11/05
9921
【JAVA面试必会】JMM高并发详解(java内存模型、JMM三大特征、volatile关键字 )「建议收藏」
JMM就是Java内存模型(java memory model)。因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。
全栈程序员站长
2022/10/04
3640
【JAVA面试必会】JMM高并发详解(java内存模型、JMM三大特征、volatile关键字 )「建议收藏」
Java内存模型与volatile关键字Java内存模型(JMM)指令重排序对于Long和double型变量的特殊规则内存屏障有序性(Ordering)先行发生原则
Java内存模型(JMM) JMM 与硬件内存架构对应关系 JMM抽象结构图 Java虚拟机规范中试图定义一种Java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,规定线程如何,何时能看到其他线程
JavaEdge
2018/05/16
1.3K0
不会Java内存模型,就先别扯什么熟悉并发编程
前两天看到同学和我显摆他们公司配的电脑多好多好,我默默打开了自己的电脑,酷睿 i7-4770,也不是不够用嘛,4 核 8 线程的 CPU,也是杠杠的。
乔戈里
2020/03/25
4130
并发编程-02并发基础CPU多级缓存和Java内存模型JMM
CPU的频率非常快,主存Main Memory跟不上。CPU缓存是CPU与内存之间的临时数据交换器,为了解决CPU运行处理速度与内存读写速度不匹配的矛盾——缓存的速度比内存的速度快多了。
小小工匠
2021/08/17
5190
Java内存模型
《Java虚拟机规范》中曾试图定义一种“Java内存模型”(Java Memory Model,JMM)来屏蔽各种硬件和操作系统的内存访问差异, 以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。
真正的飞鱼
2023/04/08
6980
Java内存模型
JMM Java内存模型
Java采用内存共享的模式来实现线程之间的通信。编译器和处理器可以对程序进行重排序优化处理,但是需要遵守一些规则,不能随意重排序。
luoxn28
2020/11/05
5490
Java内存模型和线程安全
对于多核处理器而言,每个核都会有自己单独的高速缓存,又因为这多个处理器共享同一块主内存,为了在并行运行的情况下,包装各个缓存中缓存的结果的一致性,需要引用缓存一致性协议。
大忽悠爱学习
2023/02/10
5190
Java内存模型和线程安全
吊打Java面试官-Java内存模型深入详解(JMM)
为了提高程序运行的性能,现代CPU在很多方面对程序进行了优化。: 例如: CPU高速缓存。 尽可能地避免处理器访问主内存的时间开销,处理器大多会利用缓 存(cache)以提高性能。
JavaEdge
2020/05/27
4710
吊打Java面试官-Java内存模型深入详解(JMM)
JUC系列(八)Java内存模型 volatile关键字与单例模式实践
JMM就是Java内存模型(java memory model) Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。线程不能直接读写主内存中的变量。
冷环渊
2022/12/03
3900
JUC系列(八)Java内存模型 volatile关键字与单例模式实践
相关推荐
Java内存模型
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验