多线程,并发相关

涉及线程知识及源码解析,最后会有一个高并发,高可用的项目实例

1、多线程的实现方式

==两种方式本质上是没有区别的最终都是调用start()方法来新建线程==

① 继承Thread类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
重写Run方法,example:
public class Consumer extends Thread{
private int num;
@Resource
private AbstractStorage abstractStorage;
/**
* 构造函数
* 设置仓库
* @param abstractStorage
*/
public Consumer(AbstractStorage abstractStorage){this.abstractStorage = abstractStorage;}
@Override
public void run(){
consume(num);
}
/**
* 调用Storage生产函数
* @param num
*/
private void consume(int num){
abstractStorage.consume(num);
}
public void setNum(int num){
this.num = num;
}
}

② 实现Runnable接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
实现Runnable接口把接口实例传给Thread类,example:
public class StartOrRunMethod {
public static void main(String[] args) {
Runnable runnable = new Runnable(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
};
//main方法中执行一个普通的方法
runnable.run();
//启动新线程,加入线程池
//native本地方法
new Thread(runnable).start();
}
}

2、正确优雅地中断线程

==我们要使用interrupt来进行通知而非强制 (请求线程停止好处是安全)==

①.interrupt()用法(没有sleep和wait方法,线程不阻塞)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
结束run函数,run中含退出标志位()
使用interrupt()方法中断线程

public void interrupt() { ... } //中断目标线程
public boolean isInterrupted{ ... } //返回目标线程的中断状态
public static boolean interrupted(){ ... } // 清除当前线程的中断状态,并返回它之前的值,这也是清除中断状态的唯一方式
interrupt()其本身并不是一个强制打断线程的方法,其仅仅会修改线程的interrupt标志位,然后让线程自行去读标志位,自行判断是否需要中断
example:
public class RightWayStopThreadWithoutSleepAndWait implements Runnable{

@Override
public void run() {
//查看当前是否设置了中断标示,判断当前线程是否中断 isInterrupted()方法返回true,证明线程中断
while (!Thread.currentThread().isInterrupted()){
System.out.println("在没有sleep等方法结束了线程");
}
System.out.println("线程结束了!");
}

public static void main(String[] args) throws InterruptedException {
//创建一个重写了run函数的线程对象
Thread thread =new Thread(new RightWayStopThreadWithoutSleepAndWait());
thread.start();
Thread.sleep(10);
//发送中断请求
thread.interrupt();
System.out.println("thread is stopping...");
}
}

②.线程阻塞情况下(sleep和wait)

线程阻塞:什么是线程阻塞?

在某一时刻某一个线程在运行一段代码的时候,这时候另一个线程也需要运行,但是在运行过程中的那个线程执行完成之前,另一个线程是无法获取到CPU执行权的(调用sleep方法是进入到睡眠暂停状态,但是CPU执行权并没有交出去,而调用wait方法则是将CPU执行权交给另一个线程),这个时候就会造成线程阻塞。

为什么会出现线程阻塞?

1.睡眠状态:当一个线程执行代码的时候调用了sleep方法后,线程处于睡眠状态,需要设置一个睡眠时间,此时有其他线程需要执行时就会造成线程阻塞,而且sleep方法被调用之后,线程不会释放锁对象,也就是说锁还在该线程手里,CPU执行权还在自己手里,等睡眠时间一过,该线程就会进入就绪状态,典型的“占着茅坑不拉屎”;

2.等待状态:当一个线程正在运行时,调用了wait方法,此时该线程需要交出CPU执行权,也就是将锁释放出去,交给另一个线程,该线程进入等待状态,但与睡眠状态不一样的是,进入等待状态的线程不需要设置睡眠时间,但是需要执行notify方法或者notifyall方法来对其唤醒,自己是不会主动醒来的,等被唤醒之后,该线程也会进入就绪状态,但是进入就绪状态的该线程手里是没有执行权的,也就是没有锁,而睡眠状态的线程一旦苏醒,进入就绪状态时是自己还拿着锁的。等待状态的线程苏醒后,就是典型的“物是人非,大权旁落“;

3.礼让状态:当一个线程正在运行时,调用了yield方法之后,该线程会将执行权礼让给同等级的线程或者比它高一级的线程优先执行,此时该线程有可能只执行了一部分而此时把执行权礼让给了其他线程,这个时候也会进入阻塞状态,但是该线程会随时可能又被分配到执行权,这就很”中国化的线程“了,比较讲究谦让;

4.自闭状态:当一个线程正在运行时,调用了一个join方法,此时该线程会进入阻塞状态,另一个线程会运行,直到运行结束后,原线程才会进入就绪状态。这个比较像是”走后门“,本来该先把你的事情解决完了再解决后边的人的事情,但是这时候有走后门的人,那就会停止给你解决,而优先把走后门的人事情解决了;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class RigthWayStopThreadWithSleep {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable(){

@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
System.out.println("在有sleep的情况下中断线程");
}
try {
Thread.sleep(1000);
System.out.println("b");
} catch (InterruptedException e) {
//线程堵塞导致线程无法停止,抛出异常
//当一个方法抛出InterruptedException时,它是在告诉您,如果执行该方法的线程被中断
//它将尝试停止它正在做的事情而提前返回,并通过InterruptedExceptio表明它提前返回。
// 行为良好的阻塞库方法应该能对中断作出响应并抛出InterruptedException,以便能够用于可取消活动中,而不至于影响响应。
System.out.println("线程阻塞");
e.printStackTrace();
}
}
}
Thread thread = new Thread(runnable);
thread.start();
Thread.sleep(500);
//发送中断请求
thread.interrupt();
System.out.println("thread is stopping..");
}
}

3、多线程异常处理机制

==首先:子线程抛出异常主线程不会处理、线程的异常不能用传统方法捕获==

所以要定义自己的异常处理器,实现异常处理器Thread.UncaughtExceptionHandler一个函数式接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ThreadCatchProcess3 implements Thread.UncaughtExceptionHandler {

private String name;

public ThreadCatchProcess3(String name) {
this.name = name;
}

/**
* 设置自己的异常处理器
* @param t
* @param e
*/
@Override
public void uncaughtException(Thread t, Throwable e) {
//可以进行一系列的自定义操作
System.out.println("线程异常终止进程" + t.getName());
System.out.println(name + "捕获了异常" + t.getName() + "异常");

}
}

在主线程上声明需要用到的异常处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ThreadCatch implements Runnable{
@Override
public void run() {
//抛出异常
throw new RuntimeException();
}
public static void main(String[] args) throws InterruptedException {
//利用自定义的异常处理器
Thread.setDefaultUncaughtExceptionHandler(new ThreadCatchProcess3("获取异常"));
new Thread(new ThreadCatch(),"MyThread-1").start();
Thread.sleep(3000);
new Thread(new ThreadCatch(),"MyThread-2").start();
Thread.sleep(3000);
new Thread(new ThreadCatch(),"MyThread-3").start();
}
}

4、死锁

①.什么是死锁

1.发生在并发中

2.当两个线程持有对方所需要的资源又不主动释放 导致所有人都没有办法继续前进 这就是死锁 如果系统资源充足进程的资源请求都能满足死锁的可能性就会很低 否则就会因为争夺优先的资源而陷入死锁

②.修复死锁策略:保存案发线程然后立刻重启服务器然后进行排查 重新发布

==一段时间检测是否有死锁 如果有就剥夺某一个资源来打开死锁==

③.哲学家就餐问题

还有很多其他例子,这里暂且用哲学家就餐问题来说明

哲学家就餐问题可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。哲学家就餐问题有时也用米饭和筷子而不是意大利面和餐叉来描述,因为很明显,吃米饭必须用两根筷子。

哲学家从来不交谈,这就很危险,可能产生死锁,每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)。即使没有死锁,也有可能发生资源耗尽。例如,假设规定当哲学家等待另一只餐叉超过五分钟后就放下自己手里的那一只餐叉,并且再等五分钟后进行下一次尝试。这个策略消除了死锁(系统总会进入到下一个状态),但仍然有可能发生“活锁”。如果五位哲学家在完全相同的时刻进入餐厅,并同时拿起左边的餐叉,那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再等五分钟,又同时拿起这些餐叉。

在实际的计算机问题中,缺乏餐叉可以类比为缺乏共享资源。一种常用的计算机技术是资源加锁,用来保证在某个时刻,资源只能被一个程序或一段代码访问。当一个程序想要使用的资源已经被另一个程序锁定,它就等待资源解锁。当多个程序涉及到加锁的资源时,在某些情况下就有可能发生死锁。例如,某个程序需要访问两个文件,当两个这样的程序各锁了一个文件,那它们都在等待对方解锁另一个文件,而这永远不会发生。

这里的解决方案是换手策略。
one:哲学家类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.geekagain.deadlock;

/**
* @author hly
* @Description:哲学家
* @create 2021-04-20 16:49
*/
public class Philosophers implements Runnable{

private Object leftChopstick;

private Object rightChopstick;

public Philosophers(Object leftChopstick, Object rightChopstick){
this.leftChopstick = leftChopstick;
this.rightChopstick = rightChopstick;
}
@Override
public void run() {
try {
while (true){
doAciton("thinking");
synchronized (leftChopstick){
doAciton("Pick up left chopstick");
synchronized (rightChopstick){
doAciton("Pick up right chopstick --eating");
doAciton("Put down right chopstick");
}
doAciton("Put down left chopstick");
}
}
}catch (Exception e){
e.printStackTrace();
}
}
private void doAciton(String action) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " " + action);
Thread.sleep((long) (Math.random()*10));
}
}

two:主线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.geekagain.deadlock;

/**
* 解决策略:哲学家换手
* 原本问题:1-5号哲学家,1-5号筷子,若五位哲学家同时举起左边筷子,这就造成都不能eating,当哲学家持有对方所需要的筷子又不主动释放 导致所有人都没有办法继续eating
* 解决:5号哲学家第一次举起右边的筷子,这样1号和5号会争夺1号筷子,有一位能抢到筷子,此时5号哲学家左边的筷子空出
* 这样,4号哲学家就能拿起两双筷子eating,之后再放下筷子,其他哲学家开始eating。
* @author hly
* @Description:哲学家就餐问题
* @create 2021-04-20 16:36
*/
public class DiningPhilosophers {
public static void main(String[] args) {
Philosophers[] philosophers = new Philosophers[5];
Object[] chopsticks = new Object[philosophers.length];
for (int i = 0; i < chopsticks.length; i++) {
chopsticks[i] = new Object();
}
for (int i = 0; i < philosophers.length; i++) {
Object leftChopsticks = chopsticks[i];
//因为坐圆桌
Object rigthChopsticks = chopsticks[(i+1) % chopsticks.length];
//表示第几个哲学家
if(i != philosophers.length - 1){
philosophers[i] = new Philosophers(leftChopsticks,rigthChopsticks);
}else {
//解决策略:第五位哲学家的“左边的筷子”是右边的筷子,有效解决,所有哲学家同时举起左边筷子,造成的死锁问题。
philosophers[i] = new Philosophers(rigthChopsticks,leftChopsticks);
}
new Thread(philosophers[i],"哲学家"+(i+1)+"号").start();
}
}
}

5、多线程wait notify notifyall join sleep yield作用与方法详细解读

①.wait notify notifyall 解读

1
2
3
4
5
1.wait() notify() notifyall() 方法是Object的本地final方法 无法被重写
2.wait() 使当前的线程阻塞 前提是必须获取到锁 一般配合synchronized 关键字使用 即一般在synchronized里面 使用wait notify notifyall
3.由于wait() notify() notifyall() 在synchronized里面执行 那么说明 当前线程一定是获取锁了
4.当线程执行wait的时候会释放当前锁让出CPU资源进入等待状态
5.当执行notify()/notifyall()的时候会唤醒一个或者多个处于正在等待的线程 然后继续执行知道执行完毕synchronized或者再次遇到wait

②.生产者消费者model (ProducerAndConsumerModel)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.geekagain.waitnotify.producerandconsumer;

import java.util.LinkedList;

/**
* wait and notify 模型————生产者和消费者模型
* 所有通信都是通过中间堵塞队列进行缓冲
* @author han long yi
* @create 2021-04-19 11:06
*/
public class ProducerAndConsumerModel implements AbstractStorage{
//仓库最大容量
private final int MAX_SIZE = 100;
/**
* 阻塞队列
*/
private LinkedList list = new LinkedList();
/**
* 消费者,消费商品
* @param num
*/
@Override
public void consume(int num) {
//同步
synchronized (list){
//仓库容量不足以消费
while (num > list.size()){
System.out.println("要消费的产品数量:"+num+"\t库存量:"
+list.size()+"\t暂时不能进行生产任务");
try {
list.wait();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
//消费条件满足,开始消费
for(int i = 0; i < num; i++){
list.remove();
}
System.out.println("已经消费的产品数量:"+num+"\t库存:"+list.size());
list.notifyAll();
}
}
/**
* 生产者,生产商品
* @param num
*/
@Override
public void produce(int num) throws InterruptedException {
synchronized (list){
//仓库容量不足以生产全部商品
while (list.size() + num > MAX_SIZE){
System.out.println("要生产的商品数量:"+num+"\t剩余库存量:"
+(MAX_SIZE-list.size())+"\t暂时不能进行生产任务");
//生产阻塞
list.wait();
}
//条件满足,开始生产
for (int i = 0; i < num; i++) {
list.add(new Object());
}
System.out.println("已经生产的数量:"+num+"\t现在仓库库存量:"+list.size());
list.notifyAll();
}
}
}

==问题:生产者/消费者模式,并不是一个高性能的实现。为什么性能不高呢?原因如下:==

1.涉及到同步锁。

2.涉及到线程阻塞状态和可运行状态之间的切换。

3.涉及到线程上下文的切换。

以上涉及到的任何一点,都是非常耗费性能的操作。因此,有了==协程==,协程java的生态并没有实现,在Lua,Python,Go实现了协程

协程:协程,英文Coroutines,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。

因为Java并没有实现协程,所以我没有太深究,在搜索的过程中,还了解了一种和Java互通的语言Kotlin,它可以编译成Java字节码,也可以编译成JavaScript,方便在没有JVM的设备上运行。这种语言实现了协程。

③.sleep

1
2
3
4
5
6
7
8
相同点:
1. Wait和sleep方法都可以使线程阻塞,对应线程状态是Waiting或Time_Waiting
2. wait和sleep方法都可以响应中断Thread.interrupt()
不同点:
1. wait方法的执行必须在同步方法中进行,而sleep则不需要。
2. 在同步方法里执行sleep方法时,不会释放monitor锁,但是wait方法会释放monitor锁
3. sleep 方法短暂休眠之后会主动退出阻塞,而没有指定时间的wait方法则需要被其他线程中断后才能退出阻塞。
4. wait()和notify(),notifyAll()是Object类的方法,sleep()和yield()是Thread类的方法

sleep 方法可以让线程进入Waiting状态 并且不占用CPU资源但是不会释放锁 直到规定时间后在执行 休眠期间如果被中断会抛出异常并清楚中断状态

④.join

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.geekagain.join;

/**
* @author hly
* @Description: TODO
* @create 2021-04-21 14:41
*/
public class JoinThreadState {
public static void main(String[] args) throws InterruptedException {
//主线程
Thread mainThread = Thread.currentThread();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
System.out.println(mainThread.getState());
System.out.println(Thread.currentThread().getName()+"执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
System.out.println("等待子线程执行完毕");
thread.join();
System.out.println("子线程执行完毕");
}
}

join 期间 线程的状态是 WAITING 状态

输出:
等待子线程执行完毕
WAITING
Thread-0执行完毕
子线程执行完毕

⑤.yield

使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。cpu会从众多的可执行态里选择, 也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了

6、ThreadLocal

①.threadlocal的作用和方法概念

1
2
3
4
5
6
7
8
9
10
11
12
13
1.保存一些业务内容 用户权限信息 用户系统获取用户名 userid等
2.这些信息在同一个线程里面 但是不同的线程使用的业务内容是不相同的
3.在线程的生命周期内 都通过这个静态的threadlocal实例的get() 方法来 自己set过得那个对象 避免将这个对象(例如user对象)
4.强调的是同一个请求内不同方法间的共享
5.不需要重写initialvalue方法 但是必须手动调用set方法

总结: 1.让某个需要用到的对象在线程建隔离 (每个对象都有自己独立的对象)
2.在任何方法都可以轻松获取对象

使用:
static final ThreadLocal<T> sThreadLocal = new ThreadLocal<T>();
sThreadLocal.set()
sThreadLocal.get()

②.ThreadLocal的好处

1.线程安全

2.不需要加锁 提高执行效率

3.高效利用内存和开销

4.避免传参的繁琐

③.ThreadLocal源码分析

做个不恰当的比喻,从表面上看ThreadLocal相当于维护了一个map,key就是当前的线程,value就是需要存储的对象。

这里的这个比喻是不恰当的,实际上是ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//set 方法
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//实际存储的数据结构类型
ThreadLocalMap map = getMap(t);
//如果存在map就直接set,没有则创建map并set
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

//getMap方法
ThreadLocalMap getMap(Thread t) {
//thred中维护了一个ThreadLocalMap
return t.threadLocals;
}

//createMap
void createMap(Thread t, T firstValue) {
//实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());//只能删掉自己的 this 传进去 就知道应该删除哪个了
if (m != null)
m.remove(this);
}

从上面代码可以看出每个线程持有一个ThreadLocalMap对象。每一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。在一个thread的里面有一个 成员变量 ThreadLocal.ThreadLocalMap threadLocals = null;

==接下来看看createMap方法中的实例化过程==

ThreadLocalMap

set方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//Entry为ThreadLocalMap静态内部类,对ThreadLocal的若引用
//同时让ThreadLocal和储值形成key-value的关系
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

//ThreadLocalMap构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//内部成员数组,INITIAL_CAPACITY值为16的常量
table = new Entry[INITIAL_CAPACITY];
//位运算,结果与取模相同,计算出需要存放的位置
//threadLocalHashCode比较有趣
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}

通过上面的代码不难看出在实例化ThreadLocalMap时创建了一个长度为16的Entry数组。通过hashCode与length位运算确定出一个索引值i,这个i就是被存储在table数组中的位置。

前面讲过每个线程Thread持有一个ThreadLocalMap类型的实例threadLocals,结合此处的构造方法可以理解成每个线程Thread都持有一个Entry型的数组table,而一切的读取过程都是通过操作这个数组table完成的。

总结如下:

  1. 对于某一ThreadLocal来讲,他的索引值i是确定的,在不同线程之间访问时访问的是不同的table数组的同一位置即都为table[i],只不过这个不同线程之间的table是独立的。
  2. 对于同一线程的不同ThreadLocal来讲,这些ThreadLocal实例共享一个table数组,然后每个ThreadLocal实例在table中的索引i是不同的。
get()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//ThreadLocal中get方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

//ThreadLocalMap中getEntry方法
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}

④.ThreadLocal 内存泄露

1.内存泄露 某个对象不再有用 但是占用的内存却不能被回收

2.只有两种可能性 key 泄露 或者 value 泄露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

弱引用 ThreadLocalMap 的每个Entry 都是一个key的弱引用 同时每个Entry都包含了一个对value的强引用
正常情况下 当线程终止 保存在Threadlocal 里面的value 会被垃圾回收 因为没有任何的强引用了
但是如果线程不终止 比如线程需要保持很久 那么key对应的value 就不能被回收 比如线程池就是用一个线程反复被使用 因此就有了一下的调用链:
Thread -- ThreadLocalMap -- entry (Key为null) -- value
因为 value 和Thread之间 还存在这个强引用链路 所以导致value无法回收 就可能会出现OOM
JDK已经考虑到这个问题了 所以在set remove rehash 方法中 会扫描 key为null 的 entry
并吧对应的value设置为null 这样value对象 就可以被回收

但是如果一个Threadlocal 不被使用 那么实际上set remove rehash 方法也不会被调用
如果同时线程又不停止 那么调用链就一直存在那么就会导致value的泄露

如何避免内存泄露??

调用remove方法 就会删除对应的entry对象 可以避免内存泄露 所以使用完ThreadLocal之后 应该调用remove()方法

在进行get 之前 必须先set 否则可能会报空指针异常吗 不会 返回null 但是进行装箱和拆箱 会出现错误

共享变量:如果在每个线程中ThreadLocal.set() 进去的东西本来就是多线程共享的同一个对象 比如static对象 那么多个线程的ThreadLocal.get() 取得的还是共享变量本身 还是有并发访问的问题

7、CAS原子性操作

①. CAS 是 compare And Swap 的缩写 比较交换 类似于java中的乐观锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.geek.AtomicIntegerDemo;

import java.util.concurrent.atomic.AtomicInteger;

/**
* @author hly
* @Description: CAS(compare and swap)demo
* @create 2021-04-24 0:01
*/
public class CAS {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
//value == expect update value to 10
System.out.println(atomicInteger.compareAndSet(5,10));
System.out.println(atomicInteger.get());
//value != expect 不更新
System.out.println(atomicInteger.compareAndSet(5,11));
System.out.println(atomicInteger.get());
}
}

②.CAS底层原理

1
2
3
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

底层大部分都是有unsafe完成,unsafe自己属于JDK– sun包下的,在我查看源码发现unsafe是 是CAS的核心类 由于Java 方法无法直接访问底层 ,需要通过本地(native)方法来访问,UnSafe相当于一个后门,基于该类可以直接操作特定的内存数据. UnSafe类在于sun.misc包中,其内部方法操作可以向C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于UNSafe类的方法.

CAS的全称compare-And-Swap 它是一条CPU并发原语 它的功能是判断内存某个位置是否为预期值 如果是则更改为新值整个过程是原子的

1
2
CAS并发原语提现在Java语言中就是sun.miscUnSaffe类中的各个方法.调用UnSafe类中的CAS方法,JVM会帮我实现CAS汇编指令.这是一种完全依赖于硬件 功能,通过它实现了原子操作,再次强调,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许中断,也即是说CAS是一条原子指令,不会造成所谓的数据不一致的问题. 简单来说是原子操作

③.ABA问题

1
CAS算法实现一个比较重要的前提是需要取出内存中某个时刻的数据并在当下的时刻比较并替换 那么这个时间差会导致数据的变化,比如说一个线程one从内存中位置V取出A 这时候另一个线程two也从内存位置取出A 那么线程TWO进行了一些操作将值变为了B 然后TWO又将V位置的数据变为了A 这时候线程one进行CAS操作发现内存仍然是A 然后线程one操作成功。尽管线程oneCAS操作成功但是不代表这个过程是没有问题的,因为对象地址变换了

解决方法:stampedReference.compareAndSet通过增加版本号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package com.geek.AtomicIntegerDemo;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
* @author hly
* @Description: 解决ABA问题
* CAS:对于内存中的某一个值V,提供一个旧值A和一个新值B。如果提供的旧值V和A相等就把B写入V。这个过程是原子性的。
* ABA:如果另一个线程修改V值假设原来是A,先修改成B,再修改回成A。当前线程的CAS操作无法分辨当前V值是否发生过变化。
* 解决方法:AtomicStampedReference。
* @create 2021-04-23 23:55
*/
public class ABASolution {

private static AtomicReference<Integer> atomicReference=new AtomicReference<>(100);
private static AtomicStampedReference<Integer> stampedReference=new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
System.out.println("===以下是ABA问题的产生===");
new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "t1").start();

new Thread(() -> {
//先暂停1秒 保证完成ABA
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//无法判断是否改变过,所以会写入新值2019
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
}, "t2").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("===以下是ABA问题的解决===");
new Thread(() -> {
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第1次版本号" + stamp + "\t值是" + stampedReference.getReference());
//暂停1秒钟t3线程
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

stampedReference.compareAndSet(100, 101, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t 第2次版本号" + stampedReference.getStamp() + "\t值是" + stampedReference.getReference());
stampedReference.compareAndSet(101, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t 第3次版本号" + stampedReference.getStamp() + "\t值是" + stampedReference.getReference());
}, "t3").start();

new Thread(() -> {
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t 第1次版本号" + stamp + "\t值是" + stampedReference.getReference());
//保证线程3完成1次ABA
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("====================="+stampedReference.getStamp());
//此时的stamp还是初始值,与当前stamp不同,证明被修改过,所以交换失败
boolean result = stampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t 修改成功否===" + result + "\t最新版本号" + stampedReference.getStamp());
System.out.println("最新的值\t" + stampedReference.getReference());
}, "t4").start();
}
}