详解JUC并发编程之锁

 更新时间:2022年1月1日 14:10  点击:194 作者:熟透的蜗牛

当多个线程访问一个对象时,如果不用考虑这些线程在运行环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。但是现实并不是这样子的,所以JVM实现了锁机制,今天就叭叭叭JAVA中各种各样的锁。

1、自旋锁和自适应锁

自旋锁:在多线程竞争的状态下共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复阻塞线程并不值得,而是让没有获取到锁的线程自旋(自旋并不会放弃CPU的分片时间)等待当前线程释放锁,如果自旋超过了限定的次数仍然没有成功获取到锁,就应该使用传统的方式去挂起线程了,在JDK定义中,自旋锁默认的自旋次数为10次,用户可以使用参数-XX:PreBlockSpin来更改(jdk1.6之后默认开启自旋锁)。

自适应锁:为了解决某些特殊情况,如果自旋刚结束,线程就释放了锁,那么是不是有点不划算。自适应自旋锁是jdk1.6引入,规定自旋的时间不再固定了,而是由前一次在同一个锁上的自旋 时间及锁的拥有者的状态来决定的。如果在同一个锁对象上,自旋等待刚刚成功获取过锁,并且持有锁的线程正在运行中,那么JVM会认为该线程自旋获取到锁的可能性很大,会自动增加等待时间。反之就认为不容易获取到锁,而放弃自旋这种方式。

锁消除:锁消除时指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。意思就是:在一段代码中,堆上的所有数据都不会逃逸出去而被其他线程访问到那就可以把他们当作栈上的数据对待,认为他们是线程私有的,不用再加锁。

锁粗化:

  public static void main(String[] args) {
        StringBuffer buffer = new StringBuffer();
        buffer.append("a");
        buffer.append("b");
        buffer.append("c");
        System.out.println("拼接之后的结果是:>>>>>>>>>>>"+buffer);
    }

  @Override
    @IntrinsicCandidate
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

StringBuffer 在拼接字符串时是同步的。但是在一系列的操作中都对同一个对象(StringBuffer )反复加锁和解锁,频繁的进行加锁解锁操作会导致不必要的性能损耗,JVM会将加锁同步的范围扩展到整个操作的外部,只加一次锁。

2、轻量级锁和重量级锁

这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁, 取而代之的是在monitorenter和monitorexit中只需要依靠一条CAS原子指令就可以完成锁的获取及释放。轻量级锁是相对于重量级锁而言的。

轻量级锁加锁过程

在HotSpot虚拟机的对象头分为两部分,一部分用于存储对象自身的运行时数据,如Hashcode、GC分代年龄、标志位等,这部分长度在32位和64位的虚拟机中分别是32bit和64bit,称为Mark Word。另一部分用于存储指向方法区对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分用于存储数组长度。

对象头信息是与对象自身定义的数据无关的额外存储成本,Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息。mark word中有两个bit存储锁标记位。

HotSpot虚拟机对象头Mark Word

存储内容标志位状态
对象哈希码,分代年龄01无锁
指向锁记录的指针00轻量级锁
指向重量级锁的指针10膨胀重量级锁
空,不需要记录信息11GC标记
偏向线程id,偏向时间戳,对象分代年龄01可偏向

在代码进入同步代码块时,如果此对象没有被锁定(标记位为01状态),虚拟机首先在当前线程的栈帧建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前Mark Word的拷贝,然后虚拟机使用CAS操作尝试将对象的Mark Word 更新为指向Lock Record的指针,如果操作成功了,那么这个线程就有了这个对象的锁,并且将Mark Word 的标记位更改为00,表示这个对象处于轻量级锁定状态。如果更新失败了虚拟机会首先检查是否是当前线程拥有了这个对象的锁,如果是就进入同步代码,如果不是,那就说明锁被其他线程占用了。如果有两个以上的线程争夺同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,锁标记位变为10,后面等待的线程就要进入阻塞状态。

轻量级锁解锁过程

解锁过程同样使用CAS操作来进行,使用CAS操作将Mark Word 指向Lock Record 指针释放,如果操作成功,那么整个同步过程就完成了,如果释放失败,说明有其他线程尝试获取该锁,那就在释放锁的同时,唤醒被挂起的线程。

3、偏向锁

JVM 参数 -XX:-UseBiasedLocking 禁用偏向锁;-XX:+UseBiasedLocking 启用偏向锁。

        启用了偏向锁才会执行偏向锁的操作。当锁对象第一次被线程获取时,虚拟机会把对象头中的标记位设置为01,偏向模式。同时使用CAS操作获取到当前线程的线程ID存储到Mark Word 中,如果操作成功,那么持有偏向锁的线程以后每次进入这个锁相关的同步块时,都不需要任何操作,直接进入。如果有多个线程去尝试获取这个锁时,偏向锁就宣告无效,然后会撤销偏向或者恢复到未锁定。然后再膨胀为重量级锁,标记位状态变为10。

4、可重入锁和不可重入锁

可重入锁就是一个线程获取到锁之后,在另一个代码块还需要该锁,那么不需要重新获取而可以直接使用该锁。大多数的锁都是可重入锁。但是CAS自旋锁不可重入。

package com.xiaojie.juc.thread.lock;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * @author xiaojie
 * @version 1.0
 * @description: 测试锁的重入性
 * @date 2021/12/30 22:09
 */
public class Test01 {
    public synchronized void a() {
        System.out.println(Thread.currentThread().getName() + "运行a方法");
        b();
    }
    private synchronized void b() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行b方法");
    }
    public static void main(String[] args) {
        Test01 test01 = new Test01();
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i=0;i<10;i++){
            executorService.execute(() -> test01.a());
        }
    }
}

5、悲观锁和乐观锁

悲观锁总是悲观的,总是认为会发生安全问题,所以每次操作都会加锁。比如独占锁、传统数据库中的行锁、表锁、读锁、写锁等。悲观锁存在以下几个缺点:

  • 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延迟,引起性能问题。
  • 一个线程占有锁后,其他线程就得阻塞等待。
  • 如果优先级高的线程等待一个优先级低的线程,会导致线程优先级导致,可能引发性能风险。

乐观锁总是乐观的,总是认为不会发生安全问题。在数据库中可以使用版本号实现乐观锁,JAVA中的CAS和一些原子类都是乐观锁的思想。

6、公平锁和非公平锁

公平锁:是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。

非公平锁:非公平锁不需要按照申请锁的时间顺序来获取锁,而是谁能获取到CPU的时间片谁就先执行。非公平锁的优点是吞吐量比公平锁大,缺点是有可能导致线程优先级反转或者造成过线程饥饿现象(就是有的线程玩命的一直在执行任务,有的线程至死没有执行一个任务)。

synchronized中的锁是非公平锁,ReentrantLock默认也是非公平锁,但是可以通过构造函数设置为公平锁。

7、共享锁和独占锁

共享锁就是同一时刻允许多个线程持有的锁。例如Semaphore(信号量)、ReentrantReadWriteLock的读锁、CountDownLatch倒数闩等。

独占锁也叫排它锁、互斥锁、独占锁是指锁在同一时刻只能被一个线程所持有。例如synchronized内置锁和ReentrantLock显示锁,ReentrantReadWriteLock的写锁都是独占锁。

package com.xiaojie.juc.thread.lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
 * @description: 读写锁验证共享锁和独占锁
 * @author xiaojie
 * @date 2021/12/30 23:28
 * @version 1.0
 */
public class ReadAndWrite {
    static class ReadThred extends Thread {
        private ReentrantReadWriteLock lock;
        private String name;
        public ReadThred(String name, ReentrantReadWriteLock lock) {
            super(name);
            this.lock = lock;
        }
        @Override
        public void run() {
            try {
                lock.readLock().lock();
                System.out.println(Thread.currentThread().getName() + "这是共享锁。。。。。。");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.readLock().unlock();
                System.out.println(Thread.currentThread().getName() + "释放锁成功。。。。。。");
            }
        }
    }
    static class WriteThred extends Thread {
        private ReentrantReadWriteLock lock;
        private String name;
        public WriteThred(String name, ReentrantReadWriteLock lock) {
            super(name);
            this.lock = lock;
        }
        @Override
        public void run() {
            try {
                lock.writeLock().lock();
                Thread.sleep(3000);
                System.out.println(Thread.currentThread().getName() + "这是独占锁。。。。。。。。");
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.writeLock().unlock();
                System.out.println(Thread.currentThread().getName() + "释放锁。。。。。。。");
            }
        }
    }
    public static void main(String[] args) {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        ReadThred readThred1 = new ReadThred("read-thread-1", reentrantReadWriteLock);
        ReadThred readThred2 = new ReadThred("read-thread-1", reentrantReadWriteLock);
        WriteThred writeThred1 = new WriteThred("write-thread-1", reentrantReadWriteLock);
        WriteThred writeThred2 = new WriteThred("write-thread-2", reentrantReadWriteLock);
        readThred1.start();
        readThred2.start();
        writeThred1.start();
        writeThred2.start();
    }
}

8、可中断锁和不可中断锁

可中断锁只在抢占锁的过程中可以被中断的锁如ReentrantLock。

不可中断锁是不可中断的锁如java内置锁synchronized。

总结:

名称

优点

缺点

使用场景

偏向锁

加锁和解锁不需要CAS操作,没有额外的性能消耗,和执行非同步方法相比仅存在纳秒级的差距

如果线程间存在锁竞争,会带来额外的锁撤销的消耗

适用于只有一个线程访问同步快的场景

轻量级锁

竞争的线程不会阻塞,提高了响应速度

如线程成始终得不到锁竞争的线程,使用自旋会消耗CPU性能

追求响应时间,同步快执行速度非常快

重量级锁

线程竞争不适用自旋,不会消耗CPU

线程阻塞,响应时间缓慢,在多线程下,频繁的获取释放锁,会带来巨大的性能消耗

追求吞吐量,同步快执行速度较长

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注猪先飞的更多内容!

原文出处:https://blog.csdn.net/weixin_39555954/article/details/122096

[!--infotagslink--]

相关文章

  • C#多线程编程中的锁系统(三)

    这篇文章主要介绍了C#多线程编程中的锁系统(三),本本文主要说下基于内核模式构造的线程同步方式、事件、信号量以及WaitHandle、AutoResetEvent、ManualResetEvent等内容,需要的朋友可以参考下...2020-06-25
  • vivo x9怎么设置图形解锁?vivo x9设置图形解锁教程

    本篇文章介绍了vivo x9如何设置图形解锁的教程,手机小白们快来看一看吧。 问:vivo x9怎么设置图形解锁? 答:图形解锁在某种程度上会保护我们的隐私,那么怎么设置图形...2017-01-22
  • C#多线程编程中的锁系统(四):自旋锁

    这篇文章主要介绍了C#多线程编程中的锁系统(四):自旋锁,本文讲解了基础知识、自旋锁示例、SpinLock等内容,需要的朋友可以参考下...2020-06-25
  • 举例说明Java多线程编程中读写锁的使用

    这篇文章主要介绍了举例说明Java多线程编程中读写锁的使用,文中的例子很好地说明了Java的自带读写锁ReentrantReadWriteLock的使用,需要的朋友可以参考下...2020-06-25
  • SpringCache 分布式缓存的实现方法(规避redis解锁的问题)

    这篇文章主要介绍了SpringCache 分布式缓存的实现方法(规避redis解锁的问题),本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2020-11-20
  • SpringBoot集成redis实现分布式锁的示例代码

    这篇文章主要介绍了SpringBoot集成redis实现分布式锁的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-01-25
  • Java使用synchronized实现互斥锁功能示例

    这篇文章主要介绍了Java使用synchronized实现互斥锁功能,结合实例形式分析了Java使用synchronized互斥锁功能简单实现方法与操作技巧,需要的朋友可以参考下...2020-05-14
  • Unity3D使用GL实现图案解锁功能

    这篇文章主要为大家详细介绍了Unity3D使用GL实现图案解锁功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-06-25
  • 手机的SIM卡被锁住怎么办 SIM卡解锁详细图文教程

    最近有朋友反映SIM卡被锁住了,这是怎么回事要如何解决呢?几天小编就为大家带了SIM卡解锁详细图文教程,一起看看吧...2016-08-27
  • Java多线程锁机制相关原理实例解析

    这篇文章主要介绍了Java多线程锁机制相关原理实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下...2020-08-01
  • c# 死锁和活锁的发生及避免

    多线程编程时,如果涉及同时读写共享数据,就要格外小心。如果共享数据是独占资源,则要对共享数据的读写进行排它访问,最简单的方式就是加锁。锁也不能随便用,否则可能会造成死锁和活锁。本文将通过示例详细讲解死锁和活锁是如何发生的以及如何避免它们。...2020-12-08
  • MySQL悲观锁与乐观锁的实现方案

    我们知道Mysql并发事务会引起更新丢失问题,解决办法是锁,所以本文将对锁(乐观锁、悲观锁)进行分析,这篇文章主要给大家介绍了关于MySQL悲观锁与乐观锁方案的相关资料,需要的朋友可以参考下...2021-11-02
  • 详细分析mysql MDL元数据锁

    这篇文章主要介绍了mysql MDL元数据锁的相关资料,文中讲解非常细致,代码帮助大家更好的理解和学习,感兴趣的朋友可以了解下...2020-08-07
  • C#笔试题之同线程Lock语句递归不会死锁

    这篇文章主要介绍了C$ 笔试题之同线程Lock语句递归不会死锁,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-06-25
  • 实战开发为单片机的按键加一个锁防止多次触发的细节

    今天小编就为大家分享一篇关于实战开发为单片机的按键加一个锁防止多次触发的细节,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧...2020-04-25
  • C#多线程编程中的锁系统(二)

    这篇文章主要介绍了C#多线程编程中的锁系统(二),本文讲解了volatile、Interlocked、ReaderWriterLockSlim等升级锁和原子操作的使用实例,需要的朋友可以参考下...2020-06-25
  • Mysql 数据库死锁过程分析(select for update)

    最近有项目需求,需要保证多台机器不拿到相同的数据,后来发现Mysql查询语句使用select.. for update经常导致数据库死锁问题,下面小编给大家介绍mysql 数据库死锁过程分析(select for update),对mysql数据库死锁问题感兴趣的朋友一起学习吧...2015-12-14
  • redission分布式锁防止重复初始化问题

    这篇文章主要介绍了redission分布式锁防止重复初始化问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-01-15
  • MySQL线上死锁分析实战

    这篇文章主要介绍了MySQL线上死锁分析实战,文章内容分析的很清楚,有对于这方面不懂的同学可以研究下...2021-02-25
  • JPA使用乐观锁应对高并发方式

    这篇文章主要介绍了JPA使用乐观锁应对高并发方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-10-15