java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java锁与锁的状态

Java中的锁与锁的状态升级详细解读

作者:Smallc0de

这篇文章主要介绍了Java中的锁与锁的状态升级详细解读,Java 1.6以后官方针对锁的优化,主要是增加了两种新的锁:偏向锁和轻量级锁,再加上本身重量级锁,那么锁基本上可以大致分为这三种,它们之间的区别主要是体现在等待时间上面,需要的朋友可以参考下

Java 1.6 的优化

Java 1.6以后官方针对锁的优化,主要是增加了两种新的锁:偏向锁和轻量级锁。再加上本身重量级锁,那么锁基本上可以大致分为这三种,它们之间的区别主要是体现在等待时间上面。重量级锁比较暴力,一旦某个资源被一个线程加了锁,那么其他线程就不要再有任何想法了,必须等待其释放资源才可以。如果时间较长,就会造成程序卡顿,甚至崩溃掉,为了改进加了两个新锁。这两种新锁的概念是如何优化的呢,就需要弄清楚对象和monitor的关系。

对象头结构

任何一个实例对象都有一些共同的信息即对象头、实例变量、填充数据。其中对象头就是加锁的基础,其中存储的就是加锁的信息;实例变量就是存的属性变量的信息,私有公有之类的;填充数据则是数据的起始地址。我们要说的还是对象头,下面是对象头结构:

长度内容描述
32/64 bitMark Word存储对象的hashCode或者锁信息等
32/64 bitClass Metadata Address存储到对象类型数据的指针
32/64 bitArray Length数组的长度,如果当前对象是数组

从对象头的信息来看,Mark Word就是存放锁信息的地方,一共有32位用来存放锁信息,信息格式如下:

在这里插入图片描述

锁的状态

首先先解释下上表中锁状态的含义:

无锁状态:没有加锁,属于乐观锁,锁标记01。

偏向锁:在对象第一次被某一线程占有的时候,会检查偏向锁标记,然后把偏向锁标记置1,表示线程被占用使用的是偏向锁,锁标记置为01。接着写入线程ID,表示当前对象被哪个ID的线程占有了。当此线程占有期间,其他的线程访问的时候,就会有线程竞争。如果竞争成功,使用资源;如果竞争失败则等待下次竞争,偏向锁是轻量级锁,属于乐观锁。所谓的偏向锁,会比较倾向于首次占有的线程,也就是说两个线程竞争,第一次占有的线程会有比较大的概率分配到资源,这也就是偏向锁名称的由来:会偏向于首先占有该资源的线程。那么当前线程一旦具有优先权,其他线程很可能长久无法获取资源,为了改进这样一个状态,很多时候会采用大名鼎鼎非阻塞算法Compare and Swap(CAS)算法。正是因为这种特性,偏向锁更加适合用于竞争不激烈的时候。由于偏向锁会不断地和无锁状态进行切换,因此两者使用时间也是接近的。

轻量级锁:一般在线程有交替时使用。当偏向锁算法CAS竞争失败,就会修改偏向锁标记为00,升级为轻量级锁,属于乐观锁。

重量级锁:强互斥,锁住资源后不允许其他线程竞争,其他线程要想访问必须等待资源释放,等待时间长。当某资源已经处于轻量级锁的时候,会有别的线程前来竞争,继续竞争失败,则会修改标记为10,变为重量级锁,属于悲观锁。

自旋锁与锁消除

锁的升级也是要消耗时间的,尤其是不同等级之间的转换消耗的时间更多。而轻量级锁转换成重量级锁则是用户线程和核心线程(操作系统本身的线程)之间的转换非常耗时,所以我们要尽量减少这种转换,所以有了这样一种概念:当竞争失败的时候,先不着急进行转换,而是等待一会儿,由于线程执行的非常快,可能还没有升级到重量级锁的时候,资源就已经被释放了。正是基于这样一种逻辑,就有了自旋锁这一概念。

自旋锁:当竞争失败的时候,不是马上转化级别,而是执行几次空循环,如果执行空循环的时候,占有线程释放了资源,那么就不需要进行锁的升级,直接进行占用线程执行就好了因此自旋锁只是一种概念,并不是真正的锁。

锁消除: Java的即时编译编译器(JIT)在编译的时候把不必要的锁去掉。比如下面代码,用来给变量a赋值,就完全没有必要加锁,因此这个synchronized在编译的时候会被消去。

synchronized(this){
    int a=1;
}

锁的升级

除了上面所说,还要明白一个概念,锁永远都会加载对象(或者说堆中的资源上)上而不是线程上。所以升级过程为如下的步骤:

假设:线程A访问对象资源A则此时立刻对资源A加上偏向锁保证线程A访问资源A,且线程ID会被置为线程A。如果线程B来竞争,此时偏向锁不会主动释放,线程B可以看到该锁并知晓资源A已经被线程A使用,于是检查此时资源A是否被线程A使用(即线程A是否还存活),那么:

  1. 如果线程A已经销毁,则资源A分配给线程B,即分配偏向锁给线程B并把线程ID置为线程B。
  2. 如果线程A没有销毁,则检查线程A的操作栈,检查资源A的使用情况:
    • 如果线程A仍然在使用资源A,则把资源A升级为轻量级锁,保证线程A的使用;
    • 如果资源A已经不再使用,已经被线程A释放了,则资源A分配给线程B,后续同1。
  3. 资源A已经由于竞争,升级为轻量级锁(此时资源A依然是被线程A使用),线程B仍然需要竞争资源A。 因为轻量级锁会被认为竞争程度低,所以线程B被允许尝试竞争,当线程B再次发现资源A被轻量级锁锁住时,线程B会进入自旋状态或者说自旋锁(即空转一定时间,比如空的循环等等,等待资源A被释放)。此时认为资源A会很快被释放,所以线程B进入空转等待资源A。
  4. 但是当自旋超过一定次数,或者线程C在线程B自旋时,也来请求资源A,则资源A的轻量级锁被升级为重量级锁。由于重量级锁能够使得除了拥有当前资源的线程以外的线程都阻塞,等待资源的释放。所以此时资源A和线程A被重量级锁绑在一起,不会释放给线程B、线程C,因此线程B和线程C必须等待线程A释放资源A以后在进行新的竞争

说完这些,可能大家会有个疑问:为什么要进行锁的升级?假如很多线程都是处于轻量级上,都处于自旋状态,那么其中正在运行一个线程以及释放锁了,但是此时有很多线程在自旋,都在相互竞争。可能造成所有的线程都会一直自旋下去,CPU空转。为了阻止CPU空转,所以用重量级锁把除了正在运行的线程以外的所有线程阻塞,保证不会有新的竞争。但是要注意的是锁是由低到高的升级,而且只会升级,而不会降级。因为即便是重量级锁一旦占有线程释放了,就直接是无锁状态,不存在一层一层降级的过程。

锁的重入性

当有些时候必须对某个资源进行二次加锁也是可以的,比如下面这段代码做了多重加锁。

这样也是可以的,之前的博客中说过Java中每一个对象都有一个monitor对象,也就是一个监视器,加锁就是通过这个监视器去加锁。

当某一个线程要占有这个对象的时候,先去检查monitor对象的计数器是不是0,如果是0表示没有线程占有这个对象,则成功占有这个对象,并且对这个对象的monitor计数器+1。

如果不为0,则说明这个对象已经被别的线程占用,那么就等待。当线程释放某个对象的占有时monitor的计数器-1。

注意这里是减一不是置0,也就是说同一线程可以对同一对象进行多次加锁,进行不断地+1、+1,这个就是线程的重入性。

public synchronized void methodName3(){
    synchronized (this){
        // code ......
        synchronized (this){		//加锁的对象不同也可以,但是要避免对象交叉造成的死锁
            // code ......
            synchronized (this){
                // code ......
            }
        }
    }
}

悲观锁与乐观锁

悲观锁:一般指写操作比较多,包括增删改,读操作(查)比较少的锁。也指不允许竞争的锁,遇到必须等待。

乐观锁:一般指读(查)操作比较多,但是写操作比较少的锁。也指可以竞争的锁,可以尝试竞争。 因此很多时候悲观锁一般要加锁,乐观锁一般只用版本控制,读取到最新的数据即可。只是一个概念,并不特地指某种所。

公平锁与非公平锁

公平锁:其实就是排队,先来先得FIFO的逻辑,因此每个锁都是公平的。

非公平锁:就是采用一定的算法或者优先度去对线程拿到锁的顺序进行调整,因此每个锁并不是公平获取资源的。 这两个锁各有优缺点,使用谁要看具体需求,比如紧急刹车作为一个功能来说,就必须用非公平锁。

死锁

当由加锁导致A线程等B线程释放资源,B线程等A线程释放资源,结果线程A和线程B都无法获取资源导致,程序卡死在这里的情况就是死锁。

总结

本篇延续着synchronized关键字的部分内容,对锁的概念、锁的原理以及锁的升级,进行了一个剖析,希望能够对各位理解Java中的锁有所帮助。

到此这篇关于Java中的锁与锁的状态升级详细解读的文章就介绍到这了,更多相关Java锁与锁的状态内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文