看到网上很多讲synchronized关键字用法的文章,说的都很有道理,也很深刻,但是看完总感觉脑袋里还是有点乱乱的。经过一番自己的思考后,想从自己的思考角度出发,来说一说synchronized关键字的用法。在我看来,其实想将加锁后的访问规则讲清楚其实很简单。废话就不多说了,看下面的文字吧。
一、一套清晰的规则
其实很容易发现,无论是加了锁的方法还是加了锁的代码块,无是加的是对象锁还是加的是类锁,只要上的是同一把锁,那么它们的访问规则就应该是相同的。也就是上了同一把锁的东西,要么一起可以被访问,要么一起禁止别人访问。因此,如果想搞清楚访问的规则,我们首先要搞清楚锁的类型。然后判断,只要上的是同一把锁,访问的规则就应该相同。那么java中的锁有那些类型呢。可以简单的总结为两种类型:
java中共有两种类型的锁:
(1)类锁:只有synchronized修饰静态方法或者修饰一个类的class对象时,才是类锁。
(2)对象锁:处了类锁,所有其他的上锁方式都认为是对象锁。比如synchronized修饰普通方法或者synchronized(this)给代码块上锁等
应该注意的是,因为一个类只有一个.class对象,因此所有的访问者在访问被加了类锁的东西时,都是共用同一把锁,而类的实例却可以有很多个,因此不同对象访问加了对象锁的东西,它们的访问互不干扰。
知道了锁的类型,那么我们就可以总结出一个通用且清晰的规则了。如下:
这就是终极的规则:
其实这个规则很简单:
(1)加了相同锁的东西,它们的访问规则是相同的,即当某个访问者获得该锁时,它们一起向该访问者开放访问,向其他没有获得该锁的访问者关闭访问;
(2)加了不同锁的东西访问互相不干扰
(3)而没有加锁的东西随时都可以任意访问,不受任何限制。
然后再来看怎么判断什么情况下是相同的锁。如下:
怎么判断是一把相同的锁,遵循下面的规则:
(1)不同类型的锁不是同一把锁。
(2)加的是对象锁,那么必须是同一个对象实例才是同一把锁
(3)加的是类锁,那必须是同一类才是同一把锁。
好了,也就是说我们判断访问的规则,就是基于个步骤:
1.首先判断是不是同一把锁
2.然后判断各自的访问规则
下面我们来看几个实际例子,来看看我们这个规则到底怎么使用。
二、使用这个规则
(1)实验一
新建一个java项目,新建一个SynTest类,在其中写如下代码:
1 public class SynTest { 2 3 private synchronized void test1(){ 4 for(int i=0;i<3;i++){ 5 System.out.println(i+"我们是test1"); 6 } 7 } 8 9 private void test2(){ 10 synchronized(this){ 11 for(int i=0;i<3;i++){ 12 System.out.println(i+"我们是test2"); 13 } 14 15 } 16 } 17 18 public static void main(String[] args) { 19 final SynTest st = new SynTest(); 20 Thread t1 = new Thread(new Runnable(){public void run() { 21 st.test1(); 22 }}); 23 Thread t2 = new Thread(new Runnable(){public void run() { 24 st.test2(); 25 }}); 26 t1.start(); 27 t2.start(); 28 } 29 30 }
在代码中,可以看到我们给test1方法上了对象锁,加锁的方式是给方法加锁,加的是当前对象锁;而给test2方法上也上了对象锁,加锁的方式给代码块加锁,注意上的是当前对象锁,你也可以将this替换成任意其他对象。然后我们在main方法中看到,新建了两个线程,都是用同一个对象来调用这两个方法。因此,可以判定线程t1和t2使用的是同一把锁,因此访问规则就是t1获得了这把锁后,tes1方法和test2中被加锁的代码块都允许t1访问,都拒绝t2访问,等t1运行完,t2才会获得该锁,进行访问。因此输出的结果很明显了,t1执行完,再执行t2。我们运行下程序,看看我们按照规则来推理的是否正确,运行结果如下:
和我们预想的完全一样!!
然后我们再做个试验,比如将main方法中的代码改成下面的:
1 public static void main(String[] args) { 2 final SynTest st = new SynTest(); 3 final SynTest st2 = new SynTest(); 4 Thread t1 = new Thread(new Runnable(){public void run() { 5 st.test1(); 6 }}); 7 Thread t2 = new Thread(new Runnable(){public void run() { 8 st2.test2(); 9 }}); 10 t1.start(); 11 t2.start(); 12 }
只是多出一个实例而已,然后线程t2通过st2来调用test2。那么就test1和test2加的都是当前对象的锁,显然它们的当前对象不同吧。所以它们不是同一把锁,互相不干扰。那么我们运行程序,效果如下:
果然它们各自运行各自的,所以没什么顺序。
怎么样,这样子来判断访问权限是不是很简单啊。
(2)实验二
然后修改SynTest的代码,如下:
1 public class SynTest { 2 3 private static synchronized void test1(){ 4 for(int i=0;i<3;i++){ 5 System.out.println(i+"我们是test1"); 6 } 7 } 8 9 private void test2(){ 10 synchronized(SynTest.class){ 11 for(int i=0;i<3;i++){ 12 System.out.println(i+"我们是test2"); 13 } 14 15 } 16 } 17 18 public static void main(String[] args) { 19 final SynTest st = new SynTest(); 20 Thread t1 = new Thread(new Runnable(){public void run() { 21 SynTest.test1(); 22 }}); 23 Thread t2 = new Thread(new Runnable(){public void run() { 24 st.test2(); 25 }}); 26 t1.start(); 27 t2.start(); 28 } 29 30 }
很简单,test1是一个静态方法,所以它加的锁是类锁,而test2加了一个类锁,是加载了一个代码块中,因此他们加的是相同的锁。运行程序,结果如下:
即t1访问完,t2再访问。
然后我们再修改SynTest中的代码,如下:
1 public class SynTest { 2 3 private static synchronized void test1(){ 4 for(int i=0;i<3;i++){ 5 System.out.println(i+"我们是test1"); 6 } 7 } 8 9 private synchronized void test2(){ 10 11 for(int i=0;i<3;i++){ 12 System.out.println(i+"我们是test2"); 13 14 } 15 } 16 17 public static void main(String[] args) { 18 final SynTest st = new SynTest(); 19 Thread t1 = new Thread(new Runnable(){public void run() { 20 SynTest.test1(); 21 }}); 22 Thread t2 = new Thread(new Runnable(){public void run() { 23 st.test2(); 24 }}); 25 t1.start(); 26 t2.start(); 27 } 28 29 }
代码中很显然了,test1是静态方法,它上的是类锁。而test2是普通方法,它上的对象锁。这是不同的锁。所以t1访问test1时,test2方法是不受干扰的,t2肯定可以同时访问test2.因此打印顺序为任意顺序。如下:
为乱序。此时如果你的打印结果为先打印出t1的结果再是t2的结果,也不必惊讶,因为程序简单,循环次数少,CPU性能高,所以很可能t2缸启动就瞬间运行完了t1。
(3)实验三
我们再修改SynTest的代码,如下:
1 public class SynTest { 2 3 private synchronized void test1(){ 4 for(int i=0;i<3;i++){ 5 System.out.println(i+"我们是test1"); 6 } 7 } 8 9 private void test2(){ 10 synchronized(SynTest.class){ 11 for(int i=0;i<3;i++){ 12 System.out.println(i+"我们是test2"); 13 14 } 15 } 16 } 17 18 public static void main(String[] args) { 19 final SynTest st = new SynTest(); 20 Thread t1 = new Thread(new Runnable(){public void run() { 21 st.test1(); 22 }}); 23 Thread t2 = new Thread(new Runnable(){public void run() { 24 st.test2(); 25 }}); 26 t1.start(); 27 t2.start(); 28 } 29 30 }
很明显一个类锁,一个对象锁,访问规则互不干扰。即,t1访问test1方法时,并不影响t2访问test2(但是会禁止t2访问test1,因为t2用的也是st对象,此时t1已给st对象上锁了)。所以打印顺序可能为任意顺序。好了,运行程序,结果如下:
注意:此时如果你的打印结果为先打印出t1的结果再是t2的结果,也不必惊讶,因为程序简单,循环次数少,CPU性能高,所以很可能t2缸启动就瞬间运行完了t1。
好了,还有很多的实验我们就不再做了,都是根据上面总结的规则来做的。相信这下,你对上锁和上锁后的访问规则都清楚了吧。
三、如果你还不理解的话
如果你还不理解的话,这是某位大牛做的一个很好的比喻。你可以看看。摘抄如下:
打个比方:一个object就像一个大房子,大门永远打开。房子里有 很多房间(也就是方法)。 这些房间有上锁的(synchronized方法), 和不上锁之分(普通方法)。房门口放着一把钥匙(key),这把钥匙可以打开所有上锁的房间。 另外我把所有想调用该对象方法的线程比喻成想进入这房子某个 房间的人。所有的东西就这么多了,下面我们看看这些东西之间如何作用的。 在此我们先来明确一下我们的前提条件。该对象至少有一个synchronized方法,否则这个key还有啥意义。当然也就不会有我们的这个主题了。 一个人想进入某间上了锁的房间,他来到房子门口,看见钥匙在那儿(说明暂时还没有其他人要使用上锁的 房间)。于是他走上去拿到了钥匙,并且按照自己 的计划使用那些房间。注意一点,他每次使用完一次上锁的房间后会马上把钥匙还回去。即使他要连续使用两间上锁的房间,中间他也要把钥匙还回去,再取回来。 因此,普通情况下钥匙的使用原则是:“随用随借,用完即还。” 这时其他人可以不受限制的使用那些不上锁的房间,一个人用一间可以,两个人用一间也可以,没限制。但是如果当某个人想要进入上锁的房间,他就要跑到大门口去看看了。有钥匙当然拿了就走,没有的话,就只能等了。 要是很多人在等这把钥匙,等钥匙还回来以后,谁会优先得到钥匙?Not guaranteed。象前面例子里那个想连续使用两个上锁房间的家伙,他中间还钥匙的时候如果还有其他人在等钥匙,那么没有任何保证这家伙能再次拿到。 (JAVA规范在很多地方都明确说明不保证,像Thread.sleep()休息后多久会返回运行,相同优先权的线程那个首先被执行,当要访问对象的锁被 释放后处于等待池的多个线程哪个会优先得到,等等。我想最终的决定权是在JVM,之所以不保证,就是因为JVM在做出上述决定的时候,绝不是简简单单根据 一个条件来做出判断,而是根据很多条。而由于判断条件太多,如果说出来可能会影响JAVA的推广,也可能是因为知识产权保护的原因吧。SUN给了个不保证 就混过去了。无可厚非。但我相信这些不确定,并非完全不确定。因为计算机这东西本身就是按指令运行的。即使看起来很随机的现象,其实都是有规律可寻。学过 计算机的都知道,计算机里随机数的学名是伪随机数,是人运用一定的方法写出来的,看上去随机罢了。另外,或许是因为要想弄的确太费事,也没多大意义,所 以不确定就不确定了吧。) 再来看看同步代码块。和同步方法有小小的不同。 1.从尺寸上讲,同步代码块比同步方法小。你可以把同步代码块看成是没上锁房间里的一块用带锁的屏风隔开的空间。 2.同步代码块还可以人为的指定获得某个其它对象的key。就像是指定用哪一把钥匙才能开这个屏风的锁,你可以用本房的钥匙;你也可以指定用另一个房子的钥匙才能开,这样的话,你要跑到另一栋房子那儿把那个钥匙拿来,并用那个房子的钥匙来打开这个房子的带锁的屏风。 记住你获得的那另一栋房子的钥匙,并不影响其他人进入那栋房子没有锁的房间。 为什么要使用同步代码块呢?我想应该是这样的:首先对程序来讲同步的部分很影响运行效率,而一个方法通常是先创建一些局部变量,再对这些变量做一些 操作,如运算,显示等等;而同步所覆盖的代码越多,对效率的影响就越严重。因此我们通常尽量缩小其影响范围。 如何做?同步代码块。我们只把一个方法中该同 步的地方同步,比如运算。 另外,同步代码块可以指定钥匙这一特点有个额外的好处,是可以在一定时期内霸占某个对象的key。还记得前面说过普通情况下钥匙的使用原则吗。现在不是普通情况了。你所取得的那把钥匙不是永远不还,而是在退出同步代码块时才还。 还用前面那个想连续用两个上锁房间的家伙打比方。怎样才能在用完一间以后,继续使用另一间呢。用同步代码块吧。先创建另外一个线程,做一个同步代码 块,把那个代码块的锁指向这个房子的钥匙。然后启动那个线程。只要你能在进入那个代码块时抓到这房子的钥匙,你就可以一直保留到退出那个代码块。也就是说 你甚至可以对本房内所有上锁的房间遍历,甚至再sleep(10*60*1000),而房门口却还有1000个线程在等这把钥匙呢。很过瘾吧。