前言
当我们再使用多线程的时候经常会碰到一些线程安全的问题,什么是线程安全、什么是线程锁。为什么需要用到线程锁?接下来就分析一下volatile,synchronized,Lock。
再这之前我们先要了解
- Java内存模型
- 并发编程中的三个概念
java内存模型
我们先看下图,java内存模型可以抽象如下:
首先我们有一块主内存,然后我们每开一个线程,给这个线程开辟一块内存,主内存中的共享变量在每个线程中的内存中都有一个副本。然后通过JMM控制变量的刷新到主线程中。
变量更新过程
我们举一个例子分析一下两个线程之间的通讯
如下图:
假设主内存中有一个变量X=0;线程A,线程B,都有一个变量X的副本。然后我们需要线程A改变了X的值,现在要通知线程B。步骤:
- 第一步先将x变量刷新到主内存中去,
- 第二步再从主内存中的x变量刷新到线程B当中。
并发编程中的三个概念
原子性
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
有一个很经典的例子:
银行中转账,假设A要转1000快钱给B,那么银行就需要在A账户上扣掉1000,然后再像B账户上加上1000.
这个操作必须要保证原子性,A给B赚钱,银行的两个操作不能打断一个。断了一个就转账失败了。
可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
对于可见性,Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
有序性
有序性:即程序执行的顺序按照代码的先后顺序执行。
int i = 0;
boolean flag = false;
i = 1; //语句1
flag = true; //语句2
上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。
下面解释一下什么是指令重排序:一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
注意:处理器在进行重排序时是会考虑指令之间的数据依赖性,例如:
int a = 10; //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4
这一段代码就不能随便重新排序了,就有一定的顺序了
因为语句3依赖依赖语句1,语句4依赖语句2和语句3。所以语句4必须最后运行,语句3要在语句1前运行。
volatile
volatile关键字的两层语义
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
测试
public class VolatileDemo {
int a = 0;
public void addNum() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
a++;//注意这里
}
public static void main(String[] args) {
final VolatileDemo volatileDemo = new VolatileDemo();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
volatileDemo.addNum();
System.out.println("num=" + volatileDemo.a);
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
volatileDemo.addNum();
System.out.println("num=" + volatileDemo.a);
}
}
}).start();
}
}
我们运行如上代码,发现运行的时候结果都不一样。可能两个重复的数字打印出来,现在我们给a变量加一个volatile关键字。因为上面我们说volatile可以保证变量的可见性,那么我们就能第一个线程改变了值以后第二个线程马上去到了就能保证打印到200了。继续运行。发现依然还是错误的。这是为什么呢?
注意上面的a++操作。a++其实是两个操作,假设a=0。一,a+1然后给一个暂存的变量b=1。二。a=暂存的变量a=1。我们两个线程中获取a的副本a都等于0。可能线程一运行到第一步这时候a=0,第二个线程已经运行完了第两步a=1了。这时候线程一继续运行第二步,a=1。于是就打印了两个1。这个例子证明了volatile不能保证原子性。
Volatile原理
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。
当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。
而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
volatile 性能:
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
volatile使用场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
1.状态标记量
volatile boolean inited = false;
//线程1:
context = loadContext();
inited = true;
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
2.双重检验
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}