多线程系列(三) -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 ...
随机推荐
- 开发了一个json格式化工具,使用js格式化json的代码分享
今天给大家介绍一下如何通过js来格式化json. 假设json字符串是: {"name":"刘德华","age":25.2,"bi ...
- 搞了个Blazor工具站,域名一次性买了10年!
大家好,我是沙漠尽头的狼. 在 Dotnet9 上线在线小工具和小游戏后,服务器的压力感觉挺大的,打开25个页面,内存占用170MB左右,CPU保持在60~70%,看来Server真不适合搞这类交互较 ...
- Linux-进程动态监控-top
- MySQL高可用搭建方案之(MHA)
有的时候博客内容会有变动,首发博客是最新的,其他博客地址可能会未同步,认准https://blog.zysicyj.top 首发博客地址 原文地址 MHA架构介绍 MHA是Master High Av ...
- [转帖]国产数据库到底行不行?人大金仓KINGBASE数据库与主流开源数据库性能实测
近年来,人大金仓的数据库产品受到了外界诸多的关注.做产品,免不了要接受用户的对比和选择,数据库因其行业的自身特点,还有很多开源的技术产品同台比拼,用户因此也会产生诸多疑问,国产数据库相比开源数据库到底 ...
- [转帖]PostgreSQL 压测工具pgbench
1.命令 pgbench --help pgbench is a benchmarking tool for PostgreSQL. Usage: pgbench [OPTION]... [DBN ...
- 周末拾遗 xsos 的学习与使用
周末拾遗 xsos 的学习与使用 摘要 周末陪儿子上跆拳道课. 自己一个人傻乎乎的开着笔记本想着学习点东西. 上午看到了一个sosreport的工具. 本来想学习一下. 发现xsos 应该是更好的一个 ...
- 【转帖】JAVA GC日志分析
https://zhuanlan.zhihu.com/p/613592552 目录 1. GC分类 针对HotSpot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Par ...
- 【转帖】io_uring vs epoll ,谁在网络编程领域更胜一筹?
io_uring vs epoll ,谁在网络编程领域更胜一筹? 2021-12-16 1473举报 简介: 从定量分析的角度,通过量化 io_uring 和 epoll 两种编程框架下的相关操作的耗 ...
- [转帖]SystemStap、BCC、bpftrace
https://plantegg.github.io/2019/09/16/SystemStap/ Linux 4.4+ 支持 eBPF.基于 eBPF 可以将任何内核函数调用转换成可带任何 数据的用 ...