the implementation of Synchnornized in JVM
快速结论
在JamVM中的实现如下
- 通过Object中的C结构体的
object->lock->monitor
实现锁的标志 - 调度通过pThread的
pthread_mutex_lock
等操作进行lock与unlock,相当于把最难的调度通过系统调用甩锅给操作系统了 - Synchnornized是java中互斥锁的实现,用于对并发编程进行支持
Object的线程方法
在了解Java的同步之前,先复习一下Java的线程模型,在Java中,使用Object作为最常用的锁。在Object中,有许多native方法,主要如下
函数 | 功能 |
---|---|
wait() | 释放monitor与时间片,只能超时或者有其他线程notify才能恢复 |
sleep() | 释放时间片,但是持有monitor |
notify() | 通知调度器唤醒等待此monitor的BLOCKED线程队列中的一个,转为blocked状态 |
notifyAll() | 唤醒所有等待此monitor的线程队列,转为blocked状态 |
线程的状态有如下几种
提醒一下,被
notify()
调用后是进入了block的队列,需要通过调度器“摇号”挑选其中一个后才能进入同步区块
互斥锁(Synchnornized)
互斥锁在其它编程语言中一般叫做Mutex(|mjuːtex|)
,用于实现对共享资源的独占性,当进入同步代码块时,获取并独占锁,离开时释放锁。在这段时间中,其它线程访问此资源时,会成为上图的BLOCKED状态。
在Java中线程是通过对C中的pThread包装实现的,pThread接口通过systemcall在内核实现,因此线程的管理本质都是在进行系统调用,有一定的切换消耗,故Synchnornized是一种比较重的锁。
在Java中,Synchnornized可以看作一种语法糖,不需要自己去判断、配置Mutex,只需要用synchnornized(object){}
进行包裹即可,接下来我们对这个字段进行一下入门。
1. 对象同步
对象同步中有两种方法,第一种是通过代码块{}
的包裹,编译时通过MONITORENTER
与MONITOREXIT
将Sample.this
作为锁对象;第二种是为method添加access_flag
为synchronized,JVM在执行时将根据FLAG自动加\减锁,如下两种写法在JVM中调用是效果相同的
public class Sample {
public void do_work() {
synchronized (Sample.this) {
//do synchronized work
}
}
public synchronized void do_work() {
//do synchronized work
}
}
详见: Java中非static的synchronized方法和synchronized(this)用的是一个锁么?。
synchronized在方法中不会被继承,虽然继承synchronized方法有点诡异
再说个题外话,synchronized的锁范围越小越好,不建议放在方法上作为修饰,而是使用
synchronized (xxx.this)
代码块。common-pool旧版代码中就出现过此问题,导致上游业务出现死锁。
2. 类同步
类同步的对象是Sample.class
,以下两种是相同的
public static synchronized do_work(){
//do synchronized work
}
public void do_work(){
synchronized (Sample.class){
//do synchronized work
}
}
单例的同步方法看这里:http://www.jianshu.com/p/eebcb81b1394
3. synchronized的应用
除了网上用烂的单例同步,多线程for循环自增,并发HashMap等例子外,还可以再举例
3.1. 释放锁与时间片
我曾经在OkHttp中,介绍过Socket的KeepAlive连接自动清理的实现,这里使用了wait进行阻塞,避免无谓的自旋(Spin-waiting)损耗。
while (true) {
//执行清理并返回下场需要清理的时间
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
if (waitNanos > 0) {
synchronized (ConnectionPool.this) {
try {
//在timeout内释放锁与时间片
ConnectionPool.this.wait(TimeUnit.NANOSECONDS.toMillis(waitNanos));
} catch (InterruptedException ignored) {
}
}
}
}
注意wait在Java中一般都是需要在while循环中的,否则你的wait可能被路人线程给wakeup了,上文代码就是Best practice。
3.2. Dead Lock
虽然同步简化了编程,但是还有可能发生死锁,如下例子,当两个不同线程分别调用a.swap(b)
与b.swap(a)
时,在getValue()
时可能发生死锁。
//code from <Concurrent Programming in Java>
class Cell { // Do not use
private long value;
synchronized long getValue() { return value; }
synchronized void setValue(long v) { value = v; }
synchronized void swapValue(Cell other) {
long t = getValue();
long v = other.getValue();
setValue(v);
other.setValue(t);
}
}
Monitor(管程)
重点来了!管程是JVM中的互斥锁的实现,在Java中,Object.wait/notify,以及线程的wait/notify,synchronized生成的字节码MONITORENTER/EXIT等操作在JVM中都是通过Monitor控制与实现的。
在JVM中,每个java.lang.object
对象对应的C结构体均拥有一个lock指针,指针对应一个Monitor(也就是说,对象、lock指针与Monitor是一一对应的)。一个Monitor只能同时被一个线程持有。
管程的工作机制如下图
本图来自于维基百科,可以看作是对阻塞式条件变量
的一种封装
e
表示入口队列的线程,它将尝试获取对象的锁,处于blocked状态q
表示等待队列的线程,处于waiting状态wait
表示调用pThread_wait()
进行等待,进入q
队列,大多数由开发者手动调用与恢复,下文暂不考虑notified
表示调用pThread_notify()
后进入e
队列enter
与leave
表示独占线程对锁的获取与释放
比如现在有线程队列e
同时竞争一个对象,第一个跑的最快的线程enter后,立刻获取到了object的monitor,剩下的线程由于无法获得到monitor,就在e
队列中阻塞等待锁的释放。
enter the monitor:
enter the method
if the monitor is locked
add this thread to e
block this thread
else
lock the monitor
当任务完成后,调用调度器
leave the monitor:
schedule
return from the method
调度器将会释放monitor,并通过某种调度公平的调度策略(可能是FIFO,也可能是优先权值等)将monitor分配给下一个处于blocked态的线程。
schedule :
if there is a thread on e
select and remove one thread from e and restart it
(this thread will occupy the monitor next)
else
unlock the monitor
(the monitor will become unoccupied)
以Android6.0的vm为例,参考源码如下:
使用Object作为锁好不好?
Java中,通过设置Object结构体中的某个字段映射为锁,所有对象都可以成为锁,这样固然简化了编程,但是相比Ruby等语言使用Mutex作为专用锁,Java所耗用的结构体内存更多