所谓线程安全性是指多线程访问共享变量时导致实际值与预期值不符合的问题。所谓共享变量是指可以被多个线程访问到的变量,一般指实例变量或者静态变量,局部变量与方法参数变量为单独线程所有不会存在安全问题,我们下面使用一个例子演示实例变量中的安全问题。如下代码所示,我们创建一个AutoIntegerIncr类,用于整数的自加操作。
public class AutoIntegerIncr {
private int num;
public AutoIntegerIncr(int num) {
super();
this.num = num;
}
// num做自加操作,加的次数为count
public void numIncr() {
num =num +1;
}
public int getResult() {
return this.num;
}
}
如下我们创建1000000个线程做通过AutoIntegerIncr实例做自加操作,有可能每次运行会看到不同的结果,测试代码可运行结果如下所示:
public class AutoIntegerIncrDemo {
public static void main(String[] args) throws InterruptedException {
AutoIntegerIncr integerIncr = new AutoIntegerIncr(1);
for(int i =0; i< 100000; i++) {
new Thread(new ThreadIncr(integerIncr)).start();
}
Thread.sleep(10000);
System.out.println(integerIncr.getResult());
}
}
上面代码运行了四次结果分别为//99996 //99998//99998//99997//,可以看到与我们预期结果并不相同,这就是多线程的安全问题。多个线程可以同时操作同一个方法,同一个变量。那我们要如何解决呢,就是控制同一时刻只能有一个线程访问该方法,也就是所谓的加锁:
public synchronized void numIncr() {
num =num +1;
}
synchronized关键字加在方法上之后,我们再运行程序,获取了预期的结果,下面我们接着讲述synchronized的应用。首先需要确定的是synchronized锁定的是对象。它的具体表现形式有三种:对于普通同步方法,锁是当前实例对象,对于静态同步方法,锁是当前类的class对象,对于同步方法举哀,锁是synchronized括号里的对象。实例如下:
public class SynchronizedDemo {
public synchronized void synchMethod() {
//所定当前实例对象
}
public void synchMethod2() {
//所定当前实例对象,与synchMethod一致
synchronized (this) {
}
}
public synchronized void synchStaticMethod() {
//所定SynchronizedDemo.class实例对象
}
public void synchStaticMethod2() {
//所定SynchronizedDemo.class实例对象,与synchStaticMethod一致
synchronized (SynchronizedDemo.class) {
}
}
public void synchOtherobhect() {
//所定其他对象
synchronized (AutoIntegerIncr.class) {
}
}
}
在多线程并发编程中synchronized一直被称为重量级锁。但 是,Java SE 1.6对synchronized进行了各种优化。Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在 Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状 态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏 向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略。下面我们对这几种锁进行详细的介绍。在介绍锁之前,我们需要先了解Java中是如何存储锁的。在Java中锁是存储在对象头上的Mark Word 里。如下图为32位运行期间Mark Word内容的变化表格。
锁状态 | 25bit | 4bit | 是否偏向锁 1bit | 锁标识位
| ||||
23bit | 2bit | |||||||
轻量级锁 | 指向栈中锁记录的指针 | 00 | ||||||
重量级锁 | 指向互斥量(重量级锁)的指针 | 10 | ||||||
GC标记 | 空 | 11 | ||||||
偏向锁 | 线程ID | EPoch | 对象分代年龄 | 1 | 01 |
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁只需要判断对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果存储,表示线程已经获得了锁。如果没有,则需要再判断Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。偏向锁等到竞争出现才会释放锁,偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。如下图演示了偏向锁初始化的流程和偏向锁撤销的流程。
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。如下图两个线程同时争夺锁,导致锁膨胀的流程图。
Java中的每个对象都存在一个monitor监控,任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。 JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。其中代码块同步是使用monitorenter和monitorexit指令实现的。