synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性,它可以:
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
在同步的时候是获取对象的 monitor ,即获取到对象的锁。那么对象的锁怎么理解?无非就是类似对对象的一个标志,那么这个标志就是存放在Java对象的对象头。Java对象头里的Mark Word里默认的存放的对象的Hashcode,分代年龄和锁标记位。
每个对象分为三块区域:对象头、实例数据和对齐填充
| 锁状态 | 25bit | 4bit | 1bit是否是偏向锁 | 2bit锁标记位 |
|---|---|---|---|---|
| 无锁 | 对象的haahcode | 分代年龄 | 0 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 | 合并第一列 | 合并第一列 | 00 |
| 重量级锁 | 指向互斥量(重量级锁)的指针 | 合并第一列 | 合并第一列 | 10 |
| GC标志 | 空 | 合并第一列 | 合并第一列 | 11 |
| 偏向锁 | 线程ID(23bit)和Epoch(2bit) | 对象分代年龄 | 1 | 01 |
如上表 在Mark Word 会默认存放hasdcode,年龄值以及锁标志位等信息
锁一共有4种状态,级别从低到高依次是: 无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态 ,这几个状态会随着竞争情况逐渐升级。 锁可以升级但不能降级 ,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
从语法上讲,Synchronized可以把任何一个非null对象作为"锁",在HotSpot JVM实现中,锁有个专门的名字:对象监视器(Object Monitor)。可以把它理解为 一个同步工具,也可以描述为 一种同步机制,实现了在一个时间点,最多只有一个线程在执行管程的某个子程序,这个机制的保障来源于监视锁Monitor,每个对象都拥有自己的监视锁Monitor。
我们可以把监视器理解为一个医院,医院里面只要一个医生,每次只能看一个病人(线程),如果一个病人想看病,他首先要在走廊里面排队(Entry Set),依次进入看病,但是假如某个正在看病的人可能晕血或者血糖低不能暂时继续看病(线程被挂起),这时候不能强行给他看,也不能让后面的病人等他一个,于是就要送他到休息室去休息(Wait Set),休息室里面呆的都是因为各种原因不能继续看病的病人,等休息好了,还可以继续去看病。如下图
总之,监视器是一个用来监视这些线程进入特殊的房间的。他的义务是保证(同一时间)只有一个线程可以访问被保护的数据和代码。
在Java虚拟机(HotSpot)中,Monitor是基于C++实现的 ObjectMonitor ,其主要数据结构如下
ObjectMonitor() {
_header = NULL;
_count = 0; //用来记录该线程获取锁的次数
_waiters = 0,
_recursions = 0; //锁的重入次数
_object = NULL;
_owner = NULL; //指向当前持有ObjectMonitor对象的线程
_WaitSet = NULL; //存放wait状态的线程队列
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //这是一个和_WaitSet类似存等待线程的地方,
//但是是否存在这里是要根据Policy的值(这里不知道说的对不对,顺便说下,这玩意儿每次看都以为是cxk)
FreeNext = NULL ;
_EntryList = NULL ; //存放处于等待锁的线程队列
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
复制代码
当多个线程同时访问一段同步代码时,首先进入 _EntryList ,当某个线程获取到对象的 monitor 后进入 _Owner 区域并把 monitor 中的 _owner 变量设置为当前线程,同时 _count 加一,获得对象锁。 如果持有monitor的线程被挂起(例如调用wait方法),将释放当前持有的monitor, _owner 变量回复为null, _count 减一,同时该线程进入 _WaitSet 队列中等待被唤醒(notify),如果当前线程顺利执行完代码块后会释放 monitor 并复位变量的值,以便下一个线程进来获取 monitor 锁,下面看个例子。
public class SynchronizedDemo {
public static void main(String[] args) {
synchronized (SynchronizedDemo.class) {
System.out.printf("synchronized");
}
function();
}
private static void function() {
System.out.printf("function");
}
}
上面的代码中有一个同步代码块,锁住的是类对象,并且还有一个同步静态方法,锁住的依然是该类的类对象。下面是字节码文件
public class com.example.javalib.SynchronizedDemo {
public com.example.javalib.SynchronizedDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // class com/example/javalib/SynchronizedDemo
2: dup
3: astore_1
4: monitorenter
5: aload_1
6: monitorexit
7: goto 15
10: astore_2
11: aload_1
12: monitorexit
13: aload_2
14: athrow
15: invokestatic #3 // Method function:()V
18: return
复制代码
上面的4,6,12行就是需要注意的部分了,这是添加Synchronized关键字之后才会出现的。执行同步代码块首先要执行 monitorenter ,退出的时候执行 monitorexit 指令。 使用Synchronized之所以能够进行同步,其关键就是对对象的监视器 monitor 的获取,当执行线程获取到monitor后才能继续执行下去,否则只能继续等待。 上面的demo中同步代码块后还有一个静态方法,这个方法是同步的,而且该方法锁的对象依然是这个类对象,那么执行线程就不必再去获取这个锁,从字节码中可以看到,有一条 monitorenter 指令和两条 monitorexit 指令,并没有第二次获取锁的指令,这就是 锁的重入性 :即在同一个锁程中,线程不需要去再次获取同一把锁,Synchronized先天具有重入性。 每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一 。 任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取该对象的监视器才能进入同步块和同步方法,如果没有获取到监视器的线程将会被阻塞在同步块和同步方法的入口处,进入到BLOCKED状态, 关于线程的状态可以看这篇文章
从上面我们知道了 sychronized 加锁的时候,会调用 objectMonitor 的 enter 方法,解锁的时候会调用exit方法。事实上,只有在JDK1.6之前, synchronized 的实现才会直接调用 ObjectMonitor 的 enter 和 exit ,这种锁被称之为重量级锁。为什么说这种方式操作锁很重呢? 因为Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到核心态,因此状态转换需要花费很多的处理器时间,对于代码简单的同步块(如被synchronized修饰的get 或set方法)状态转换消耗的时间有可能比用户代码执行的时间还要长,所以说synchronized是java语言中一个重量级的操纵。 所以,在JDK1.6中出现对锁进行了很多的优化,进而出现轻量级锁,偏向锁,锁消除,适应性自旋锁,锁粗化(自旋锁在1.4就有 只不过默认的是关闭的,jdk1.6是默认开启的),这些操作都是为了在线程之间更高效的共享数据 ,解决竞争问题。