并发编程三个特性:
1、原子性:一段操作一旦开始就会一直运行到底,中间不会被其它线程打断,这段操作可以是一个操作,也可以是多个操作。
2、可见性:当一个线程修改了共享变量的值,其它线程能立即感知到这种变化。
3、有序性:在本线程内观察,操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行语义(WithIn Thread As-if-Serial Semantics)”,后半句是指“指令重排”现象和“工作内存和主内存同步延迟”现象。
volatile 是Java提供的一种轻量级的同步机制。
相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。
volatile 特性:
保证了可见性,不保证原子性
禁止指令重排
指令重排是指在程序执行过程中, 为了性能考虑, 编译器和CPU可能会对指令重新排序.
volatile 保证了可见性,不保证原子性
可见性验证
假设A,B两个线程操作主存中的同一变量,当A线程修改了变量后,并把修改后的变量重新写入主存,此时B线程并不知道变量已被修改:
public class Demo01 {
private static int num = 0;//共享变量
public static void main(String[] args) {//主线程 A
new Thread(()->{ //副线程 B
while(num == 0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;// 修改num的值
System.out.println(num);
}
}
运行查看结果:
当主线程A修改变量num=1后,副线程B并没有得到最新的num值,还是原来的num=0,所以副线程B陷入死循环。
当给变量num加上 volatile 关键字后:
private volatile static int num = 0;
再次运行查看结果:
副线程B得到最新的num值,退出while循环。
当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。
验证非原子性
原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。
a++可以分解为三个操作:
package com.cheng.volatiletest;
import java.util.concurrent.TimeUnit;
public class Demo02 {
private static int num = 0;
public static void add(){
num++; //非原子性操作
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {//创建20个线程
new Thread(()->{
for (int j = 0; j < 1000; j++) {//每个线程执行100次add()方法
add();
}
}).start();
}
//如果当前存活线程大于两个
while (Thread.activeCount() > 2){// main,GC
Thread.yield();//让出cpu使用权
}
System.out.println(Thread.currentThread().getName()+" "+num);
}
}
运行查看结果:
因为add方法是个非原子性操作,所以线程在执行add方法的操作时,会被其他线程插入,导致执行出现问题。
使用 Synchronized 或 Lock 可以保证原子性,在执行add方法时,将不会被其他线程插队。
使用原子类
除了使用 Synchronized 和 Lock ,JUC包下的原子类也可以保证原子性。
package com.cheng.volatiletest;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class Demo02 {
private static AtomicInteger num = new AtomicInteger();//定义原子类AtomicInteger
public static void add(){
num.getAndIncrement();//以原子的方式将当前值加 1
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {//创建20个线程
new Thread(()->{
for (int j = 0; j < 1000; j++) {//每个线程执行100次add()方法
add();
}
}).start();
}
//如果当前存活线程大于两个
while (Thread.activeCount() > 2){// main,GC
Thread.yield();//让出cpu使用权
}
System.out.println(Thread.currentThread().getName()+" "+num);
}
}
getAndIncrement() 源码:
我们取得了旧值,然后把要加的数传过去,调用getAndAddInt () 进行原子更新操作,实际最核心的方法是 compareAndSwapInt(),使用CAS进行更新。
● volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好; volatile只能修饰变量,而synchronized可以修饰方法,代码块. 随着JDK新版本的发布,synchronized的执行效率也有较大的提升,在开发中使用sychronized的比率还是很大的。
● 多线程访问volatile变量不会发生阻塞,而synchronized可能会阻塞。
● volatile能保证数据的可见性,但是不能保证原子性; 而synchronized可以保证原子性,也可以保证可见性。
● 关键字volatile解决的是变量在多个线程之间的可见性; synchronized关键字解决多个线程之间访问公共资源的同步性。