• 多线程模块 | java中的各种锁


    java中的锁

    乐观锁和悲观锁

    根据线程是否要锁住同步资源,分为悲观锁(锁)和乐观锁(不锁)

    悲观锁 :认为自己再使用数据的时候一定有别的线程来修改数据,在获取数据的时候会先加锁,确保数据不会被别的线程修改。

    锁实现方式:关键字 Synchornized,接口 Lock 的实现类

    适用场景:写操作较多

    乐观锁:认为自己再使用数据的时候不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候判断之前有没有别的线程更新了这个数据。

    锁实现方式:CSA 算法,例如 AtomicInteger 类的原子自增是通过 CAS 自旋(如果内存中的版本与该线程中复制到的版本号不同时,会自旋,自旋是重新去读内存中的信息到线程中,再进行修改操作,操作完再尝试更新)实现的

    适用场景:读操作较多,不加锁的特点能够使其读操作的性能大幅度提升。

    读锁(共享锁)、写锁(排他锁)

    读锁(共享锁)(用数据库来举例)

    针对同一份数据,多个读操作可以同时进行而不互相影响。

    当一个进程对表加了读锁后:该进程和其他进程都可对该表进行读操作; 该进程不能对表进行修改会产生 error;

    该进程在释放该表的读锁前也不能读取其他的表;其他进程想对该表进行修改时,会进入阻塞状态,当锁释放后完成修改。

    写锁(排它锁)

    当写操作没有完成前,会阻断其他写锁和读锁。进程能够读自己上写锁的表;

    进程能够写自己上写锁的表;该进程在释放该表的写锁之前不能读取其他表;
    其他进程要读这个上了写锁的表,会进入阻塞状态,等锁释放后,完成读操作。

    读写锁:ReentrantReadWriteLock lock= new ReentrantReadWriteLock();

    读写锁下面分读锁和写锁,进行写操作可以上写锁:

    lock.writeLock() 进行读操作可以上读锁:
    lock.readLock()(读不加锁的话可能会产生脏读这些问题)
    

    自旋锁、非自旋锁

    自旋锁:当一个线程在获取锁的过程中,发现锁已经被其他线程获取,那么该线程循环等待,然后不断等待该锁是否能够被成功获取,自旋知道获取到锁才会退出。

    自旋锁的意义及使用场景:

    因为阻塞与唤醒需要操作系统切换 cpu 状态(涉及到上下文切换),需要消耗一定时间。有时自旋的时间比阻塞唤醒所需要的时间还短

    自旋锁:固定次数自旋。自旋次数完成后还没有拿到锁,就认为更新失败

    自适应自旋锁:假设不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,因此,可以根据上一次自旋的时间与结果调整下一次自旋的时间。

    JDK1.6 中 可 以 通 过 -XX : -UseSpining 参 数 关 闭 自 旋 锁 优 化 , - XX:PreBlockSpin 参数修改默认的自旋次数

    JDK1.7 之后自旋锁的参数被取消,虚拟机不再支持用户配置自旋锁,自旋锁总是会被执行,并且自旋次数也由虚拟机自动调整。

    显式锁、隐式锁

    隐式锁,synchronized 是基于 jvm 的内置锁,加锁与解锁的过程不需要我们在代码中人为控制,jvm 会自动去加锁和解锁

    显式锁,整个加锁跟解锁过程需要手动编写代码去控制,例如 ReentrantLock

    可重入锁、非重入锁

    可重入锁一个线程已经获得某个锁,可以再次获取锁而不会出现死锁。就是可以重复获取相同的锁。

    只判断这个锁有没有被锁上,只要被锁上申请锁的线程都会被要求等待。实现简单。在锁设计时,不仅判断锁有没有被锁上,还会判断锁是谁锁上的,当就是自己锁上的时候,那么他依旧可以再次访问临界资源,并把加锁次数加一。设计了加锁次数,以在解锁的时候,可以确保所有加锁的过程都解锁了,其他线程才能访问。

    不可重入锁当 A 方法获取 lock 锁去锁住一段需要做原子性操作的 B 方法时,如果这段 B 方法又需要

    锁去做原子性操作,那么 A 方法就必定要与 B 方法出现死锁。这种会出现问题的重入一把锁的情况,叫不可重入锁。在锁设计时, 只判断这个锁有没有被锁上,只要被锁上申请锁的线程都会被要求等待。实现简单。

    如何实现互斥锁(mutex)?

    在Java里面,最基本的互斥同步手段就是synchronized关键字,这是一种块结构(Block Structured)的同步语法。synchronized关键字经过Javac编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象。如果Java源码中的synchronized明确指定了对象参数,那就以这个对象的引用作为reference。如果没有明确指定,那将根据synchronized修饰的方法类型(如实例方法或类方法),来决定是取代码所在的对象实例还是取类型对应的Class对象来作为线程要持有的锁。

    自JDK 5起,Java类库中新提供了java.util.concurrent包(J.U.C包),其中的java.util.concurrent.locks.Lock接口便成了Java的另一种全新的互斥同步手段。基于Lock接口,用户能够以非块结构(Non-Block Structured)来实现互斥同步,从而摆脱了语言特性的束缚,改为在类库层面去实现同步,这也为日后扩展出不同调度算法、不同特征、不同性能、不同语义的各种锁提供了广阔的空间。

    Lock和synchronized的区别

    Lock: 是Java中的接口,可重入锁、悲观锁、独占锁、互斥锁、同步锁。

    1. Lock需要手动获取锁和释放锁。就好比自动挡和手动挡的区别

    2. Lock 是一个接口,而 synchronized 是 Java 中的关键字, synchronized 是内置的语言实现。

    3. synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁。

    4. Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中断。

    5. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。

    6. Lock 可以通过实现读写锁提高多个线程进行读操作的效率。

    ReentrantLock 和synchronized的区别

    ReentrantLock是Java中的类 : 继承了Lock类,可重入锁、悲观锁、独占锁、互斥锁、同步锁。

    划重点

    相同点:

    1. 主要解决共享变量如何安全访问的问题

    2. 都是可重入锁,也叫做递归锁,同一线程可以多次获得同一个锁,

    3. 保证了线程安全的两大特性:可见性、原子性。

    不同点:

    1. ReentrantLock 就像手动汽车,需要显示的调用lock和unlock方法, synchronized 隐式获得释放锁。

    2. ReentrantLock如果获取时间过长会自动释放,synchronized获取不到锁会一直等待

    3. ReentrantLock 是 API 级别的, synchronized 是 JVM 级别的

    4. ReentrantLock 可以实现公平锁、非公平锁,默认非公平锁,synchronized 是非公平锁,且不可更改。

    5. ReentrantLock 通过 Condition 可以绑定多个条件

    6. synchronized适合于并发低的情况,因为synchronized存在锁升级,如果升级为重量级锁将会持续向cpu申请锁资源;ReentrantLock提供了阻塞队列,在高并发的情况下挂起,减少竞争,提高并发能力

    Synchronized的优化

    引入了锁升级机制、自旋锁和自适应自旋、锁消除、锁粗化

    自旋锁与自适应自旋

    在许多应用中,锁定状态只会持续很短的时间,为了这么一点时间去挂起恢复线程,不值得。我们可以让等待线程执行一定次数的循环,在循环中去获取锁。这项技术称为自旋锁,它可以节省系统切换线程的消耗,但仍然要占用处理器。在 JDK1.4.2 中,自选的次数可以通过参数来控制。 JDK 1.6又引入了自适应的自旋锁,不再通过次数来限制,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

    锁消除

    虚拟机在运行时,如果发现一段被锁住的代码中不可能存在共享数据,就会将这个锁清除。

    锁粗化

    当虚拟机检测到有一串零碎的操作都对同一个对象加锁时,会把锁扩展到整个操作序列外部。如StringBuffer 的 append 操作。

    Lock底层原理

    Lock的实现是基于AQS实现的,

    AQS 使用一个被volatile修饰的 int 类型state变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。

    状态信息通过 protected 类型的getState()setState()compareAndSetState() 进行操作

    //返回同步状态的当前值
    protected final int getState() {
            return state;
    }
     // 设置同步状态的值
    protected final void setState(int newState) {
            state = newState;
    }
    //原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
    protected final boolean compareAndSetState(int expect, int update) {
            return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
    

    AQS 定义两种资源共享方式

    • Exclusive(独占):只有一个线程能执行,如ReentrantLock又可分为公平锁和非公平锁:
      • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
      • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
    • Share(共享):多个线程可同时执行,如 CountDownLatchSemaphoreCyclicBarrierReadWriteLock 我们都会在后面讲到。

    所有通过AQS实现功能的类都是通过修改state的状态来操作线程的同步状态。比如在ReentrantLock中,一个锁中只有一个state状态,当state为0时,代表所有线程没有获取锁,当state为1时,代表有线程获取到了锁。通过是否能把state从0设置成1,当然,设置的方式是使用CAS设置,代表一个线程是否获取锁成功。

    volatile的实现原理

    volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障,内存屏障会提供3个功能:

    1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
    2. 它会强制将对缓存的修改操作立即写入主存;
    3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

    CAS的原子性操作如何保证

    是利用CPU原语来实现的,java的方法无法直接访问底层的系统,需要通过native方法来访问,Unsafe类里面的所有CAS方法都是native修饰的,也就是说unsafe类中的方法都直接调用操作系统底层资源执行相应的任务,JVM会帮助我们实现出CAS的汇编指令,这是完全依赖于硬件的功能,在实行的过程中不允许被中断,所以CAS是原子操作

    ThreadLocal

    Thread的核心机制

    每个Thread线程内部都有一个Map。Map里面存储线程本地对象(key)和线程的变量副本(value),但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
    所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

    问题

    由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收

    当线程没有结束,但是ThreadLocal已经被回收,则可能导致线程中存在ThreadLocalMap的键值对,造成内存泄露。

    如何避免泄漏

    为了防止此类情况的出现,我们有两种手段。

    1、使用完线程共享变量后,显示调用ThreadLocalMap.remove方法清除线程共享变量;

    既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

    2、JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了。

  • 相关阅读:
    【编程题】【Scratch四级】2021.06 食堂取餐
    Python pyintsaller打包异常 type object ‘Callable‘ has no attribute ‘_abc_registry‘
    Java面试之数据库面试题
    【数据结构】线性表(一)线性表的定义及其基本操作(顺序表插入、删除、查找、修改)
    8/19 cf思维+马拉车算法
    第66篇 QML 之 详解JS的函数使用
    cnpm安装步骤
    ZigBee 3.0理论教程-通用-1-10:安全加密-应用子层(APS)安全
    【MATLAB第80期】基于MATLAB的结构核岭回归SKRR多输入单输出回归预测及分类预测模型
    PyCharm安装部署(一) 百篇文章学PyQT
  • 原文地址:https://blog.csdn.net/qq_43167873/article/details/127117997