0%

浅谈Java锁机制

概述

锁的概念,来自于操作系统。
在操作系统中,进程是内存独立,实现进程间的通信需要通过一些其他的手段;但是线程之间是内存共享的,当有多个线程对某一临界资源(有可能被多个线程访问,存在线程安全的数据)进行读写访问时,就会出现线程安全的问题。
锁概念

根据锁对访问限制规则的不同,可以将锁进行分类。下面介绍一下在Java中主要的锁。

乐观锁

悲观锁

自旋锁

适应性自旋锁

公平锁

非公平锁

可重入锁

不可重入锁

共享锁

排他锁

Java锁实现

在Java中,除了使用关键字synchronized进行加锁,在Java5以后,还引入了一个juc包,其中增加了许多适用于Java的线程安全的实现,比如一系列Lock类。
实现Lock的锁类型,可以通过lock.lock()或lock.unLock()进行灵活的加锁和释放锁。其底层实现原理其实是通过Java代码调用native层,再调系统接口实现的。而synchronized则主要是JVM层实现。这里主要讲一下synchronized的运作机制。

实现方式

可以看到,经过编译以后,synchronized关键字的代码块,会被编译为两个虚拟机指令,分别对应着加锁和释放锁操作。

这里我想说一下自己对加锁的理解。Java中的加锁操作,其实是把某个对象当做锁(monitor),也就是每一个对象都是一个锁,对象头中的MaskWord中,记录着该对象的锁状态及拥有的线程信息。

当代码运行进入到同步代码块中,执行monitorenter指令,jvm会去尝试获取monitor的所有权。如果monitor的进入数为0,则线程可以拥有该monitor,并且进入数置1。synchronized是一种可重入锁,因此如果获取所有权时发现已有线程拥有,但是同一个线程,则可以直接进入,并且进入数+1。

当同步代码块运行结束,执行monitorexit,jvm会先去判断,执行该指令的必须是monitor的拥有者,然后进入数-1。如果进入数为0,则该线程不再是该monitor拥有者。

两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

实现机制

在Java1.6以前,synchronized的性能是比较糟糕的,比不上Lock。但是在Java1.6以后,对synchronized做了性能优化,和Lock其实是并驾齐驱的,可以根据具体场景进行选择。

在1.6中,synchronized增加了一个锁升级的过程。

首先,synchronized默认是乐观锁,可以认为是无锁,即假设没有线程竞争,通过cas操作实现。

如果锁一直只被一个线程持有,那么乐观锁转变为偏向锁。直接检测对象头里是否存在该线程id,而不是用cas操作,进而提高了性能。并且不会主动释放偏向锁。

当有其他线程访问同步代码块时,锁会升级为轻量锁,区别在于等待线程会进行自旋

自旋超过一定次数 或者有第三个线程竞争,则升级为重量锁,等待的线程直接挂起


完结 撒花 ฅ>ω<*ฅ