万字长文带你彻底理解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关键字,我们才可以更加深入的学习。本篇文章不可能面面俱到,只能说列出来一些常见的知识点。更加深入的理解我也会在后续的文章中指出。感谢大家的支持。



相关文章
|
10月前
|
Java
关于关键字volatile的一二
关于关键字volatile的一二
47 0
|
11月前
|
消息中间件 缓存 Java
volatile 关键字与计算机底层的一些杂谈
volatile 是 Java 并发编程中一个非常重要,也是面试常问的一个技术点,用起来很简单直接修饰在变量前面即可,但是我们真的懂这个关键字吗?它在 JVM 底层,甚至在 CPU 层面到底是如何发挥作用的?
|
存储 缓存 安全
基础篇:深入JMM内存模型解析volatile、synchronized的内存语义
总线锁定:当某个CPU处理数据时,通过锁定系统总线或者是内存总线,让其他CPU不具备访问内存的访问权限,从而保证了缓存的一致性
81 0
|
编译器 C语言
C语言程序设计——volatile关键字、函数重入
C语言程序设计——volatile关键字、函数重入
100 0
C语言程序设计——volatile关键字、函数重入
|
SQL 缓存 安全
Java并发编程学习系列七:深入了解volatile关键字
Java并发编程学习系列七:深入了解volatile关键字
103 0
Java并发编程学习系列七:深入了解volatile关键字
|
安全 Java
万字长文带你彻底理解synchronized关键字(上)
Synchronized关键字一直是工作和面试中的重点。这篇文章准备彻彻底底的从基础使用到原理缺陷等各个方面来一个分析,这篇文章由于篇幅比较长,但是如果你有时间和耐心,相信会有一个比较大的收获,所以,学习请慢慢来。这篇文章主要从以下几个方面进行分析讲解. 1、Synchronized关键字的简介,主要是为什么要使用Synchronized关键字,极其作用地位。 2、Synchronized关键字的使用,主要是从对象锁和类锁两个角度。 3、Synchronized关键字的使用注意事项。分析了6种常见的使用情况。 4、Synchronized关键字的两个性质,主要是可重入性和不可中断性。
82 0
万字长文带你彻底理解synchronized关键字(上)
|
存储 安全 Java
JMM高并发详解(java内存模型、JMM三大特征、volatile关键字 )
JMM高并发详解(java内存模型、JMM三大特征、volatile关键字 )
JMM高并发详解(java内存模型、JMM三大特征、volatile关键字 )
|
存储 缓存 算法
并发编程(一)| Volatile 与 Synchronized 深度解析
今天这篇是我的好朋友 evil say的投稿,这小伙现在大四,客观来说,大四有这个实力,我觉得很不错。他目前正在找实习,如果看了本文觉得他可以,有公司有坑位、愿意抛出橄榄枝的话。请联系他:hack7458@outlook.com
|
缓存 Java 编译器
Java并发编程学习笔记:volatile关键字解析
Java并发编程学习笔记:volatile关键字解析
|
存储 安全 Java
漫画:什么是volatile关键字?(整合版)
Java内存模型简称JMM(Java Memory Model),是Java虚拟机所定义的一种抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异,让java程序在各种平台下都能达到一致的内存访问效果。
漫画:什么是volatile关键字?(整合版)