原子性,可见性与有序性
在多线程中,线程同步的时候一般需要考虑原子性,可见性与有序性
原子性
原子性定义:一个操作或者多个操作在执行过程中要么全部执行完成,要么全部都不执行,不存在执行一部分的情况。
以我们在Java代码中经常用到的自增操作i++
为例,i++
实际上并不是一步操作,而是首先对i的值加一,然后将结果再赋值给i。在单线程中不会存在问题,但如果在多线程中我们考虑这样一个情况:i是一个共享变量,初始值为0,假设线程一以执行到某一步正好进行自增操作i++
,刚好对i进行了加一但是还没将值重新赋给i,此时当前线程被cpu挂起,而另一个线程二开始执行,刚好也对i进行了一个赋值操作i=10;
,等线程一重新执行后会将i自增后的值1赋给i,此时相当于覆盖了线程二的赋值操作。此时将会产生线程不安全的情况。
可见性
多个线程同时访问一个共享的变量的时候,每个线程的工作内存有这个变量的一个拷贝,变量本身还是保存在共享内存(堆)中。所以并不是每一次一个线程修改了值后其他线程都可以立即取到修改后的值。可见性是指当其他的线程访问同一个变量时,当一个线程修改了这个变量的值,其他线程也能够立即看得到修改的值。
有序性
有序性是指程序的执行严格按照我们写的代码的顺序进行执行。
指令重排
一般情况下,CPU和编译器为了提升程序执行的效率,会按照一定的规则允许对指令进行优化,即调整实际指令运行的顺序。指令重排不会对单线程的程序造成任何不利的影响,但是多线程环境下将会产生一些影响。指令重排的前提条件是指令调整后不会影响单线程程序的执行:
int i = 2; //statement 1
int j = 1;//statement 2
int k = i*j;//statement 3
在上面的代码中对于语句1和语句2相互之间没有任何依赖,所以可能发生指令重排,但是语句3和语句1,2都有关系,所以语句3一定是在语句1和语句2之后执行的。所以单线程情况下是绝对不会出现问题的。但是对于多线程可能就发生只是初始化了语句1或者语句2就执行语句3了。
要保证在多线程下线程安全,这三大性质都是必须要保证的,而一旦其中一项无法保证那么不是线程安全的。前面的synchronized关键字就是实现了这三大特性的。
volatile
虽然已经有了synchronized关键字保证了线程安全需要的三大特性,但是在JDK1.8优化synchronized之前,synchronized关键字都是一个重量级的锁,对程序的效率有着比较大的影响。在java中还有一个synchronized关键字的轻量级的实现-volatile关键字。volatile关键字是在JDK1.5之后重新被重用的一个关键字,它可以保证上诉三大特性中的有序性和可见性,但是不能保证原子性,所以它实际上是线程不安全的。
保证可见性
出现可见性的原因在于私有栈帧中的值和公共堆中的共享值不同得问题。
当一个线程在修改普通变量时,其他线程不能立刻看到修改后的值,如果此时有其他线程读取该变量的值,实际上读到的是没有修改的值。
volatile关键字作用在于当要使用时强制从主内存中读取值,保证每次读取的都是公共内存中的值。
防止指令重排
内存屏障也称为内存栅栏或栅栏指令,是一种屏障指令,它使CPU或编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。 这通常意味着在屏障之前发布的操作被保证在屏障之后发布的操作之前执行。
volatile关键字功能的实现既是通过内存屏障完成的,当使用volatile关键字修饰的变量进行读写是便会加上内存屏障来保证设计变量的操作顺序执行,需要注意的是其和synchronized关键字的同步是不一样的。
为什么不是原子性的?
实际上volatile关键字保证的事所有线程从主存中取到的值是最新的,但是多个线程修改了改变量的值并不会通知其他线程,除非其他线程再次从主存中取值。
在以上阶段中,比如存在两个线程且两个线程都已经加载了变量count的值,这时线程一将count修改为10,线程二将值修改为20,但是两个线程之间并不知道对方都改了值,而最终写到主存的值也是后写入的那一个,即始终都一个线程修改的值被覆盖,所以其并不是原子性的。在涉及到多线程操作共享变量是还是应该加锁进行操作。
synchronized和volatile关键字的比较
- volatile关键字并不是同步操作,其在多线程访问下不会进行阻塞,而synchronized关键字会发阻塞。
- volatile关键字能保证有序性和可见性,但不能保证原子性。synchronized三种特性都能保证,所以synchronized是线程同步的,而volatile不是。
- volatile是线程同步的轻量级实现,所以效率较之synchronized要高。
等待通知机制
在多线程程序中可能会存在多个程序相互配合完成一项功能,这是就需要线程之间进行通信,在一个线程的工作完成后通知后续线程工作。通常情况下我们可以在一个线程中进行一个while循环操作,设置一个标志flag,当该线程的前置线程完成后修改flag,后面的while得到这个标志后知道自身需要开始工作了,跳出循环。但是这种方法的劣势在于while循环使得该线程一个需要处于运行中,同时当多个线程相互之间都需要进行通信时会使得程序变得极其复杂。为了解决这个问题,有人提出了一种等待通知机制。
等待通知机制是利用JDK中提供的API中的wait()
和notify/notifyAll()
方法来进行实现(实际上Lock类中的方法也能实现),wait()
方法是使得当前线程进入等待队列中,notify/notifyAll()
是将等待的线程唤醒。
等待方
- 获取对象锁
- 如果条件不满足,调用对象的wait方法,被通知后依然要检查条件是否满足
- 条件满足以后,才能执行相关的业务逻辑
Synchronized(对象){
While(条件不满足){
对象.wait()
}
// do your working
}
通知方
- 获得对象的锁;
- 改变条件;
- 通知所有等待在对象的线程
Synchronized(对象){
业务逻辑处理,改变条件
对象.notify/notifyAll
}
实例
public class User {
private int age = 30;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
/**
* 1、获取对象锁
* 2、如果条件不满足,调用对象的wait方法,被通知后依然要检查条件是否满足
* 3、条件满足以后,才能执行相关的业务逻辑
*/
public synchronized void waitAge(){
System.out.println("age is " + this.age);
while(this.age >= 20){
//条件不满足
try {
System.out.println("current thread is waiting");
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//满足条件后执行
System.out.println("current thread" + Thread.currentThread().getName() + " age is " + this.age);
}
/**
* 1、 获得对象的锁;
* 2、 改变条件;
* 3、 通知所有等待在对象的线程
*/
public synchronized void changeAge(){
//修改条件
this.age = new Random().nextInt(20);
System.out.println("inform all thread");
//这里使用notifyAll()是因为notify()方法无法指定唤醒某一个线程,notify()的唤醒是随机的
//notifyAll()唤醒所有等待线程
notifyAll();
}
}
测试类:
public class WaitAndInform extends Thread{
private static User user = new User();
@Override
public void run() {
user.waitAge();
}
public static void main(String[] args) throws InterruptedException {
for(int i=0;i<=4;i++){
new WaitAndInform().start();
}
Thread.sleep(1000);
//修改条件 唤醒其他线程
user.changeAge();
}
}