目录
1.3 Thread,ThreadLocal,ThreadLocalMap 关系
三、AbstractQueuedSynchronizer之AQS
一、ThreadLocal
1.1 概念
ThreadLocal使得每一个线程都能拥有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份),主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。
小总结
因为每个 Thread 内有自己的实例副本且该副本只由当前线程自己使用,既然其它 Thread 不可访问,那就不存在多线程间共享的问题。
可以统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的
加入synchronized或者Lock控制资源的访问顺序
人手一份,大家各自安好,没必要抢夺
1.2 典型示例
SimpleDateFormat类内部有一个Calendar对象引用,它用来储存和这个SimpleDateFormat相关的日期信息,例如sdf.parse(dateStr),sdf.format(date) 诸如此类的方法参数传入的日期相关String,Date等等, 都是交由Calendar引用来储存的.这样就会导致一个问题如果你的SimpleDateFormat是个static的, 那么多个thread 之间就会共享这个SimpleDateFormat, 同时也是共享这个Calendar引用。
public class DateUtils
{
private static final ThreadLocal<SimpleDateFormat> sdf_threadLocal =
ThreadLocal.withInitial(()-> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
/**
* ThreadLocal可以确保每个线程都可以得到各自单独的一个SimpleDateFormat的对象,那么自然也就不存在竞争问题了。
* @param stringDate
* @return
* @throws Exception
*/
public static Date parseDateTL(String stringDate)throws Exception
{
return sdf_threadLocal.get().parse(stringDate);
}
public static void main(String[] args) throws Exception
{
for (int i = 1; i <=30; i++) {
new Thread(() -> {
try {
System.out.println(DateUtils.parseDateTL("2020-11-11 11:11:11"));
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
1.3 Thread,ThreadLocal,ThreadLocalMap 关系
Thread是我们的线程
ThreadLocal是一个线程工具类
而ThreadLocalMap是ThreadLocal维护的一个内部类
在Thread中有一个类型为ThreadLocalMap,初始值为null的全局成员变量。
在ThreadLocal初次调用get方法的时候,会先创建一个ThreadLocalMap对象,赋值给Thread中的变量threadLocals。所以Thread和ThreadLocalMap是有一个一对一的关系,一个线程有且仅有一个类型为ThreadLocalMap的变量与之对应。但是一个线程可以持有很多个ThreadLocal。
threadLocalMap实际上就是一个以threadLocal实例为key,任意对象为value的Entry对象。 当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放。(ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map)
每个Thread对象维护着一个ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值Value是传递进来的对象
调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
ThreadLocal本身并不存储值,它只是自己作为一个key来让线程从ThreadLocalMap获取value,正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响~
1.4 ThreadLocal的内存泄漏问题
内存泄漏:不再会被使用的对象或者变量占用的内存不能被回收。
ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以它为Key),不过是经过了两层包装的ThreadLocal对象:
(1)第一层使用 WeakReference<ThreadLocal<?>> 将ThreadLocal对象变成一个弱引用的对象;
(2)第二层定义了一个专门的类 Entry 来扩展 WeakReference<ThreadLocal<?>>
有关强引用、弱引用、软引用、虚引用
强引用(默认)-
当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收。
软引用-
当系统内存充足时它不会被回收;当系统内存不足时它会被回收。
SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
弱引用-
只要垃圾回收机制运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
WeakReference<MyObject> weakReference = new WeakReference<>(new MyObject());
虚引用-
虚引用的主要作用是跟踪对象被垃圾回收的状态。 仅仅是提供了一种确保对象被 finalize以后,做某些事情的机制。 PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。
换句话说,设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。
ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue(); PhantomReference<MyObject> phantomReference = new PhantomReference<>(new MyObject(),referenceQueue);
为什么ThreadLocal要使用弱引用?
假设有这样一个方法:
public void function01()
{
ThreadLocal tl = new ThreadLocal<Integer>(); //t1强引用ThreadLocal对象
tl.set(2021); //line2
tl.get(); //line3
}
内存结构是这样的:
当该方法执行完时,栈帧销毁,指向ThreadLocal的强引用没了。假如ThreadLocalMap中指向ThreadLocal的key是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏。
若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的雷)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。
此后我们调用get,set或remove方法时,就会尝试删除key为null的entry,可以释放value对象所占用的内存。
注意:
虽然弱引用保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄露。
我们要在不使用某个ThreadLocal对象后,必须手动调用remove方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。
1.5 总结
- ThreadLocal 并不解决线程间共享数据的问题
- ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
- ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
- 每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
- ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题
- 都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身,从而防止内存泄漏,属于安全加固的方法
- 群雄逐鹿起纷争,人各一份天下安
二、 对象内存布局与对象头
对象内部结构分为:对象头、实例数据、对齐填充(保证8个字节的倍数)。
对象头分为对象标记(markOop)和类元信息(klassOop),类元信息存储的是指向该对象类元数据(klass)的首地址。
在64位系统中,Mark Word占了8个字节,类型指针占了8个字节,一共是16个字节。
2.1 MarkWord
MarkWord默认存储对象的HashCode、分代年龄和锁标志位等信息。这些信息都是与对象自身定义无关的数据,所以MarkWord被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWord里存储的数据会随着锁标志位的变化而变化。
2.2 类元信息(又叫类型指针)
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例.
2.3 实例数据
存放类的属性(Field)数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
2.4 对齐填充
虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐这部分内存按8字节补充对齐。
三、AbstractQueuedSynchronizer之AQS
3.1 基本概念
字面意思:抽象的队列同步器。
是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石(辅助类、可重入锁),通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态。
CLH:Craig、Landin and Hagersten 队列,是一个单向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO
锁 -> 面向锁的使用者
定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可。
同步器 -> 面向锁的实现者
比如Java并发大神DougLee,提出统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等。
3.2 AQS 的结构与作用
加锁会导致阻塞,有阻塞就需要排队,实现排队必然需要队列
抢到资源的线程直接使用处理业务,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能且获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。
为什么要有一个虚拟的head节点?
每个节点都必须设置前置节点的 ws (waitStatus)为 SIGNAL(-1)。每个节点在休眠前,都需要将前置节点的 ws 设置成 SIGNAL。否则自己永远无法被唤醒,所以必须要一个前置节点,而这个前置节点,实际上就是当前持有锁的节点。
而第一个节点是没有前置节点的,所以需要创建一个虚拟节点。
它将请求共享资源的线程封装成队列的结点(Node),通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态(是否空闲),使并发达到同步的效果。
Node的结构:
static final class Node {
/** 共享锁*/
static final Node SHARED = new Node();
/** 独占锁*/
static final Node EXCLUSIVE = null;
/** waitStatus-线程被取消 */
static final int CANCELLED = 1;
/** waitStatus-后继线程需要被唤醒*/
static final int SIGNAL = -1;
/** waitStatus-等待condition唤醒 */
static final int CONDITION = -2;
/** waitStatus-下一个被获取的对象应该无条件地传播*/
static final int PROPAGATE = -3;
//节点状态,初始为0
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
。。。 。。。
AQS 定义了两种资源共享方式:
1.Exclusive:独占,只有一个线程能执行,如ReentrantLock
2.Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
3.3 AQS流程概述
线程获取锁,如果获取了锁就保存当前获得锁的线程,如果没获取就创造一个节点通过compareAndSetTail(CAS操作)操作的方式将创建的节点加入同步队列的尾部。
在同步队列中的节点通过自旋的操作不断去获取同步状态【当然由于FIFO先进先出的特性】等待时间越长就越先被唤醒。
当头节点释放同步状态的时候,首先查看是否存在后继节点,如果存在就唤醒自己的后继节点,如果不存在就获取等待时间最长的符合条件的线程。