导读与注意点:
- P5、Lock锁,介绍了公平锁与非公平锁。
- P6、synchronized和lock区别
- P7、P8、synchronized版和lock版的生产者消费者问题。(AQS 管程)
- P10、八锁现象******
- P11、12、CopyOnWrite 写入时复制思想
- P15-17、常用工具类:CountDownLatch、CyclicBarrier、Semaphore
- P18、ReadWriteLock
- P22、23、线程池的 3大方法,7大参数,4种拒绝策略
- P25、26、四大函数式接口(关联P27、28、29)
- P27、Stream流式计算
- P28、ForkJoin分支合并
- P29、异步回调
- P30-32、JMM 和 volatile (接下来的视频例子易理解连贯性也较大)
- P34、35、深入理解CAS(比较并交换)=>会导致 “ABA问题”
- P36-39、Java的锁
- P33、彻底玩转单例模式(进阶装X重点)
- ================================
- 锁的状态有几种
- synchronized锁升级
- ThreadLocal原理和使用场景
- ThreadLocal内存泄露原因,如何避免
P5、Lock锁,介绍了公平锁与非公平锁。
有一个线程执行任务需要3h,另一个只需3s,用非公平好还是公平好?肯定非公平好,不然我一个3s就能执行完的要我等3h才能执行,这效率多低啊
公平锁:线程先来后到
- 多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
- 优点:所有的线程都能得到资源,不会饿死在队列中。
- 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:线程之间可以插队
- 多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
- 优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
- 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
然后自己去了解一下什么是乐观锁和悲观锁:
synchronized就是典型的悲观锁
乐观锁(Optimistic Lock)
每次获取数据的时候,都不会担心数据会被修改,所以每次获取数据时都不会进行加锁。
但是在更新数据的时候,会有个版本号(version),每修改一次就+1,需要判断该数据是否被别人修改过(更新的时候通过对比版本号和一开始读取的版本号是否一致来判断),如果数据被其他线程修改过,则不进行数据更新。
如果数据没有被其他线程修改,则进行数据更新。由于数据没有进行加锁,期间该数据可以被其他线程进行读写操作。
ps:这里会涉及到ABA问题的知识点,文章下面的:深入理解CAS中会举例说明
悲观锁(Pessimistic Lock)
每次获取数据的时候,都会担心数据会被修改,所以每次获取数据的时候都会进行加锁,
确保在自己使用的过程中数据不被别人修改,使用完后进行数据解锁。
由于数据会进行加锁,期间对该数据进行读写和其他线程都会进行等待。
适用场景
- 乐观锁:
比较适合读取操作比较频繁的场合。
如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,
应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。- 悲观锁:
比较适合写入操作比较频繁的场合。
如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。- 总结:两种锁各有各的优点,读取频繁使用乐观锁,写入频繁使用悲观锁。
参考:
https://www.imooc.com/article/302143/
https://www.cnblogs.com/fanhaiping/p/9577486.html
P6、synchronized和lock区别
6. synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
7. Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
P7、P8、synchronized版和lock版的生产者消费者问题。(AQS 管程)
AQS 是多线程同步器,它是 J.U.C 包中多个组件的底层实现,如 Lock、 CountDownLatch、Semaphore 等都用到了 AQS. 从本质上来说,AQS 提供了两种锁机制,分别是排它锁,和共享锁。 排它锁,就是存在多线程竞争同一共享资源时,同一时刻只允许一个线程访问该 共享资源,也就是多个线程中只能有一个线程获得锁资源,比如 Lock 中的 ReentrantLock 重入锁实现就是用到了 AQS 中的排它锁功能。 共享锁也称为读锁,就是在同一时刻允许多个线程同时获得锁资源,比如 CountDownLatch 和 Semaphore 都是用到了 AQS 中的共享锁功能。
设计AQS整个体系需要解决的三个核心的问题:①互斥变量的设计以及多线程同时更新互斥变量时的安全性②未竞争到锁资源的线程的等待以及竞争到锁资源的线程释放锁之后的唤醒③锁竞争的公平性和非公平性。
AQS采用了一个int类型的互斥变量state用来记录锁竞争的一个状态,0表示当前没有任何线程竞争锁资源,而大于等于1表示已经有线程正在持有锁资源。一个线程来获取锁资源的时候,首先判断state是否等于0,如果是(无锁状态),则把这个state更新成1,表示占用到锁。此时如果多个线程进行同样的操作,会造成线程安全问题。AQS采用了CAS机制来保证互斥变量state的原子性。未获取到锁资源的线程通过Unsafe类中的park方法对线程进行阻塞,把阻塞的线程按照先进先出的原则加入到一个双向链表的结构中,当获得锁资源的线程释放锁之后,会从双向链表的头部去唤醒下一个等待的线程再去竞争锁。另外关于公平性和非公平性问题,AQS的处理方式是,在竞争锁资源的时候,公平锁需要判断双向链表中是否有阻塞的线程,如果有,则需要去排队等待;而非公平锁的处理方式是,不管双向链表中是否存在等待锁的线程,都会直接尝试更改互斥变量state去竞争锁。
Java中的管程:https://www.cnblogs.com/xidongyu/p/10891303.html
代码:https://gitee.com/busanl/practice/blob/master/practice01/src/ThreadStudy/notify/ProducerAndConsumer.java
提到一点:虚假唤醒,特别是线程通信的条件判断,不能用if,应该用while
P10、八锁现象******
被synchronized和static修饰的方法,锁的对象是类的Class对象。
仅仅被synchronized修饰的方法,锁的对象是方法的调用者。
两个锁是不一样的。
P11、12、CopyOnWrite 写入时复制思想
读不加锁,写加锁。读写分离?因为读操作频率远高于写操作,所以效率高
详情:
https://www.cnblogs.com/jmcui/p/12377081.html
https://www.cnblogs.com/chengxiao/p/6881974.html
P15-17、常用工具类:CountDownLatch、CyclicBarrier、Semaphore
CountDownLatch 减法计数器
CyclicBarrier 加法计数器,加到一定值才能继续
Semaphore 信号量,用于多个共享资源的互斥使用和用于并发线程数的控制
P18、ReadWriteLock
注意:读锁和写锁是互斥关系,不管先锁谁都互斥。写锁和写锁是互斥的。读锁和读锁是并发的。底层用的aqs框架。
学习过程中可能遇到问题:
- ReadWriteLock 和 CopyOnWrite 的区别是什么?
读写锁是遵循写写互斥、读写互斥、读读不互斥的原则,而copyOnWrite则是写写互斥、读写不互斥、读读不互斥的原则。 - 什么是脏读和幻读?
首先复习一下:数据库中事务的隔离级别(读未提交、读已提交、重复读、可串行化)
然后这里专门解释一下什么是脏读和幻读:
https://blog.csdn.net/weixin_46286156/article/details/122091185
P22、23、线程池的 3大方法,7大参数,4种拒绝策略
3大方法:
Executors.newSingleThreadExecutor() //只有一个线程
Executors.newFixedThreadPool(int) //创建一个线程池,一池有N个固定的线程,有固定线程数的线程
Executors.newCachedThreadPool(); //可扩容,遇强则强
不过,根据阿里手册提到,非常不建议用 Executors 来创建线程池(同样这三大方法也不建议)。
应该用 ThreadPoolExecutor 来创建(自定义线程池才是最好的)
通过查看这三大方法的底层源码,发现本质都是调用了 new ThreadPoolExecutor
那不得自己动手丰衣足食更好??
7大参数
ThreadPoolExecutor 的7大参数
视频中举的例子讲解的很生动
源码:
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数,当阻塞(等待)队列满了,才会开启,后备隐藏能源?haha
long keepAliveTime, // 空闲的线程保留的时间
TimeUnit unit, // 空闲线程的保留时间单位
BlockingQueue<Runnable> workQueue, // 阻塞队列,存储等待执行的任务
ThreadFactory threadFactory, // 线程工厂,用来创建线程,一般默认即可
RejectedExecutionHandler handler // 拒绝策略(有4种)
) { ... }
关于 RejectedExecutionHandler handler
有哪些参数可选呢?这就涉及到——
4种拒绝策略
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务 (重复此过程)
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
最后提一下:ThreadPoolExecutor 底层工作原理
P25、26、四大函数式接口(关联P27、28、29)
函数式接口 | 参数类型 | 返回类型 | 用途 |
---|---|---|---|
Consumer< T >消费型接口 | T | void | 对类型为T的对象应用操作,包含方法:void accept(T t) |
Supplier< T >供给型接口 | 无 | T | 返回类型为T的对象,包含方法:T get(); |
Function<T, R>函数型接口 | T | R | 对类型为T的对象应用操作,并返回结果。结果是R类型的对象。包含方法:R apply(T t); |
Predicate< T >断定型接口 | T | boolean | 确定类型为T的对象是否满足某约束,并返回boolean值。包含方法boolean test(T t); |
了解这些,为下面 Stream流式计算、ForkJoin分支合并、异步回调 的学习做铺垫—— |
P27、Stream流式计算
代码:https://gitee.com/busanl/practice/tree/master/practice01/src/JUC_Study/stream
P28、ForkJoin分支合并
它的思想就是讲一个大任务分割成若干小任务,最终汇总每个小任务的结果得到这个大任务的结果。
这思想和Hadoop的MapReduce很像
另外,forkjoin有一个工作窃取的概念。简单理解,比如一个工作线程下的两条子线程一起执行,A线程很快就执行完了,等着也无聊,就把B线程的任务拿过来做。
方法和使用就不在这啰嗦了,百度好吧。
代码:https://gitee.com/busanl/practice/tree/master/practice01/src/JUC_Study/forkjoin
P29、异步回调
代码:https://gitee.com/busanl/practice/blob/master/practice01/src/JUC_Study/future/FutureDemo.java
P30-32、JMM 和 volatile (接下来的视频例子易理解连贯性也较大)
volitile 是 Java 虚拟机提供的轻量级的同步机制,三大特性:
- 保证可见性(涉及到JMM)
- 不保证原子性
- 禁止指令重排
那什么是JMM?JMM 本身是一种抽象的概念,并不真实存在,它描述的是一组规则或者规范
JMM 关于同步的规定:
1、线程解锁前,必须把共享变量的值刷新回主内存
2、线程加锁前,必须读取主内存的最新值到自己的工作内存
3、加锁解锁是同一把锁
详细理解:https://www.cnblogs.com/null-qige/p/9481900.html
学习中可能遇到的问题:
原子性?看视频教程,会提到原子包下的类(轻量级、高效),这涉及到CAS,内存操作,Unsafe类
什么是指令重排?看视频教程,会提到内存屏障
synchronized 能防止指令重排序吗?
P34、35、深入理解CAS(比较并交换)=>会导致 “ABA问题”
Java层面的CAS:比较并交换 compareAndSet
//new一个原子包下的Integer类,值为5
AtomicInteger atomicInteger = new AtomicInteger(5);
// 如果和期望的5相等,则改为 2020 , 所以结果为 true,2020
System.out.println(atomicInteger.compareAndSet(5, 2020)+"=>"+atomicInteger.get());
// 如果和期望的5相等,则改为 1024 , 所以结果为 false,2020
System.out.println(atomicInteger.compareAndSet(5, 1024)+"=>"+atomicInteger.get());
真实值和期望值相同,就修改成功,真实值和期望值不同,就修改失败!
深入探究:
CAS 底层原理?如果知道,谈谈你对UnSafe的理解?
例如:atomicInteger.getAndIncrement();
这里的自增 + 1怎么实现的!
点进源码,最后会发现底层是在 Unsafe 类下的 getAndAddInt 方法,而且还可以发现Unsafe类下的方法大部分是 native 方法(很熟悉?在 我的JVM初识理解与探究 那有讲过)
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 获取传入对象的地址
var5 = this.getIntVolatile(var1, var2);
// 比较并交换,如果var1,var2 还是原来的 var5,就执行内存偏移+1; var5 + var4
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5; }
底层的CAS:比较并交换 compareAndSwapInt
问题:这个UnSafe类到底是什么?
UnSafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,UnSafe相当于一个后门,基于该类可以直接操作特定内存的数据,Unsafe类存在于 sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
注意:Unsafe类中的所有方法都是Native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
最后解释CAS 是什么
CAS 的全称为 Compare-And-Swap,它是一条CPU并发原语。
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
CAS并发原语体现在JAVA语言中就是 sun.misc.Unsafe 类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
CAS 的缺点:会导致 “ABA问题”
学习历程:CAS => UnSafe => CAS 底层思想 => ABA => 原子引用更新 => 如何规避ABA问题
ABA问题图例(来源:三太子敖丙):
线程1读取了数据A
线程2读取了数据A
线程2通过CAS比较,发现值是A没错,可以把数据A改成数据B
线程3读取了数据B
线程3通过CAS比较,发现数据是B没错,可以把数据B改成了数据A
线程1通过CAS比较,发现数据还是A没变,就写成了自己要改的值
- 在这个过程中任何线程都没做错什么,但是值被改变了,线程1却没有办法发现,其实这样的情况出现对结果本身是没有什么影响的,但是我们还是要防范。
ABA这个问题在 乐观锁 那也有,解决方法和思想也是大同小异:加 “版本号”(version)
这里是版本号原子引用 AtomicStampedReference
用这个类解决
P36-39、Java的锁
- 公平锁和非公平锁
- 乐观锁和悲观锁
- 可重入锁
- 自旋锁
- 死锁
P33、彻底玩转单例模式(进阶装X重点)
面试手写:
单例模式,
排序算法
生产者和消费者
死锁
================================
锁的状态有几种
无锁,偏向锁,轻量级锁,重量级锁
详解:https://zhuanlan.zhihu.com/p/157781401
synchronized锁升级
jdk 1.6以前synchronized 关键字只表示重量级锁,1.6之后区分为偏向锁、轻量级锁、重量级锁。
所谓锁的升级、降级,就是 JVM 优化 synchronized 运行的机制,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。
锁升级的过程:无锁 => 偏向锁 => 轻量级锁 => 重量级锁
简单介绍过程:最开始是无锁状态,然后是偏向锁时,当前获取到锁资源的这个线程,会优先让它再获取到这个锁,如果有另一个线程在使用时与它争抢 或者 另一个线程与其来回使用偏向锁达到一定次数(默认是20),就升级成一个轻量级的CAS的锁,就是一个乐观锁,然后乐观锁是比较与交换的过程,如果没有设置成功的话,它会进行自旋,自旋到一定次数之后(默认是10)才会升级成Synchronized重量级锁。
详情:https://www.zhihu.com/question/267980537
ThreadLocal原理和使用场景
每一个 Thread 对象均含有一个 ThreadLocalMap
类型的成员变量 threadLocals
( ThreadLocalMap
是 ThreadLocal
的匿名内部类),它存储本线程中所有ThreadLocal对象及其对应的值。
ThreadLocalMap
由一个个 Entry
对象构成。
Entry
继承自 WeakReference<ThreadLocal<?>>
,一个 Entry
由 ThreadLocal
对象和 Object
构成。由此可见, Entry
的key是ThreadLocal对象,并且是一个弱引用。当没指向key的强引用后,该key就会被垃圾收集器回收。
当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中。
get方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。
由于每一条线程均含有各自私有的 ThreadLocalMap 容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。
使用场景:
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离。
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。
Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的 connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种 隔离
ThreadLocal内存泄露原因,如何避免
内存泄漏最终会导致OOM
什么是内存泄漏?
内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,
内存泄漏是指不再被使用的对象或者变量一直被占据在内存中
不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
强引用:使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。
ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本。
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链(红色链条)
- key 使用强引用
当ThreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。 - key 使用弱引用
当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal正确的使用方法
- 每次使用完ThreadLocal都调用它的remove()方法清除数据。
- 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。