synchronized
是Java并发中最常见的关键字之一,使用锁保证线程间同步,下面介绍synchronized
相关内容。
synchronized的三种用法
synchronized
主要有三种用法,分别是修饰普通方法、静态方法和代码块,下面用代码分别演示一下三种用法。
修饰普通方法
synchronized
修饰普通方法作用于对象实例,进入同步代码前获得当前对象实例的锁。
public class SynchronizedDemo {
public static void main(String[] args) {
MyRunnable r = new MyRunnable();
Thread t1 = new Thread(r);
Thread t2 = new Thread(r);
t1.start();
t2.start();
}
}
class MyRunnable implements Runnable {
//修饰普通方法
public synchronized void fun() {
try {
System.out.println("fun()");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
fun();
}
}
修饰静态方法
synchronized
修饰静态方法作用于类的所有对象实例,因为静态方法属于类而不属于对象。下面演示一下同一个类的不同对象实例的同步代码。
public class SynchronizedDemo {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable());
Thread t2 = new Thread(new MyRunnable());
t1.start();
t2.start();
}
}
class MyRunnable implements Runnable {
//修饰静态方法
public static synchronized void fun() {
try {
System.out.println("fun()");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void run() {
fun();
}
}
修饰代码块
synchronized
修饰代码块的情况比较复杂,还可以再细分。synchronized(obj)
和synchronized(this)
可以获取给定对象的锁,而synchronized(类名.class)
获得当前class的类锁。下面代码演示获取类锁的过程。
public class SynchronizedDemo {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable());
Thread t2 = new Thread(new MyRunnable());
t1.start();
t2.start();
}
}
class MyRunnable implements Runnable {
public void fun() {
synchronized(MyRunnable.class) {
try {
System.out.println("fun()");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
fun();
}
}
synchronized底层原理
我们用javap -c SynchronizedDemo.class
对字节码文件进行反编译,看看synchronized
底层是如何实现加锁的。
首先是对修饰方法的代码进行反编译,可以看到方法使用ACC_SYNCHRONIZED
标识符修饰,JVM将被该标识符修饰的方法视为同步方法。
然后是对修饰代码块的代码反编译,这次我们发现没有了ACC_SYNCHRONIZED
标识符,取而代之的是monitorenter
和monitorexit
两条指令,monitorenter
和monitorexit
分别表示同步代码块开始和结束的位置。细心的朋友应该会看到其实下面的图中有两个monitorexit
,主要的原因是如果程序发生异常退出而没有释放锁,则可能发生死锁,这里加上退出指令可以保证异常情况下也可以释放锁。
总结一下上面两种方法的原理就是,synchronized
修饰方法时使用ACC_SYNCHRONIZED
标志,而修饰代码块时使用monitorenter
和monitorexit
指令,这两者本质上都是获取监视器锁monitor。
synchronized的优化
JDK6之前,synchronized
使用重量级锁保证线程间同步,每次加锁和释放锁的开销都很大,从JDK6开始引入了一系列synchronized
优化的部分,如适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等,下面介绍一下这些优化的内容。
首先我们需要了解一个内容,经过优化后的synchronized
锁的级别不再是单一的重量级锁了,而是有无锁、偏向锁、轻量级锁和重量级锁四种状态。synchronized
通过偏向锁到轻量级锁,再到重量级锁的升级,进行了较大优化。
适应性自旋锁
为了帮助大家理解自旋锁,我们来想象一个场景。我们在食堂吃饭时,有的窗口需要领号码牌,一般这种窗口需要的时间都比较久,比如做麻辣烫的窗口,有的窗口只需要在后面排队很快就能打到饭了,比如快餐窗口。快餐窗口不需要领号码牌去等待而是直接在窗口前排队的样子非常类似我们的自旋锁。自旋锁就是一些线程执行速度较快,加锁反而会增加线程切换的开销,得不偿失。因此,自旋锁让线程等一下,不放弃CPU执行时间,等别的线程运行结束后当前线程就可以直接获得同步资源避免产生线程切换的开销。
而自旋锁在JDK1.4时就已经被加入了,只不过是默认关闭的,从JDK6开始自旋锁默认开启,并且引入了适应性自旋锁。自旋锁在自旋等待了一定次数而没有获得同步资源后就会使用传统方法挂起线程以免白白浪费CPU资源,而适应性自旋锁比起自旋锁将更加灵活。比如在同一个锁对象上,刚刚别的线程成功自旋等待成功,那么虚拟机认定自旋可能再次成功的概率很大,可能自旋会持续较长时间,而如果自旋很少成功,可能获取同步资源时会省略自旋的过程。
锁消除
还用上面食堂的例子来解释锁消除,当麻辣烫窗口只有你一个人时,还需要发放号码牌吗?显然就没有必要了。其实这就是所谓的锁消除,这是JIT对锁的一种优化,利用逃逸分析技术判断锁对象是否只被一个线程访问,如果是的话就取消上锁的过程。
锁粗化
大部分情况下,我们对一段程序加锁需要尽量缩小范围,因为如果范围太大,执行时间也会增加。但是也有特殊情况,我们看看下面这种情况。
public class LockDemo {
public void fun() {
for (int i = 0; i < 10000; ++i) {
synchronized(LockDemo.class) {
System.out.println("test...");
}
}
}
}
上面的for循环整个过程都在频繁加锁和释放锁,非常耗费性能,因此JIT将会把加锁的过程优化到for循环外面,如下面的代码所示,这就是锁粗化的过程。
public class LockDemo {
public void fun() {
synchronized(LockDemo.class) {
for (int i = 0; i < 10000; ++i) {
System.out.println("test...");
}
}
}
}
偏向锁
偏向锁一般用于同一个线程多次申请同一个锁的情况,如果有多个线程都在竞争锁资源,偏向锁就不起作用了。
当一个线程第一次申请一个锁时,会在Java对象头的Mark Word锁标志位设为01,并且记录线程ID,此时处于偏向锁状态。后续没有线程竞争锁资源的话无需再进行同步操作。
轻量级锁
当有多个线程尝试获取锁时,偏向锁就升级为轻量级锁。
对于轻量级锁,比较适合多线程交替获取锁资源或自旋后很快获得锁的情况。当线程1释放锁后,线程2尝试获取锁资源,此时撤销偏向锁,Mark Word也不再存放线程ID,而是存放hashcode和GC分代年龄,此时处于轻量级锁状态。
重量级锁
如果线程1和线程2交替获取锁资源,那么轻量级锁还是可以完成任务的。但是如果线程1正占有锁资源,线程2就开始请求锁资源了,然后又有线程3、线程4都竞争锁资源,那么就需要重量级锁了。
并发三大特性分析
原子性
原子性指的是一个或多个操作执行过程中不被打断的特性。被synchronized
修饰的代码是具有原子性的,要么全部都能执行成功,要么都不成功。
前面我们提到过,synchronized
无论是修饰代码块还是修饰方法,本质上都是获取监视器锁monitor。获取了锁的线程就进入了临界区,锁释放之前别的线程都无法获得处理器资源,保证了不会发生时间片轮转,因此也就保证了原子性。
可见性
所谓可见性,就是指一个线程改变了共享变量之后,其他线程能够立即知道这个变量被修改。我们知道在Java内存模型中,不同线程拥有自己的本地内存,而本地内存是主内存的副本。如果线程修改了本地内存而没有去更新主内存,那么就无法保证可见性。
synchronized
在修改了本地内存中的变量后,解锁前会将本地内存修改的内容刷新到主内存中,确保了共享变量的值是最新的,也就保证了可见性。
有序性
有序性是指程序按照代码先后顺序执行。
synchronized
是能够保证有序性的。根据as-if-serial语义,无论编译器和处理器怎么优化或指令重排,单线程下的运行结果一定是正确的。而synchronized
保证了单线程独占CPU,也就保证了有序性。