JUC并发编程(高级)

目录

1.管程Monitor(监视器),也就是我们平时说的锁

2.守护线程

3.CompletableFuture优点总结

join和get对比

4.CompletableFuture常用API

5.乐观锁和悲观锁

6.8锁案例

ObjectMonitor.cpp中引入了头文件(include)objectMonitor.hpp

7.公平锁vs非公平锁

8.可重入锁

9.LockSupport与线程中断

Object和Condition使用的限制条件

10.Java内存模型之JMM

11.JMM规范下,三大特性

原子性

可见性

有序性是什么?

12.happens-before总原则

13.volatile

14.内存屏障

粗分两种

保证可见性

 不保证原子性

面试回答

禁止指令重排

 面试

15.CAS

1 循环时间长开销很大

16.原子操作类之18罗汉增强

引用类型原子类

面试

LongAdder为什么这么快

为啥在并发情况下sum的值不精确?

17.ThreadLocal

18.Java对象内存布局和对象头

19.Synchronized与锁升级

 偏向锁

轻锁

主要作用

重锁

20.AQS

 21.ReentrantLock、ReentrantReadWriteLock、StampedLock

1.3 为什么要锁降级?

2.4 锁饥饿问题

2. 邮戳锁StampedLock

2.2 StampedLock总结

22.附加

 1. wait和sleep

2.用户线程和守护线程

3.synchronized和Lock两者差异

4.虚假唤醒

5.JUC强大辅助类

减少计数CountDownLatch

循环栅栏 CyclicBarrier

信号灯 Semaphore

Fork 与 Join分支


1.管程
Monitor(监视器),也就是我们平时说的锁

管程:Monitor(监视器),也就是我们平时说的锁。监视器锁

信号量及其操作原语“封装”在一个对象内部)管程实现了在一个时间点,最多只有一个线程在执行管程的某个子程序。 管程提供了一种机制,管程可以看做一个软件模块,它是将共享的变量和对于这些共享变量的操作封装起来,形成一个具有一定接口的功能模块,进程可以调用管程来实现进程级别的并发控制。

执行线程就要求先成功持有管程,然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管理。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。

2.守护线程

是一种特殊的线程,为其他线程服务的,在后台默默地完成一些系统性的服务,比如垃圾回收线程

        守护线程作为一个服务线程,没有服务对象就没有必要继续运行了,如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可退出了。假如当系统只剩下守护线程的时候,java虚拟机会自动退出

异步多线程任务执行且有返回结果,三个特点:多线程/有返回/异步任务(班长作为老师去买水作为新启动的异步多线程任务且买到水有结果返回)

3.CompletableFuture优点总结

  • 异步任务结束时,会自动回调某个对象的方法;

  • 主线程设置好毁掉后,不再关心异步任务的执行,异步任务之间可以顺序执行

  • 异步任务出错时,会自动回调某个对象的方法。、

join和get对比

  • 功能几乎一样,区别在于编码时是否需要抛出异常

    • get()方法需要抛出异常

    • join()方法不需要抛出异常

利用核心的四个静态方法创建一个异步操作 | 不建议用new

关键就是 |有没有返回值|是否用了线程池|

参数说明:

没有指定Executor的方法,直接使用默认的ForkJoinPool.commPool()作为它的线程池执行异步代码。

如果指定线程池,则使用我们定义的或者特别指定的线程池执行异步代码。

4.CompletableFuture常用API

补充:Code之任务之间的顺序执行

thenRun

thenRun(Runnable runnable)

任务A执行完执行B,并且B不需要A的结果

thenAccept

thenAccept(Consumer action)

任务A执行完执行B,B需要A的结果,但是任务B无返回值

thenApply

thenApply(Function fn)

任务A执行完执行B,B需要A的结果,同时任务B有返回值
 

5.乐观锁和悲观锁

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

  • 悲观锁的实现方式

    • synchronized关键字

    • Lock的实现类都是悲观锁

乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作

乐观锁的实现方式

版本号机制Version。(只要有人提交了就会修改版本号,可以解决ABA问题)

ABA问题:再CAS中想读取一个值A,想把值A变为C,不能保证读取时的A就是赋值时的A,中间可能有个线程将A变为B再变为A。

6.8锁案例

  • 作用域实例方法,当前实例加锁,进入同步代码块前要获得当前实例的锁。

  • 作用于代码块,对括号里配置的对象加锁。

  • 作用于静态方法,当前类加锁,进去同步代码前要获得当前类对象的锁

大厂:为什么任何一个对象都可以成为一个锁?
溯源

Java Object 类是所有类的父类,也就是说 Java 的所有类都继承了 Object,子类可以使用 Object 的所有方法。

ObjectMonitor.java→ObjectMonitor.cpp→objectMonitor.hpp

ObjectMonitor.cpp中引入了头文件(include)objectMonitor.hpp

7.公平锁vs非公平锁

非公平锁
默认是非公平锁

非公平锁可以插队,买卖票不均匀。

是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转或饥饿的状态(某个线程一直得不到锁)

公平锁
ReentrantLock lock = new ReentrantLock(true);

买卖票一开始a占优,后面a b c a b c a b c均匀分布

是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买后来的人在队尾排着,这是公平的。

为什么会有公平锁/非公平锁的设计?为什么默认是非公平?
恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU 的时间片,尽量减少 CPU 空闲状态时间。

使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。

8.可重入锁

可重入锁又名递归锁

是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。

9.LockSupport与线程中断

 

 

  • wait和notify方法必须要在同步块或者方法里面,且成对出现使用

  • 先wait后notify才OK,顺序

Object和Condition使用的限制条件

  • 总结

    • 线程先要获得并持有锁,必须在锁块(synchronized或lock)中

    • 必须要先等待后唤醒,线程才能够被唤醒

Lock Support是用来创建锁和其他同步类的基本线程阻塞原语。
Lock Support是一个线程阻塞工具类, 所有的方法都是静态方法, 可以让线程在任意位置阻塞, 阻塞之后也有对应的唤醒方法。归根结底, Lock Support调用的Unsafe中的native代码。

Lock Support提供park() 和unpark() 方法实现阻塞线程和解除线程阻塞的过程
Lock Support和每个使用它的线程都有一个许可(permit) 关联。
每个线程都有一个相关的permit, permit最多只有一个, 重复调用un park也不会积累凭证。

10.Java内存模型之JMM

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

  • 原则:

JMM的关键技术点都是围绕多线程的原子性可见性有序性展开的

  • 能干嘛?

1 通过JMM来实现线程和主内存之间的抽象关系。

2 屏蔽各个硬件平台操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的内存访问效果。

11.JMM规范下,三大特性

原子性

  • 指一个操作是不可中断的,即多线程环境下,操作不能被其他线程干扰

可见性

  • 可见性

    • 是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道该变更 ,JMM规定了所有的变量都存储在主内存中。
  • Java中普通的共享变量不保证可见性,因为数据修改被写入内存的时机是不确定的,多线程并发下很可能出现"脏读",所以每个线程都有自己的工作内存,线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等 )都必需在线程自己的工作内存中进行,而不能够直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成

有序性是什么?

  • 对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。但为了提供性能,编译器和处理器通常会对指令序列进行重新排序。Java规范规定JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序化执行的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。
  • 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致
    处理器在进行重排序时必须要考虑指令之间的数据依赖性

    多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测

12.happens-before总原则

如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

两个操作之间存在happens-before关系,并不一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法

13.volatile

volatile的内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量立即刷新回到主内存中。

当读一个volatile变量时,JMM会把该线程对应的工作内存设置为无效,直接从主内存中读取共享变量。

所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。

14.内存屏障

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

内存屏障之前的所有写操作都要回写到主内存,
内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。

粗分两种

写屏障

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

读屏障

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

保证可见性

使用volatile修饰共享变量,就可以达到上面的效果,被volatile修改的变量有以下特点:

  1. 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值 ,然后将其复制到工作内存

  2. 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存

 volatile变量的读写过程

 不保证原子性

  • 对于volatile变量具备可见性 ,JVM只是保证从主内存加载到线程工作内存的值是最新的,也仅是数据加载时是最新的。但是多线程环境下,“数据计算”和“数据赋值”操作可能多次出现,若数据在加载之后,若主内存volatile修饰变量发生修改之后,线程工作内存中的操作将会作废去读主内存最新值,操作出现写丢失问题。即各线程私有内存和主内存公共内存中变量不同步 ,进而导致数据不一致。由此可见volatile解决的是变量读取时的可见性问题,但无法保证原子性,对于多线程修改主内存共享变量的场景必须使用加锁同步。

面试回答

对于volatile变量,JVM只是保证从主内存加载到线程工作内存的值是最新的,也只是数据加载时是最新的。如果第二个线程在第一个线程读取旧值写回新值期间读取i的阈值,也就造成了线程安全问题。

禁止指令重排

编译器和处理器在重排序时,会遵守数据依赖性,不会改变存在依赖关系的两个操作的执行,但不同处理器和不同线程之间的数据性不会被编译器和处理器考虑,其只会作用于单处理器和单线程环境,下面三种情况,只要重排序两个操作的执行顺序,程序的执行结果就会被改变。

 面试

3句话总结

volatile写之前的的操作,都禁止重排到volatile之后

volatile读之后的操作,都禁止重排到volatile之前

volatile写之后volatile读,禁止重排序

15.CAS

compare and swap的缩写,中文翻译成比较并交换,实现并发算法时常用到的一种技术。它包含三个操作数——内存位置、预期原值及更新值。

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

1 Unsafe

CAS这个理念 ,落地就是Unsafe类

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

2 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。

3 变量value用volatile修饰

我们知道i++线程不安全的,那atomicInteger.getAndIncrement()
CAS的全称为Compare-And-Swap,它是一条CPU并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令 。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语 ,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
CAS缺点

1 循环时间长开销很大

2 引出来ABA问题
什么是ABA问题

CAS会导致“ABA问题”。

CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。

比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,

然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。

尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。

如何解决

AtomicStampedReference版本号 

16.原子操作类之18罗汉增强

引用类型原子类

`AtomicReference` 可以带泛型(前面讲过)

`AtomicReference<xxx> `*

   `AtomicStampedReference` 带版本号以防CAS中的ABA问题(前面讲过)

携带版本号的引用类型原子类,可以解决ABA问题。解决修改过几次的问题。*

   `AtomicMarkableReference`类似于上面的 ,但解决**一次性**问题

构造方法`AtomicMarkableReference(V initialRef, boolean initialMark)`

原子更新带有标记位的引用类型对象

解决是否修改过,它的定义就是将`状态戳`**简化**为`true|false`,类似一次性筷子

面试

面试官问你:你在哪里用了volatile?

在AtomicReferenceFieldUpdater中,因为是规定好的必须由volatile修饰的

还有的话之前我们在DCL单例中,也用了volatile保证了可见性

LongAdder为什么这么快

其实在小并发下情况差不多;但在高并发情况下,在AtomicLong中,等待的线程会不停的自旋,导致效率比较低;而LongAddercell[]分了几个块出来,最后统计总的结果值(base+所有的cell值),分散热点

一句话

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

为啥在并发情况下sum的值不精确?

sum执行时,并没有限制对base和cells的更新(一句要命的话)。所以LongAdder不是强一致性的,它是最终一致性的。

首先,最终返回的sum局部变量,初始被复制为base,而最终返回时,很可能base已经被更新了 ,而此时局部变量sum不会更新,造成不一致。
其次,这里对cell的读取也无法保证是最后一次写入的值。所以,sum方法在没有并发的情况下,可以获得正确的结果。
 

17.ThreadLocal

ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。 ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。

 

 为什么源代码用弱引用?
当function01方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象
若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;
若这个key引用是弱引用,就大概率会减少内存泄漏的问题(还有一个key为null的雷,后面讲)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。

当我们为threadLocal变量赋值,实际上就是当前的Entry(threadLocal实例为key,值为value)往这个threadLocalMap中存放。Entry中的key是弱引用,当threadLocal外部强引用被置为null(tl=null),那么系统 GC 的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(这个tl就不会被干掉),这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

当然,如果当前thread运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。

但在实际使用中我们有时候会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们小心


18.Java对象内存布局和对象头

 

 

 

19.Synchronized与锁升级

 
偏向锁

当线程A第一次竞争到锁时,通过修改Mark Word中的偏向ID、偏向模式。如果不存在其他线程竞争,那么持有偏向锁的线程将永远不需要进行同步。

主要作用
当一段同步代码一直被同一个线程多次访问,由于只有一个线程访问那么该线程在后续访问时便会自动获得锁。

理论落地:

在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用他的线程拥有,这个线程就是锁的偏向线程。

那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接会去检查锁的MarkWord里面是不是放的自己的线程ID)。

如果相等,表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程[D与当前线程1D是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

如果不等,表示发生了竞争,锁己经不是总是偏向于同一个线程了,这个时候会尝试使用CAS来替换MarkWord里面的线程ID为新线程的ID,

竞争成功,表示之前的线程不存在了,MarkWord里面的线程1D为新线程的ID,锁不会升级,仍然为偏向锁;

竞争失败,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。

注意,偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。技术实现:

一个synchronized方法被一个线程抢到了锁时,那这个方法所在的对象就会在其所在的Mark Word中将偏向锁修改状态位,同时还会占用前54位来存储县城指针作为标识。若该线程再次访问同一个synchronized方法时,该线程只需要去对象头的Mark Word中去判断一下是否有偏向锁指向本身的ID,无需再进入Monitor去竞争对象了

轻锁

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

主要作用

  • 有线程来参与锁的竞争,但是获取锁的冲突时间极短

  • 本质就是自选锁CAS

轻量级锁的加锁

JVM会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,官方成为Displaced Mark Word。若一个线程获得锁时发现是轻量级锁,会把锁的MarkWord复制到自己的Displaced Mark Word里面。然后线程尝试用CAS将锁的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。
 

重锁

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

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

在无锁状态下,Mark Word中可以存储对象的identity hash code值。当对象的hashCode()方法第一次被调用时,JVM会生成对应的identity hash code值并将该值存储到Mark Word中。

对于偏向锁,在线程获取偏向锁时,会用Thread |D和epoch值覆盖identity hash code所在的位置。如果一个对象的hashCode()方法己经被调用过一次之后,这个对象不能被设置偏向锁。因为如果可以的化,那Mark Word中的identity hash code必然会被偏向线程Id给覆盖,这就会造成同一个对象前后两次调用hashCode()方法得到的结果不一致。

升级为轻量级锁时,JVM会在当前线程的栈帧中创建一个锁记录(Lock Record)空间,用于存储锁对象的Mark Word拷贝,该拷贝中可以包含identity hash code,所以轻量级锁可以和identity hash code****共存,哈希码和GC年龄自然保存在此,释放锁后会将这些信息写回到对象头。

升级为重量级锁后,Mark Word保存的重量级锁指针,代表重量级锁的ObjectMonitor类里有字段记录非加锁状态下的Mark Word,锁释放后也会将信息写回到对象头。

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

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

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

20.AQS

 21.ReentrantLock、ReentrantReadWriteLock、StampedLock

读写锁:一个资源能够被多个读线程访问,或者被一个写线程访问但是不能同时存在读写线程

写锁的降级,降级成为了读锁
1)如果同一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁。这就是写锁的降级,降级成为了读锁。
2)规则惯例,先获取写锁,然后获取读锁,再释放写锁的次序。
3)如果释放了写锁,那么就完全转换为读锁。
锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性

如果有线程在读,那么写线程是无法获取写锁的,是悲观锁的策略

在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会被阻塞。所以,需要释放所有读锁,才可获取写锁。

写锁和读锁是互斥的(这里的互斥是指线程间的互斥,当前线程可以获取到写锁又获取到读锁,但是获取到了读锁不能继续获取写锁),这是因为读写锁要保持写操作的可见性。因为,如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作。

ReentrantReadWriteLock读的过程中不允许写,只有等待线程都释放了读锁,当前线程才能获取写锁,也就是写入必须等待,这是一种悲观的读锁,人家还在读着那,你先别去写,省的数据乱。

1.3 为什么要锁降级?


锁降级确实不太贴切,明明是锁切换,在写锁释放前由写锁切换成了读锁。问题的关键其实是为什么要在锁切换前就加上读锁呢?防止释放写锁的瞬间被其他线程拿到写锁然后修改了数据,然后本线程在拿到读锁后读取数据就发生了错乱。但是,我把锁的范围加大一点不就行了吗?在写锁的范围里面完成读锁里面要干的事。缺点呢就是延长了写锁的占用时长,导致性能下降。对于中小公司而言没必要,随便在哪都能把这点性能捡回来了!

2.4 锁饥饿问题


ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,假如当前1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那1个写线程就悲剧了因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写。

如何缓解锁饥饿问题?
使用"公平"策略可以一定程度上缓解这个问题,但是"公平"策略是以牺牲系统吞吐量为代价的

StampedLock类的乐观读锁闪亮登场

2. 邮戳锁StampedLock


2.1 StampedLock横空出世
StampedLock(也叫票据锁)是JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化。

stamp(戳记,long类型)
代表了锁的状态。当stamp返回零时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,需要传入最初获取的stamp值。

ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,所以,在获取乐观读锁后,还需要对结果进行校验。

2.2 StampedLock总结


StampedLock的特点
所有获取锁的方法,都返回一个邮戳( Stamp) , Stamp为零表示获取失败,其余都表示成功;
所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
StampedLock是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)

StampedLock有三种访问模式
Reading (读模式悲观):功能和ReentrantReadWriteLock的读锁类似
Writing(写模式):功能和ReentrantRedWriteLock的写锁类似
Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认对为读取时没人修改,假如被修改再实现升级为悲观读模式

主要API
tryOptimisticRead():加乐观读锁
validate(long stamp):校验乐观读锁执行过程中有无写锁搅局

StampedLock的缺点
StampedLock 不支持重入,没有Re开头
StampedLock的悲观读锁和写锁都不支持条件变量(Condition),这个也需要注意。
使用StampedLock一定不要调用中断操作,即不要调用interrupt()方法

22.附加

 1. wait和sleep


sleep是Thread的静态方法;wait是Object的方法,任何对象实例都能调用。
sleep不会释放锁,它也不需要占用锁;wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)
它们都可以被interrupt方法中断

2.用户线程和守护线程

用户线程:自定义线程

主线程结束了,用户线程还在运行,jvm还存活

守护线程:比如说垃圾回收线程

没有用户线程了,只有守护线程,jvm结束

3.synchronized和Lock两者差异


synchronized是java关键字,内置,而lock不是内置,是一个类,可以实现同步访问且比 synchronized中的方法更加丰富

synchronized不会手动释放锁,而lock需手动释放锁(不解锁会出现死锁,需要在 finally 块中释放锁)

lock等待锁的线程会相应中断,而synchronized不会相应,只会一直等待

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

Lock 可以提高多个线程进行读操作的效率(当多个线程竞争的时候)
锁会出现死锁,需要在 finally 块中释放锁)

lock等待锁的线程会相应中断,而synchronized不会相应,只会一直等待

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

Lock 可以提高多个线程进行读操作的效率(当多个线程竞争的时候)

4.虚假唤醒

也就是说,这种现象叫做【虚假唤醒】。所谓虚假唤醒,就是 wait()方法的一个特点,总结来说 wait() 方法使线程在哪里睡就在哪里醒。 这是什么意思呢?那就以上述代码为例。

当 A 进入临界区,BCD三个线程在 if 判断后进入 wait() 等待,当A线程完成操作,此时 number 值为1,notifyAll() 会随机唤醒一个线程。

现在C被唤醒,由于 wait() 方法使线程在哪里睡就在哪里醒,所以接下来C在执行时不会再通过 if 判断而是直接+1,此时 number 就是2了。从而导致最后输出的结果和我们预想的不一致。

5.JUC强大辅助类

减少计数CountDownLatch

CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法之后的语句。具体步骤可以演化为定义一个类,减1操作,并等待到0,为0执行结果

循环栅栏 CyclicBarrier

该类是 允许一组线程 互相 等待,直到到达某个公共屏障点,在设计一组固定大小的线程的程序中,这些线程必须互相等待,因为barrier在释放等待线程后可以重用,所以称为循环barrier

public class CyclicBarrirtTest {
    // 创建固定值
    private static final int NUMBER  = 7;
    public static void main(String[] args) {
        // 每次执行 CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后的语句。
        CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER, () -> {
            System.out.println("****集齐7颗龙珠就可以召唤神龙");
        });
        // 创建六个线程,模拟六个学生
        for (int i = 1; i <= 7; i++) {
            new Thread(()->{
                System.out.println(Thread.currentThread().getName()+" 星龙被收集到了");
                try {
                    // 计数 +1
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }

            },String.valueOf(i)).start();
        }
    }
}
 

信号灯 Semaphore

一个计数信号量,从概念上将,信号量维护了一个许可集,如有必要,在许可可用前会阻塞每一个acquire(),然后在获取该许可。每个release()添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采取相应的行动
public class SemaphoreTest {
    public static void main(String[] args) {
        //创建Semaphore,设置许可数量
        Semaphore semaphore = new Semaphore(3);
        for (int i = 1; i <= 6; i++) {
            new Thread(()->{
                try {
                    // 抢占
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"抢到了车位");
                    // 设置停车时间
                    TimeUnit.SECONDS.sleep(new Random().nextInt(5));
                    // 离开车位
                    System.out.println(Thread.currentThread().getName()+"------离开了车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //释放
                    semaphore.release();
                }
            },String.valueOf(i)).start();
        }
    }
}

Fork 与 Join分支

将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值