使用两个线程交替打印1-10的数字
用一个面试题来感受一下synchronized:使用两个线程交替打印1-10的数字,打印结果如下:

package com.example.demo.service;
import lombok.SneakyThrows;
public class SynchronizedDemo {
static int flag = 0;
static final Object lockObject = new Object();
public static void main(String[] args) {
new Thread(“线程1”) {
@SneakyThrows
public void run() {
while(flag<10) {
synchronized (lockObject){
System.out.println(Thread.currentThread().getName()+”打印:”+(++flag));
lockObject.notify();
lockObject.wait();
}
}
};
}.start();
new Thread(“线程2”) {
@SneakyThrows
public void run() {
while(flag<10) {
synchronized (lockObject){
System.out.println(Thread.currentThread().getName()+”打印:”+(++flag));
lockObject.notify();
lockObject.wait();
}
}
};
}.start();
}
}
上面代码使用synchronized、对象的notify和wait方法实现,synchronized锁住了 lockObject对象,线程1和线程2都是使用lockObject这个对象进行加锁,在进行++flag操作之后使用 notify方法通知另外一个线程,然后自己调 wait()进行block并释放锁,而另外一个线程被唤醒并竞争到锁执行之前的操作。 从上面的一道面试题可以看出,synchronized只能锁对象:修饰非静态方法锁的是当前对象,修饰静态方法锁的是class对象,锁指定对象
1.synchronized实现原理

不管是class对象还是new出来的对象都会有monitor,synchronized就是借助对象的monitor来实现,每个对象都有monitor,monitor有_owner,_count,_entryList,_waitSet等属性,当一个线程访问synchronized修饰的代码块或方法时,首先会检查_owner是否指向线程,如果指向线程则判断是否是一个线程,是则获取锁,_count+1,不是则进入_entryList等待持有线程释放锁,_waitSet是在持有锁的线程调用了wait方法时进入等待的集合。
不知道你发现没有,这个monitor数据结构跟java中的lock体系的数据结构是不是差不多:在lock的AQS中有个node节点组成的单链表,在线程获取锁失败之后就会被封装成node放入这个单链表中,这个单链表就相当于monitor的entry_list.而lock中的state就相当于monitor的_count,lock中的exclusiveOwnerThread相当于monitor的_owner, condition的await()和signal()方法相当于对象的notify和wait,await形成的单链表就相当于monitor中的_waitSet。是不是感觉有点殊途同归的感觉,其实看多了你会发现很多的设计思想都差不多。
2.编译器是如何知道这段代码或方法被synchronized修饰?
synchronized修饰同步代码块时会生成monitorenter,monitorexit指令,当执行monitorenter时就会促发上诉流程,在执行完之后或抛出异常时会执行monitorexit。
synchronized修饰方法时会生成ACC_SYNCHRONIZED标识,通过读取flags中的ACC_SYNCHRONIZED标识来促发上诉流程。
3.为什么这样的synchronized是一个重量锁?
synchronized依赖monitor来实现,monitor则通过操作系统的独占锁来实现,独占锁又被称为重量锁,而且没获得锁的线程都会阻塞,阻塞后被唤醒需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。
4.Java 1.6之后的synchronized优化?

在优化之后,synchronized锁机制是从偏向锁转换为轻量锁,再转换成重量锁。偏向锁在同一时间只能有一个线程执行,如果有两个线程竞争锁则会膨胀成轻量锁,轻量锁也就是自旋,可分为自旋锁和自适应自旋锁。自旋是会消耗cpu资源的,所以在自旋指定次数后会再次膨胀为monitor锁,monitor锁中等待的线程会暂时让出cpu资源。
5.各种锁的优缺点?
偏向锁:同一个线程进入同步区域是不需要再次加锁释放锁的,而且实现起来也是在对象头的mark word中添加了threadId,比较简单,缺点是只要有两个线程同时竞争锁,偏向锁就失效了,所以绝大多数情况下是不符合业务需求的。
轻量锁:轻量锁从自旋进化到自适应自旋,是一种乐观锁,只要持锁线程释放锁就立刻参与获得锁,在并发不是很大的情况下一般立刻就能获取锁。自旋的缺点是一直占有cpu资源,所以JVM规定在自旋10次之后会膨胀为重量锁。而自适应自旋是认为获取过锁的线程再次获取锁的概率会更大,会给获取过锁的线程更多的自旋次数,而没获取过的线程获取锁的概率就会很小,会降低没获取过锁的线程自旋次数。
重量锁:monitor锁,依靠操作系统的排他锁来实现,而monitor锁本身是靠c++实现,缺点是在线程从阻塞到唤醒过程时间很长,需要操作系统把线程从用户态转换到内核态。
6.各种锁的选择?
个人感觉偏向锁几乎没什么用,本来偏向锁就是在并发情况下保证数据安全的一种选择,而偏向锁只能由一个线程进入。轻量锁可能是大多数情况的选择,重量锁则是非常高并发下的选择。