一、CompletableFuture使用

    1.1 Future(异步)

    Futue接口定义了异步执行的一些方法,如获取子线程执行的结果、取消任务执行、判断任务是否被取消、任务是否执行完毕等。也就是监听子线程执行。搭配Callable接口使用。

    满足Future的需求分为两个:1)线程异步执行;2)线程有返回值;只有Callable满足。

    1.2 FutureTask(继承Future和Runnable类)

    构造器注入有Callable。
    我们发现FutureTask.get()方法,会阻塞主线程等待直接完毕获取子线程执行结果,像join。
    基于设计原理,当我们主线程执行完毕,我们可以调用get方法。

    FutureTask优点:
    1)搭配线程池可以做到一个任务拆解,提高执行效率。

    FutureTask缺点:
    1)主线程直接调用get()方法阻塞等待。就白使用了。get方法可以设置等待时间。总结:get()容易阻塞。
    (不过可以搭配isDone(), 当isDone=true,时调用get())但是需要轮训判断isDone()返回状态。
    image.png
    因此总结FutureTask,它要么阻塞获取子线程执行结果,要么循环判断获取结果。不管这么样都会有消耗。这也是CompletionFuture诞生原因。

    再总而言之:阻塞的方式于异步执行设计理论相违背,循环导致消耗CPU资源。

    1.3 CompletableTask

    新的需求:多个任务前后依赖组合处理。 for example, 有5个线程,ABC…, B线程执行需要依赖A线程的执行结果,也就是AB线程执行不能并发,串行。AB和C要并发。 最后需要汇总所有线程的执行结果。
    image.png
    如果只传递Runnable和Supplier对象,则使用默认的线程池。
    传递线程池则可以使用创建的线程池。

    CompletableFuture相当于 = FutureTask + Thread.start启动。
    1)如果使用默认的线程池,则需要主线程执行业务持续3秒钟,因为默认线程池是守护线程,如果主线程执行的太快,默认线程池则会被回收。(或者使用主线程调用get方法也是执行)
    2)使用自定义线程池,则会自动启动。

    为了避免使用默认的线程池,主线程执行太快,没有执行。推荐使用自定义线程池。
    image.png

    CompletableFuture优点:
    1)异步任务结束后,会自动回调某个对象的方法
    2)主线程设置好回调之后,就不需要再关心异步任务执行,异步任务之间可以顺序执行,还是线程池中的线程。
    3)异步任务出错后,会自动回调某个对象的方法。

    1.4 电商案例

    1)按照结果和触发计算:
    按照返回值分为get(), join(), getNow(),complete()四个方法。
    其中get和join为阻塞类型。
    返回值类型complete类型。由于get会阻塞主线程等待,导致性能下降。join和get一样。
    getNow()立刻获取返回不阻塞,如果执行完毕则返回结果,否则返回默认值。
    complete()方法和getNow()相似,但是还需要搭配Join使用。

    2)对计算结果进行处理
    就是将子线程处理串行处理。

    第一个方法thenApply()方法,参数是上一个执行方法的返回值。
    image.png
    第二个是handle(v, e), 相对于thenApply来说,handle可以处理异常信息。
    image.png

    3)对计算结果进行消费

    thenAccept方法,接受任务的结果,并做最终处理,没有返回值。
    image.png

    还有thenRunAsync()方法,只有第一线程使用线程池,剩下的线程都是使用forkjoin线程池。不会使用我们自己的线程池。

    4)对计算效率进行计算

    applyToEither方法可以对比两个CompletableFuture方法执行效率。

    1. public static void main(String[] args) throws ExecutionException, InterruptedException {
    2. CompletableFuture<String> playA = CompletableFuture.supplyAsync(() -> {
    3. System.out.println("A is comming");
    4. try {
    5. TimeUnit.SECONDS.sleep(2);
    6. } catch (InterruptedException e) {
    7. e.printStackTrace();
    8. }
    9. return "playA";
    10. });
    11. CompletableFuture<String> playB = CompletableFuture.supplyAsync(() -> {
    12. System.out.println("B is comming");
    13. try {
    14. TimeUnit.SECONDS.sleep(3);
    15. } catch (InterruptedException e) {
    16. e.printStackTrace();
    17. }
    18. return "playB";
    19. });
    20. CompletableFuture<String> completableFuture = playA.applyToEither(playB, f -> {
    21. return f + "is winner";
    22. });
    23. System.out.println(completableFuture.get());
    24. }
    25. }

    5)对计算结果进行合并
    image.png

    二、多线程锁

    阿里规范,能够不要使用锁,就不要使用锁,如果使用锁,则尽可能减小锁的粒度。
    对象锁 < 方法锁 < 代码块锁

    2.1 乐观锁和悲观锁

    悲观锁,Java加锁基本上都是悲观锁。适合于写操作偏多。
    乐观锁,在Java当中即为无锁操作,适合于读操作偏多。

    2.2 Synchronized关键字原理剖析(面试)

    Synchronzied关键字实际上就是使用Mintor监视器,也就是管程实现。
    对于静态方法是对模板类进行加锁。
    对于方法是对类对象进行加锁。
    对于代码块则需要定义竞争对象。
    锁的粒度从大到小排序。

    通过monitorenter和monitorexit加锁和解锁。

    1)为什么任何一个对象都可以称为一个锁呢?
    每一个对象都有Monitor监视器。这是由C++实现的。ObjectMonitor.cpp。
    image.png

    2)知道Synchronzied原理吗?
    底层是通过对象的监视器锁实现的,每一个对象都有一个监视器锁,底层通过C++实现。它依赖于操作系统的底层的互斥锁实现。

    通过反编译我们发现,被加锁的代码有monitorentor和monitorexit, monitorentor是竞争这个对象的
    锁,monitorexit是释放对象的监视器锁。

    加锁的流程是:
    1)当一个线程获取监视器锁时(也就是monitorenter时),如果监视器计数器为0,则获取监视器锁成功,将锁对象的持有线程置为当前线程,并且计数器+1
    2)如果说别的线程竞争监视器锁时,发现计数器不为0,则阻塞存放到EntryList阻塞队列中,并将计数器+1,
    3)当一个线程释放监视器锁(也及时monitorexit时),计数器-1。唤醒阻塞线程竞争。
    4)如果锁对象持有线程调用wait()方法,则会处于阻塞状态,置为waitSet队列当中,然后去唤醒阻塞队列。

    3)Synchronzied可重入原理?
    我们都知道Synchronzied是通过对象的监视器实现的。ObjectMonitor对象中有一个属性为锁的可重入次数_recursions。

    如果一个线程加锁时,计数器不为0,但是当前锁对象持有线程为本身,则也会加锁成功,并将可重入计数器+1。当线程再次获取这个监视器锁时,可重入次数+1。

    2.3 公平锁和非公平锁

    有5个线程并发处理任务,结果第一个线程执行太快,导致其他线程没有任务执行,这个就是非公平锁。
    公平锁就是保证每个线程雨露均沾,都有执行任务。

    公平锁虽然能够保证每个线程雨露均沾,但是这样CPU切换多。非公平锁则相反。

    2.4 可重入锁

    指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不会发生死锁,这样的锁就是可重复入锁。
    当一个线程持有一把锁执行方法时,方法内部又再次竞争这把锁时就不竞争。 不会被阻塞。

    实例:同步方法和同步块都是可以的。
    image.png

    2.5 死锁

    一个线程持有锁A,又去竞争锁B。另一个线程持有锁B,竞争锁A导致两个线程处于阻塞等待现象。

    死锁的排查方案:
    jps -l 查看运行线程进程ID
    jstack -进程ID 查看进程ID对应的堆栈信息

    解决死锁的方案:
    1)一个线程持有锁资源
    2)对于多个线程竞争锁,按照统一顺序加锁。统一解锁。

    三、线程-中断机制
    概述:
    线程一般来说不应该被其他线程来强制中断或停止。而应该由线程自己自定停止。自己决定自己的命运。
    以前Java提供了Thread.stop, suspend, resume等方法都抛弃。

    中断只是一种协商机制,Java没有给中断增加任何语法,中断过程完全需要自己实现。
    中断只是一种协商,不会强制关闭。

    3.1 中断方法API
    image.png
    1)interrupt()方法设置中断状态为true,不会立刻停止。只是设置中断标志位。
    2)interrupted()方法获取线程是否被中断,获取完毕后清空中断状态。

    3.2 面试

    1)如何停止中断运行中的线程?
    排除掉stop等强制停止方法。
    使用中断协商方法。

    第一种方法可以通过共有变量做控制,当这个变量为true条件时,则线程退出。
    这个变量由另一个线程控制,从而达到线程之间控制的效果。
    这个变量可以选择加volatile关键字。保证内存中变量永远是最新的。
    也可以使用AtomicBoolean原子性变量操作。

    第二种方法是通过线程提供的三大中断API实现需求。
    被控制线程调用Thread.interrupted()方法判断是否可以结束。控制线程调用被控制线程的intereupt()方法来通知可以结束。
    image.png

    2)当前线程的中断标志为true,是不是线程立刻会被停止?
    不会。
    image.png
    image.png
    这段代码被其他线程调用中断方法,结果发现抛出异常,而且还是死循环。
    抛出异常可以理解,上述已经描述过。但是死循环呢?

    实际上是这样的,interrupte()方法虽将线程的中断标志位置为true, 但是遇到线程阻塞,睡眠等状态,则会抛出中断异常,并将中断标志位清空(false)。
    image.png

    因此解决方案:再次设置中断状态为true,调用interrupt方法。
    image.png

    3)Thread.interrupted()静态方法如何理解?

    获取线程的中断状态,并将线程的中断状态置为false清楚。
    底层调用当前线程的isInterrupted()方法。
    Thread.interrupted()和 isInterrupted()底层调用同一个方法,一个传入true,一个传入false。

    四、LockSupport

    用于创建锁和其他同步类的基本线程阻塞原语。
    image.png
    park()方法阻塞线程,需要等待许可证才会唤醒。
    unpark(thread)方法唤醒线程。向某一个线程发许可证。就会自动唤醒park线程。
    可以认为LockSupport是线程阻塞和唤醒更新后的方法。

    Java当中让线程处于阻塞和唤醒的方法有:
    1)使用Object中wait让线程阻塞,和使用Object中的notify唤醒
    2)使用JUC中的Condition的await()阻塞,和使用signal()方法唤醒。
    3)使用LockSupport的park阻塞, 和使用unpark唤醒。

    使用Object的阻塞和唤醒需要搭配Synchronzied关键字,必须先持有锁才可以使用,不然会抛出异常。我们都知道
    Synchronized底层是加锁操作。因此调用JUC的Condition的方法也是需要竞争锁之后才可以调用阻塞和唤醒方法。
    第二点就是必须先阻塞才能唤醒。如果提前唤醒的话,阻塞则没有用。

    总结而言就是: 老的阻塞方法会有加锁和阻塞唤醒流程限制。
    1)我们现在通过LockSupport就可以解决上述的问题。
    2)不需要加锁,其次就是先发放许可证,后park阻塞都可以被唤醒。通行证只能发一次。

    由于通行证只能发一个,因此park()和unpark()方法尽可能使用一次。

    五、JVM与线程相关内容

    JVM实现线程和主内存之间的抽象关系。

    5.1 JMM三大特性

    可见性、原子性、有序性。

    1)内存可见性
    可见性指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。JMM 内部的实现通常是依赖于所谓的内存屏障,通过禁止某些重排序的方式,提供内存可见性保证,也就是实现了各种 happen-before 规则。与此同时,更多复杂度在于,需要尽量确保各种编译器、各种体系结构的处理器,都能够提供一致的行为。

    2)原子性
    多个线程想要对JVM主内存的变量修改,则需要竞争。

    3)有序性
    为了提升执行效率,编译器和处理器会将指令序列重排,不见得从上到下的执行。
    image.png
    重排必须考虑指令数据依赖性。
    image.png

    5.2 JMM中happend-before

    举例:a = 10; b=a; 两个线程执行, 第一个线程执行没有会写结果挂起,另一个线程读取的a没有修改后的值,而是默认值。这样就会有问题。

    总原则:
    happend-before:第一个操作的执行结果将对第二个操作可见,也就是说第一个操作必须发生在第二个操作之前。
    happend-before关系并不意味着按照原有执行顺序执行,只要执行结果相同,存在优化,就会重排序。

    happend-before之8条:
    1)次序规则:一个线程内,写在前面的操作先行发生于写在后面的操作。
    2)锁定规则:一个线程先要加锁,得先等到上一个获取的线程释放锁
    3)volatile变量规则:对一个volatile变量的写操作先行发生于对这个变量的读操作。
    4)传递规则:A先行B执行,B先行C执行,则A先行C执行
    5)线程启动规则:线程start()启动先行发生线程里面执行方法。
    6)线程中断规则: 对于线程interrupt调用先行发生与isInterrupted()检查中断发生。
    7)线程终止规则:线程中的所有操作都先行发生于此线程的终止检测。
    8)线程终结规则:一个对象的初始化完成先行发生于它的finalize()调用。

    happend-before使用案例:
    image.png
    两个线程分别调用setValue() 【优先】和getValue(),会出现什么后果。答案:0,1
    因为setValue方法可能执行一半挂起,让另一个线程getValue()方法执行,返回了默认值0。

    优化方案一:
    image.png
    两个方法加锁。但是这样锁粒度太大,并发性不好。

    优化方案二:
    image.png
    使用happend-before中volatile原则,volatile变量写操作优先于读操作。

    六、volatile关键字(面试)

    volatile两个特性:
    1)内存可见性
    2)防止指令重排,防止指令优化(防止指令优化带来的问题)

    如果指令有依赖,则不能重排,没有依赖可以重排。

    volatile不能保证线程安全,需要搭配锁使用。

    6.1 volatile内存可见性
    image.png
    保证不同的线程对volatile变量修改后,所有的线程的volatile变量都是都会及时发生变化。

    6.2 volatile内存屏障

    内存屏障分为读屏障和写屏障。这也是volatile读和写保证的底层实现。
    读屏障:在读指令之前插入读屏障,让工作内存或者CPU高速缓存中的缓存数据失效。重新回到主内存中读取数据。
    写屏障:在写指令插入写屏障,强制把缓存中的数据写入主内存。
    image.png
    image.png

    6.3 volatile适用场景

    线程之间通过flag变量协调变更。
    单例模式中懒汉模式的就必须使用static修饰。

    6.4 volatile总结(面试就背这个)

    1)volatile具有内存可见性和禁止重排(指令优化)
    2)volatile对于写操作,写完立刻刷到主内存中,读会重新拷贝主内存的值。
    3)禁止重排底层通过内存屏障实现。
    对于写操作分为StoreStore屏障:禁止普通写和volatile写重排,禁止volatile写和volatile写重排。
    还有StoreLoad:禁止volatile写和普通读重排,禁止volatile写和volatile读重排。
    对于读操作分为LoadLoad屏障:禁止volatile读和volatile读重排序,volatile读和普通读重排序
    还有LoadStore:禁止volatile读与普通写和volatile写重排序。
    image.png

    七、CAS以及CAS增强类

    所有的原子操作都是使用java.util.concurrent.atomic包下的。
    无锁编程的实现方式。其实就是乐观锁的实现方式。

    实现的流程:我们每一个线程都有自己操作空间,还有主内存。举个例子:主内存A=7, 每个线程都想对A+1, 一个线程相加回写的时候会先判断主内存的值和自己持有的副本值做对比,如果还是相同,则会将修改后的值回写入主内存。

    AVB操作,A读取主内存的副本,V代表主内存的值,B代表更新后的值。
    如果想要修改,则需要对比A是否等于V,如果A=V,则修改V=B,认为没有其他线程修改过这个值。
    否则其他线程修改过这个值, 需要重新获取,进行更新操作,再次判断。在这块也被称为自旋锁。

    CAS是JDK提供的非阻塞原子性操作,他通过硬件保证了比较-更新的原子性。底层通过UnSafe提供的CAS方法实现。

    7.1 CAS底层实现
    image.png
    CAS底层实现通过UnSafe类实现,这个类里面的方法都是native本地方法,他能够允许Java操作内存。
    再加上自旋的实现。然后讲一下ABV操作。
    CAS利用CPU指令保证了操作的原子性,已达到锁的效果。

    compareAndSet(user1, user2); 期待值为user1, 更改为user2。

    7.2 自旋锁实现

    自旋锁的特点是自己竞争锁的时候不会阻塞,而是循环判断是否可以获取到锁。这一点减少了CPU的切换,缺点是消耗CPU, 适用于线程持有锁的时间较短。是Linux底层实现中很重要的锁。
    在Java当中,CAS是自旋锁的实现,CAS利用CPU指令保证了操作的原子性。已达到锁的效果。

    通过AtomicReference原子引用中compareAndSet(“期待值”,”更改后值”)

    1. public class SpinLockDemo {
    2. private static AtomicReference<User> atomicReference = new AtomicReference<>();
    3. public static void main(String[] args) {
    4. User user01 = new User("A", 1);
    5. Thread t1 = new Thread(() -> {
    6. System.out.println("t1 is come");
    7. while (!atomicReference.compareAndSet(null, user01)) {
    8. // 一致循环竞争获取锁
    9. System.out.println(Thread.currentThread().getName() + "自选竞争获取锁");
    10. }
    11. // 获取到锁
    12. try {
    13. TimeUnit.SECONDS.sleep(5);
    14. } catch (InterruptedException e) {
    15. e.printStackTrace();
    16. }
    17. // 释放锁
    18. atomicReference.compareAndSet(user01, null);
    19. System.out.println("t1 释放锁");
    20. }, "t1");
    21. Thread t2 = new Thread(() -> {
    22. System.out.println("t2 is come");
    23. while (!atomicReference.compareAndSet(null, user01)) {
    24. // 一致循环竞争获取锁
    25. System.out.println(Thread.currentThread().getName() + "自选竞争获取锁");
    26. }
    27. // 获取到锁
    28. // 获取到锁
    29. try {
    30. TimeUnit.SECONDS.sleep(5);
    31. } catch (InterruptedException e) {
    32. e.printStackTrace();
    33. }
    34. // 释放锁
    35. atomicReference.compareAndSet(user01, null);
    36. System.out.println("t2 释放锁");
    37. }, "t2");
    38. t1.start();
    39. t2.start();
    40. }
    41. }

    7.3 自旋锁优缺点

    优点:解决了重量级加锁带来的CPU切换损耗。
    缺点:
    1)循环等待开销大:CAS底层使用循环判断A=V,然后更新B的流程,如果此线程一直A!=V,则线程循环等待。增加CPU的开销。因此这个循环等待不能等待很长时间。
    2)ABA:第二个缺点就是ABA问题,一个线程A将值改为B最后又改为A,另一个线程进来发现还是A,以为没有被修改过,更改A值为B。

    如何解决ABA问题?
    使用带有邮戳版本的原子类,不仅仅检查结果还检查流水号。通过AtomicStampedReference类实现, 切忌两个线程操作时要保证初入版本号相同。

    1. public static void main(String[] args) {
    2. AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(100, 1);
    3. int originalStamp = reference.getStamp();
    4. Thread t1 = new Thread(() -> {
    5. reference.compareAndSet(100, 101, originalStamp, originalStamp+1);
    6. System.out.println("t1 已经将100->101");
    7. reference.compareAndSet(101, 100, reference.getStamp(), reference.getStamp()+1);
    8. System.out.println("t2 又将101->100");
    9. }, "t1");
    10. Thread t2 = new Thread(() -> {
    11. boolean compareAndSet = reference.compareAndSet(100, 101, originalStamp, originalStamp + 1);
    12. if (compareAndSet) {
    13. System.out.println("t2已经将100->101");
    14. } else {
    15. System.out.println("t2失败,重新获取版本号,修改");
    16. int newStamp = reference.getStamp();
    17. reference.compareAndSet(100, 1000, newStamp, newStamp);
    18. }
    19. }, "t2");
    20. t1.start();
    21. t2.start();
    22. }

    7.3 CAS增强类LongAdder
    image.png
    红色标志为原有的原子类操作,后面红色的为优化后的原子类。

    AtomicStampedReference, 带有version的流水号,版本号可以通过变更的次数来判断被修改过多少次。
    AtomicMarkableReference,带有标记位。他只有修改后就会状态戳置为true。他无法判断有几个线程修改过。

    1. public static void main(String[] args) {
    2. AtomicMarkableReference<Integer> reference = new AtomicMarkableReference<>(100, false);
    3. boolean marked = reference.isMarked();
    4. new Thread(() -> {
    5. System.out.println("t1 is come");
    6. reference.compareAndSet(100, 101, marked, !marked);
    7. System.out.println("t1 100->101");
    8. reference.compareAndSet(101,100, marked, !marked);
    9. System.out.println("t1 101->100");
    10. },"t1").start();
    11. new Thread(() -> {
    12. System.out.println("t2 is come");
    13. boolean b = reference.compareAndSet(100, 101, marked, !marked);
    14. if (b) {
    15. System.out.println("t2 100->101");
    16. } else {
    17. System.out.println("t2 error");
    18. }
    19. },"t2").start();
    20. }


    AtomicIntegerFieldUpdater等基于反射实现原子操作。修改的属性必须为public volatile类型。
    image.png

    1. class BankAccount {
    2. String name = "hmw";
    3. // 更新属性必须使用public volatile, 使用原子操作更新,不需要加锁,保证线程安全。
    4. public volatile int money = 0;
    5. AtomicIntegerFieldUpdater<BankAccount> fieldUpdater =
    6. AtomicIntegerFieldUpdater.newUpdater(BankAccount.class, "money");
    7. /**
    8. * 使用原子操作增加
    9. * @param account
    10. */
    11. public void transMoney(BankAccount account) {
    12. fieldUpdater.getAndIncrement(account);
    13. }
    14. }

    针对BankAccount来说,里面的money属性就可以保证线程安全了。

    1. class MyVar {
    2. public volatile Boolean isInit = false;
    3. AtomicReferenceFieldUpdater<MyVar, Boolean> fieldUpdater =
    4. AtomicReferenceFieldUpdater.newUpdater(MyVar.class, Boolean.class, "isInit");
    5. public void init(MyVar myVar) {
    6. if (fieldUpdater.compareAndSet(myVar, Boolean.FALSE, Boolean.TRUE)) {
    7. // 原子操作成功, 加锁
    8. System.out.println("执行MyVar初始化任务:: For" + Thread.currentThread().getName());
    9. } else {
    10. System.out.println("已被初始化过");
    11. }
    12. }
    13. }

    这样就可以保证MyVar类中init方法,只有回一个线程调用。只会被初始化一次。

    原子分类增强类:
    image.png
    通过测试发现AtomicLong没有LongAdder和LongAccumulator效率高。因为这两个底层减少对乐观锁的重试。

    为什么LongAdder比AtomicLong快?(面试)
    image.png
    首先先说一个AtiomicLong的缺点,除了ABA外,最大的点就是循环抢占资源。占用CPU资源。
    当线程比较少时,每个线程都有很大机会抢占成功,则还可以。
    但是高并发下,只有一个线程使用资源,导致很多线程空循环抢占。

    那么LongAdder如何解决呢? 答案就是分散热点。底层还是调用CAS操作,但是分为很多CELL数组,自己维护自己的CAS操作。 然后将所有CAS运算结果总和返回。
    这样可以减少线程同一块空间判断,而是对多个空间。
    也可以说CAS分段。空间换取时间。

    result = Base + ∑Cell[i]

    八、Synchronized锁升级(面试)

    首先说说Synchronized锁慢的原因?
    这是一个重量级锁, 底层通过操作系统的互斥锁实现。Java的线程是映射到操作系统原生线程上的。
    如果想要阻塞和唤醒一个线程就需要操作系统的介入,需要在用户态和内核态之间进行切换。
    由于切换带来的损耗和本身互斥锁的重量级,导致JDK6之前,Synchronized性能还不如操作系统加锁和解锁操作。

    后来引入了轻量级锁和偏向锁。

    8.1 JVM对象内存分布

    Java中对象分为对象头、实例数据、对齐填充。
    对象头又分为MarkWord(8字节) + 类型指针(8字节)。

    MarkWord包含GC分代年龄、哈希值、线程持有锁、锁状态标志,偏向线程ID,偏向时间戳。
    类型指针:执行方法区的类元信息。

    实例数据为类里面的属性信息。
    对齐填充则是按照8字节填充。

    因此一个类什么都没有,都会有对象头。
    不管后面的什么,GC回收、对象布局、锁升级对象标记MarkWord里面的标志位的变化。
    image.png
    hashCode是本地方法,C++代码实现hashCode逻辑。
    只有调用hashCode方法才会真正产生哈希编码。

    8.2 锁升级过程(面试)

    Synchronized升级依赖与对象头MarkWord中的锁标志位和释放偏向锁标志位。
    早先Synchonized只有无锁和重量级锁两种状态。随着锁优化出现了偏向锁和轻量级锁。
    按照锁粒度的大小从小到大排序:无锁->偏向锁->轻量级锁->重量级锁。

    1)无锁状态
    如果一个对象没有被任何线程竞争,则偏向锁位为0, 锁标志位为01。

    2)偏向锁
    竞争锁的线程有可能多次都是同一个线程。
    我们都知道切换线程获取锁,需要同步到操作系统层面切换线程。这里会有用户态切换内核态然后切换锁的流程。如果说确实不同线程竞争持有对象资源,这样的操作在所难免。
    但如果只有同一个线程持有,就会做一些无意义的操作。1)用户内核态切换。

    因此偏向锁偏向于由同一个线程多次获取的情况,它的出现解决了只有在一个线程执行同步时提升性能。
    在这种情况下,取消同步语句,懒得连CAS操作都不做了,直接提升性能。
    image.png
    image.png
    image.png
    偏向锁有4秒延迟,开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
    关闭偏向锁:-XX:-UseBiasedLocking

    偏向锁撤销和偏向锁升级:当不再是同一个线程抢占对象时,则偏向锁指向的线程ID不再匹配。这个时候会使用CAS更新线程ID为新的线程ID。如何为竞争失败(撤销)
    image.png
    3)轻量级锁
    假如线程A已经拿到锁(偏向锁),这时线程B来抢占该对象的锁。而线程B抢占时发现对象头Mark Word中的线程ID不是线程B自己而是线程A。那么线程B就会进入CAS操作希望获得锁。

    此时线程B有两种操作。
    1)获取锁成功,直接替换Mark Word中线程IDA->B,B上位。
    2)获取锁失败,则偏向锁升级成轻量级锁,由原有线程持有,继续执行自己的任务。而竞争的线程B则会进入自旋等待获取轻量级锁。
    image.png
    轻量级锁加锁过程:JVM会为每一个线程在当前线程栈帧中创建用于存储锁记录的空间,称为Displaced Mark Word。若一个线程获取锁时发现是轻量级锁,则会把锁对象头里面的MarkWord拷贝到本地空间,然后尝试CAS更改锁对象头里面的MarkWord,如果更改成功则意味着获取轻量级锁成功,则否获取锁失败,接着自旋获取(自旋虽然不加锁,但有可能导致CPU空转,但是不阻塞)

    自旋多少次进化为重量级锁,1)JDK6以前是8次;2)JDK6以后自适应,如果上次自旋成功了,则下次自旋的次数就会减少; 相反则会减小自旋次数。

    4)重量级锁
    重量级锁则是阻塞加锁。也就是Synchonized以前老的加锁的性能。基于Monitor对象加锁、解锁。
    轻量级锁升级为重量级锁:轻量级锁时线程自旋很多次没有获取到锁,则会进化为重量级锁。
    image.png

    8.3 总结

    hashCode值通过本地方法调用获取,即为每一个对象的身份标识。他在程序启动和结束是唯一的。
    它存放在对象头的Mark Word31位字节中。这是无锁对象头。

    又因为每个对象都有锁的特性,而锁的升级是通过对象头MarkWord实现的。针对不同的锁会导致MarkWord内容结构发生变化,故此导致hashCode受到影响。

    第一个偏向锁,偏向锁中有获取锁的线程ID,分代年龄,唯独没有hashCode存放空间,因此偏向锁和hashCode不能共存。也就是说如果一个对象调用hashCode获取哈希值,则加锁也不回是偏向锁。

    第二个轻量级锁, 轻量级锁中存放Lock Record指针(即为本地无锁的MarkWord副本),相当于保存了一个锁对象的MarkWord副本(无锁的MarkWord),而这里面就有hashCode值。故此轻量级锁和hashCode可以共存。

    第三个重量级锁,存放Monitor指针,Monitor在ObjectMonitor类中就有非加锁状态下的MarkWord,即也保存了hashCode信息。

    1)如果说在偏向锁的状态,调用该对象的hashCode方法,则会导致锁膨胀,直接进化为重量级锁。
    2)如果说一个对象已经调用过hashCode(),则获取该对象锁直接会为轻量级锁。
    hashCode只有在调用才会生成,不然不会保存在对象的hashCode中。hashCode和轻量级不共存。
    image.png

    锁消除:这样的加锁毫无意义,JIT则取消加锁操作。
    image.png
    锁粗化:这样的加锁无意思,则会合并在一个加锁里面操作。
    image.png

    九、AQS(抽象同步器)

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

    底层搭配一个先进先出的队列和状态值表示。int状态值表示锁的状态。
    image.png
    image.png
    image.png
    如下图为链表中线程的Node信息。
    image.png

    9.1 AQS源码剖析

    我们都知道AQS底层是通过status状态值和双向链表组成的。
    而双向链表则是获取锁和竞争锁的线程。链表中的元素为Node,而Node当中有获取锁的状态标志和上下链接。

    对于公平锁,线程在获取锁时,如果这个锁的等待队列已经有线程在等待,那么当前线程就会进入等待队列中。
    对于非公平锁,不管是否有等待队列,如果可以获取到锁,则立刻占有锁对象,这样就会导致一个线程一直获取锁的情况。(当然这种情况逻辑处理会少,效率相对于公平锁高一些)
    在实现方面两个锁都会调用acquire方法,

    但是非公平锁会调用compareAndSetState方法,这个方法会判断status状态是否为0(即此时i没有线程占用他)这个操作是乐观锁操作。

    总而言之,AQS中lock加锁可以分为:1)尝试加锁;2)加锁失败,线程进入队列;3)线程入队之后,进入阻塞状态。

    1)尝试加锁:调用acquire方法, 不过针对非公平锁会先调用CAS抢占下锁资源,如果抢占到直接使用。也就是判断status状态是否为0。而公平锁则是保证线程执行的先来后到。
    2)加锁失败,线程进入队列,把竞争锁的线程添加到双向链表中。
    3)阻塞线程和执行线程都是在双线链表中,都是通过waitstatis维护自己的状态。

    十、读写锁降级

    对于Synchronized来说,里面有锁升级。(无锁->偏向锁->轻量级锁->重量级锁)
    对于Lock来说,(无锁->可重入锁->读写锁->邮戳锁)

    读写锁相对于可重入锁来说, 提高了并发性。这次就是针对读写锁做研究。
    ReadWriteLock、ReentrantReadWriteLock -> StampedLock

    ReentrantReadWriteLock,可以提高并发性能,但是会存在读锁、写锁之间死锁。
    但是这个会导致写锁饿死问题,我们都知道写锁的优先级大于读锁。
    因此将写锁变成读锁被称为锁降级,读锁变化为写锁成为锁升级。
    锁升级不可以,但是锁降级却是可以的。

    就是一个读写锁在获取到写锁之后,还可以获取读锁,锁降级。最后释放写锁。
    支持锁降级的目的是减少没必要的读写竞争。
    而获取到读锁之后,不能获取写锁,锁升级。

    10.1 读写锁

    读写锁是解决可重入锁性能的提升,针对不同场景提出的。可重入锁中读和写所有操作都是串行,锁粒度大。
    image.png
    锁降级测试用例:
    image.png
    image.png

    可重入读写锁会产生的问题:
    1)一直读,导致不能写,写饥饿问题。
    2)写锁和读锁死锁问题。

    后续的邮戳锁就是解决写饥饿问题。

    10.2 StampedLock(邮戳锁)

    对于可重入读写锁的升级,由于可重入锁本身锁饥饿问题产生的。
    由于读线程比较多,写线程少,可能一直读线程一直抢占到锁,导致一直读,没有写。这就是写线程饥饿问题。
    写多时候可以读,这个让锁降级解决了。

    一般来说可重入读写锁底层也是通过AQS实现。默认创建的读写锁为非公平锁。这种情况也就是上述所说的问题。如果说使用公平锁,那么还是有一定的效果的。 按照双向链表中的顺序唤醒嘛。但是这个并发性和机器的吞吐量会差很多。

    最终由JDK1.8提出的邮戳锁解决问题。

    StampedLock针对写锁也是可是直接获得的。读的过程也允许写过程的加入。
    因此StampedLock = ReentrantReadWriteLock + Stamp邮戳标记。

    1)StampedLock加锁的时候,会返回一个邮戳Stamp, 如果Stamp=0,则获取锁失败。
    2)当释放锁的时候,需要传递Stamp。

    3)StampedLock不仅拥有ReentrantReadWriteLock锁功能还包含了新的功能。
    因此StampedLock有三种模式:1)读模式(悲观读);2)写模式;3)读模式(乐观读)‘