深入synchronized关键字

1 线程堆栈分析

synchronized关键字提供了一种互斥机制,也就是说在同一时刻,只能有一个线程访问同步资源,很多资料、书籍将synchronized(mutex: 互斥锁)称为锁,其实这种说法是不严谨的,准确地讲应该是某线程获取了与mutex关联的monitor锁

public class Mutex {

    private final static Object MUTEX = new Object();

    /**
     * 模拟访问共享资源
     */
    public void accessResource() {
        synchronized (MUTEX) {
            try {
                TimeUnit.MINUTES.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        final Mutex mutex = new Mutex();
        // 创建5个线程
        for (int i = 0; i < 5; i++) {
            new Thread(mutex::accessResource).start();
        }
    }
}

上面的代码创建了5个线程,当其中一个线程抢到执行权之后,就会持有锁,进入休眠,不会释放锁,所以剩下的线程都会被block住,等待获取锁。

使用jconsle工具看一下:
选中要建立连接的本地进程,然后点击【连接】按钮进入JConsole控制台,将tab切换至【线程】

随便选中程序中创建的某个线程,会发现只有一个线程在TIMED_WAITING(sleeping)状态,其他线程都进入了BLOCKED状态:
在这里插入图片描述

在这里插入图片描述
使用jstack命令打印进程的线程堆栈信息,选取其中几处关键的地方对其进行分析:

在这里插入图片描述

  • Thread0持有monitor<0x000000076baecde8>的锁并且处于休眠状态中,那么其他线程将会无法进入accessResource方法
  • Thread-1等剩下4个线程进入BLOCKED状态并且等待着获取monitor<0x000000076baecde8>的锁

2 JVM指令分析

使用JDK命令javap对Mutex class进行反汇编,输出了大量的JVM指令,在这些指令中,你将发现monitor enter和monitor exit是成对出现的(有些时候会出现一个monitor enter多个monitor exit,但是每一个monitor exit之前必有对应的monitor enter,这是肯定的)。

javap -c .\Mutex.class

结果

public class study.wyy.thread.package01.Mutex {
  public study.wyy.thread.package01.Mutex();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void accessResource();
    Code:
       0: getstatic     #2                  // Field MUTEX:Ljava/lang/Object;
       3: dup
       4: astore_1
       5: monitorenter
       6: getstatic     #3                  // Field java/util/concurrent/TimeUnit.MINUTES:Ljava/util/concurrent/TimeUnit;
       9: ldc2_w        #4                  // long 10l
      12: invokevirtual #6                  // Method java/util/concurrent/TimeUnit.sleep:(J)V
      15: goto          23
      18: astore_2
      19: aload_2
      20: invokevirtual #8                  // Method java/lang/InterruptedException.printStackTrace:()V
      23: aload_1
      24: monitorexit
      25: goto          33
      28: astore_3
      29: aload_1
      30: monitorexit
      31: aload_3
      32: athrow
      33: return
    Exception table:
       from    to  target type
           6    15    18   Class java/lang/InterruptedException
           6    25    28   any
          28    31    28   any

  public static void main(java.lang.String[]);
    Code:
       0: new           #9                  // class study/wyy/thread/package01/Mutex
       3: dup
       4: invokespecial #10                 // Method "<init>":()V
       7: astore_1
       8: iconst_0
       9: istore_2
      10: iload_2
      11: iconst_5
      12: if_icmpge     42
      15: new           #11                 // class java/lang/Thread
      18: dup
      19: aload_1
      20: dup
      21: invokevirtual #12                 // Method java/lang/Object.getClass:()Ljava/lang/Class;
      24: pop
      25: invokedynamic #13,  0             // InvokeDynamic #0:run:(Lstudy/wyy/thread/package01/Mutex;)Ljava/lang/Runnable;
      30: invokespecial #14                 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
      33: invokevirtual #15                 // Method java/lang/Thread.start:()V
      36: iinc          2, 1
      39: goto          10
      42: return

  static {};
    Code:
       0: new           #16                 // class java/lang/Object
       3: dup
       4: invokespecial #1                  // Method java/lang/Object."<init>":()V
       7: putstatic     #2                  // Field MUTEX:Ljava/lang/Object;
      10: return
}

2.1 Monitorenter

每个对象都与一个monitor相关联,一个monitor的lock的锁只能被一个线程在同一时间获得,在一个线程尝试获得与对象关联monitor的所有权时会发生如下的几件事情:

  • 如果monitor的计数器为0,则意味着该monitor的lock还没有被获得,某个线程获得之后将立即对该计数器加一,从此该线程就是这个monitor的所有者了
  • 如果一个已经拥有该monitor所有权的线程重入,则会导致monitor计数器再次累加。
  • 如果monitor已经被其他线程所拥有,则其他线程尝试获取该monitor的所有权时,会被陷入阻塞状态直到monitor计数器变为0,才能再次尝试获取对monitor的所有权。

2.2 Monitorexit

释放对monitor的所有权,想要释放对某个对象关联的monitor的所有权的前提是,你曾经获得了所有权。释放monitor所有权的过程比较简单,就是将monitor的计数器减一,如果计数器的结果为0,那就意味着该线程不再拥有对该monitor的所有权,通俗地讲就是解锁。与此同时被该monitor block的线程将再次尝试获得对该monitor的所有权。

参考:synchronized原理

3 使用synchronized需要注意的问题

  1. 与monitor关联的对象不能为空
private final static Object MUTEX =  null;

Mutex为null,很多人还是会犯这么简单的错误,每一个对象和一个monitor关联,对象都为null了,monitor肯定无从谈起。

  1. synchronized作用域太大
    由于synchronized关键字存在排他性,也就是说所有的线程必须串行地经过synchronized保护的共享区域,如果synchronized作用域越大,则代表着其效率越低,甚至还会丧失并发的优势。synchronized关键字应该尽可能地只作用于共享资源(数据)的读写作用域

  2. 不同的monitor企图锁相同的方法,就是说如果多个线程想要解决线程安全问题,必须使用的是同一把锁
    下面这个就是个错误示例:

public class Mutex extends Thread{

    private final static Object MUTEX = new Object();

    @Override
    public void run() {
        accessResource();
    }

    public void accessResource() {
        synchronized (MUTEX) {
           // do something
        }
    }

    public static void main(String[] args) {

        for (int i = 0; i < 5; i++) {
            // 这里每次都是new,所以线程内部的MUTEX都是不同线程实例的属性
            // 也就是说这5个线程是使用的不是同一个monitor
           new Mutex().start();
        }
    }
}
  1. 多个锁的交叉导致死锁
    多个锁的交叉很容易引起线程出现死锁的情况,程序并没有任何错误输出,但就是不工作

4 This Monitor和Class Monitor的详细介绍

前面提过:

同步方法的monitor:this的monitor
同步静态方法的monitor:就是这个方法所在类的class对象的monitor

这里就研究一下下这两个monitor。

4.1 This Monitor

4.1.1 验证同步方法的monitor是this的monitor
public class ThisMonitor {

    public synchronized void method1() {
        System.out.println(currentThread().getName() + " enter to method1");
        // 这里就简单模拟
        while (true){
            
        }
    }

    public void method2() {
        synchronized (this) {
            System.out.println(currentThread().getName() + " enter to method2");
              // 这里就简单模拟
            while (true){
            
        	}
        }
    }

    public static void main(String[] args) {
        ThisMonitor thisMonitor = new ThisMonitor();
        new Thread(thisMonitor::method1, "T1").start();
        new Thread(thisMonitor::method2, "T2").start();
    }
}

创建了两个线程,每个线程内部都加了同步,一个是同步方法,一个是同步代码块显式的指明了使用this。

运行就会发现,始终只会打印T1 enter to method1 或者 T2 enter to method2,因为这两个是同一个monitor
只要一个抢到了,接下来就进入死循环,一直持有锁,所以导致另一个无法抢到锁执行代码。

但是如果改一下:

public class ThisMonitor {

    // 改成静态方法,静态方法使用的就是当前类的class对象
    public static synchronized void method1() {
        System.out.println(currentThread().getName() + " enter to method1");
        while (true){

        }
    }

    public void method2() {
        synchronized (this) {
            System.out.println(currentThread().getName() + " enter to method2");
            while (true){}
        }
    }

    public static void main(String[] args) {
        ThisMonitor thisMonitor = new ThisMonitor();
        new Thread(ThisMonitor::method1, "T1").start();
        new Thread(thisMonitor::method2, "T2").start();
    }
}

发现输出:

T1 enter to method1
T2 enter to method2

这两个方法都会执行,因为method1我已经改成静态了,此时这两个线程已经不在使用的是同一个monitor了,导致同步代码失效。

足以说明同步方法使用的是this monitor

4.1.2 验证一下同步静态方法的monitor是class monitor
public class ClassMonitor{

    public static synchronized void method1()
    {
        System.out.println(currentThread().getName() + " enter to method1");
         while (true){

        }
    }

    public static void method2()
    {
        synchronized (ClassMonitor.class)
        {
            System.out.println(currentThread().getName() + " enter to method2");
            while (true){

       	 	}
        }
    }

    public static void main(String[] args)
    {
        new Thread(ClassMonitor::method1, "T1").start();
        new Thread(ClassMonitor::method2, "T2").start();
    }
}

运行就会发现,始终只会打印T1 enter to method1 或者 T2 enter to method2,因为这两个是同一个monitor(当前类的class对象的monitor)
只要一个抢到了,接下来就进入死循环,一直持有锁,所以导致另一个无法抢到锁执行代码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值