并发编程JUC


在学习并发编程之前我们先来复习一点线程的基础知识

程序:机器中静态的代码

**进程:**是操作系统分配资源的单位

线程:是进程内的执行单位,是操作系统调度的最小的单位

线程创建的四种方式:

1.继承Thread类

2.实现Runnable接口,new Thread

3.时间Callable接口,有返回值,可以抛出异常

4.线程池

多线程:在一个进程中,同时有多个线程,java语言支持多线程

优点:提高程序处理速度,提高CPU利用率

缺点:多个线程对同一个共享的数据进行访问,安全性无法保证

那么如何解决这个问题呢

一.锁

1.使用synchronized关键字

​ 修饰方法:

​ 非静态方法: 锁对象是this

​ 静态方法: 锁对象是类的Class对象

​ 修饰代码块:

​ 只对某段代码块进行加锁控制

​ 需要自己传入一个锁对象,要求锁对象必须是唯一的.

​ 实现是依赖底层的指令来控制,

​ 是隐式的,自动的添加,自动是方法

2.使用Lock接口下的实现类

​ 是java代码层面的控制,只能对某段代码进行加锁控,只能 手动添加,手动是方法

复习到这里就开始正式学习并发编程

Java并发编程

并发:同一时间内,交替做多件事

并行:同一时间内,多个事情被同时做

举例来说就是并发相当于一个窗口卖票,由于买票速度很快给人的感觉是一个时间点买很多张票.并行就相当于多个窗口同时卖票

并发编程就是在很多线程对共享资源进行访问时要通过控制让多个线程对共享的数据访问

1.并发编程引发的三大问题

因为在硬件中,cpu,内存,i/o设备的速度差异引发了一系列问题,为了平衡差距就做出了如下的优化

1.cpu增加了缓存

2.操作系统以线程分时复用

3.编译程序优化指令执行次序

但是这些优化也会带来了很多问题

1.1可见性

在多核CPU中,每一个内核都会有一个高速缓存,每个高速缓存的数据只被他所在的处理器内核可见,无法保证cpu缓存与内存数据一致,为了避免处理器停下来向内存中写入数据而产生的停顿,因此处理器使用写缓存区保存向内存中写入的数据,并以批处理的方式刷新到内存中,但是缓存的不能及时刷新导致了可见性的问题

1.2有序性

为了优化性能可能会改变程序语句中的先后顺序,就会造成有序性的问题

1.3原子性

一个或多个操作在cpu中执行时不被中断的特性称为原子性.原子性拒绝多线程交叉操作,不论单核还是多核,同一给食客智能有一个线程对它操作.

但是java并发执行是基于多线程的,会涉及到线程切换,就打破了原子性

针对这些问题也是有解决办法的

1.4解决有序性与可见性 —volatile关键字

被volatile关键字修饰的共享变量(类的成员变量,类的静态变量):

保证了不同线程对这个变量改变的实时性并且禁止进行指令重排序,这样volatile关键字就可以保证可见性与有序性


volatile关键字底层实现原理–内存屏障

**有序性实现:**主要通过对 volatile 修饰的变量的读写操作前后加上各种特定的内存屏障来禁止指令重排序来保障有序性的。

**可见性实现:**主要是通过 Lock 前缀指令 + MESI 缓存一致性协议来实现的。

那么我们如何保证原子性的问题呢?

1.5原子类来保证原子性

除了syn互斥锁以外还有一种方式如果你粗略的看一下 JUC(java.util.concurrent 包),那么你可以很显眼的发现它俩:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pJboF5JT-1666668433965)(C:\Users\24625\Desktop\csdn\juc\1665991001682.png)]

一个是 locks 包,一个是 atomic 包,它们可以解决原子性问题

AtomicInteger 为例来了解为什么原子类可以保证原子性

它是通过CAS+volatile实现原子性,volatile是用来保证有序性和可见性,cas来保证原子性

那么什么是原子CAS呢

CAS

**CAS(Compare-And-Swap) :比较并交换,该算法是硬件对于并发操作的支持。**底层通过Unsafe类中的compareAndSwaplInt等方法实现.

即每次判断我的预期值和内存中的值是不是相同,如果不相同则说明该内存值 已经被其他线程更新过了,因此需要拿到该最新值作为预期值,重新判断。而该 线程不断的循环判断是否该内存值已经被其他线程更新过了,这就是自旋的思想

CAS 包含了三个操作数:

①内存值 V

②预估值 A (比较时,从内存中再次读到的值)

③更新值 B (更新后的值)

当且仅当预期值 A==V,将内存值 V=B,否则什么都不做。

这种做法的效率高于加锁,当判断不成功不能更新值时,不会阻塞,继续获得 cpu 执行权,继续判断执行

缺点:因为cas采用自旋锁的方式不断循环判断,会导致cpu不断消耗,因此不适用于高并发量大的,而且一次只能保证一个共享变量的原子性(从源码中,我们知道 Object var1其实就是对象自己。拿上一篇文章举的例子来说,其实就是atomicInteger自己,也就是共享变量。CAS的do while只能一个this一个this的比较。从这里就可以看出 底层dowhile语句通过this关键字)

还有一种会产生aba问题

ABA

ABA 问题,即某个线程将内存值由 A 改为了 B,再由 B 改为了 A。当另外一个 线程使用预期值去判断时,预期值与内存值相同,当前线程的 CAS 操作无法分辨 当前 V 值是否发生过变化。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J536hkLt-1666668433966)(C:\Users\24625\Desktop\csdn\juc\1665992310169.png)]

解决 ABA 问题的主要方式,通过使用类添加版本号,来避免 ABA 问题。如原先的内存值为(A,1),线程将(A,1)修改为了(B,2),再由(B,2)修改 为(A,3)。此时另一个线程使用预期值(A,1)与内存值(A,3)进行比较, 只需要比较版本号 1 和 3,即可发现该内存中的数据被更新过了。

2.java锁分类

Java 中很多锁的名词,这些并不是全指锁,有的指锁的特性,有的指锁的设计,有的指锁的状态,下面总结的内容是对每个锁的名词进行一定的解释

实际的锁:synchronize,reentrantlock以及读写锁

2.1乐观锁,悲观锁

乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的

CAS就是乐观锁的实现

悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题.synchronized就是悲观锁

因此在java中乐观锁适用于读操作多的场景,二悲观锁则适用于写操作多的场景

2.2可重入锁

可重入锁又名递归锁,Reentrant Lock,Synchronized都是可重入锁

当一个线程进入到外层方法获取锁时,如果这个方法调用了另一个同样被锁修饰的方法,那么线程可以直接进入,不会出现死锁状态

广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IGjDpqkp-1666668433967)(C:\Users\24625\Desktop\csdn\juc\1666085528695.png)]

这段代码中就体现了可重入锁的特点最后输出"方法A"“方法B”

2.3读写锁

读写锁分为独锁和写锁,读锁允许多个线程同时获得,而写锁则是互斥锁不允许多线程,并且写锁优先于读锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R3BpxCZa-1666668433968)(C:\Users\24625\Desktop\csdn\juc\1666091323986.png)]

2.4分段锁

分段锁并非一种实际的锁,而是一种思想,用于将数据分段,并在每个分段上都会单独加锁,把锁进一步细粒度化,以提高并发效率。

在hashmap中,在底层的哈希数组的每个节点作为锁对象,这样就允许多线程操作

2.5自旋锁

自旋锁的思想就是,线程在抢占锁失败后会不断的尝试继续去抢占,不会让线程阻塞,但是比较耗费cpu.所以加锁时间短的场景适合自旋锁,cas就采用的是自旋锁

2.5共享锁,独占锁

独占锁也叫互斥锁,这个锁一次只能被一个线程有,ReentrantLock,Synchronized ,写锁而言,都是独享锁

共享锁可以被多个线程持有,并发访问共享资源,读锁也是共享锁

2.6公平锁,非公平锁

**公平锁(Fair Lock)**是指按照请求锁的顺序分配,拥有稳定获得锁的机会,但 是性能可能比非公平锁低,ReentrantLock 默认是非公平锁,但是底层可以通过 AQS 的来实现线程调度,所以可以使其变成公平锁。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wkTmnHs4-1666668433968)(C:\Users\24625\Desktop\csdn\juc\1666095068386.png)]

**非公平锁(Nonfair Lock)**是指不按照请求锁的顺序分配,不一定拥有获得锁的机会,但是性能可能比公平锁高synchronized是非公平 锁

2.7synchronize锁优化(锁状态)

锁的状态分为

  • 无锁状态
  • 偏向锁状态
  • 轻量级锁状态
  • 重量级锁状态

这四种状态是专门针对synchronize锁来说的,是JVM为了提高锁的获取与释放效率而做的优化

偏向锁

偏心锁是一个线程一直访问一段同步代码,此时会将线程的id存入对象头中,下次线程获取锁时直接分配即可

轻量级锁

当锁状态变为偏心锁时,又有线程进行访问,那么锁会升级到轻量级锁状态,让线程自旋获取锁,不阻塞线程以提高效率

重量级锁

当锁状态为轻量级锁,另一个线程进行自旋,当自旋到一定程度,该线程就会进入阻塞状态,锁膨胀为重量级锁,需要由操作系统调度分配

2.8对象结构

对象在内存中的布局分为三个部分:对象头,实例数据,对齐填充

对象头:用于存储对象自身运行时的数据,例如哈希码,GC分带年龄,锁状态标志,线程持有的锁,偏向线程id,它是偏向锁和轻量级锁的关键

实例数据:存储对象的有效数据,即我们在程序代码里面所定义的 各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。

对象对齐填充:

由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的 整数倍,换句话说就是任何对象的大小都必须是 8 字节的整数倍。对象头部分已 经被精心设计成正好是 8 字节的倍数(1 倍或者 2 倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全

3.AQS

它的全称是AbstractQueuedSynchronizer,类 在 java.util.concurrent.locks 包下面。 AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应 用 广 泛 的 大 量 的同步器.AQS是JUC实现线程安全的核心组件,是java代码级别的实现

内部实现原理

内部维护一个state 变量表示是否锁可用,初始值为0,多线程条件下线程执行代码就必须先获取state,获取后state+1,这时其他线程想要进入的话会去FIFO队列等待占有state的线程释放锁后去唤醒下一个等待线程(head中的下一个节点).因为state是多线程共享变量异因此必须用volatile关键字修饰以保证可见性.

另外 AQS 中实现的 FIFO 队列(CLH 队列)其实是双向链表实现的,head 结点代表当前占用的线程,其他节点由于暂时获取不到锁所以依次排队等待锁释放。

队列由 Node 对象组成,Node 是 AQS 中的内部类

4.synchronize锁

synchronize和reentrantlock锁的最大区别是synchronize是一个关键字,而reentrantlock则是java.util.concurrent.locks 包下的类.

synchronized修饰的对象有几种:

**修饰一个类:**其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象;

**修饰一个方法:**被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;

**修饰一个静态的方法:**其作用的范围是整个方法,作用的对象是这个类的所有对象;

**修饰一个代码块:**被修饰的代码块称为同步语句块,其作用范围是大括号{}括起来的代码块,作用的对象是调用这个代码块的对象;
java每个对象都可以把它当做监视器锁,当一个线程要进入被synchronize修饰的代码块时,会先获取锁,之后进来的线程只能等待持有锁的线程执行完或者抛出异常或者执行wait方法释放锁.

当synchronized修饰方法:使用ACC_SYNCHRONIZED指令来标记他是一个同步方法,在对象头中的锁标志+1,方法运行结束或抛出异常时锁标志-1,锁被释放

当synchronized修饰代码块:当代码执行到monitorenter 指令时,会尝试获取锁,获取锁以后锁标志+1;当执行 monitorexit 指令时将模计数器-1;当计数器为 0 时,锁就被释放了

5.ReentrantLock 锁

ReentrantLock 是 java.util.concurrent.locks 包 下 的 类 , 实 现 Lock 接口,Lock 的意义在于提供区别于 synchronized 的另一种具有更多广泛操作的同步方式,它能支持更多灵活的结构

它和syn锁不同的点就是,reentrantlock锁底层是完全依靠java代码来实现,不是通过指令来控制这也是它灵活的原因

ReentrantLock 内部有三个内部类,基于AQS,在并发编程中可以实现公平锁与非公平锁来对共享资源进行同步

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5T0RoWpH-1666668433969)(C:\Users\24625\Desktop\csdn\juc\1666504571042.png)]

ReentrantLock 内部定义了sync继承AbstractQueuedSynchronizer抽象类提供释放资源的实现,NonfairSync、FairSync继承自sync实现lock类,如果调用默认构造方法就是非公平锁实现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DuQ30YS2-1666668433969)(C:\Users\24625\Desktop\csdn\juc\1666506242323.png)]

fairsync底层则是直接调用了acquire锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z6ZFxGYS-1666668433970)(C:\Users\24625\Desktop\csdn\juc\1666506257776.png)]

6.JUC常用类

6.1ConcurrentHashmap类

ConcurrentHashmap同步容器类是jdk1.5后新添加的线程安全的哈希表,介于hashmap与hashtable之间

在jdk1.8之前ConcurrentHashmap内部是Segment分段锁,但是在1.8之后舍弃了分段锁,采用node锁和CAS操作+synchronized实现更加细粒度的锁

put方法实现

1.根据k值算出hash值

2.判断是否需要进行初始化

3.定位到node,拿到首节点f,并进行判断

  • 如果为null,则通过cas方式自旋获取锁

  • 如果为f.hash = moved = -1,说明其他线程正在进行扩容,蔡宇一起扩容

  • 若都不满足,synchronize锁住f节点进行遍历插入

4.当在链表长度到达8时,数组扩容或者将立案表转换为红黑树

简单说就是通过hash值找到相应位置后查看第一个节点是不是为空,如果为空就cas方式添加,如果不为空就将头节点当做锁标记对象

ConcurrentHashMap 不支持存储 null 键和 null 值. 为了消除歧义

ConcurrentHashMap 不能 put null 是因为 无法分辨是 key 没找到的 null 还是有 key 值为 null,这在多线程里面是模糊不清的,所以压根就不让 put null。

CopyOnWriteArrayList

但是在很多应用场景中,读操作可能会远远大于写操作。由于读操作根本不会修改原有的数据,因此如果每次读取都进行加锁操作,其实是一种资源浪费。我们应该允许多个线程同时访问 List 的内部数据,毕竟读操作是线程安全的。

JDK 中提供了 CopyOnWriteArrayList 类,将读取的性能发挥到极致,取是完全不用加锁的,并且更厉害的是:写入也不会阻塞读取操作,只有写入和写入之间需要进行同步等待,读操作的性能得到大幅度提升

实现

写入数据时,会先创建一个新的数组副本,将新添加的元素写入到新数组中,写完后再将新数组副给底层原来的数组引用.

CopyOnWriteArraySet实现基于 CopyOnWriteArrayList,不能存储重复数据

7.线程池

7.1概述

当我们的程序中并发线程数量过多时,并且每个线程只执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁的创建和销毁线程会耗费很多时间.但是在java中可以通过线程池来解决这个问题,线程池中的线程执行完程序后不会里克斯旺,而是会回到线程池中称为空闲状态,等待下次调用

优点

  • 可以重复利用线程,降低消耗
  • 可以对线程进行统一管理,方便创建和销毁
  • 提高程序运行速度,线程已经创建好,任务到来可以知己处理,省区创建时间

7.2线程池的使用

线程池的实现类是 ThreadPoolExecutor继承AbstractExecutorService 类,有四种构造方法,但实际上前面三个构造器都是调用第四个构造器完成初始化工作

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}
 
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         threadFactory, defaultHandler);
}
 
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), handler);
}
 
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

7.3构造器中的七个参数

1.corePoolSize:核心池的大小,默认线程数量为0,也可以调用prestartAllCoreThreads()或者 prestartCoreThread()方法在任务到来之前预创建线程

2.maximumPoolSize:线程池允许的最大线程数量,当任务队列已经满了,并且当前线程数量小于最大线程数量就会创建新的线程

3.keepAliveTime:表示线程多久没有线程任务执行的存活时间,一般针对超出corePoolSize的线程

4.unit:keepAliveTime的时间单位,有七种取值

5.workQueue:一个阻塞队列,用来存储等待执行的任务

6.threadFactory:线程工厂,用来创建线程

7.handler:表示当前的拒绝策略

7.4线程池执行流程

再向线程池提交任务时,通常使用execute方法

  • 当线程池存活的核心线程小于corePoolSize时,线程池会创建一个核心线程去处理提交的任务
  • 当线程池核心线程数已满,新提交的任务会被放进workQueen牌堆等待执行
  • 当任务队列也满了,但还没有达到maximumPoolSize,创建非核心线程执行提交任务
  • 当前线程若是达到maximumPoolSize,并且还有新的任务到来就会采用拒绝策略处理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rvqgvnfE-1666668433970)(C:\Users\24625\Desktop\csdn\juc\1666525572522.png)]

7.5线程池的队列

**SynchronousQueue:**同步队列是一个容量只有 1 的队列,这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务,每个 put必须等待一个 take.

**ArrayBlockingQueue:**有界队列,是一个用数组实现的有界阻塞队列,按 FIFO排序量。

**LinkedBlockingQueue:**可设置容量队列,基于链表结构的阻塞队列,按 FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为 Integer.MAX_VALUE;

7.6线程池的拒绝策略

构造方法的中最后的参数 RejectedExecutionHandler 用于指定线程池的拒绝策略。当请求任务不断的过来,而系统此时又处理不过来的时候,我们就需要采 取对应的策略是拒绝服务。

默认有四种类型:

AbortPolicy 策略:该策略会直接抛出异常,阻止系统正常工作。

CallerRunsPolicy 策略:只要线程池未关闭,该策略在调用者线程中运行当前的任务(如果任务被拒绝了,则由**提交任务的线程(例如:main)**直接执行此任务)。

DiscardOleddestPolicy 策略:该策略将丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。

DiscardPolicy 策略:该策略丢弃无法处理的任务,不予任何处理。

7.7execute 与 submit 的区别

执行任务除了可以使用 execute 方法还可以使用 submit 方法。它们的主要区别是:execute 适用于不需要关注返回值的场景,submit 方法适用于需要关注返回值的场景。

7.8关闭线程池

关闭线程池可以调用 shutdownNow 和 shutdown 两个方法来实现。

shutdownNow:对正在执行的任务全部发出 interrupt(),停止执行,对还未开 始执行的任务全部取消,并且返回还没开始的任务列表。

shutdown:当我们调用 shutdown 后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务。

8.对象引用

除了垃圾回收标记垃圾对象以外,我们还需要分类垃圾对象的状态

在 JDK1.2 版之后,

Java 对引用的概念进行了扩充,将引用分为:

强引用

软引用(SoftReference)

弱引用(WeakReference)

虚引用(PhantomReference)

这4种引用强度依次逐渐减弱。除强引用外,其他3种引用均可以在java.lang.ref包中找到它们的身影。

强引用:有引用指向的对象,GC无法回收的对象

软引用:使用SoftReference 来管理的对象属于软引用,内存充足时不着急回收,一旦内存快要溢出则会回收弱引用对象

**弱引用:**使用WeakReference来管理的对象,属于弱引用,管理的对象只能存活到下次垃圾回收.

虚引用(幽灵引用) :使用PhantomReference管理,需要提供一个队列维护, 随时可以被回收,主要就是记录跟踪对象是否被回收.

9.ThreadLocal

treadlocal顾名思义,是一个线程变量,它可以为变量在每一个线程中创建一个副本,让每个线程可以去拥有自己的共享变量,这个变量对于其他线程来说是隔离的,在线程结束后使用的实例副本都可以被回收

底层实现

ThreadLocal是一个泛型类,可以接受任何类型的对象,内部实现一个ThreadLocalMap的静态内部类

public void set(T value) {
        //1、获取当前线程
        Thread t = Thread.currentThread();
        //2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,
        //则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            // 初始化thradLocalMap 并赋值
            createMap(t, value);

    public T get() {
        //1、获取当前线程
        Thread t = Thread.currentThread();
        //2、获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //3、如果map数据不为空,
        if (map != null) {
            //3.1、获取threalLocalMap中存储的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
        return setInitialValue();
    }
 
 
private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

内存泄漏问题

  static class ThreadLocalMap {
 
        static class Entry extends WeakReference<ThreadLocal<?>> {
           
            Object value;
 
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
 
        
    }

从底层代码中可以看出来,threadlocal作为key是被弱引用管理的,只能存活到下一次垃圾回收前,而value则是被强引用管理的,因此当key被GC回收后,value还会存在一条强引用链无法被回收造成内存泄漏,因此需要每次使用完 ThreadLocal 都调用它的 remove()方法清除数据.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值