juc并发编程

JUC

JUC高级类

CompletableFuture:future的功能增强版,减少阻塞与轮询可以传入回调函数当异步任务完成时或发生异常时自动调用回调对象的回调方法

通过四个静态方法来创建一个异步任务

  1. runAsync 无返回值

     public static CompletableFuture<Void> runAsync(Runnable runnable)
           // void也是一个类
      public static CompletableFuture<Void> runAsync(Runnable runnable,     Executor executor)
    
  2. supplyAsync 有返回值

     public static <U> CompletableFuture<U>supplyAsync(Supplier<U> supplier)
         
      public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,   Executor executor) 
    

接口CompletionStage:

代表异步计算过程中的某一个阶段一个阶段结束后可能触发另一个阶段

一个阶段的计算执行可以是function,consumer,runnable,一个阶段的执行可能是被单个阶段的完成触发也可能是由多个阶段一起触发
在这里插入图片描述

completableFuture常用方法

  1. 获得结果和主动计算:

    • 获取结果【get(),join(),getNow(T valuelfAbsent):[计算为完成返回默认值] 注:join与get的功能一样其主要区别子在于get要抛出异常,join不会】

    • 主动触发计算:complete(T value):[是否打断get方法立即返回括号里面的值]

  2. 计算结果进行处理

    • thenApply:计算结果存在依赖关系,这两个线程串行化,存在依赖关系(当前步错,不走下一步)直接叫停
    • handle:有异常也可以往下一步走,根据带的异常参数进行下一步处理
  3. 对计算结果进行消费:接受任务的处理结果并消费处理,无返回结果

    • thenAccept
    • 没有传入自定义线程池都是用默认线程池ForkJoinPool,
    • 如果执行第一个任务的时候,传入了一个自定义线程池:调用thenRun方法执行第二个任务时,则第二个任务和第一个任务是共用一个线程池,调用thenRunAsync执行第二个任务时,则第一个任务使用的是自己传入的线程池,第二个任务使用的是ForkJoin线程池

5.对计算速度进行选用

  • 谁快用谁 applyToEither

6.对计算结果进行合并

两个completionStage任务都完成后最终能把两个任务结果一起交给thenCombine来处理先完成的先等着,等待其他分支任务

  • thenCombine

java锁

//悲观锁
public synchronized  void m1(){
    //枷锁后的逻辑
}
//悲观锁
private ReentrantLock Lock=new ReentrantLock();
public  void m2(){
    Lock.lock();
    try {

    }finally {
        Lock.unlock();
    }
}
//乐观锁,保证多个线程使用的是同一个AtomicInteger
private AtomicInteger atomicInteger=new AtomicInteger();

public void m3(){
    atomicInteger.incrementAndGet();  
}

synchronized

当一个线程访问了资源类时其他线程将在其结束之前无法访问,synchronized锁的是当前对象所有加了synchronized关键字的方法,在没有加synchronized的方法照样执行,对于静态同步方法加的是类锁,对于同步方法块锁的synchronized括号内的对象,类锁与对象锁互不干涉

高并发时,同步调用应该去考虑锁的性能损耗,能用无锁数据结构,就不要用锁,能锁区块就不要锁整个方法,能锁对象就不要锁类

同步代码块:

在反编译文件中synchronized同步代码块使用的是monitorenter和monitorexit指令来保证代码正常运行,一般情况下是一个enter配两个exit(防止synchronized锁的代码块对象中还有锁对象),但当在同步代码块中抛出异常时就是一对一关系

普通同步方法:

调用指令将会检查方法的ACC_SYNCHRONIZEDF访问标志是否被设置如果设置了执行线程会将先持有monitor锁,然后执行方法,最后子啊方法完成(无论是正常还是非正常)时释放monitor

静态同步方法:

静态同步方法在flags标志上会多加一个ACC_STATIC,其他与普通方法一致,可以识别去找类还是找对象

每个java对象天生都带着一个对象监视器,每一个被锁住的对象都会和Monitor关联起来

问:为什么会有公平锁与非公平锁的设计?为什么默认非公平锁?

恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但从cpu的角度看,时间差存在十分明显,所以非公平锁更好的利用好CPU的时间片,尽量减少CPU空闲状态时间

使用多线程很重要的考量点是线程的切换开销,采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态所以刚释放锁的线程在此刻再次获取同步状态的几率就会变得非常大,所以就采用了线程的开销

在java中ReentrantLock和synchronize都是可重入锁其优点是一定程度避免死锁

synchronize的重入的实现机理:每个锁对象拥有一各锁计数器和一个指向持有该锁的线程的指针

LockSupport与线程中断

java中断机制:一个线程不应该有其他线程来强制中断或停止,中断是一种协商机制,java中中断线程需要手动调用interrupt方法进行标志位设置

如何停止中断运行中的线程:

  • 通过一个volatile变量实现
/*
将自己线程的结束掌握在自己手中

*/
public class Instance { 
    private static volatile  boolean isStop=false;
    public static void main(String[] args) {
        new Thread(()->{
while (true){

    if (isStop){
            System.out.println("t1结束执行");
    break;
    }}
            System.out.println("t1执行");
        }).start();

        new Thread(()->{
            System.out.println("休息1s");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
             //atomicBoolean.set(true);
            isStop=true;
            System.out.println("t2执行");
        }).start();
    }
}

  • 通过AtomicBoolean
 private static AtomicBoolean atomicBoolean=new AtomicBoolean();

if (atomicBoolean.get()){
            System.out.println("t1结束执行");
    break;
    }
//设置true见上方代码

  • 通过Thread类自带的中断api实例方法实现
/**
 *  public void interrupt() 设置线程的中断状态为true,发起一个协商而不会立刻停止线程
 *  public boolean isInterrupted() 实例方法判断当前线程是否被中断(获取中断标志位)
 *
 * */
public class Instance {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("t1结束执行");
                    break;
                }
            }
            System.out.println("t1执行");
        }, "t1");
        thread.start();
        new Thread(()->{
            System.out.println("休息1s");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            //atomicBoolean.set(true);
            thread.interrupt();
            System.out.println("t2执行");
        },"t2").start();

    }
}

在一个线程调用interrupt()时只会将标志位设置为true并不会中断线程

如果线程处于被阻塞状态(sleep,wait,join等状态)在别的线程中调用当前线程对象的interrupt方法那么线程将立即退出被阻塞状态,并抛出一个InteeeuptedException异常

public class Instance {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(10000);//该处抛出异常后中断标志位重置为false导致无线循环
                System.out.println("等待结束");
            } catch (InterruptedException e) {
                //Thread.currentThread().interrupt()  没有他程序不停止,但可以通过抛异常的方式处理
                throw new RuntimeException(e);
            }
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("t1结束执行");
                    break;
                }
            }
            System.out.println("t1执行");
        }, "t1");
        thread.start();

        Thread thread1 = new Thread(() -> {
            System.out.println("休息1s");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
            //atomicBoolean.set(true);
            thread.interrupt();
            System.out.println("t2执行");
        }, "t2");
        thread1.start();

    }
}

在这里插入图片描述

静态方法Thread.interrupted():

  1. 返回当前线程的中断状态,测试当前线程是否已被中断
  2. 将当前线程的中断状态清零并重新设为false,清除线程的中断状态
  3. 如果连续两次调用此方法这第二次调用将返回false,因为连续调用两次的结果可能不一样

lockSupport

作用:用来创建锁和同步类的基本线程阻塞原语(为线程等待唤醒机制的另一种提升)

普通等待唤醒机制需要用synchronized同步代码块进行包裹,并且必须遵守先wait再notify不然程序会被直接卡死

condition接口中的await和signal方法实现线程的等待唤醒:使用该唤醒机制需要将await和signal包裹在lock中,且不能交换等待唤醒顺序。

lockSupport:使用一种名为Permit的概念来做到阻塞和唤醒的功能,每个线程都有一个许可(Permit),且其累加上线是一,其底层调用的是Unsafe类中的native代码

/*
lockSupport在不加锁块的前提下可以执行,且线程之间不存在先后关系
但lock之间的许可证只有一个,就是说只能使用一次加解锁方法
*/
public class Instance {
    public static void main(String[] args) {
        Thread hello = new Thread(() -> {
            System.out.println("park执行");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            LockSupport.park();
            System.out.println("hello");
        });
        hello.start();
        Thread word = new Thread(() -> {
            LockSupport.unpark(hello);
            System.out.println("word");
        });
        word.start();

    }
}

内存模型JMM

JVM视图定义一种java内存模型来屏蔽各种硬件和操作系统的内存访问差异以实现让java程序在各种平台下都能达到一致的内存访问效果

JMM定义

一种抽象的概念模型,并不是真实存在,它仅仅是一组约定或规范,通过这组规范定义了程序中各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术都是围绕多线程的原子性,可见性和有序性展开

JMM作用:1.实现线程和主内存之间的抽象关系,2.屏蔽各个硬件平台和操作系统的内存访问差异以实现让java程序在各种平台上都能达到一致的内存访问效果

JMM三大特性

可见性在这里插入图片描述

有序性
在这里插入图片描述

volatile内存语义:

当写一个变量时,JMM会把线程对应的本地内存中的共享变量值立即刷新回主内存中,当读取一个volatile变量时JMM会把该线程对应的本地内存设置为无效,重新回到主内存中读取最新的共享变量,所以volatile的写内存语义直接刷新到主内存中,读的内存语义是直接从主内存中读取

内存屏障:

概念:一类同步屏障指令,从CPU或编译器在在对内存访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作,避免代码重排序,内存屏障其实就是一种jvm指令,java内存模型的重排规则会要求java编译器在生成jvm指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了java内存模型中的可见性和有序性,但volatile无法保证原子性

读屏障:在读指令之前插入读屏障,让工作内存或CPU高速缓存当中的缓存数据失效,重新回到主内存中获取最新数据

写屏障:在写指令之后插入写屏障,强制把写缓冲区的数据刷新回到主内存中

happens-before之volatile变量规则:
在这里插入图片描述

java线程执行流程:
在这里插入图片描述

  1. read:作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
  2. load:作用于工作内存,将read从主内存传输的变量值放入工作内存变量副本中,即加载数据
  3. use:作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作
  4. assign:作用于工作内存,将从执行引擎接受到的值赋值给工作内存变量,每当JVM遇到需要该变量的字节码指令时会执行该操作
  5. store:作用于工作内存,将赋值完毕的工作变量的值写会给主内存
  6. write:作用于主内存:将store传输来的变量赋值给主内存中的变量

指令重排:
在这里插入图片描述

CAS

概念:原子类(compare and swap)

操作数:内存位置,预期原值,更新值

在执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置更新为新值,如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功

在这里插入图片描述

CAS是jdk提供的非阻塞原子性的操作,通过硬件保证类硬件更新的原子性,是一条CPU的原子指令,不会造成数据的不一致问题,Unsafe提供的CAS方法底层实现即为CPU指令的cmpxchg

Unsafe类

CAS的核心类由于java无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门基于该类可以直接操作特定的内存数据,Unsafe类存在于sun.misc包中,其内部方法可以像C的指针一样操作直接操作内,java中的CAS操作的执行依赖于Unsafe类中的方法

: Unsafe类中的方法都是native修饰的,Unsafe中的方法都直接调用都直接调用操作系统底层的资源执行相应任务

原子引用: AtomicReference<> 将普通对象封装为原子类

自旋锁(Spinlock)

CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,自旋锁指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当线程被占用时,会不断循环判断锁的状态直到获取,这样的好处在于减少线程上下文切换的消耗缺点是循环会消耗CPU

/*
* 实现一个自旋锁,相当于占坑后必须为空才能另一个人进,一次保证每次访问都是一个线程
* */
public class Instance { 
    private static  int age;
 AtomicReference<Thread> atomicReference= new AtomicReference<>();

 public void  lock(){
     //获取当前线程
    while (!atomicReference.compareAndSet(null,Thread.currentThread()));

 }
 public void unlock(){
  atomicReference.compareAndSet(Thread.currentThread(),null);
 }
    public static void main(String[] args) {
        Instance instance = new Instance();

        new Thread(()->{
         while (age<1000){
            instance.lock();
             try {
                 Thread.sleep(10);
             } catch (InterruptedException e) {
                 throw new RuntimeException(e);
             }
             age++;
             //此处打印为什么要在锁类不然不会出现顺序相加
             System.out.println("1执行"+age);
         instance.unlock();
        }
     }).start();

        new Thread(()->{
            while (age<1000){
                instance.lock();
            age++;
                 //此处打印为什么要在锁类不然不会出现顺序相加
                System.out.println("2执行"+age);
            instance.unlock();
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
              }
        }).start();
    }
}

CAS缺点:

  • 循环时间长导致CPU开销大
  • ABA问题:描述的是一个变量v,它的值经历A -> B ->A的变化,导致看起来好像没有变一样,其实,第二次出现的A已经不是第一次出现的A了,是被修改过的。(你的对手在磨刀,隔壁老王在练腰),可以使用版本号(戳级流水)来规避ABA问题 (AtomicStampedReference类)

原子操作类:java.util.concurrent.atomic

volatile解决多线程内存不可见问题对于一写多读,是可以解决变量同步问题,但是多写,同样无法解决线程安全,jdk8后推荐使用LongAdder对象,比AtomicLong性能更好(减少乐观锁重试次数)
在这里插入图片描述

CountDownLatch:

CountDownLatch有一个正数计数器,countDown()方法对计数器做减操作,await()方法等待计数器达到0。所有await的线程都会阻塞直到计数器为0或者等待线程中断或者超时

原子操作增强类原理:(出现于java8)

  • DoubleAccumulator()
  • DoubleAdder()
  • LongAccumulator()
  • LongAdder()

LongAdder

longAdder是Striped64的子类而striped64有继承了Number类,使用于高并发极大的提高了性能
在这里插入图片描述

longAdder的基本思想是分散热点,将value值分散到一个cell数组中,不同线程会命中到数组的不同槽中各个线程只对自己槽中的那个值进行CAS操作,这样热点就分散了,冲突的概率就小很多,如果要获取真正的long值,只要将各个槽中的变量值累加返回

longAdder扩容机制:

public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[getProbe() & m]) == null ||
            !(uncontended = a.cas(v = a.value, v + x)))
            longAccumulate(x, null, uncontended);
    }
}
  1. 如果Cells表为空尝试在CAS更新base字段,成功则退出
  2. 如果Cells表为空,CAS更新base字段失败,出现竞争,uncontended为true调用LongAccumulate
  3. 如果Cells表为非空,当前线程的槽为空,uncontended为true调用lognAccumulate
  4. 如果Cells表非空,且前线程映射的槽为非空,CAS更新Cell的值,成功调用则返回,否则uncontended设为false调用longAccumulate

ThreadLocal

作用:实现每个线程都有自己专属的副本,解决额那个每个线程绑定自己的值通过get()和set()方法,获取默认值或将其值更改为当前线程所存在的副本的值从而避免线程安全问题

jvm内部维护了一个线程版的Map<ThreadLocal,value>(通过threadLocal对象的set方法,结果把ThreadLocal对象当做key放进threadLocal中,每个线程要用到这个的时候,用当前的线程去Map里面获取,通过这样的方式每个线程都拥有自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量

threadLocal内存泄漏

虚引用(PhantomReference):

  1. 必须和引用对列(ReferenceQueue)联合使用,形同虚设,与其他引用不同,虚引用不会决定对象的生命周期,如果一个对象仅持有一个虚引用,那么他和没有引用是一样的,在任何时候都可能被垃圾回收器回收,不能单独使用,也不能通过他访问对象
  2. 虚引用的get方法总是返回null,无法返回访问对应的引用对象,其主要作用是跟踪对象被垃圾回收的状态,仅仅提供了一种确保对象被finalize以后做某些事情的通知机制,
  3. 目的:设置虚引用的对象的唯一目的就是在这个对象在被收集器回收的时候,收到一个系统通知,或者后续添加进一步的处理,用来实现比finalize机制更加灵活的回收操作
    在这里插入图片描述

threadLocal使用弱引用原理:

其内部线程实体是使用键值对的形式存储,若key引用是弱引用则大概率减少内存泄漏问题,使用弱引用可以使ThreadLocal对象子啊方法执行完毕后顺利被回收且Entry的key引用指向为null,并且在threadLocal生命周期里面,针对threadLocal存在的内存泄漏问题都会通过expungeStateEntry,cleanSomeSlots,replaceStateEntry则三个方法清理掉key为null的脏entry

对象的布局

  • 对象头(占8个字节,类型指针占8个字节共16个字节)

    1. 对象标记
    2. 内元信息(对象指针)
      在这里插入图片描述
  • 实例数据:存放类的属性(Field)数据信息,包括父类的属性信息

  • 对齐填充:虚拟机要求对象的起始地址必须是8字节的整数倍

synchronize锁升级

java中的每个对象都可以成为一个锁

Monitor:可以理解为一种同步工具,或者同步机制,java对象是天生的Monitor,都有成为Monitor的潜质,在 java的设计中每个对象自创造出来就带了一把看不见的锁,叫做内部锁或者Monitor锁,而Monitor的本质是依赖于底层操作系统的Mutex Lock实现操作系统实现线程之间的切换需要从用户态到内核态的转化,成本很高

轻量级锁:

偏向锁:单线程竞争:当线程A第一次竞争锁时,通过操作修改Mark work中的偏向进程ID,偏向模式,如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步【当一段代码被同一个线程多次访问由于只有一个线程,那么线程在后续访问时便会自动获得锁】

/*
偏向锁开启默认在虚拟机默认延迟4s之后
*/
//开启偏向锁:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
    //关闭偏向锁
-XX:-UseBiasedLocking -XX:BiasedLockingStartupDelay=0
    //将偏向锁延时设置为0,可以立即启动偏向锁功能,不然启动jvm后让线程先睡4m
    -XX:BiasedLockingStartupDelay=0
        

偏向锁的撤销:

偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来的线程才会被释放。撤销需要等待全局安全点(该时间点上没有字节码正在执行,同时检查持有的偏向锁的线程是否还在执行)

  • 第一个线程正在执行synchronize方法(处于同步块),它还没有执行完,其他线程来抢夺,该偏向锁会被取消并出现锁升级,此时轻量级锁由原持有偏向锁的线程持有,继续执行同步代码,而正在竞争的线程会进入到自旋等待获得该轻量级锁
  • 第一个线程执行完成synchronized方法(退出同步块),则将对象头设置为无锁状态并撤销偏向锁,重新偏向
    在这里插入图片描述

轻量级锁

概念:多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻塞

作用:有线程来参与竞争,但是获取锁的冲突时间极短,本质就是自旋锁CAS
在这里插入图片描述

轻量级加锁
在这里插入图片描述

轻量级锁的释放

在释放锁时,当前线程会使用CAS操作将Displaced Mark Word的内容复制回锁的Mark Work里面,如果没有发生竞争,那么这个复制的操作会成功,如果有其他线程因为自旋次数导致轻量级锁升级成了重量级锁,那么CAS操作会失败此时会释放锁并唤醒被阻塞的线程

关闭偏向锁就可以直接进入轻量级锁

重量级锁

原理:java中synchronized的重量级锁,是基于进入和退出Monitor对象实现的,子啊编译时会将同步代码块的开始位置插入monitor enter指令在结束位置插入monitor exit指令。

当线程执行到monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了锁。会在Monitor的owner中存放当前线程的id,这样他将处于锁定状态,除非退出同步代码块,否则其他线程无法获取到这个Monitor

总结

在这里插入图片描述
在这里插入图片描述

锁的使用场景:

偏向锁:适用于单线程,在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁

轻量级锁:适用于竞争不激烈的情况,采用轻量级(和乐观锁使用范围类似) 存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是对比使用轻量级锁还是很高效的

重量级锁:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长那么使用轻量级锁自旋带来的性能消耗就会比使用重量级锁更加严重,这时候需要升级为重量级锁

jit编译器对锁的优化

JIT:(Just In Time Compiler) 即时编译器

锁消除:每个线程锁的对象不一致

锁粗化:同一个线程中多个synchronized内部的代码进行合并为一个synchronized代码块

没有锁:自由自在;偏向锁:唯我独尊;轻量级锁:楚汉争霸;重量级锁:群雄逐鹿

AQS

概念:AbstractQueuedSynchronizer,。AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,通过FIFO和state保证锁的分配和相关的同步线程的管理

作用:是用来实现锁或者其他同步器组件的公共基础的抽象实现,是重量级基础框架及整个JUC体系的基石,主要用于解决锁分配给"谁"的问题

在这里插入图片描述

CLH: Craig,Landin and Hagersten对列,AQS中的对列是CLH变体的虚拟双向对列FIFO

锁和同步器的关系

  • 锁:面向对象的使用者:定义类程序员和锁交互的使用层API,隐藏了实现的细节,调用即可
  • 同步器:面向锁的实现者:DougLee提出统一规范并简化锁的实现,将其抽象出来,屏蔽了同步状态管理,同步对列的管理和维护,阻塞线程排队和通知、唤醒机制等,是一切锁和同步组件实现的-----公共基础部分

AQS使用一volatile的int类型的成员变量来表示同步状态,通过内置的FIFO对列来完成资源的排队工作将每条将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值得修改
在这里插入图片描述

实现公共与非公平锁的逻辑在AQS层面都是调用了acquire方法


/*
tryAcquire(arg),尝试抢锁是否成功,当抢锁失败则执行排队操作
addWaiter 创建一个入队的节点按照当前线程给定的模式
*/
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

读写锁

排序: 无锁—>独占锁—>读写锁—>邮戳锁

读写锁定义:一个资源能够被多个读线程访问或者被一个写线程访问,但是不能同时存在读写线程(一体两面,读写互斥,读读共享)

锁降级: ReentrantReadWriteLock锁降级:将写入锁降级为读锁,锁的严苛程度变强叫做升级,反之降级,

特性:

  1. 公平性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
  2. 重进入:该锁支持重进入,以读锁为例:读线程在获取类读锁之后,能够再次获取读锁,而写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁
  3. 锁降级:遵循获取写锁,获取读锁在释放写锁的次序,写锁能够降级为读锁

写锁的降级策略

  • 如果同一个线程持有了写锁,在没有释放写锁的情况下,他是可以继续获得读锁,这就是写锁的降级,降级为读锁
  • 规则惯例,先获取写锁,然后获取读锁,在释放写锁的次序
  • 如果释放了写锁,那么就完全转换为读锁
  • 读没有完成时候写锁无法获得锁,必须要等读锁读完后才有机会
  • 如果有线程子啊读,那么写线程是无法获取写锁的,是悲观锁的策略

在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该写锁线程会被阻塞,所以需要释放所有读锁,才可以获取写锁
在这里插入图片描述

锁降级策略
在这里插入图片描述

StampedLock锁

概念:代表锁的状态,当stamp返回零时,表示线程获取锁失败,并且当释放锁或者转换锁的时候,都要传入最初获取的stamp值

锁饥饿问题:

ReentrantReadWriteLock实现了读写分离,但是一旦读操作多得时候,想要获取写锁就变得比较困难,当前可能会一直存在读锁而无法获得写锁
在这里插入图片描述
特点:

  • 获取锁的方法都会返回一个邮戳(stamp),stamp为零表示获取失败,其余表示成功
  • 释放锁的方法都需要一个邮戳(stamp),这个stamp必须是和成功获取锁时得到的stamp一致
  • stampedLock是不可重入锁,(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
  • stampedLock的三种访问模式:
    1. reading(读模式悲观):功能和ReentrantReadWriteLock的读锁类似
    2. writing(写模式):功能和ReentrantReadWriteLock的读锁类似
    3. Oprimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,认为在读取时没人修改,假如被修改再实现升级为悲观读模式

缺点:

  • stampedLock不支持重入,没有Re开头
  • stampedLock的悲观读模式和写锁都不支持条件变量(Condition)
  • 使用StampedLock一定不要调用中断,即不要调用interrupt()方法
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值