1. volatile
-
定义:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。
-
实现原理:
Java代码如下:
instance = new Singleton();//instance是volatile变量
转换成汇编代码:
0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: lock addl $0x0,(%esp);
有volatile变量修饰的共享变量进行写操作的时候会多第二行汇编代码,lock前缀的指令在多核处理器下会引发了两件事情。
- 将当前处理器缓存行的数据会写回到系统内存。
- 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。
处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完之后不知道何时会写到内存,如果对声明了Volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
-
volatile实现原则
- Lock前缀指令会引起处理器缓存回写到内存。
- 一个处理器的缓存回写到内存会导致其他处理器的缓存无效。
-
volatile使用条件
volatile变量具有 synchronized 的可见性特性,但是不具备原子性。这就是说线程能够自动发现 volatile 变量的最新值。只能在有限的一些情形下使用 volatile 。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
- 对变量的写操作不依赖于当前值。
- 该变量没有包含在具有其他变量的不变式中。
反例:volatile变量不能用于约束条件中,下面是一个非线程安全的数值范围类。它包含了一个不变式 —— 下界总是小于或等于上界。
public class NumberRange {
private volatile int lower;
private volatile int upper;
public int getLower() { return lower; }
public int getUpper() { return upper; }
public void setLower(int value) {
if (value > upper)
throw new IllegalArgumentException(...);
lower = value;
}
public void setUpper(int value) {
if (value < lower)
throw new IllegalArgumentException(...);
upper = value;
}
}
将 lower 和 upper 字段定义为 volatile 类型不能够充分实现类的线程安全;而仍然需要使用同步——使 setLower()和 setUpper() 操作原子化。否则,如果凑巧两个线程在同一时间使用不一致的值执行 setLower 和 setUpper 的话,则会使范围处于不一致的状态。例如,如果初始状态是(0, 5),同一时间内,线程 A 调用setLower(4) 并且线程 B 调用setUpper(3),显然这两个操作交叉存入的值是不符合条件的,那么两个线程都会通过用于保护不变式的检查,使得最后的范围值是(4, 3) —— 一个无效值
-
volatile的适用场景
状态标志:也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。
volatile boolean isShutdown;
...
public void shutdown() {
isShutdown = true;
}
public void doWork() {
while (!isShutdown) {
// do stuff
}
}
线程1执行doWork()的过程中,可能有另外的线程2调用了shutdown,所以boolean变量必须是volatile。
这种类型的状态标记的一个公共特性是:通常只有一种状态转换;shutdownRequested 标志从false 转换为true,然后程序停止。
开销较低的“读-写锁”策略:如果读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。
如下显示的线程安全的计数器,使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。
public class CheesyCounter {
// Employs the cheap read-write lock trick
// All mutative operations MUST be done with the 'this' lock held
@GuardedBy("this") private volatile int value;
//读操作,没有synchronized,提高性能
public int getValue() {
return value;
}
//写操作,必须synchronized。因为x++不是原子操作
public synchronized int increment() {
return value++;
}
}
使用锁进行所有变化的操作,使用 volatile 进行只读操作。
其中,锁一次只允许一个线程访问值,volatile 允许多个线程执行读操作。
2. synchronized
-
介绍
在多线程并发编程中synchronized一直是元老级角色,很多人都会称呼它为重量级锁,但是随着Java SE1.6对Synchronized进行了各种优化之后,有些情况下它并不那么重了。Java SE1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。
关键字synchronized可以修饰方法或者以同步块的形式来使用,他主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,他保证了线程对变量访问的可见性和排他性。
-
synchronized特点
把代码块声明为 synchronized,有两个重要后果,通常是指该代码具有原子性(atomicity)和 可见性(visibility)。
- 原子性意味着个时刻,只有一个线程能够执行一段代码,这段代码通过一个monitor object保护。从而防止多个线程在更新共享状态时相互冲突。 所谓原子性操作是指不会被线程调度机子打断的操作,这种操作一旦开始,就一直到幸运星结束,中间不会有任何切换(切换线程)。
- 可见性则更为微妙,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的。 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。
-
synchronized实现同步的基础
Java中的每一个对象都可以作为锁。具体表现为一下3中形式:
- 对于普通的同步方法,锁是当先实例对象。
- 对于静态同步方法,锁是当前类的Class对象。
- 对于同步方法块,锁是Synchronized括号里配置的对象。
示例代码:
public class SynchronizedTest2 {
// synchronized关键字修饰静态的方法 同步方法
public synchronized static void printNum1(){
for(int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
// synchronized关键字使用类锁 同步代码块
public static void printNum2(){
synchronized(SynchronizedTest2.class) {
for(int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
}
// synchronized关键字修饰 同步方法 这里使用的是对象锁
public synchronized void printNum3(){
for(int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
final SynchronizedTest2 test = new SynchronizedTest2();
Thread t1 = new Thread(new Runnable() {
public void run() {
SynchronizedTest2.printNum1();
}
},"A");
Thread t2 = new Thread(new Runnable() {
public void run() {
SynchronizedTest2.printNum2();
}
},"B");
Thread t3 = new Thread(new Runnable() {
public void run() {
test.printNum3();
}
},"C");
t1.start();
t2.start();
t3.start();
}
}
以上代码中 静态方法printNum1() 和 printNum2() 使用的都是类锁,方法printNum3()使用的是当前对象的锁。
我们开了三个线程A,B,C分别运行printNum1(),printNum2(),printNum3()方法 从结果中我们可以看到 线程A和线程B是始终同步的,线程C和线程A,B之间没有同步关系
-
实现原理
示例代码: Synchronized.java
public class Synchronized {
public static void main(String[] args) {
synchronized (Synchronized.class){//对Synchronized class对象进行加锁
}
m();//静态同步方法,对Synchronized class对象进行加锁
}
public synchronized static void m() {
}
}
在Synchronized.class同级目录执行javap -v Synchronized.class,以下是相关部分的输出:
public static void main(java.lang.String[]);
// 方法修饰符,表示:public staticflags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc #1 // class com/murdock/books/multithread/book/Synchronized
2: dup
3: monitorenter // monitorenter:监视器进入,获取锁
4: monitorexit // monitorexit:监视器退出,释放锁
5: invokestatic #16 // Method m:()V
8: return
public static synchronized void m();
// 方法修饰符,表示: public static synchronized
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=0, args_size=0
0: return
- 上面class信息中,对于同步块的实现使用了monitorenter和monitorexit指令,而同步方法则是依靠方法修饰符上的ACC_SYNCHRONIZED来完成的。无论采用哪种方式,其本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。
- 任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。
下图描述了对象、对象的监视器、同步队列和执行线程之间的关系。
从图中可以看到,任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。
-
线程等待/同步机制
等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()和notify/notifyAll()的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
下面我们直接使用对象锁的相关条件实现一个生产者和消费者案例:
public class SynchronizedTest3 {
private Object object = new Object();
private List<Integer> list = new ArrayList<Integer>();
private boolean flag = true;
// 这里我们使用object对象的锁,以及该锁的条件对象
// 生产者线程一次生产一个数据5
public void produce() throws InterruptedException {
synchronized(object) {
while(flag){
if (list.size() > 0){
object.wait();
} else {
list.add(5);
System.out.println("生产者生产数据");
object.notifyAll();
}
}
}
}
// 消费者线程每次消费一个数据
public void consume() throws InterruptedException {
synchronized(object){
while(flag){
if (list.size() <= 0) {
object.wait();
} else {
System.out.println(list.remove(0));
System.out.println("消费者消费数据");
object.notifyAll();
}
}
}
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public static void main(String[] args) {
// TODO Auto-generated method stub
final SynchronizedTest3 test = new SynchronizedTest3();
Thread t1 = new Thread(new Runnable() {
public void run() {
try {
test.produce();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
Thread t2 = new Thread(new Runnable() {
public void run() {
try {
test.consume();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
t1.start();
t2.start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
test.setFlag(false);
}
}
- 生产者线程检查容器list中是否有数据,如果有数据则调用object.wait()使得生产者线程进入该条件的等待集中,如果容器中没有数据,则生产者线程生产数据放入list中,让后调用object.notifyAll()方法从该条件等待集中所有线程的阻塞状态。
- 消费者线程检查容器中是否有数据,如果有数据则消费数据然后调用notifyAll()方法,是的处于该条件等待集中的生产者线程解除阻塞状态。如果没有数据调用object.wait()方法是的消费者线程进入该条件的等待集中。
下图描述了上述示例的全过程。