

多线程系列(三) -synchronized 关键字使用详解 - 程序员志哥
source link: https://www.cnblogs.com/dxflqm/p/18022798
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在之前的线程系列文章中,我们介绍了线程创建的几种方式以及常用的方法介绍。
今天我们接着聊聊多线程线程安全的问题,以及解决办法。
实际上,在多线程环境中,难免会出现多个线程对一个对象的实例变量进行同时访问和操作,如果编程处理不当,会产生脏读现象。
二、线程安全问题介绍
我们先来看一个简单的线程安全问题的例子!
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 系列中进行详解。文章内容难免有所遗漏,欢迎网友留言指出。
Recommend
-
56
synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。 synchronized的四种使用方式 修饰代码块:被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用于调用对象
-
7
聊聊 Java 中绕不开的 Synchronized 关键字 Posted on 2021-06-20...
-
5
synchronized关键字 什么是synchronized JDK官网对synchronized关键字有个比较权威的解释。 Synchronized keyword enable a simple strategy for preventing thread interference and memory consistency errors: if an object...
-
4
记一次Synchronized关键字使用不合理,导致的多线程下线程阻塞问题排查 | HeapDump性能社区记一次Synchronized关键字使用不合理,导致的多线程下线程阻塞问题排查杭盖S...
-
5
多线程的同步机制对资源进行加锁,使得在同一个时间,只有一个线程可以进行操作,同步用以解决多个线程同时访问时可能出现的问题。 同步机制可以使用synchronized关键字实现。 当synchronized关键字修饰一个方法的时候...
-
7
想必在面试中经常会被问到Synchronized关键字,它有什么特性,原理什么 它的主要特性是同步锁、非公平锁、阻塞锁、可以保证线程安全(可见性、原子性、有序性) JDK1.6之后对Synchronized有优化,有个锁升级过程
-
6
作者:小牛呼噜噜 | https://xiaoniuhululu.com 计算机内功、JAVA底层、面试、职业成长相关资料等更多精彩文章在公众号「
-
11
从字节码看 synchronized 关键字是怎么工作的 2023-04-05 2023-04-07...
-
2
Synchronized关键字的底层原理? 作者:springboot葵花宝典 2024-03-15 15:12:27 在 Java 中,关键字 synchronized 可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中...
-
3
在之前的博文中,我们介绍了volatile关键字,Java中的锁以及锁的分类,今天我们花5分钟时间,一起学习一下另一个关键字:synchronized。 synchronized是什么? 首先synchronized是Java中的一...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK