万字长文带你彻底理解synchronized关键字(下)

简介: 1、Synchronized关键字的简介,主要是为什么要使用Synchronized关键字,极其作用地位。2、Synchronized关键字的使用,主要是从对象锁和类锁两个角度。3、Synchronized关键字的使用注意事项。分析了6种常见的使用情况。4、Synchronized关键字的两个性质,主要是可重入性和不可中断性。5、Synchronized关键字的底层原理。6、Synchronized关键字的常见缺陷。以上我们主要是从这7个角度来分析Synchronized关键字,每一个角度说实话都能单独拿出来作为一篇文章来分析。但是由于考虑到文章的连贯性,所以综合在了一

三、6个常见的使用情况


我们先给出这6种常见的情况,然后一个一个分析。


1、两个线程同时访问一个对象的同步方法。

2、两个线程访问的是两个对象的同步方法。

3、两个线程访问的是synchronized的静态方法。

4、两个线程同时访问同步方法与非同步方法。

5、一个线程访问一个类的两个普通同步方法。

6、同时访问静态同步方法和非静态同步方法。


为了对这6种情况做到心中有数,不至于搞混了,我们画一张图,对每一种情况进行分析。

v2-136d77ee721ced3e99598226b2c02365_1440w.jpg

上面是框架图,下面我们基于开始来分析:


1、两个线程同时访问一个对象的同步方法


这种情况对应于以下这张图:

v2-b05088696c6bb82ada2787c7a6f5815f_1440w.jpg

这种情况很简单,我们在上面也演示过,结果就是同一个时刻只能有一个方法进入。这里就不再演示了。


2、两个线程访问的是两个对象的同步方法


这种情况对应于下面这种:

v2-bfc197d02b255129bbb27ef3f1e78448_1440w.jpg

也就是一个方法有两把锁,线程1和线程2互不干扰的访问。锁是不起作用的。


3、两个线程访问的是synchronized的静态方法


这种情况对应于下面这种情况:

v2-9a2637ef22fab404eeac5fa95fd1541f_1440w.jpg

我们对这种情况来测试一下吧。


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、两个线程同时访问同步方法与非同步方法


这种情况对应于下面这张图:

v2-16681136dc67f11e9d412f71134bcf3f_1440w.jpg

我们对这种情况使用代码进行演示一遍:


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() + "离开了普通方法");
    }
}

在上面的代码中,我们定义一个对象,但是使用了两个线程去分别同时访问同步和非同步方法。我们看结果:

v2-337119eb532f07c0c54b28c0cfefbe00_1440w.jpg

也就是说,同步方法依然会同步执行,非同步方法不会受到任何影响。


5、一个线程访问一个类的两个普通同步方法


这种情况对应于下面这张图:v2-3b2b498a360803a02ae47a4fc5b20d65_1440w.jpg

我们代码来测试一下:

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、同时访问静态同步方法和非静态同步方法


这种情况对应于下面这张图:

v2-f75cab667fd3810c3bedd19ece06c4cf_1440w.jpg

我们使用代码来测试一波:

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实例,使用两个线程同时访问普通同步方法和静态同步方法。下面运行一下,看看输出结果:

v2-6363d9fd17ae2671197a118e698eb630_1440w.jpg

上面输出结果表明普通同步方法和静态同步方法是没有关联的,这是为什么呢?这是因为普通同步方法的锁是对象,但是静态同步方法的锁是类,所以这是两把锁。锁自然也就是失效了。


四、性质


读到这里,不知道你是不是已经很疲惫了,反正我写的是很难受,不过剩下的这些部分才是精华,也是面试或者是工作中提升你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。我们可以看一下运行结果。

v2-15eb642445d5c8c70a7d97765393434a_1440w.png

也就是说在同一个方法中是可重入的。下面我们接着测试。


(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。我们同样测试一下。

v2-558d22ba8d47d3e00567637c3ebeb9c4_1440w.jpg

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方法,我们测试一下看看输出结果:

v2-49b613dbe6a1a81b10b228c974ff52f6_1440w.jpg

2、不可中断性质


不可中断的意思你可以这样理解,别人正在打游戏,你也想玩,你必须要等别人不想玩了你才能去。在java中表示一旦这个锁被别人抢走了,你必须等待。等别的线程释放了锁,你才可以拿到。否则就一直等下去。


这一点看起来是个有点但其实在某些场景下弊端超级大,因为假如拿到锁得线程永远的不释放,那你就要永远的等下去。


五、底层原理



对于原理,最好的方式就是深入到JVM中去。我们可以编译看看其字节码文件,再来分析,因此在这里举一个最简单的例子。


1、定义一个简单例子


public class SynTest11 {
    private Object object = new Object();
    public void test() {
        synchronized(object){
            System.out.println("java的架构师技术栈");
        }
    }
}


2、分析


分析的步骤很简单,我们通过反编译字节码文件。记住我们的类名是SynTest11。

先编译生成字节码文件。

v2-3bef59f08a36b065fa3a21d3b274c6e5_1440w.jpg

然后,我们再反编译字节码文件。

v2-ff7032c9d4c6d023021118a68cf22205_1440w.jpg

以上我们知道其是就是设置了一个监控器monitor。线程进来那就是monitorenter,线程离开是monitorexit。这就是synchronized关键字最基本的原理。


3、可重入原理


在上面我们曾提到可重入的性质,那么synchronized关键字是如何保证的呢?其是工作是由我们的jvm来完成的,线程第一次给对象加锁的时候,计数为1,以后这个线程再次获取锁的时候,计数会依次增加。同理,任务离开的时候,相应的计数器也会减少。


4、从java内存模型分析


java内存模型不是真正存在的,但是我们可以给出一个内存模型。synchronized关键字,会对同步的代码会先写到工作内存,等synchronized修饰的代码块一结束,就会写入到主内存,这样保证了同步。

v2-f70a54bbe8b655383c163e7e9727d07f_1440w.jpg


六、缺陷


synchronized关键字既有优点也有缺点,而且缺点贼多,所以后来出现了比他更好的锁。下面我们就来分析一下,这也是面试常问问题。


1、效率低


我们之前曾经分析过synchronized关键字是不可中断的,这也就意味着一个等待的线程如果不能获取到锁将会一直等待,而不能再去做其他的事了。


这里也说明了对synchronized关键字的一个改进措施,那就是设置超时时间,如果一个线程长时间拿不到锁,就可以去做其他事情了。


2、不够灵活


加锁和解锁的时候,每个锁只能有一个对象处理,这对于目前分布式等思想格格不入。


3、无法知道是否成功获取到锁


也就是我们的锁如果获取到了,我们无法得知。既然无法得知我们也就很不容易进行改进。


既然synchronized有这么多缺陷。所以才出现了各种各样的锁。


七、总结


终于写完了,synchronized涉及到的知识点,以及能够引出来的知识点超级多,不过只有理解synchronized关键字,我们才可以更加深入的学习。本篇文章不可能面面俱到,只能说列出来一些常见的知识点。更加深入的理解我也会在后续的文章中指出。感谢大家的支持。



相关文章
|
2月前
|
缓存 Java 编译器
【多线程-从零开始-伍】volatile关键字和内存可见性问题
【多线程-从零开始-伍】volatile关键字和内存可见性问题
33 0
|
6月前
|
缓存 Java 编译器
必知的技术知识:Java并发编程:volatile关键字解析
必知的技术知识:Java并发编程:volatile关键字解析
27 0
|
Java
关于关键字volatile的一二
关于关键字volatile的一二
79 0
|
消息中间件 缓存 Java
volatile 关键字与计算机底层的一些杂谈
volatile 是 Java 并发编程中一个非常重要,也是面试常问的一个技术点,用起来很简单直接修饰在变量前面即可,但是我们真的懂这个关键字吗?它在 JVM 底层,甚至在 CPU 层面到底是如何发挥作用的?
《JUC并发编程 - 高级篇》05 -共享模型之无锁 (CAS | 原子整数 | 原子引用 | 原子数组 | 字段更新器 | 原子累加器 | Unsafe类 )(二)
《JUC并发编程 - 高级篇》05 -共享模型之无锁 (CAS | 原子整数 | 原子引用 | 原子数组 | 字段更新器 | 原子累加器 | Unsafe类 )
《JUC并发编程 - 高级篇》05 -共享模型之无锁 (CAS | 原子整数 | 原子引用 | 原子数组 | 字段更新器 | 原子累加器 | Unsafe类 )(二)
|
存储 缓存 安全
《JUC并发编程 - 高级篇》05 -共享模型之无锁 (CAS | 原子整数 | 原子引用 | 原子数组 | 字段更新器 | 原子累加器 | Unsafe类 )(三)
《JUC并发编程 - 高级篇》05 -共享模型之无锁 (CAS | 原子整数 | 原子引用 | 原子数组 | 字段更新器 | 原子累加器 | Unsafe类 )
《JUC并发编程 - 高级篇》05 -共享模型之无锁 (CAS | 原子整数 | 原子引用 | 原子数组 | 字段更新器 | 原子累加器 | Unsafe类 )(三)
|
缓存 安全 Java
《JUC并发编程 - 高级篇》05 -共享模型之无锁 (CAS | 原子整数 | 原子引用 | 原子数组 | 字段更新器 | 原子累加器 | Unsafe类 )
《JUC并发编程 - 高级篇》05 -共享模型之无锁 (CAS | 原子整数 | 原子引用 | 原子数组 | 字段更新器 | 原子累加器 | Unsafe类 )
《JUC并发编程 - 高级篇》05 -共享模型之无锁 (CAS | 原子整数 | 原子引用 | 原子数组 | 字段更新器 | 原子累加器 | Unsafe类 )
|
编译器 C语言
C语言程序设计——volatile关键字、函数重入
C语言程序设计——volatile关键字、函数重入
139 0
C语言程序设计——volatile关键字、函数重入
|
编译器
C零散知识点汇总之volatile关键字
C零散知识点汇总之volatile关键字
|
SQL 缓存 安全
Java并发编程学习系列七:深入了解volatile关键字
Java并发编程学习系列七:深入了解volatile关键字
117 0
Java并发编程学习系列七:深入了解volatile关键字