目录
- 1.前言
- 1.synchronized 关键字
- 1. 互斥
- 2.保证内存可见性
- 3.可重入
- 2. volatile 关键字
- 1.保证内存可见性
- 2.无法保证原子性
- 3.synchronized 与 volatile 的区别
1.前言
synchronized
关键字和volatile
是大家在Java
多线程学习时接触的两个关键字,很多同学可能学习完就忘记了,本文帮助大家回顾以及学习两个关键字的作用,以及说出它们的区别,同时也为了自己学习巩固。
1.synchronized 关键字
1. 互斥
属于synchronized
最关键的特性,可以起到互斥的作用,当某个线程执行到某个对象的synchronized
中时,其他线程如果也执行到同一个对象 的synchronized
时就会进行阻塞等待。
- 进入
synchronized
修饰的代码块此时相当于 加锁 - 退出
synchronized
修饰的代码块此时相当于 释放锁
其解决的问题是在多线程环境下,多个线程对于同一个变量进行读写操作时可能产生的线程安全问题。
如下图代码:
public class Main {
static int count = 0;
static void add() {
count++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
按照预期,我们希望count
的值应该是100000
,当执行后输出的答案却是:
无论多次执行多少次,答案总是与预期相差甚远。这是最简单的多线程安全问题。因为count++
这个操作,需要先将count
从主内存读入到工作内存,然后增值,再将更改后的值写回主内存,这一系列操作必须保证原子性,而在多线程环境下是无法保证的,所以我们需要加上synchronized
进行上锁,以此保证自增这个操作的原子性。
只需更改add
方法如下:
synchronized static void add() {
count++;
}
再次运行后答案与预期相符合:
需要注意一点,synchronized
修饰方法时如果是静态方法,则加的是该类对象的锁,如果是成员方法,则加的是对象锁。
2.保证内存可见性
从上面也可以看出synchronized
的工作过程:
- 1.获得互斥锁
- 2.从主内存拷贝变量的最新副本到工作内存
- 3.执行代码
- 4.讲更改后的共享变量的值更新回工作内存
- 5.释放互斥锁
这样的工作流程,是一定可以保证内存可见性的。当然有的同学并不了解什么是内存可见性,下文讲volatile
时我们稍微讲一下,因为它也能保证内存可见性。
3.可重入
synchronized
同步块,对于同一条线程来说是可重入的,不会出现将自身锁死的情况。
当然大家可能对 自身锁死 这个情况不太理解,我们举例一个代码:
public class Main {
//锁对象
public static Object lock = new Object();
public static void main(String[] args) {
//一次加锁
synchronized (lock) {
//二次加锁
synchronized (lock) {
System.out.println("正确输出");
}
}
}
}
当线程在一次加锁时,会成功加锁,当第二次加锁时,此时lock
已被上锁,于是该线程进行阻塞等待,但其实这个锁是被它自己拿着的,它又不进行释放锁操作,于是将自己锁死。这样的锁称之为 不可重入锁。
当我们Java
中的synchronized
是可重入锁,不会出现上面的问题,它可以正确打印:
如果对上述代码还不够理解,可以再看一个二次加锁的例子:
public class Main {
public int count = 0;
synchronized void increase() {
count++;
}
synchronized void increase2() {
increase();
}
}
在上诉代码中:
-
increase
和increase2
两个方法都加了synchronized
,而且它们的锁对象都是针对当前对象加锁的。 - 在调用
increase2
时,会先给该对象上锁,执行调用increase
时,会二次上锁(此时上个锁还未释放),这是没问题的,因为synchronized
是可重入锁。
那是否真的上了两把锁呢?
其实并非如此,在可重入锁的内部,包含了 线程持有者 和 计数器 两个信息。
- 如果某个线程加锁时,发现锁已被占用,但又发现占用的恰好是自己时,那么然后可以获取到这个锁,并让计数器自增
- 解锁时首先会让计数器自减,但只有真正自减到
0
时,我们才会真正意义上的将该锁释放,以供其他线程获取到。
2. volatile 关键字
相对于 synchronized
来说,大家可能对volatile
会比较陌生,我们来看看其有哪些作用。
1.保证内存可见性
简单来说,线程在工作时,会去主内存中读取数据到工作内存中,然后从工作内存读取数据。但是,线程从工作内存读取数据的速度,要远远的大于从主内存读取数据。
当一个线程大量地从主内存请求同一个变量的值时,它会发现这个值一直没变,此时jvm
会 “自作主张” 的进行优化,直接从工作内存读取之前读到的值。这就会导致一个问题,其他线程对这个共享变量值进行修改,这个线程不能及时地被看到,也就读到了一个错误的值。
比如如下代码:
public class Main{
static int isQuit = 0;
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (isQuit == 0) {
}
System.out.println("t线程执行结束");
});
t.start();
Scanner sc = new Scanner(System.in);
isQuit = sc.nextInt();
System.out.println("main线程执行结束");
}
}
执行以后随便输入一个非零整数:
发现t
线程仍然在进行,而main
线程已经结束,但按照逻辑其实此时isQuit
值被修改为非零,t
线程也应该结束。这就是由于内存可见性产生的问题,main
线程修改了isQuit
的值t
线程并不能及时的接收到。
解决的方法也很简单,只需要给isQuit
加上volatile
关键字,这样每次t
线程都会强制去主内存中读取isQuit
的值,从而保证了内存可见性。
2.无法保证原子性
volatile
相较于synchronized
来说,主要在于其无法保证原子性,也就是对于下面这个程序,即使给count
加上volatile
,我们也无法让count
的值为100000
。
public class Main {
static int count = 0;
static void add() {
count++;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
3.synchronized 与 volatile 的区别
根据上面的总结,我们可知:synchronized
既可以保证原子性还可以保证内存可见性,而volatile
只能保证内存可见性。那有的人是不是肯定想,那我们无脑使用synchronized
不就好了吗?
那肯定不对,synchronized
会进行加锁,使得效率大大的较低,而volatile
却不会影响效率,所以只有必须要保证原子性的操作,我们才选择synchronized
进行加锁。