Java并发实现原理-多线程基础-synchronized关键字(3)
1.锁的对象是什么?
对于不熟悉多线程原理的人来说,很容易误解synchronized关键字:它通常加在所有的静态成员函数和非静态成员函数之前,表面看好像是“函数之间的互斥”,其实不是。synchronized关键字其实是“给某个对象加了把锁”,这个锁究竟加在了什么对象上面?如下所示,给f1()、f2()加上synchronized关键字。
class A{
public void synchronized fl(){...}
public static void synchronized f2(){...}
}
等价于如下代码:
class A{
public void f1(){
synchronized(this){...}
}
public static void f2(){
synchronized(A.class){...}
}
}
A a = new A();
a.f1();
a.f2();
对于非静态成员函数,锁其实是加载对象a上面的;
对于静态成员函数,锁是加载A.class上面的。 当然,class本身也是对象。
这回答了一个关于synchronized的常见问题:一个静态成员函数和一个非静态成员函数,都加了synchronized关键字,分别被两个线程调用,他们是否互斥?很显然,因为是两把不同的锁,所以不会互斥。
2.锁的本质是什么?
无论使用什么语言,只要是多线程的,就一定会涉及锁。既然锁如此常见,那么锁的本质是什么呢?
如图所示,多个线程访问同一个资源。线程就是一段运行中的代码;资源访问就是一个变量、一个对象或一个文件等;而锁就是要实现线程对资源的访问控制,保证同一个时间只能有一个线程去访问某一个资源。比如,线程就是一个游客,资源就是一个待参观的房子。这个房子同一时间值允许一个游客进去参观,当一个人出来后下一个人才能进去。而锁,就是这个房子门口的守卫。如果同一个时间允许多个游客参观,锁就会变成信号量,这点后面讨论。
从程序角度来看,锁就是一个“对象”,这个对象要完成以下几件事情:
1.这个对象内部得由一个标志位(state变量),记录自己有没有被某个线程占用(也就是记录当前有没有游客已经进入了房子)。最简单的情况是这个state有0和1两个取值,0标识没有线程占用,1标识某个线程占用了这个锁。
2.如果这个对象被某个线程占用,它得记录这个线程的thread ID,得知道自己是被那个线程占用了(也就是记录现在是谁在房子里面)。
3.这个对象还得维护一个thread id list,记录其他所有阻塞的、等待拿这个锁的线程(也就是记录所有在外面的等待的游客)。在当前线程释放锁之后(也就是把state从1变为0),从这个thread id list 里面去一个线程唤醒。
既然锁是一个“对象”,要访问的共享资源本身也是一个对象,例如前面的对象a,这两个对象可以合成一个对象。代码就变成synchronized(this){…},我们要访问的共享资源是对象a,锁也加在a上面的。当然,也可以另外新建一个对象,代码变成synchronized(obj1){…}。这时访问的共享资源对象是a,而锁是加载新建的对象obj1上面的。
资源和锁合二为一,使得在Java里面,synchronized关键字可以加在任何对象的成员变量上。这意味着,这个对象既是共享资源,同时也具备“锁”的功能。下面来看Java是如何做到让任何一个对象都具备“锁”的功能的,这也是synchronized的实现原理。
3.synchronized实现原理
答案在Java的对象头里。在对象头里,有一个块数据较Mark Word。在64位机器上,Mark Word是8个字节(64位)的,这64位中有2个重要字段:锁标志位和占用该锁的thread ID。因为不同版本JVM的实现,对象头的数据结构会有差异,这里不再论述。
此处值说明锁的实现思路,后面将ReentrantLock详细实现时,也基于类似的思路。在这个思路上,synchronized还会有偏向、自旋等优化策略,ReentrantLock同样也会用到这些优化策略。