三、6个常见的使用情况
我们先给出这6种常见的情况,然后一个一个分析。
1、两个线程同时访问一个对象的同步方法。
2、两个线程访问的是两个对象的同步方法。
3、两个线程访问的是synchronized的静态方法。
4、两个线程同时访问同步方法与非同步方法。
5、一个线程访问一个类的两个普通同步方法。
6、同时访问静态同步方法和非静态同步方法。
为了对这6种情况做到心中有数,不至于搞混了,我们画一张图,对每一种情况进行分析。
上面是框架图,下面我们基于开始来分析:
1、两个线程同时访问一个对象的同步方法
这种情况对应于以下这张图:
这种情况很简单,我们在上面也演示过,结果就是同一个时刻只能有一个方法进入。这里就不再演示了。
2、两个线程访问的是两个对象的同步方法
这种情况对应于下面这种:
也就是一个方法有两把锁,线程1和线程2互不干扰的访问。锁是不起作用的。
3、两个线程访问的是synchronized的静态方法
这种情况对应于下面这种情况:
我们对这种情况来测试一下吧。
public class SynTest6 implements Runnable { public static void main(String[] args) throws InterruptedException { SynTest6 instance1 = new SynTest6(); SynTest6 instance2 = new SynTest6(); Thread thread1 = new Thread(instance1); Thread thread2 = new Thread(instance2); thread1.start(); thread2.start(); } @Override public void run() { method1(); } public synchronized static void method1() { try { System.out.println(Thread.currentThread().getName() + "进入到了静态方法"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "离开静态方法,并释放锁"); } catch (InterruptedException e) { e.printStackTrace(); } } }
在这个例子中我们实例化了两个对象instance1和instance2,并且存放在了两个不同的线程中,我们测试一下访问同一个static同步方法你会发现。即使是实例不同,锁也会生效,也就是同一时刻只能有一个线程进去。
4、两个线程同时访问同步方法与非同步方法
这种情况对应于下面这张图:
我们对这种情况使用代码进行演示一遍:
public class SynTest7 implements Runnable { public static void main(String[] args) throws InterruptedException { SynTest7 instance1 = new SynTest7(); Thread thread1 = new Thread(instance1); Thread thread2 = new Thread(instance1); thread1.start(); thread2.start(); } @Override public void run() { method1(); method2(); } public synchronized void method1() { try { System.out.println(Thread.currentThread().getName() + "进入到了同步方法"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "离开同步方法"); } catch (InterruptedException e) { e.printStackTrace(); } } public void method2() { System.out.println(Thread.currentThread().getName() + "进入了普通方法"); System.out.println(Thread.currentThread().getName() + "离开了普通方法"); } }
在上面的代码中,我们定义一个对象,但是使用了两个线程去分别同时访问同步和非同步方法。我们看结果:
也就是说,同步方法依然会同步执行,非同步方法不会受到任何影响。
5、一个线程访问一个类的两个普通同步方法
这种情况对应于下面这张图:
我们代码来测试一下:
public class SynTest8 implements Runnable { public static void main(String[] args) throws InterruptedException { SynTest8 instance1 = new SynTest8(); Thread thread1 = new Thread(instance1); thread1.start(); } @Override public void run() { if(Thread.currentThread().getName().equals("Thread-0")) { method1(); }else { method2(); } } public synchronized void method1() { try { System.out.println(Thread.currentThread().getName() + "进入到了同步方法1"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "离开同步方法1"); } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized void method2() { try { System.out.println(Thread.currentThread().getName() + "进入到了同步方法2"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "离开同步方法2"); } catch (InterruptedException e) { e.printStackTrace(); } } }
上面这个例子我们创建了一个对象instance1,然后使用一个线程分别去访问同步方法1和同步方法2。结果呢可想而知,所一定会失效。因为在一开始我们已经验证了,此时同步方法1和同步方法2中synchronized锁的就是this对象,所以是同一把锁。当然会生效。
6、同时访问静态同步方法和非静态同步方法
这种情况对应于下面这张图:
我们使用代码来测试一波:
public class SynTest9 implements Runnable { public static void main(String[] args) throws InterruptedException { SynTest9 instance1 = new SynTest9(); Thread thread1 = new Thread(instance1); Thread thread2 = new Thread(instance1); thread1.start();thread2.start(); } @Override public void run() { method1(); method2(); } public synchronized void method1() { try { System.out.println(Thread.currentThread().getName() + "进入到了同步方法1"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "离开同步方法1"); } catch (InterruptedException e) { e.printStackTrace(); } } public synchronized static void method2() { try { System.out.println(Thread.currentThread().getName() + "进入到了静态同步方法2"); Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + "离开静态同步方法2"); } catch (InterruptedException e) { e.printStackTrace(); } } }
在上面的代码中,我们创建了一个instance实例,使用两个线程同时访问普通同步方法和静态同步方法。下面运行一下,看看输出结果:
上面输出结果表明普通同步方法和静态同步方法是没有关联的,这是为什么呢?这是因为普通同步方法的锁是对象,但是静态同步方法的锁是类,所以这是两把锁。锁自然也就是失效了。
四、性质
读到这里,不知道你是不是已经很疲惫了,反正我写的是很难受,不过剩下的这些部分才是精华,也是面试或者是工作中提升你zhuangbility的一个点。希望你一定要注意。认真读下去。
对于synchronized关键字主要有两个性质:可重入性质和不可中断性质。我们分别来看。
1、可重入性质
什么是可重入呢?指的是同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁。我们举一个例子来说明,一句话吃着碗里的看着锅里的。嘴里面还没吃完就继续再去拿吃的。这就是可重入。不可重入的意思正好相反,你吃完了这碗饭才能盛下一碗。
可重入的程度可以细分为三种情况,我们分别测试一下:
(1)同一个方法中是不是可重入的。就好比是递归调用同步方法。
(2)不同的方法是不是可重入的。就好比是一个同步方法调用另外一个同步方法。
(3)不同的类方法是不是可重入的。
下面我们就是用代码来测试一遍:
(1)同一个方法是不是可重入的
public class SynTest10 { private int a=1; public static void main(String[] args) throws InterruptedException { SynTest10 instance1 = new SynTest10(); instance1.method1(); } public synchronized void method1() { System.out.println("method1: a= " + a); if(a == 3) { return ; }else { a++; method1(); } } }
代码很简单,也就是我们定义了一个变量a,只要a不等于3,就一直递归调用方法method1。我们可以看一下运行结果。
也就是说在同一个方法中是可重入的。下面我们接着测试。
(2)不同的方法是不是可重入的
public class SynTest10 { public static void main(String[] args) throws InterruptedException { SynTest10 instance1 = new SynTest10(); instance1.method1(); } public synchronized void method1() { System.out.println("method1"); method2(); } public synchronized void method2() { System.out.println("method2" ); } }
我们在同步方法1中调用了同步方法2。我们同样测试一下。
method1和method2可以依次输出,说明了在不同的方法中也是可重入的。
(3)、不同的类方法是不是可重入的
既然是不同的类,那么我们就在这里定义两个类,一个是Father,一个是Son。我们让son调用father中的方法。
public class Father{ public synchronized void father() { System.out.println("父亲"); } } class Son extends Father{ public static void main(String[] args) { Son instance1 = new Son(); instance1.son(); } public synchronized void son() { System.out.println("儿子"); super.father(); } }
在这里son类中使用super.father()调用了父类中的synchronized方法,我们测试一下看看输出结果:
2、不可中断性质
不可中断的意思你可以这样理解,别人正在打游戏,你也想玩,你必须要等别人不想玩了你才能去。在java中表示一旦这个锁被别人抢走了,你必须等待。等别的线程释放了锁,你才可以拿到。否则就一直等下去。
这一点看起来是个有点但其实在某些场景下弊端超级大,因为假如拿到锁得线程永远的不释放,那你就要永远的等下去。
五、底层原理
对于原理,最好的方式就是深入到JVM中去。我们可以编译看看其字节码文件,再来分析,因此在这里举一个最简单的例子。
1、定义一个简单例子
public class SynTest11 { private Object object = new Object(); public void test() { synchronized(object){ System.out.println("java的架构师技术栈"); } } }
2、分析
分析的步骤很简单,我们通过反编译字节码文件。记住我们的类名是SynTest11。
先编译生成字节码文件。
然后,我们再反编译字节码文件。
以上我们知道其是就是设置了一个监控器monitor。线程进来那就是monitorenter,线程离开是monitorexit。这就是synchronized关键字最基本的原理。
3、可重入原理
在上面我们曾提到可重入的性质,那么synchronized关键字是如何保证的呢?其是工作是由我们的jvm来完成的,线程第一次给对象加锁的时候,计数为1,以后这个线程再次获取锁的时候,计数会依次增加。同理,任务离开的时候,相应的计数器也会减少。
4、从java内存模型分析
java内存模型不是真正存在的,但是我们可以给出一个内存模型。synchronized关键字,会对同步的代码会先写到工作内存,等synchronized修饰的代码块一结束,就会写入到主内存,这样保证了同步。
六、缺陷
synchronized关键字既有优点也有缺点,而且缺点贼多,所以后来出现了比他更好的锁。下面我们就来分析一下,这也是面试常问问题。
1、效率低
我们之前曾经分析过synchronized关键字是不可中断的,这也就意味着一个等待的线程如果不能获取到锁将会一直等待,而不能再去做其他的事了。
这里也说明了对synchronized关键字的一个改进措施,那就是设置超时时间,如果一个线程长时间拿不到锁,就可以去做其他事情了。
2、不够灵活
加锁和解锁的时候,每个锁只能有一个对象处理,这对于目前分布式等思想格格不入。
3、无法知道是否成功获取到锁
也就是我们的锁如果获取到了,我们无法得知。既然无法得知我们也就很不容易进行改进。
既然synchronized有这么多缺陷。所以才出现了各种各样的锁。
七、总结
终于写完了,synchronized涉及到的知识点,以及能够引出来的知识点超级多,不过只有理解synchronized关键字,我们才可以更加深入的学习。本篇文章不可能面面俱到,只能说列出来一些常见的知识点。更加深入的理解我也会在后续的文章中指出。感谢大家的支持。