zoukankan      html  css  js  c++  java
  • 锁理论

    一、概述

    1. Java读写锁理论

    锁的种类:

    (1) 独享/共享锁  (广义)

    独占锁:是指该锁一次只能被一个线程锁持有,eg:ReentrantLock、Sychronized;

    共享锁:是指该锁可被多个线程持有,eg: ReentrantReadWriteLock,其读锁是共享锁,其写锁是独占锁;

    (2) 互斥/读写锁  (具体)

    互斥锁:在访问共享资源之前对其进行加锁操作,在访问完成之后进行解锁操作;

        加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前线程解锁,eg: ReentrantLock;

    读写锁:读写锁既是独占锁,又是共享锁,读时共享,写时互斥,eg: ReentrantReadWriteLock;

    (3) 公平/非公平锁

    公平锁:公平锁是指多个线程按照申请锁的顺序来获取锁;

    非公平锁:非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,有可能会造成饥饿现象。

        对于Java ReentrantLock和而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。

        对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的控制线程对锁的获取, 所以并没有任何办法使其变成公平锁。

    (4) 可重入/不可重入锁

    可重入锁又名递归锁,是指同一个线程在外层方法获取锁之后,在进入内层方法会也可以获取锁,

    即:在一个线程中可以多次获取同一把锁。eg: ReentrantLock、ReadWriteLock和Synchronized都是可重入锁。

    可重入锁的一个好处是可一定程度避免死锁,可重入锁加锁和解锁的次数要相等。

    (5) 乐观锁/悲观锁

    乐观锁/悲观锁不是指具体类型的锁,而是看待并发的角度。

    悲观锁: 悲观锁认为存在很多并发更新操作,采取加锁操作,如果不加锁一定会有问题

    乐观锁: 乐观锁认为不存在很多的并发更新操作,不需要加锁。数据库中乐观锁的实现一般采用版本号,Java中可使用CAS实现乐观锁。

    (6) 分段锁

    分段锁是一种锁的设计,并不是一种具体的锁。对于ConcuttentHashMap就是通过分段锁实现高效的并发操作。

    (7) 自旋锁

    自旋锁是指尝试获取锁的线程不会阻塞,而是采用循环的方式尝试获取锁。好处是减少上下文切换,缺点是一直占用CPU资源。

    (8) 偏向锁 / 轻量级锁 / 重量级锁

    这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

    偏向锁: 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。

    轻量级锁: 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

    重量级锁: 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申

    请的线程进入阻塞,性能降低。

    多个线程同时读一个资源类的时候是没有任何问题,所以为了满足并发量,读取共享资源应该同时进行。但是,如果有一个线程想去写共享资源,就不应该有其他线程去读或者写。

    写操作:原子+独占,整个过程必须是一个完整体,中间不能有任何中断。

    读-读共存、读-写不共存、写-读不共存、写-写不共存

    2. 原子性与原子操作

    线程安全性:

      当多个线程访问某个类时,不管运行时环境采用何种调度方式或者着写进程如何交替执行,并且在主调代码中不需要任何额外的同步或者协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

    线程安全主要体现在三个方面:原子性、可见性、有序性。

    2.1 原子性

    原子性提供了互斥访问,同一时刻只能有一个线程对它进行操作。

    所以解决原子性问题的重要条件是:同一时刻只能有一个线程对共享变量进行操作,即互斥。如果我们能够保证对共享变量的修改是互斥的,那么无聊单核CPU还是多核CPU,都能保证原子性。

    以下三种方法都可以保证原子操作:

    (1) Atomic

     Atomic包下提供的类利用CAS保证操作的原子性。

    (2) 原子性锁

    实现锁的两种方式:

    1)synchronized:在作用对象的作用范围内,依赖JVM实现操作的原子性;

    2)Lock:依赖特殊的CPU指令,代码实现,如ReentrantLock(本章不做说明);

    注:子类继承父类时,如果父类中有syncronized修饰的方法,syncronized关键字是不会继承过去的,因为syncronized关键字不属于方法声明的一部分。

    (3) Atomic、Synchronized、Lock对比
    syncronized:不可中断锁,适合竞争不激烈场景,可读性好;

    Lock:可中断锁,多样化同步,竞争激烈时能维持常态,需要大量代码实现;

    Atomic:竞争激烈时能维持常态,比Lock性能好;缺点是一次只能同步一个值,虽然提供了AtomicReference、AtomicReferenceFieldUpdater也只是一次同步一个对象。

    此外,CAS操作也可以保证原子性。

    2.2 可见性

    一个线程对主内存的修改可以及时的被其他线程观察到。

    导致共享变量在线程间不可见的原因:

    a. 线程交叉执行;

    b. 代码重排序结合线程交叉执行;

    c. 共享变量更新后的值没有在工作内存与主内存之间及时更新;

    解决:

    (1) syncronized
        JMM关于syncronized的两条规定:

        1) 线程解锁前,必须把共享变量的最新值刷新到主内存中;

        2) 线程加锁时,将清空工作内存中共享变量的值,从而使得使用共享变量时需要从主内存中重新读取最新的值(注意,加锁与解锁是同一把锁)

        由于syncronized可以保证原子性及可见性,变量只要被syncronized修饰,就可以放心的使用.

    (2) volatile
        通过加入内存屏障和禁止重排序优化来实现可见性。

        具体实现过程:

        1)对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存;

        2)对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量;

    注:volatile不能保证操作的原子性,也就是不能保证线程安全性。

        如果需要使用volatile必须满足以下两个条件:

        1)对变量的写操作不依赖与变量当前的值。

        2)该变量没有包含在具有其他变量的不变的式子中。

        所以volatile修饰的变量适合作为状态标记量。

    2.3 有序性

        Java内存模型中,允许编译器和处理器对指令进行重排序,但重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

        volatile、syncronized、Lock都可保证有序性。

        一系列操作如果无法保证happens-before原则,就说这段操作无法保证有序性。

    happens-before原则:

        1)程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;

        2)锁定规则:一个unLock操作先行发生于后面对同一个锁的Lock()操作;

        3)volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;

        4)传递规则:如果操作A先行发生与操作B,而操作B先行发生于操作C,则操作A先行发生于操作C;

        5)线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作;

        6)线程终端规则:对线程interrupt()方法的调用先行发生与被中断线程的代码检测到中断事件的发生(只有执行了interrupt()方法才可以检测到中断事件的发生);

        7)线程终结规则:线程中所有操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值手段检测到线程已经终止执行;

        8)对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

    总结:

    原子性:Atomic包、CAS算法、synchronized、Lock
    可见性:synchronized、volatile
    有序性:happens-before原则

    此外,在实际的业务过程中也经常用到原子性,只顾过和多线程的原子性有点区别:

    原子性

    原子性是指一个或者多个操作要么全部执行,并且执行的过程中不会被任何因素打断,要么全部都不执行的特性。

    例如账户之间的转账问题,就是一个很经典的原子性问题:A账户向B账户转入100元钱,A账户减去100和B账户加上100就必须具备原子性,这才不会有问题。

    原子操作

    原子操作(atomic operation),是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何的环境切换。

    eg: java中的事物管理

    3. 锁模型

    我们把一段需要互斥执行的代码称为临界区。线程在进入临界区之前,首先尝试加锁,如果成功,则进入临界区,此时我们称这个线程持有锁。否则就等待或者阻塞,直到持有锁的线程释放锁。持有锁的线程执行完临界区的代码后,执行解锁。

    二、使用

    1. ReentrantLock / ReentrantReadWriteLock

    ReentrantLock

    ReentrantLock重入锁和synchronize关键字都是互斥排他锁,,作用是一样的,都是实现锁的功能,同一时间只能有一个线程在执行任务,虽然保证了线程的安全性,但是效率不高。但是ReentrantLock需要手写代码对锁进行获取和释放(一定要在finally里面释放),要不然就永远死锁了。ReentrantLock也可以用来做线程之间的挂起和通知,synchronize一般是使用object的wait和notify来实现,ReentrantLock使用Condition来实现线程之间的通信。

    ReentrantReadWriteLock

    ReentrantReadWriteLock是接口ReadWriteLock的实现类,ReentrantReadWriteLock读写锁是一个读写分离的锁,其读锁是共享锁,其写锁是独占锁,读锁的共享性可保证并发读是非常高效的,读写、写读、写写的过程都是互斥的。

    这种锁主要用于读多写少的业务场景,口诀就是:读读共享、写写互斥、读写互斥。

    示例:
    暂时略,后面在补充......
     
     
     
    参考:
  • 相关阅读:
    FastStone Capture(FSCapture) 注册码
    Qt下开发及调用带界面的DLL
    Gin生成证书开启HTTPS
    Gin+Vue3开启nginx gzip但是不生效。
    GIn+Docker+docer-compose
    Go字符串切片
    Vue使用AG Grid嵌套element-plus
    GIN转换UTC时间
    GORM对实现datetime和date类型时间
    (二)PaddleOCR 编译 ocr_system.dll
  • 原文地址:https://www.cnblogs.com/shiyun32/p/12623783.html
Copyright © 2011-2022 走看看