多线程系列(三) -synchronized 关键字使用详解
一、简介
在之前的线程系列文章中,我们介绍了线程创建的几种方式以及常用的方法介绍。
今天我们接着聊聊多线程线程安全的问题,以及解决办法。
实际上,在多线程环境中,难免会出现多个线程对一个对象的实例变量进行同时访问和操作,如果编程处理不当,会产生脏读现象。
二、线程安全问题介绍
我们先来看一个简单的线程安全问题的例子!
public class DataEntity {
private int count = 0;
public void addCount(){
count++;
}
public int getCount(){
return count;
}
}
public class MyThread extends Thread {
private DataEntity entity;
public MyThread(DataEntity entity) {
this.entity = entity;
}
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
entity.addCount();
}
}
}
public class MyThreadTest {
public static void main(String[] args) {
// 初始化数据实体
DataEntity entity = new DataEntity();
//使用多线程编程对数据进行计算
for (int i = 0; i < 10; i++) {
MyThread thread = new MyThread(entity);
thread.start();
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("result: " + entity.getCount());
}
}
多次运行结果如下:
第一次运行:result: 9788554
第二次运行:result: 9861461
第三次运行:result: 6412249
...
上面的代码中,总共开启了 10 个线程,每个线程都累加了 1000000 次,如果结果正确的话,自然而然总数就应该是 10 * 1000000 = 10000000。
但是多次运行结果都不是这个数,而且每次运行结果都不一样,为什么会出现这个结果呢?
简单的说,这是主内存和线程的工作内存数据不一致,以及多线程执行时无序,共同造成的结果!
我们先简单的了解一下 Java 的内存模型,后期我们在介绍里面的原理!
如上图所示,线程 A 和线程 B 之间,如果要完成数据通信的话,需要经历以下几个步骤:
- 1.线程 A 从主内存中将共享变量读入线程 A 的工作内存后并进行操作,之后将数据重新写回到主内存中;
- 2.线程 B 从主存中读取最新的共享变量,然后存入自己的工作内存中,再进行操作,数据操作完之后再重新写入到主内存中;
如果线程 A 更新后数据并没有及时写回到主存,而此时线程 B 从主内存中读到的数据,可能就是过期的数据,于是就会出现“脏读”现象。
因此在多线程环境下,如果不进行一定干预处理,可能就会出现像上文介绍的那样,采用多线程编程时,程序的实际运行结果与预期会不一致,就会产生非常严重的问题。
针对多线程编程中,程序运行不安全的问题,Java 提供了synchronized
关键字来解决这个问题,当多个线程同时访问共享资源时,会保证线程依次排队操作共享变量,从而保证程序的实际运行结果与预期一致。
我们对上面示例中的DataEntity.addCount()
方法进行改造,再看看效果如下。
public class DataEntity {
private int count = 0;
/**
* 在方法上加上 synchronized 关键字
*/
public synchronized void addCount(){
count++;
}
public int getCount(){
return count;
}
}
多次运行结果如下:
第一次运行:result: 10000000
第二次运行:result: 10000000
第三次运行:result: 10000000
...
运行结果与预期一致!
三、synchronized 使用详解
synchronized
作为 Java 中的关键字,在多线程编程中,有着非常重要的地位,也是新手了解并发编程的基础,从功能角度看,它有以下几个比较重要的特性:
- 原子性:即一个或多个操作要么全部执行成功,要么全部执行失败。
synchronized
关键字可以保证只有一个线程拿到锁,访问共享资源 - 可见性:即一个线程对共享变量进行修改后,其他线程可以立刻看到。执行
synchronized
时,线程获取锁之后,一定从主内存中读取数据,释放锁之前,一定会将数据写回主内存,从而保证内存数据可见性 - 有序性:即保证程序的执行顺序会按照代码的先后顺序执行。
synchronized
关键字,可以保证每个线程依次排队操作共享变量
synchronized
也被称为同步锁,它可以把任意一个非 NULL 的对象当成锁,只有拿到锁的线程能进入方法体,并且只有一个线程能进入,其他的线程必须等待锁释放了才能进入,它属于独占式的悲观锁,同时也属于可重入锁。
关于锁的知识,我们后面在介绍,大家先了解一下就行。
从实际的使用角度来看,synchronized
修饰的对象有以下几种:
- 修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
- 修饰一个静态的方法:其作用的范围是整个静态方法,作用的对象是这个类的所有对象
- 修饰一个代码块:被修饰的代码块称为同步语句块,其作用的范围是大括号
{}
括起来的代码,作用的对象是调用这个代码块的对象,使用上比较灵活
下面我们一起来看看它们的具体用法。
3.1、修饰一个方法
当synchronized
修饰一个方法时,多个线程访问同一个对象,哪个线程持有该方法所属对象的锁,就拥有执行权限,否则就只能等待。
如果多线程访问的不是同一个对象,不会起到保证线程同步的作用。
示例如下:
public class DataEntity {
private int count;
/**
* 在方法上加上 synchronized 关键字
*/
public synchronized void addCount(){
for (int i = 0; i < 3; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public int getCount() {
return count;
}
}
public class MyThreadA extends Thread {
private DataEntity entity;
public MyThreadA(DataEntity entity) {
this.entity = entity;
}
@Override
public void run() {
entity.addCount();
}
}
public class MyThreadB extends Thread {
private DataEntity entity;
public MyThreadB(DataEntity entity) {
this.entity = entity;
}
@Override
public void run() {
entity.addCount();
}
}
public class MyThreadTest {
public static void main(String[] args) {
// 初始化数据实体
DataEntity entity = new DataEntity();
MyThreadA threadA = new MyThreadA(entity);
threadA.start();
MyThreadB threadB = new MyThreadB(entity);
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("result: " + entity.getCount());
}
}
运行结果如下:
Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6
当两个线程共同操作一个对象时,此时每个线程都会依次排队执行。
假如两个线程操作的不是一个对象,此时没有任何效果,示例如下:
public class MyThreadTest {
public static void main(String[] args) {
DataEntity entity1 = new DataEntity();
MyThreadA threadA = new MyThreadA(entity1);
threadA.start();
DataEntity entity2 = new DataEntity();
MyThreadA threadB = new MyThreadA(entity2);
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("result: " + entity1.getCount());
System.out.println("result: " + entity2.getCount());
}
}
运行结果如下:
Thread-0:0
Thread-1:0
Thread-0:1
Thread-1:1
Thread-0:2
Thread-1:2
result: 3
result: 3
从结果上可以看出,当synchronized
修饰一个方法,当多个线程访问同一个对象的方法,每个线程会依次排队;如果访问的不是一个对象,线程不会进行排队,像正常执行一样。
3.2、修饰一个静态的方法
synchronized
修改一个静态的方法时,代表的是对当前.java
文件对应的 Class 类加锁,不区分对象实例。
示例如下:
public class DataEntity {
private static int count;
/**
* 在静态方法上加上 synchronized 关键字
*/
public synchronized static void addCount(){
for (int i = 0; i < 3; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static int getCount() {
return count;
}
}
public class MyThreadA extends Thread {
@Override
public void run() {
DataEntity.addCount();
}
}
public class MyThreadB extends Thread {
@Override
public void run() {
DataEntity.addCount();
}
}
public class MyThreadTest {
public static void main(String[] args) {
MyThreadA threadA = new MyThreadA();
threadA.start();
MyThreadB threadB = new MyThreadB();
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("result: " + DataEntity.getCount());
}
}
运行结果如下:
Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6
静态同步方法和非静态同步方法持有的是不同的锁,前者是类锁,后者是对象锁,类锁可以理解为这个类的所有对象。
3.3、修饰一个代码块
synchronized
用于修饰一个代码块时,只会控制代码块内的执行顺序,其他试图访问该对象的线程将被阻塞,编程比较灵活,在实际开发中用的应用比较广泛。
示例如下
public class DataEntity {
private int count;
/**
* 在方法上加上 synchronized 关键字
*/
public void addCount(){
synchronized (this){
for (int i = 0; i < 3; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public int getCount() {
return count;
}
}
public class MyThreadTest {
public static void main(String[] args) {
// 初始化数据实体
DataEntity entity = new DataEntity();
MyThreadA threadA = new MyThreadA(entity);
threadA.start();
MyThreadB threadB = new MyThreadB(entity);
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("result: " + entity.getCount());
}
}
运行结果如下:
Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6
其中synchronized (this)
中的this
,表示的是当前类实例的对象,效果等同于public synchronized void addCount()
。
除此之外,synchronized()
还可以修饰任意实例对象,作用的范围就是具体的实例对象。
比如,修饰个自定义的类实例对象,作用的范围是拥有lock
对象,其实也等价于synchronized (this)
。
public class DataEntity {
private Object lock = new Object();
/**
* synchronized 可以修饰任意实例对象
*/
public void addCount(){
synchronized (lock){
// todo...
}
}
}
当然也可以用于修饰类,表示类锁,效果等同于public synchronized static void addCount()
。
public class DataEntity {
/**
* synchronized 可以修饰类,表示类锁
*/
public void addCount(){
synchronized (DataEntity.class){
// todo...
}
}
}
synchronized
修饰代码块,比较经典的应用案例,就是单例设计模式中的双重校验锁实现。
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
采用代码块的实现方式,编程会更加灵活,可以显著的提升并发查询的效率。
四、synchronized 锁重入介绍
synchronized
关键字拥有锁重入的功能,所谓锁重入的意思就是:当一个线程得到一个对象锁后,再次请求此对象锁时可以再次得到该对象的锁,而无需等待。
我们看个例子就能明白。
public class DataEntity {
private int count = 0;
public synchronized void addCount1(){
System.out.println(Thread.currentThread().getName() + ":" + (count++));
addCount2();
}
public synchronized void addCount2(){
System.out.println(Thread.currentThread().getName() + ":" + (count++));
addCount3();
}
public synchronized void addCount3(){
System.out.println(Thread.currentThread().getName() + ":" + (count++));
}
public int getCount() {
return count;
}
}
public class MyThreadA extends Thread {
private DataEntity entity;
public MyThreadA(DataEntity entity) {
this.entity = entity;
}
@Override
public void run() {
entity.addCount1();
}
}
public class MyThreadB extends Thread {
private DataEntity entity;
public MyThreadB(DataEntity entity) {
this.entity = entity;
}
@Override
public void run() {
entity.addCount1();
}
}
public class MyThreadTest {
public static void main(String[] args) {
// 初始化数据实体
DataEntity entity = new DataEntity();
MyThreadA threadA = new MyThreadA(entity);
threadA.start();
MyThreadB threadB = new MyThreadB(entity);
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("result: " + entity.getCount());
}
}
运行结果如下:
Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6
从结果上看线程没有交替执行,线程Thread-0
获取到锁之后,再次调用其它带有synchronized
关键字的方法时,可以快速进入,而Thread-1
线程需等待对象锁完全释放之后再获取,这就是锁重入。
五、小结
从上文中我们可以得知,在多线程环境下,恰当的使用synchronized
关键字可以保证线程同步,使程序的运行结果与预期一致。
- 1.当
synchronized
修饰一个方法时,作用的范围是整个方法,作用的对象是调用这个方法的对象; - 2..当
synchronized
修饰一个静态方法时,作用的范围是整个静态方法,作用的对象是这个类的所有对象; - 3.当
synchronized
修饰一个代码块时,作用的范围是代码块,作用的对象是修饰的内容,如果是类,则这个类的所有对象都会受到控制;如果是任意对象实例子,则控制的是具体的对象实例,谁拥有这个对象锁,就能进入方法体
synchronized
是一种同步锁,属于独占式,使用它进行线程同步,JVM 性能开销很大,大量的使用未必会带来好处。
关于更深入的原理知识,我们会在 JVM 系列中进行详解。文章内容难免有所遗漏,欢迎网友留言指出。
六、参考
多线程系列(三) -synchronized 关键字使用详解的更多相关文章
- Solr系列三:solr索引详解(Schema介绍、字段定义详解、Schema API 介绍)
一.Schema介绍 1. Schema 是什么? Schema:模式,是集合/内核中字段的定义,让solr知道集合/内核包含哪些字段.字段的数据类型.字段该索引存储. 2. Schema 的定义方式 ...
- Java精通并发-synchronized关键字原理详解
关于synchronized关键字原理其实在当时JVM的学习[https://www.cnblogs.com/webor2006/p/9595300.html]中已经剖析过了,这里从研究并发专题的角度 ...
- Netty4.x中文教程系列(三) Hello World !详解
Netty 中文教程 (二) Hello World !详解 上一篇文章,笔者提供了一个Hello World 的Netty示例. 时间过去了这么久,准备解释一下示例代码. 1.HelloServer ...
- struts2系列(三):struts2配置详解
原文链接:http://www.cnblogs.com/fmricky/archive/2010/05/20/1740479.html 1.<include> 利用include标签,可以 ...
- centos性能监控系列三:监控工具atop详解
引言 Linux以其稳定性,越来越多地被用作服务器的操作系统(当然,有人会较真地说一句:Linux只是操作系统内核:).但使用了Linux作为底层的操作系统,是否我们就能保证我们的服务做到7*24地稳 ...
- 深入理解JAVA I/O系列三:字符流详解
字符流为何存在 既然字节流提供了能够处理任何类型的输入/输出操作的功能,那为什么还要存在字符流呢?容我慢慢道来,字节流不能直接操作Unicode字符,因为一个字符有两个字节,字节流一次只能操作一个字节 ...
- OSGi 系列(三)之 bundle 详解
OSGi 系列(三)之 bundle 详解 1. 什么是 bundle bundle 是以 jar 包形式存在的一个模块化物理单元,里面包含了代码,资源文件和元数据(metadata),并且 jar ...
- Java并发关键字Volatile 详解
Java并发关键字Volatile 详解 问题引出: 1.Volatile是什么? 2.Volatile有哪些特性? 3.Volatile每个特性的底层实现原理是什么? 相关内容补充: 缓存一致性协议 ...
- [js高手之路] es6系列教程 - 对象功能扩展详解
第一:字面量对象的方法,支持缩写形式 //es6之前,这么写 var User = { name : 'ghostwu', showName : function(){ return this.nam ...
- java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析
java多线程系列(五)---synchronized ReentrantLock volatile Atomic 原理分析 前言:如有不正确的地方,还望指正. 目录 认识cpu.核心与线程 java ...
随机推荐
- 【rt-thread】构建自己的项目工程 -- 初始篇
现以stm32f429igt6芯片的板子 & Keil5编译环境为例,记述构建适配自己板子的rt-thread工程的过程 1.拿到rt-thread源码,进入bsp/stm32/librari ...
- Go-错误栈信息
Go中错误栈信息 .\main.go:22:2: number1 declared but not used .\main.go 错误发生的文件 22:2 文件的22行第2列 number1 decl ...
- Django应用中的静态文件处理
在日常开发中,我们都是把Django的Debug模式打开,方便调试,在这个模式下,由Django内置的Web服务器提供静态文件服务,不过需要进行一些配置,才能正确访问. 配置settings # St ...
- [转帖]【oracle】oracle查询表存储大小和表空间大小
目录 查看表分配的物理空间大小 查看表实际存储空间大小 查看每个表空间的大小 查看表空间大小及使用率 查看数据库中数据文件信息 查看临时表空间信息 oracle表大小有两种含义,即表分配的空间大小和实 ...
- [转帖]S3FS 简介及部署
PS:文章一般都会先首发于我的个人Blog上:S3FS 简介及部署 · TonghuaRoot's BloG. ,有需要的小伙伴可以直接订阅我的Blog,获取最新内容. 0x00 前言 S3FS可以把 ...
- [转帖]DD硬盘性能相关因素
https://www.jianshu.com/p/a15d7a65c876 本文简单介绍下DD测试硬盘性能时,各个因素的影响 首先列出测试结果 image.png oflag分析--/home ...
- [转帖]10--k8s之数据持久化
https://www.cnblogs.com/caodan01/p/15136217.html 目录 一.emptDir 二.hostPath 三.pv 和 pvc 1.环境准备 2.创建pv 3. ...
- [转帖]ubuntu下配置iptables、ufw端口转发
iptables 端口转发(CentOS) 注意:一来一去 在中转服务器操作 iptables -t nat -A PREROUTING -p tcp --dport [端口号] -j DNAT -- ...
- [粘贴]环绕闸极不能让三星在3nm工艺领先台积电
环绕闸极不能让三星在3nm工艺领先台积电 http://www.pinlue.com/article/2019/08/1510/599507978757.html 转身遇见她 2019-08-15 收 ...
- 隐私集合求交(PSI)协议研究综述
摘要 隐私集合求交(PSI)是安全多方计算(MPC)中的一种密码学技术,它允许参与计算的双方,在不获取对方额外信息(除交集外的其它信息)的基础上,计算出双方数据的交集.隐私集合求交在数据共享,广告转化 ...