深入理解synchronized
面试官:请谈谈你对synchronized的理解。
小白:这是一个java的关键字,用来控制并发的,被它锁住的代码同一时刻只能有一个线程访问。
面试官:还有吗?
小白:没有了……
面试官:那你先回去等通知吧!
synchronized,相信学过java的都知道它,但是面试一被问到这个,又总是答不出多少东西来。下面我就将synchronized的知识点列举出来,深入理解(要深入它,才能征服它)。
1. 用来干嘛的?
这是一个同步关键字,保证同一时刻只能有一个线程执行被其修饰的方法或代码块,可以保证线程安全。
2. 怎么用呢?
这是一个关键字,可以用来修饰静态方法、实例方法、代码块。注意这里的代码块不是类中的静态代码块和构造代码块,而是方法中的代码块。
- 修饰静态方法:
介绍它修饰静态方法之前,先来回忆一下静态方法的特点。静态是该类所有实例共享的,JVM加载该类时就会对其进行初始化,因为不属于任何一个实例,所以静态方法里面不能用this关键字。如果synchronized修饰静态方法,那么锁对象是啥呢?首先排除this,因为调用静态方法的时候可能该类都还没有实例。所以修饰静态方法的时候,锁对象其实是当前class。
// 静态方法
public synchronized static void staticFun(){
System.out.println("synchronized修饰静态方法,锁对象是当前class");
// 业务代码……
}
- 修饰实例方法:
既然都说了是实例方法,那么锁对象就是当前类的实例。
// 实例方法
public synchronized void instanceFun(){
System.out.println("synchronized修饰实例方法,锁对象是类实例");
// 业务代码……
}
- 修饰代码块:
修饰代码块,锁对象可以是class,也可以是给定的对象。如果是class,那就是不管该类new几个实例,都是属于这个类的,都会被锁住;如果是对象,那么不同对象去访问时是可以获取到锁的,所以class作为锁其实粒度更粗。
public void fun(){
synchronized (TestSync.class){ // 锁对象是当前class
// synchronized (this){ // 锁对象是实例
System.out.println("synchronized修饰代码块,锁对象可以是实例,可以是类");
}
}
3. 线程A调用类的同步实例方法,线程B可以同时调用类的同步静态方法吗?为什么?
我们先用代码看结果,再解释为什么。
// 静态方法
public synchronized static void staticFun(){
System.out.println("synchronized修饰静态方法,锁对象是当前class");
System.out.println(Thread.currentThread().getName() + "进入同步静态方法");
System.out.println(Thread.currentThread().getName() + "执行结束");
}
// 实例方法
public synchronized void instanceFun(){
System.out.println("synchronized修饰实例方法,锁对象是类实例");
System.out.println(Thread.currentThread().getName() + "进入同步实例方法");
try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName() + "执行结束");
}
public static void main(String[] args){
TestSync testSync = new TestSync();
new Thread(() -> {
testSync.instanceFun();
}, "线程A").start();
new Thread(() -> {
staticFun();
}, "线程B").start();
new Thread(() -> {
testSync.instanceFun();
}, "线程C").start();
}
运行结果:
运行结果上面的代码,线程A调用实例方法,并且进入方法后线程睡了5秒钟;线程B调用静态方法,还没等线程A结束,线程B已经执行结束了,线程B不需要等线程A释放锁也可以执行。而线程C,因为是同一个对象去调用的同步实例方法,所以得等线程A释放了锁,线程C才能拿到执行权。假如线程C是另外再new一个对象去调用的,那么也不需要等待线程A释放锁。
从结果可以得出答案:线程A调用类的同步实例方法,线程B可以同时调用类的同步静态方法。原因就是同步实例方法的锁是对象锁,而同步静态方法的锁是类锁,锁对象不同,所以可以同时调用
4. 可以用String字符串来做锁对象吗?
可以,但没必要。代码块的锁对象其实可以是任意对象,不过一般都用class或者this,并不建议用string做锁对象,因为用string很容易造成死锁。为什么容易造成死锁呢?因为JVM中有个常量池,比如你定义两个字符串:
String str1 = "haha";
String str2 = "haha";
这里明明是两个字符串,但其实是同一个对象,因为这样赋值的String,首先会看常量池中有没有,没有就往常量池中添加一个,并指向它,有的话,就直接指向。所以str1和str2都是指向常量池中同一个对象。
5. synchronized可以修饰构造方法吗?为什么?
不能修饰构造方法,构造方法只能有权限修饰符,比如public、private之类的,它本身就是线程安全的。
6. jdk1.6开始对synchronized做了哪些优化?
jdk1.6之前,synchronized是很重的锁,jdk1.6开始,做了大量的优化,比如用偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销(当你这么回答的时候,估计面试官紧接着就问什么是偏向锁、自旋锁……有什么区别,这些后续再详细地说)。
7. 你知道synchronized的底层原理吗?
- 同步代码块:方法里面的同步代码块,
synchronized
底层是通过监视器monitor
来实现的。通过指令
javap -c -s -v -l Xxx.class
可以发现monitorenter
指令指向同步代码块开始的位置,同时会尝试获取锁,锁的计数器为0表示可以获取锁,获取后计数器变为1;monitorexit
指令指向同步代码块结束的位置,同时释放锁,将锁的计数器置为0。所以获取锁就是获取Monitor
的执行权。Monitor
是基于C++,由ObjectMonitor
实现的,每个对象都内置了ObjectMonitor。另外,wait/notify方法也是基于monitor来实现的。
- 同步方法:执行上述的javap指令查看同步方法,可以发现并没有monitorenter和monitorexit指令,但是在方法开头有个名为
ACC_SYNCHRONIZED
的flag标识,同步方法就是通过这个标识来控制同步操作的。
8. synchronized和ReentrantLock有何异同?
相同点:
-
两者都是可重入锁;
-
都可实现选择性通知;
不同点:
-
synchronized是JVM层面的,ReentrantLock是API层面的;
-
synchronized是非公平锁,ReentrantLock可以指定为公平锁或者非公平锁;
-
synchronized无需手动释放锁,ReentrantLock需要手动释放锁;
-
synchronized等待不能中断,ReentrantLock等待可通过
lock.lockInterruptibly()
中断等待;