学习下synchronized锁的实现原理

20次阅读

共计 3133 个字符,预计需要花费 8 分钟才能阅读完成。

synchronized 锁是 Java 中的一种重量级锁。他有三种实现方法:

  1. 对代码块加锁,这时候锁住的是括号里配置的对象。
  2. 对普通方法加锁,这时候锁住的是当前实例对象(this)
  3. 对静态方法加锁,锁住的是当前 class 实例,又因为 Class 的相关数据存储在永久带 PermGen(jdk1.8 则是 metaspace), 永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁住所有调用该方法的线程。

下面逐个分析下各种实现方式。

对代码块加锁
先来看一段示例:

public class Test implements Runnable{
    private int index = 1 ;
    private final static int MAX = 100;// 总票数是 100 张
    @Override
        public  void  run(){while (index<MAX){   // 如果已卖出的数量小于总票数,继续卖
            System.out.println(Thread.currentThread() + "的电影票编号是:"+ (index));// 出票
            index++;  // 卖出的票数加 1
    } 
 }
    public static void main(String[] args) {final Test task = new Test();
        // 5 个线程卖票
        new Thread(task,"一号窗口").start();
        new Thread(task,"二号窗口").start();
        new Thread(task,"三号窗口").start();
        new Thread(task,"四号窗口").start();
        new Thread(task,"五号窗口").start();}
}

这是一段经典的卖票示例代码。运行之后就会发现出了问题(至于为什么会出问题,后面会专门出一篇文章来分析,这篇文章只是侧重学习下 synchronized 锁的原理)。

这时候我们只要对会出现并发问题的代码加上 synchronized 锁就能正常运行了。(这里使用的是对代码块加锁的方式)

public  void  run(){synchronized (MUTEX) {while (index<=MAX) {   // 如果还有余票,继续卖
          System.out.println(Thread.currentThread() + "的电影票编号是:" +(index));// 出票
          index++; // 售出的票数加 1
     }
  }
}

synchronized 锁为什么能解决多线程问题呢,在 idea 中使用插件查看 JVM 指令可以看到加上 synchronized 锁的 run 方法的 JVM 指令中多了下面两个指令

点开 monitorenter 指令,这个插件会自动帮我们跳转到官方文档的页面。可以看到对 monitorenter 有如下描述:

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(管程), 这个监视器只有被占有时,它才会锁住。正在执行 monitorenter 指令的
线程 A 会尝试获取 monitor 的所有权。具体执行规则如下:
1. 如果 monitor 的计数器为 0,线程将进入 monitor 并将计数器设置为, 然后线程就占有这个 monitor 了。
2. 如果线程已经占有 monitor,它会重入 monitor,并将计数器继续加 1.
3. 如果另一个线程此时占有了该 monitor,线程 A 就会一直阻塞,直到 monitor 的计数器变为 0 时,他才会尝试重新获取 monitor 的所有权。
再点开 monitorexit 指令,可以看到下面的描述:
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 指令的线程必须是 monitor 的占有者。线程执行 monitorexit 指令后会将 monitor 的计数器减一,当计数器的值变成 0 时,线程将退出 monitor。其他被阻塞的线程将尝试获取 monitor 的所有权。
通过上面的说明,我们可以知道这两个指令的作用了,通过对 monitor 分别执行加减来维护 monitor 的状态,当 monitor 的状态是 0 时,表示 monitor 可以进入,当 monitor 的状态大于 0 时,表示有线程正在占有 monitor,其他线程要等待。

对方法加锁

试一下对整个方法加锁。

    @Override
        public synchronized void  run(){while (index<=MAX) {   // 如果还有余票,继续卖
                System.out.println(Thread.currentThread() + "的电影票编号是:" + (index));// 出票
                index++; // 售出的票数加 1
            }
    }

这时候再查看 JVM 指令会发现 monitor 指令居然不见了。通过上面的分析我们知道那对 monitor 指令是来对线程加锁的,那么为什么对整个方法加锁的时候,这对指令没有使用呢。通过查找资料,得到下面的解释:方法级的同步是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之间。虚拟机可以从方法常量池的方法表结构中的 ACC_SYNCHRONIZED 访问标志得知一个方法是否声明为同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程就要先成功持有 monitor,然后才能执行方法,最后当方法完成时释放 monitor(无论是否正常完成都会释放)。在方法执行期间,执行线程持有了 monitor,其他任何线程都无法再获取到同一个 monitor。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那么这个方法所持有的 monitor 将在异常抛到同步方法之外时自动释放。

正文完
 0