本站消息

站长简介/公众号

  出租广告位,需要合作请联系站长


+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

暂无数据

大厂之路一由浅入深、并行基础、源码分析一synchronized关键字

发布于2021-05-29 23:46     阅读(892)     评论(0)     点赞(18)     收藏(5)




  • synchronized概念
    • synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。
    • 在java中,每一个对象注意:对象的重要性。) 有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在
    • 当我们调用某对象的synchronized方法时,就获取了该对象的同步锁。例如,synchronized(obj)就获取了 “obj这个对象” 的同步锁。
    • 不同线程对同步锁的访问是互斥的。也就是说,同一个时间点,只有一个线程能得到对象锁。

  • synchronized的基本使用
  • synchronized的作用主要有三个:
    • 确保线程互斥的访问同步代码
    • 保证共享变量的修改能够及时可见(因为同一时间只能一个线程访问共享变量)
    • 有效解决重排序问题。(重排序带来的结果错误)
    • 从语法上讲,Synchronized总共有三种用法:
      • 修饰普通方法 (实例锁)
      • 修饰代码块 (实例锁)
      • 修饰静态方法 (全局锁)(对类加锁)

  • synchronized基本规则(3条)
    当一个线程访问“某对象”的 “synchronized方法” 或者 “synchronized代码块” 时:
    • ①、其他线程对“该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
    • ②、其他线程仍然可以访问“该对象”的非同步代码块
    • ③、其他线程对“该对象”的其他的“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
    • 之所以如此,是因为我们锁的是对象!!!!

①、其他线程对“该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。

package com.wwj.text;

public class SynchronizedDemo {
    public static void main(String[] args){
        ThreadDemo runnable = new ThreadDemo();

        Thread t1 = new Thread(runnable , "t1"); //t1基于runnable这个Runnable对象
        Thread t2 = new Thread(runnable , "t2"); //t2是基于runnable这个Runnable对象
        t1.start();
        t2.start();
    }
}
class ThreadDemo implements Runnable{
    @Override
    public void run() {
        synchronized (this){
            try{
                for(int i=0 ; i<5 ; i++){
                    Thread.sleep(100);
                    System.out.println(Thread.currentThread().getName() + "loop" + i);
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
  • 运行结果:
t1loop0
t1loop1
t1loop2
t1loop3
t1loop4
t2loop0
t2loop1
t2loop2
t2loop3
t2loop4
Process finished with exit code 0
  • 结果分析:
    • run()方法中存在“synchronized(this)代码块”,而且t1和t2都是基于runnable这个Runnable对象"创建的线程。
    • 这就意味着,我们可以将synchronized(this)中的this看作是“runnable这个Runnable对象”;
    • 因此,线程t1和t2共享“runnable对象的同步锁”。所以,当一个线程运行的时候,另外一个线程必须等待“运行线程”释放“runnable的同步锁”之后才能运行。
  • 那什么叫锁对象呢?
    • 我们对上面的代码进行纠正
package com.wwj.text;

public class SynchronizedDemo {
    public static void main(String[] args){
       // ThreadDemo runnable = new ThreadDemo();

        Thread t1 = new Thread( new ThreadDemo() , "t1");
        Thread t2 = new Thread( new ThreadDemo(),"t2"); //t1、t2是基于不同的ThreadDeamo对象,
        t1.start();
        t2.start();
    }
}
class ThreadDemo implements Runnable{
    @Override
    public void run() {
        synchronized (this){
            try{
                for(int i=0 ; i<5 ; i++){
                    Thread.sleep(100);
                    System.out.println(Thread.currentThread().getName() + " loop" +":"+ i);
                }
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
  • 结果:
t2 loop:0
t1 loop:0
t2 loop:1
t1 loop:1
t2 loop:2
t1 loop:2
t2 loop:3
t1 loop:3
t2 loop:4
t1 loop:4

  • 结果分析:
    • 本应该是t1先执行,t2再执行,而现在的结果却不是,文章中的synchronized(this)中的this代表的是ThreadDemo 对象而t1和t2是两个不同的MyThread对象,因为我们里面写的是 new ThreadDemo(),创建的是不同的对象 因此t1和t2在执行synchronized(this)时,获取的是不同对象的同步锁

  • ②和③就不写例子了,顾名思义。

  • synchronized方法 和 synchronized代码块
    • “synchronized方法”是用synchronized 修饰方法,
      • 在这里插入图片描述
      • 注意:这时 synchronized修饰的是实例方法,如果修饰静态方法**,那就相当于修饰类了,也就是全局锁,后面会讲解。只要用synchronized修饰的方法,都可以当作静态方法执行的格式执行。
    • “synchronized代码块”则是用synchronized 修饰代码块
      • 在这里插入图片描述
  • 对比: synchronized代码块比synchronized方法 效率更高,因为synchronized代码块可以更精确的控制临界区(也就是共享资源)

  • 实例锁 和 全局锁
    • 实例锁锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念,对应的就是synchronized关键字 (synchronized方法和synchronized代码块)
    • 全局锁 – 该锁针对的是类、或者静态方法无论实例多少个对象,那么线程都共享该锁。对应的就是static synchronized (或者是锁在该类的class或者classloader对象上)。

  • 全局锁与实例锁的比较:(通过代码)
    • 借鉴第一篇博客中的例子
    • 关于“实例锁”和“全局锁”有一个很形象的例子:
pulbic class Something {   //假设,Something有两个实例x和y。分析下面4组表达式获取的锁的情况。
    public synchronized void isSyncA(){}   //实例方法
    public synchronized void isSyncB(){}   //实例方法
    public static synchronized void cSyncA(){}   //静态方法
    public static synchronized void cSyncB(){}   //静态方法
}
  • 假设,Something有两个实例x和y。分析下面4组表达式获取的锁的情况
package com.wwj.text;

public class SynchronizedTest {

    SomeThing x = new SomeThing();
    SomeThing y = new SomeThing();

    private void test1(){
        Thread t11 = new Thread(new Runnable() {
            @Override
            public void run() {
                x.isSyncA();
            }
        } , "t11");
        Thread t12 = new Thread(new Runnable() {
            @Override
            public void run() {
                x.isSyncB();
            }
        },"t12");
        t11.start();
        t12.start();
    }
    private void test2(){
        Thread t21 = new Thread(new Runnable() {
            @Override
            public void run() {
                x.isSyncA();
            }
        },"t21");
        Thread t22 = new Thread(new Runnable() {
            @Override
            public void run() {
                y.isSyncA();
            }
        },"t22");
        t21.start();
        t22.start();
    }
    private void test3(){
        Thread t31 = new Thread(new Runnable() {
            @Override
            public void run() {
                SomeThing.cSyncA();
            }
        },"t31");
        Thread t32 = new Thread(new Runnable() {
            @Override
            public void run() {
                SomeThing.cSyncB();
            }
        },"t32");
        t31.start();
        t32.start();
    }
    private void test4(){
        Thread t41 = new Thread(new Runnable() {
            @Override
            public void run() {
                x.isSyncA();
            }
        },"t41");
        Thread t42 = new Thread(new Runnable() {
            @Override
            public void run() {
                SomeThing.cSyncA();
            }
        },"t42");
        t41.start();
        t42.start();
    }
    /*
    test1:不能同时访问,因为isSyncA()和isSyncB()都是对同一个对象(对象x)的同步锁
    test2:可以同时访问,因为访问的不是同一个对象的同步锁,x.isSyncA()是对x的同步锁,而y.isSyncA()是对y的同步锁。
    test3:不可以同时访问,因为cSyncB()和cSyncA()都是静态方法,也就是对类(Something)进行加锁
    test4:可以同时访问,因为isSyncA()是实例方法,x.isSyncA()使用的是对象x的锁;而cSyncA()是静态方法,Something.cSyncA()可以理解对使用的是“类的锁”。因此,它们是可以被同时访问的
     */
    public static void main(String[] args){
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        //synchronizedTest.test1();
        synchronizedTest.test2();
        //synchronizedTest.test3();
        //synchronizedTest.test4();
    }
}
class SomeThing{
    public synchronized  void isSyncA(){
        try{
            for(int i=0 ; i<5 ; i++){
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName()+" : isSyncA");
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    public synchronized  void isSyncB(){
        try{
            for(int i=0 ; i<5 ; i++){
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName()+" : isSyncB");
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    public static synchronized void cSyncA(){
        try{
            for(int i=0 ; i<5 ; i++){
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName()+" : cSyncA");
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
    public static synchronized  void cSyncB(){
        try {
            for(int i=0 ; i<5 ; i++){
                Thread.sleep(100);
                System.out.println(Thread.currentThread().getName()+" : cSyncB");
            }
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}
  • 通过test1()、test2()、test3()、test4()、我们可以得到四个结论:
    • test1:不能同时访问,因为isSyncA()和isSyncB()都是对同一个对象(对象x)的同步锁
    • test2:可以同时访问,因为访问的不是同一个对象的同步锁,x.isSyncA()是对x的同步锁,而y.isSyncA()是对y的同步锁。
    • test3:不可以同时访问,因为cSyncB()和cSyncA()都是静态方法,也就是对类(Something)进行加锁
    • test4:可以同时访问,因为isSyncA()是实例方法,x.isSyncA()使用的是对象x的锁;而cSyncA()是静态方法,Something.cSyncA()可以理解对使用的是“类的锁”。因此,它们是可以被同时访问的
  • 注意技巧:
    • 对于实例方法、实力类、谁调用这个方法,那么所么锁定的就是谁的对象实例,比如isSyncA()是实例方法,x.isSyncA()锁定的就是该isSyncA()所在类的实例,也就是x.
    • 对于静态方法,那么锁定的就是,比如cSyncA()、cSyncB()都是静态方法,那么不管是x.cSyncA(),还是x.cSyncB(),因为它们都是对"类"加锁,而cSyncA()、cSyncB()都属于同一个类,所以不可能同时访问
    • 对于test4,我们可以知道对这个类加锁,与对这个类的实例加锁,它们互不干涉,所以可以同时访问。

  • synchronized修饰代码块底层原理
    • 通过字节码层面synchronized修饰代码块的作用
    • 在这里插入图片描述
    • 通过编码我们可以发现:synchronized修饰代码块对应的字节码为:monitorenter和monitorexit
    • 我们进一步分析这两个字节码

  • 字节码层面:monitorenter :
    • JVM规范中描述:
      • Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
      • If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
      • If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
      • If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.
    • 翻译过来就是:
      • 每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
      • 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者
      • 如果该线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.(能反复加锁)
      • 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

  • 字节码层面:monitorexit :
    • JVM规范中描述:
      • The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
      • The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
    • 翻译过来就是:
      • 执行monitorexit的线程必须是与objectref所对应的monitor的所有者。
      • 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被 这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
  • 通过这两段描述,我们应该能很清楚的看出synchronized的实现原理,synchronized的语义底层是通过一个monitor的对象来完成

  • 字节码层面说明wait()/notify()方法为什么要在同步块和方法中才能调用?
  • 其实wait/notify()等方法也依赖于monitor对象这就是为什么只有在同步块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

  • synchronized修饰实例方法底层原理
    • 通过字节码层面synchronized修饰实例方法的作用
    • 在这里插入图片描述
    • 通过编码我们可以发现:synchronized修饰实例方法对应的字节码没有看到monitorenter和monitorexit,却额外多了ACC_SYNCHRONIZED, 是用来标志用的,通过字节码无法看出是否使用了monitorenter和monitorexit,通过查阅发现:
    • 对于synchronized方法执行中的线程识别该方法的 method_info 结构是否有 ACC_SYNCHRONIZED 标记设置然后它自动获取对象的锁,调用方法,最后释放锁。如果有异常发生,线程自动释放锁

  • 总结:从反编译获得的字节码可以看出:
    • synchronized代码块实际上多了monitorenter和monitorexit两条指令。monitorenter指令执行时会让对象的锁计数加1,而monitorexit指令执行时会让对象的锁计数减1,其实这个与操作系统里面的PV操作很像,操作系统里面的PV操作就是用来控制多个线程对临界资源的访问。
    • 对于synchronized方法,执行中的线程识别该方法的 method_info 结构是否有 ACC_SYNCHRONIZED 标记设置,然后它自动获取对象的锁,调用方法,最后释放锁。如果有异常发生,线程自动释放锁。

  • 加锁出现异常:
  • 注意: 对于synchronized方法或者synchronized代码块,当出现 异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。


所属网站分类: 技术文章 > 博客

作者:听说你很拽

链接:http://www.javaheidong.com/blog/article/207902/f4c3159eabcae991e1f1/

来源:java黑洞网

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

18 0
收藏该文
已收藏

评论内容:(最多支持255个字符)