1.安全性问题
并发带来的安全性问题是指在多线程环境下代码的执行结果和单线程的结果不一致的问题。下面来看一个例子:
public class Sequence {
private int value;
public int getNext() {return value++;
}
public static void main(String[] args) {
final Sequence s = new Sequence();
new Thread(new Runnable() {
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + " " + s.getNext());
try {
Thread.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + " " + s.getNext());
try {
Thread.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + " " + s.getNext());
try {
Thread.sleep(500L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
我们实现了一个计数器,理想状况下计数器的值应该是递增的,不应该出现重复的值,但运行上面的程序会发现类似的情况:
竟然出现了两个9,这显然和我们的预期不符,这就是线程所带来的安全性问题。这是为什么呢?归根结底是因为value++操作不是原子的,而是分好几步完成的,并且多个线程共享一个变量value,这样在每个线程执行流中对线程工作内存中变量的修改不能立刻反映到主内存而导致其他线程看到的不是最新的值。这我们可以从Sequence生成的字节码中看出,++操作不是一步完成的。
public int getNext();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=4, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field value:I
5: dup_x1
6: iconst_1
7: iadd
8: putfield #2 // Field value:I
11: ireturn
所以出现线程安全性问题的条件是:多线程的环境,多个线程共享一个资源,对资源进行非原子操作。解决方案,用synchronized修饰getNext()方法,使得该方法每次只能被一个线程执行。
2.Synchronized原理与使用
使用Synchronized修饰的代码会在执行前尝试获取对象锁。每个Java对象都是一个内置锁,线程在进入同步代码块之前都要获取锁。Synchronized可以修饰普通方法,此时的锁对象为this,还可以修饰静态方法,此时的锁对象为类所对应的字节码对象,还可以修饰代码块,此时可以指定对象锁。被synchronized修饰的代码块在编译成字节码指令后,被修饰的代码生成的字节码指令会被包在monitorenter和monitorexit之间,用来标识进入同步代码块和退出同步代码块。
任何对象都可以作为锁,那么锁信息又存在对象的什么地方呢?锁信息存在对象头中,对象头中的信息包括Mark Word(对象的hash值,锁,线程id,Epoch,对象的分代年龄信息,是否是偏向锁,锁标志位)、Class Metadata Address,如果是数组的话还有Array Length。