咱们先从有名的happen-before规则说起。
多线程并发设计需要遵循happen-before规则。
在java虚拟机的内存模型中,有主内存和工作内存的概念,每个线程对应一个工作内存,并共享主内存的数据。如果变量不用volatile修饰,则在工作内存中进行读写,不会将数据立即同步到主内存。变量如果用volatile修饰了,线程会从主内存读取变量的值(而不是工作内存),写数据时也会写到主内存。故Java可以确保所有线程看到这个变量的值是一致的,如果某个线程对volatile修饰的共享变量进行更新,那么其他线程可以立马看到这个更新。这种特性称为变量的内存可见性。
volatile变量的内存可见性是基于内存屏障(Memory Barrier)实现的,什么是内存屏障?内存屏障,又称内存栅栏,是一个CPU指令。在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM为了保证在不同的编译器和CPU上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序。
代码示例:
class Singleton {
private volatile static Singleton instance;
private int a;
private int b;
private int c;
public static Singleton getInstance() {
if (instance == null) {
syschronized(Singleton.class) {
if (instance == null) {
a = 1; // 1
b = 2; // 2
instance = new Singleton(); // 3
c = a + b; // 4
}
}
}
return instance;
}
}
1、如果变量instance没有volatile修饰,语句1、2、3可以随意的进行重排序执行,即指令执行过程可能是3214或1324。
2、如果是volatile修饰的变量instance,会在语句3的前后各插入一个内存屏障。
所以,volatile能禁止指令重排序优化。
volatile不具备原子性,这是volatile与java中的synchronized、java.util.concurrent.locks.Lock最大的功能差异。
volatile相对于synchronized稍微轻量些(不会引起线程上下文的切换和调度),在某些场合它可以替代synchronized,但是又不能完全取代synchronized,只有在某些场合才能够使用volatile。使用它必须满足如下两个条件:
1、对变量的写操作不依赖当前值(i++依赖当前值,不能用volatile修饰,应该用synchronized);
2、该变量没有包含在具有其他变量的不变式中(例如 “start <=end”)。
volatile不只对简单数据类型起作用,对Object也有作用。
volatile使用示例:修饰状态变量
public class ServerHandler {
private volatile isopen; // isopen已经修改,其他线程就马上拿到最新值
public void run() {
if (isopen) {
...
} else {
...
}
}
public void setIsopen(boolean isopen) {
this.isopen = isopen; //不依赖当前值,并且是原子操作(赋值语句是原子操作)
}
}
哈哈,还是老规矩,做道课后作业看看掌握了没有。
找找下面代码有什么问题,怎么修改?
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread extends Thread {
@Override
public void run() {
while(!ready) {
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String[] args) {
new ReaderThread().start();
number = 42;
ready = true;
}
}
synchronized作用:(1)确保线程互斥的访问同步代码(2)保证共享变量的修改能够及时可见(3)有效解决重排序问题。其中,(1)就是代码的原子性,是volatile不具备的特性。
常见用法一:修饰实例方法
public synchronized void doSomething() {
i++;
}
常见用法二:修饰静态方法
public static synchronized void increase(){
i++;
}
用法一锁住的是实例方法,当2个线程使用2个实例时,数据会出错。
例如:
public class AccountingSyncBad implements Runnable{
static int i=0;
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
//new新实例
Thread t1=new Thread(new AccountingSyncBad());
//new新实例
Thread t2=new Thread(new AccountingSyncBad());
t1.start();
t2.start();
//join含义:等待t1线程终止之后再执行后面的代码
t1.join();
t2.join();
System.out.println(i); //打印结果不是2000000
}
}
由于2个线程实例不同,synchronized锁住的是2个不同的方法,所以t1和t2线程使用的是不同的锁,因此线程安全是无法保证的。
常见用法三:修饰代码块(只锁需要同步的代码,使锁持有时间最短,提升性能)
synchonized(LockObject) {
//代码块
for(int j=0;j<1000;j++){
i++;
}
}
LockObject: 可以是任意Object
每次当线程进入synchronized包裹的代码块时就会要求当前线程持有LockObject的对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行代码块。
关于synchronized值得注意的几个点:
1) 所有对象都含有一个锁,当调用到synchronized(object)块时,先检测obj有没有加锁,如果有,阻塞,如果没有,对object加锁, 执行完后释放锁。
2) synchronized void f() {//…} 等价于 void f() { synchronized(this) {//…} }, 在当前对象this上加锁,f()执行完再释放锁。
3) synchronized 提供原子性和可视性, 被它完全保护的变量不需要用volatile。
4) synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法。
5)不同的对象实例的synchronized(this)方法是不相互干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法。这时可以用synchronized(ClassName.class)对类进行加锁,该类的所有对象都会加锁。
下面代码会打印奇数吗?
class AtomTest implements Runnable {
private volatile int i = 0;
public int getVal() {return i;}
public synchronized void inc() {i++; i++;}
@Override
public void run() {
while (true) {
inc();
}
}
}
public class TestThread {
public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
AtomTest at = new AtomTest();
exec.execute(at);
while (true) {
int val = at.getVal();
if (val % 2 != 0) {
System.out.println("奇数:"+val);
System.exit(0);
}
}
}
}
Java的标准库中提供了大量的同步机制,如队列、future、executor等,大多位于 java.util.concurrent包中。应首先考虑使用这些工具,而非低级的锁、volatile变量、原子类。比起基本的锁、volatile变量,这些工具实现同步更简单,而且更难以出错。Java API中详细地描述了各个类、接口所保证的happens-before关系,阅读时需特别留意。一般来说,将数据存入容器的操作(如入队列)先发生于(happens before)将这个数据从容器中取出(如出队列)的操作。
1、NoVisibility可能会持续循环下去,因为读线程可能永远都看不到ready的值。
2、NoVisibility可能会输出0,因为读线程可能看到了写入ready的值,但却没有看到之后写入number的值,这种现象被称为“重排序”。
解决办法:对ready、number添加volatile。
public class NoVisibility {
private volatile static boolean ready;
private volatile static int number;
……
会打印奇数。原因是getVal读到了inc的中间值。 这种情况要在getVal方法前加synchronized。