一篇文章带你搞懂Java多线程宇宙

Java多线程
1.程序与进程线程
程序是静态的概念-程序是写好的一次性文本
进程/线程是动态的概念-程序及程序某段独立执行路径在机器上可反复执行


2.进程与线程
定义:
进程是程序的一次执行过程,生命周期由生到死,是操作系统进行资源分配的基本单位。
线程是进程内部的程序某段独立执行路径的一次执行过程轻量级的进程,生命周期由生到死,是操作系统进行调度的基本单位。一个进程可以包含若干个线程,但至少有一个线程,他们共享进程所拥有的全部资源。
操作系统用进程控制块和线程控制块结构体来标识和管理当正在运行的进程和线程。在Java中用Thread对象来表示某个线程程序中任何位置的代码/方法都是放在某个Thread线程执行的或者说某某Thread线程执行了某段代码/方法。

举例:
在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,
视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。
某些进程内部还需要同时执行多个子任务。例如,
我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,
同时还可以在后台进行打印,我们把子任务称为线程。
进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。


3.Java程序
当Java程序启动的时候,实际上是启动了一个JVM进程,
然后,JVM启动主线程来执行main()方法。在main()方法中,我们又可以启动其他线程。
这里Main线程和其他线程叫做用户线程。JVM还有一类专门为其他线程服务的线程叫做
守护线程。守护线程一般负责垃圾回收等监视和清理性质的工作。
当一个线程中的任务执行完毕(即一个线程对象Thread实例run()方法执行结束),线程结束。
在JVM中,所有非守护线程都执行完毕后,JVM进程就会自动退出结束。不关心有没有守护线程或者守护线程是否已结束。


4.进程是操作系统资源分配的基本单位,线程是操作系统调度的基本单位。
进程——资源隔离,提升系统稳定性
线程——调度开销小,提升cpu利用率

每个线程共享进程的代码段内存空间,所以我们编写多线程代码的时候,可以在任何线程调用任何函数。
每个线程共享进程的数据段内存空间,所以我们编写多线程代码的时候,可以在任何线程访问全局变量。
每个线程共享进程的堆,所以我们编写多线程代码的时候,可以在一个线程访问另外一个线程new/malloc出来的内存对象。
每个线程都有自己的栈的空间,所以可以独立调用执行函数(参数,局部变量,函数跳转)相互之间不受影响。


5.并发与并行
并发是一种现象: 逻辑上多个线程一起运行,实际上某个时刻只有一个线程在执行的现象叫并发。
并行也是一种现象: 如果cpu支持超线程技术(即cpu有多个核心或者一个核心可以运行多个线程),那么在同一时刻可以运行多个线程的现象叫并行。
并发是程序逻辑上的概念(靠多线程技术实现),
并行是物理的概念(靠cpu硬件配置实现),

如果程序是采用多线程编写的,那么运行在单核单线程cpu的机器上就会并发执行,
运行在多核多线程cpu的机器上就会并行执行。

Q&A:多核cpu上跑多线程技术编写的代码,同步机制还管用么?

 管用,只要解决了多线程环境中共享数据可见性问题。 

可见性问题可见性是一个线程对共享变量的修改,另一个线程能够立刻看到,如果不能立刻看到,就可能造成并发程序数据处理混乱,导致数据不一致。

单核CPU跑多线程代码单核CPU由于同一时刻只会有一个线程执行,而每个线程执行的时候操作的都是同一个CPU的缓存,所以,单核CPU不存在可见性问题。

一篇文章带你搞懂Java多线程宇宙_第1张图片

 多核cpu跑多线程代码,多核CPU上,每个CPU的内核都有自己的缓存。当多个不同的线程运行在不同的CPU内核上时,这些线程操作的是不同的CPU缓存。一个线程对其绑定的CPU的缓存的写操作,对于另外一个线程来说,不一定是可见的,这就造成了线程的可见性问题。

一篇文章带你搞懂Java多线程宇宙_第2张图片

 归根结底,导致可见性问题的根本原因是使用了cpu缓存,线程要是直接操作主内存中的共享变量就不会出现这种问题了。所以最直接的方式就是禁用缓存,但是这样问题虽然解决了,我们程序的性能可就堪忧了。合理的方案应该是按需禁用缓存,所谓“按需禁用”其实就是指按照程序员的要求来禁用。 JVM给程序员提供了按需禁用缓存方法,这些方法包括 volatile、synchronized 和 final 三个关键字。


6.线程调度无法预知
操作系统调度就绪态的线程是随机的,程序本身无法决定。任何一个线程都有可能先执行代码,任何一个线程都有可能在任何指令处被操作系统暂停,然后在某个时间段后继续执行。


7.Thread对象线程状态state转移:

一篇文章带你搞懂Java多线程宇宙_第3张图片

一篇文章带你搞懂Java多线程宇宙_第4张图片
当New出来的线程启动后,它可以在Runnable、Blocked、Waiting和Timed Waiting这几个状态之间切换,
直到最后变成Terminated状态,线程终止。

重要的话:只有Running态的线程持有cpu可以执行代码!!!!

状态 1:NEW
当线程被new创建出来还没有被调用start()时候的状态。

状态 2:RUNNABLE
Runnable包括Ready和Running
当线程被调用了start(),线程就进入了RUNNABLE(可运行状态)。
如果线程还没有被操作系统调度拿到cpu使用权,当前线程处于Ready(就绪状态)
如果线程被操作系统调度拿到cpu使用权,开始执行自己代码,当前线程处于Running(正在运行)
ps:处于就绪中的线程会存储在一个就绪队列中等待着被操作系统的调度机制选中

操作系统的线程切换指的是在Ready(就绪状态)和Running(正在运行态)的线程间切换。

状态 3:BLOCKED
当Running态(正在运行)的线程,执行到Synchronized同步代码块getLock时,没拿到锁,
被动阻塞,线程进入BLOCKED(被阻塞状态),放弃CPU使用权,暂时停止运行。
"直到线程获取到锁",进入就绪状态,才有机会转到运行状态。
ps:"直到线程获取到锁"不是说当前这个BLOCKED态的线程,又在尝试执行getLock代码了,
注意!BLOCKED态的线程没有cpu使用权,它不可能执行代码的。

实际上是:没能获取到Synchronized锁(某个对象的monitor锁)的BLOCKED态的所有线程
都被放入到该锁对象的【同步队列】中, 当持有当前锁的另一个线程,Running态时,
执行"unlock代码释放锁之后,底层调用会唤醒该锁对象同步队列中的所有线程,
让这些线程尝试抢锁。抢到了的线程, 状态修改为Ready就绪状态"代码逻辑,如此往复。
"直到线程获取到锁"这块逻辑是在唤醒者线程中执行的


状态 4:WAITING
当Running态(正在运行)的线程,执行到代码中wait()/join()/LockSupport.park()
方法时,主动阻塞,
线程进入WAITING(等待状态),放弃CPU使用权,暂时停止运行。
"直到线程被别的线程唤醒",进入就绪状态,才有机会转到运行状态。
wait-notify/notifyAll:
当前Running态的线程执行wait()方法后会底层会发生三件事:

1. 释放synchronized锁(隐含着必须先获取到这个锁才行)
2. 将当前线程状态修改为WAITING(等待状态)
3. 将当前线程被放入这个synchronized锁对象的【等待队列】
此时当前这个线程放弃了CPU使用权,暂时停止运行。
"直到线程被别的线程唤醒"的含义:

另一个获得了此synchronized锁的Running线程,执行同一个锁对象的 
notify/notifyAll方法,底层会做三件事
:
1.从此锁对象的等待队列中的线程随机选一个线程/全部选中
将线程状态修改为Ready(就绪状态)
2.将这些线程从此synchronized锁对象的等待队列中移除
3.释放synchronized锁(隐含着必须先获取到这个锁才行)
这(这些)Ready态的线程获取到时间片后进入Running态可以继续
执行下面代码了,不过执行wait()方法后面的代码前,需要重新去
getLock获取刚刚释放的synchronized锁
获取不到线程就会进入
Blocked状态,被放入锁对象的【同步队列】。

举例:
public synchronized String getTask() {
    while (queue.isEmpty()) {
        // 释放this锁:
        this.wait();
        // 重新获取this锁
    }
    return queue.remove();
}

join-jvm隐藏的notifyAll:
Main(){
    Thread t = new Thread(()->{...});
    t.join();
    ...
}
当前Running态的线程执行t.join()方法后会发生三件事。join()底层是wait()
1. 释放synchronized锁(隐含着必须先获取到这个锁才行)
2. 将当前线程状态修改为WAITING(等待状态)
3. 将当前线程被放入这个synchronized锁对象其实就是t对象的【等待队列】
此时当前这个线程放弃了CPU使用权,暂时停止运行。
而Running态t线程在执行完毕自己的run()方法后,jvm会自动调用t.notifyAll(),
将等待队列中的线程全都唤醒包括Main线程
即:
1. 将等待队列中的线程状态修改为Ready(就绪状态)
2. 将这些线程从此synchronized锁对象的等待队列中移除
3. 释放synchronized锁
此后Ready态就绪后的Main()线程重新获取到cpu时间片进入Running态,
重新获取t锁,拿到锁后继续往下执行自己的代码...
所以,其实 join 就是 wait,线程结束就是 notifyAll。现在,是不是更清晰了。

跟wait()/notify类似,但更高级的可以用在更多场景使用的阻塞和唤醒线程的方法:
LockSupport.park()与LockSupport.unpark(t):
为什么叫park呢,park英文意思为停车。
我们如果把Thread看成一辆车的话,
park就是让车停下,unpark就是让车启动然后跑起来。

当前Running态的线程执行LockSupport.park()方法,底层会把线程状态修改为WAITING(等待状态),此时当前这个线程放弃了CPU使用权,暂时停止运行。
另一个Running态的线程执行LockSupport.park(t)方法,底层会将上面的线程状态修改为
Ready就绪态, 让其重新等待被操作系统调度获取时间片,继续执行剩下的代码。

从线程状态流转来看,与 wait 和 notify 相同。从实现机制上看,他们甚至更为简单。
1. park 和 unpark 无需事先获取锁,或者说跟锁压根无关。
2. 没有什么等待队列一说,unpark 会精准唤醒某一个确定的线程

举例:
@Test
public void testLockSupport() {
    Thread parkThread = new Thread(() -> {
        System.out.println("开始线程阻塞");
        LockSupport.park();
        System.out.println("结束线程阻塞");
    });
    parkThread.start();
    System.out.println("开始线程唤醒");
    LockSupport.unpark(parkThread);
    System.out.println("结束线程唤醒");
}


对比:wait()/notify()必须在synchronized代码块中使用,这种使用方式很局限啊!
@Test
public void testWaitNotify() {
    Object obj = new Object();
    Thread waitThread = new Thread(() -> {
        synchronized (obj) {
            System.out.println("start wait!!!");
            try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}
            System.out.println("end wait!!!");
        }
    });
    Thread notifyThread = new Thread(() -> {
        synchronized (obj) {
            System.out.println("start notify");
            obj.notify();
            System.out.println("end notify!!!");
        }
    });
    waitThread.start();
    notifyThread.start();
}

状态 5:TIMED_WAITING
当Running态(正在运行)的线程,执行到代码中wait(等待时间)/join(等待时间)/
LockSupport.park(等待时间)或者sleep(睡眠时间)
,主动阻塞,线程进入TIMED_WAITING(等待状态),放弃CPU使用权(注意是立即放弃,不要被后面的long time迷惑了),暂时停止运行,
"直到线程被别的线程唤醒或者自己定时任务线程唤醒来"进入就绪状态,才有机会转到运行状态。
将上面导致线程变成 WAITING 状态的那些方法,都增加一个超时参数,就变成了将线程变成 
TIMED_WAITING 状态的方法了,
这些方法的唯一区别就是,从 TIMED_WAITING 返回 RUNNABLE,不但可以通过之前的方式,
还可以通过到了超时时间,返回 RUNNABLE 状态。
"自己醒来"的含义:是别的Running态的【某个定时任务】线程long time时间后将
TIMED_WAITING态的线程状态改为Ready就绪态
当前TIMED_WAITING态的线程
是没有cpu使用权的,不可能执行"唤醒"逻辑。

那有没有一个方法,仅仅让线程挂起,只能通过等待超时时间到了再被唤醒呢。——sleep(long)


状态 6:TERMINATED
当Running态(正在运行)的线程执行完了run()方法,线程就进入了。
线程Terminated终止的原因有
线程正常终止:run()方法执行到return语句返回;
线程意外终止:run()方法因为未捕获的异常导致线程终止;
别的线程中对当前线程的Thread实例调用stop()方法强制终止(强烈不推荐使用)。


【补充问题】
Q&A: 调用 JDK 的 Lock 接口中的 lock,如果获取不到锁,线程将挂起,此时线程的状态是什么呢?
有多少同学觉得应该和 synchronized 获取不到锁的效果一样,是变成 BLOCKED 状态?
JDK中锁的实现,是基于 AQS 的,而 AQS 的底层,是用 park 和 unpark 来挂起和唤醒线程,
所以应该是变为 WAITING 或 TIMED_WAITING 状态。

Q&A: 调用阻塞 IO 方法,线程变成什么状态?
比如 socket 编程时,调用如 accept(),read() 这种阻塞方法时,线程处于什么状态呢?
答案是处于 RUNNABLE 状态,但实际上这个线程是得不到运行权的,因为在操作系统层面处于阻塞态,需要等到 IO 就绪,才能变为就绪态。

一篇文章带你搞懂Java多线程宇宙_第5张图片

 
Q&A: 当前线程调用哪些方法会释放cpu资源?释放锁资源?
--释放cpu资源,释放锁资源的方法
wait()/wait(long)
join()/join(long)
--仅释放cpu资源,不释放锁资源
sleep()
--释放锁资源,释放cpu资源未知
notify()
notifyall()

8.Time-waiting/waiting与blocked
等待和阻塞-线程都会挂起即暂停执行。
区别是-
等待是主动的,线程中主动调用等待函数wait(),sleep()进入等待状态。
阻塞是被动的,线程因在synchronized同步机制中getlock没拿到锁进入阻塞状态。
等待函数包括不带时间的和带时间的wait(),join()与wait(long ),
join(long ),sleep(long ),wait/time-wait态的线程共同点是,在别的线程中调用相同锁对象的notify/notifyall可以将其唤醒,进入到Ready就绪态,重新等待被操作系统调度拿到cpu使用权Running态继续执行线程剩下的代码。
不同点是time-wait态的线程在等待时间到期后,定时任务线程将自己唤醒即把当前线程状态改为Ready就绪态,重新等待被操作系统调度拿到cpu使用权Running态继续执行线程剩下的代码。


9.Ready与Running
当一个线程的时间片用完的时候就会重新处于Ready就绪状态让出cpu给其他线程使用,
这个过程就属于一次线程切换。就绪的含义就是重新等待操作系统调度。线程从就绪态ready只能转换成运行中running态,不能转换成其它态,其它态都是running运行中的线程发生的事情(主动等待,被动阻塞)。

10.线程间通信
1)线程通信之join()方法
场景:一个线程还可以等待另一个线程运行结束再继续往下执行:
在当前线程中对目标线程Thread实例调用join()方法可以实现目标,此方法是阻塞的,当前线程会进入waiting状态。直到目标线程执行完毕jvm notifyALL把当前线程唤醒,重新状态流转Ready态-》Running态然后继续执行自己的代码。
如过join(long)指定了超时时间long time,此方法仍然是阻塞的,当前线程会进入time-waiting状态。直到目标线程执行完毕jvm notifyALL把当前线程唤醒 或者 超时后,专属的定时任务线程将当前线程唤醒,重新状态流转Ready态-》Running态然后继续执行自己的代码。


2)线程通信之interrupt()方法与isInterrupted()
应用场景:假设从网络下载一个100M的文件,如果网速很慢,用户等得不耐烦,
就可能在下载过程中点“取消”,这时,程序就需要中断下载线程的执行。
场景:一个线程想中断另一个线程,让对方赶紧结束(线程终止):
在当前线程中对目标线程Thread类实例调用interrupt()方法,发出"中断请求"信号,
目标线程在run()方法while循环中isInterrupted()检测到了这个信号,
放下手里的活儿, 执行线程终止逻辑代码。当然这"中断信号"只是请求信号哈
,目标线程如果不理会,不处理,即代码中没有isInterrupted()检测中断请求信号
并做处理逻辑,就达不到中断目的。

#注:
导致当前线程进入waiting/time-waiting状态的方法[sleep/join]调用都会
有发生InterruptedException的可能,因为如果别的线程调用当前线程实例的
interrupt()方法,而当前线程处于waiting/time-waiting态就会发生
InterruptedException异常。IDEA写代码时都会有异常提示。

3)线程通信之wait-notify使用(搭配synchronized锁)
场景:多个线程协调完成一件事情,当条件不满足时,线程进入等待状态;当条件满足时,线程被唤醒,继续执行任务。
wait()作用是使线程进入等待状态(Waiting/TimeWaiting),
notify()或notifyAll()作用是可以唤醒等待状态的线程(线程状态流转Waiting/TimeWaiting->Ready)。这种
等待-通知机制的实现原理就是锁对象的等待队列中线程入队和出队。
正确使用wait-notify注意的几点就是:
使用wait/notify之前应该首先获取到锁
调用wait/notify的对象应该与锁对象一致
体现在代码里:
(1)wait()、notify()和notifyAll()必须在synchronized修饰的方法或代码块中使用。
(2)在while循环里而不是if语句下使用wait(),确保在线程睡眠前后都检查wait()触发的条件(防止虚假唤醒)
(3)wait()/notify()方法必须在多线程共享的对象上调用。

举例:任务管理器
class TaskQueue {
    Queue queue = new LinkedList<>();

    public synchronized void addTask(String s) {
        this.queue.add(s);
        this.notifyAll();
    }

    public synchronized String getTask() throws InterruptedException {
        while (queue.isEmpty()) {
            this.wait();
        }
        return queue.remove();
    }
}

4)线程通信之await-signal使用(搭配ReentrantLock锁)

jdk包里面提供了两套同步方案Synchronized和Lock,两套线程通信方案(等待-通知机制)基于synchronized锁等待队列的wait-notify/notifyall,和基于Lock锁condition等待队列的await-signal/signalall。效果是一样的,

await()作用是使线程进入等待状态(Waiting/TimeWaiting)。
signal/signalall()作用是可以唤醒某个等待队列里等待状态的线程(线程状态流转Waiting/TimeWaiting->Ready)。

这种等待-通知机制的实现原理就是Lock锁condition等待队列中线程入队和出队。

一篇文章带你搞懂Java多线程宇宙_第6张图片

【总结】

Condation就是加强版的Object.wait/notify;必须在lock/unlock()之前使用,就像wait/notify必须在synchronized块中使用一样。

举例:任务管理器
class TaskQueue {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private Queue queue = new LinkedList<>();

    public void addTask(String s) {
        lock.lock();
        try {
            queue.add(s);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public String getTask() {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                condition.await();
            }
            return queue.remove();
        } finally {
            lock.unlock();
        }
    }
}

Q&A:如果面试官问你 Lock的Condition是什么?
JUC Condition[本质上就]是一个队列,让线程进入队列进行阻塞等待、或被唤醒出队列。
ReentrantLock 一把锁可以有多个队列,这是synchronized无法比拟的,synchronized只有一个队列。
condition1.await()就是进入condition1的队列; 
condition1.signal()唤醒condition1队列中的一个阻塞线程(唤醒后还需重新获得锁);

 

使用wait-notify/或者await-signal写代码容易出现的问题:

信号丢失:通知别人的线程先抢到了锁先执行代码,被通知的那个线程没抢到锁,阻塞起来(进的是同步队列),而非调用await()阻塞进来(进的是等待队列),也就是说通知别人的线程调用signal()时没啥用,等待队列里面没有线程,唤醒个寂寞,释放锁后,同步队列里面那个被通知的那个线程被唤醒,进入就绪态被调度执行重新去抢锁,抢到了锁之后可以执行代码,如果没加条件判断就调用await(),那就永远处于阻塞状态了,因为已经没有线程去调signal发信号去唤醒它了,永远休眠解决方式是被通知的那个线程一定要加条件判断,如果条件满足了就不用调用await了。即使信号丢失也不会导致自己一直休眠。

虚假唤醒: 多个线程因为条件不满足都调用await()释放手里的锁,处于等待状态,等待被唤醒,通知别人的线程拿到了锁执行了代码使条件满足了,调用signal唤醒所有等待队列中的等待线程,然后这些被通知线程都进入就绪态,重新被调度抢锁,抢到了锁继续往下执行,如果先抢到锁的线程从await起执行完剩下的代码,让此时的条件已经不满足了释放锁后,后面抢到锁的线程,没有重新判断条件是否满足,按造条件满足的假设去执行剩下代码,结果发生错误。解决方式是条件判断放在while循环中,这样拿到锁的线程重新判断条件是否满足,不满足继续await休眠。

【错误示例代码】

package cgglnc;


import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Hello world!
 */
public class App {

    private static Lock lock = new ReentrantLock();
    private static Condition toA = lock.newCondition();
    private static Condition toB = lock.newCondition();
    private static Condition toC = lock.newCondition();
    //private static int num = 1;
    public static void main(String[] args) {
        Thread a = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                try {
                    //while (num!=1){
                        toA.await();
                    //}
                    for (int i = 0; i < 5; i++) {
                        System.out.println("线程A打印第" + (i+1) + "次");
                    }
                    toB.signal();
                    //num =2;
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }, "线程A");

        Thread b = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                try {
                    //while (num!=2){
                        toB.await();
                    //}
                    for (int i = 0; i < 10; i++) {
                        System.out.println("线程B打印第" + (i+1) + "次");
                    }
                    toC.signal();
                    //num = 3;
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }, "线程B");

        Thread c = new Thread(new Runnable() {
            @Override
            public void run() {
                lock.lock();
                try {
                    //while (num!=3){
                        toC.await();
                    //}
                    for (int i = 0; i < 15; i++) {
                        System.out.println("线程C打印第" + (i+1) + "次");
                    }
                    toA.signal();
                    //num = 1;
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }, "线程C");

        a.start();
        b.start();
        c.start();


    }
}

11.线程同步含义:线程间按顺序执行某段代码
为什么需要线程同步
线程同步是为了解决多个线程同时读写共享变量,会出现数据不一致的问题。"同时"指二者都in progress ,但进度不一样,有先后顺序;"数据不一致"指代码执行完数据变化不符合预期结果;

线程同步非用不可? 根据具体场景来,如果你用了多线程,但是各线程只在自己的专属数据集上操作,自个玩自个儿的,互不干扰,就不需要用到Java提供的同步机制(锁)。

Q&A: 线程不安全的定义?

线程不安全, 讨论是方法/代码块在多线程并发执行的情况下,可能会出现数据不一致的情况。讨论的对象是方法和代码,不安全说的是数据不一致线程不安全说完整点说法是这个方法线程不安全或者说这是个线程不安全的方法


12.锁
1)锁是资源访问标识,是特殊的变量,是线程同步解决方案。
拿到锁的线程才能执行某个业务方法/代码块或操作某个共享变量,
拿不到锁的线程就无法执行某个业务方法/代码块或操作某个共享变量,
这样就确保了在多个线程并发时,只有一个线程在同一时刻执行某个业务方法/代码块
或操作某个共享变量,不会造成数据不一致问题。

2)什么变量可以作为锁
获取方式上有特殊性的变量,不是自己随便定义一个变量就能当锁用,
它要能保证一个线程获得了,别的线程就不能获得。Jdk给我们提供了一些常用锁工具。
--可以作为锁的变量:
共享变量本身即对象自己(this)-Monitor锁
synchronized(this) 
共享变量对应的Class实例
synchronized(Counter.class)-Monitor锁
共享变量内部持有的锁对象-ReentrantLock可重入锁
private final Lock lock = new ReentrantLock();
共享变量内部持有的锁对象-ReentrantReadWriteLock读写锁
private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
private final Lock rlock = rwlock.readLock();
private final Lock wlock = rwlock.writeLock();

3)代码中怎么用锁
--哪些地方需要加锁:
哪个方法/哪段代码多线程并发执行可能会导致数据不一致问题
--加锁的粒度:
给整个方法的加锁,还是部分代码块加锁,
加的锁是全局的唯一的一把锁,还是仅针对当前具体某一场景的定义的锁,
共享变量内部可以定义多把锁,每把锁针对不同的场景。
简单粗暴的加锁(synchronized/ReentrantLock),或者精细化加锁ReentrantReadWriteLock。
--正确加锁:
注意线程得争抢同一把锁,才能起到线程同步效果。
一个线程拿这把锁,另一个线程拿另一把锁,无法起到线程同步效果。
--模板化的代码写法:
同步方法:
public synchronized void fun(xxx){
    xxxx;
}

同步代码块:
synchronized(this) {
    xxxx;
}

可重入锁:
lock.lock();
try {
    ...
} finally {
    lock.unlock();
}

if (lock.tryLock(1, TimeUnit.SECONDS)) {
    try {
        ...
    } finally {
        lock.unlock();
    }
}


读写锁:
wlock.lock(); // 对写操作加写锁
try {
     ...
} finally {
    wlock.unlock(); // 释放写锁
}

rlock.lock(); // 对读操作加读锁
try {
     ...
} finally {
    rlock.unlock(); // 释放读锁
}


13.JDK自带锁对比1
Sychronized和ReentrantLock类型的锁没有细分读锁和写锁,
就只有一把,且线程执行读操作和写操作都依赖这一把锁。
这种类型的锁特点是【任何时候,系统中只能有一个线程拥有它】。
拿到锁的线程执行读操作时,别的线程拿不到锁,想读数据(执行读代码)读不了,
想写数据(执行写代码)写不了;
拿到锁的线程执行写操作时,别的线程拿不到锁,想读数据(执行读代码)读不了,
想写数据(执行写代码)写不了;
没拿到锁的线程一直阻塞在get lock那一行代码,结果是自己的时间片结束,
等下一次被操作系统调度,或者等拿到锁的线程把锁释放自己拿到,停止阻塞,
执行get lock下一行代码,执行流程继续往下走。
粗暴的解决了并发编程中数据不一致的问题。

ReadWriteLock类型的锁细分读锁和写锁,
一把ReadWriteLock锁包含一把读锁(Read lock)和一把写锁(Write lock),
线程执行读操作依赖ReadWriteLock锁中的读锁,读锁的特点是【系统中可以有多个线程同时拥有它】,
线程执行写操作依赖ReadWriteLock锁中的写锁,写锁的特点是【任何时候,系统中只能有一个线程拥有它】。
ReadWriteLock锁内读锁与写锁【在获取上有相互制约的关系】:
读锁被一个/多个线程获取后,写锁就无法被任何线程获取,直到系统中没有一个线程拥有读锁;
写锁被某个线程获取后,读锁就无法被任何线程获取,直到该线程将获得的写锁释放。
拿到读锁的线程执行读操作时,别的线程也可以拿到读锁,也能够执行读代码读数据。
但是别的线程无法拿到写锁,想写数据(执行写代码)写不了;
拿到写锁的线程执行写操作时,别的线程拿不到读锁,想读数据(执行读代码)读不了,
当然别的线程也拿不到写锁,想写数据(执行写代码)写不了;

总之使用ReadWriteLock锁可以提高读取效率:
ReadWriteLock只允许一个线程写入;
ReadWriteLock允许多个线程在没有写入时同时读取;
ReadWriteLock适合读多写少的场景。


14.JDK自带锁对比2
Synchronized 和 Lock 的主要区别
存在层面:Syncronized 是Java 中的一个关键字,存在于 JVM 层面,Lock 是 Java 中的一个接口
锁的获取: 
在 Syncronized 中,如果线程执行到sychronnized自动去拿锁,拿锁拿不到, 就会一直阻塞等待。
在 Lock 中,会分情况而定,如果线程执行lock()拿不到也会阻塞等待, 执行trylock()拿不到则立即返回false。
锁的释放条件:
Syncronized 1.获取锁的线程执行完同步代码后,自动释放;2. 线程发生异常时,JVM会让线程释放锁;
Lock 必须在 finally 关键字中手动释放锁,不然容易造成线程死锁

锁的状态:Synchronized 无法判断锁的状态,Lock 则可以判断,trylock()拿到了返回true,没拿到就返回false。
注意lock()方法不能判断状态,换句话说阻塞方法不能判断状态,非阻塞方法可以判断状态。

等待可中断: 在 Syncronized中,线程拿不到锁阻塞等待时,不会中断等待。
在 Lock 中,如果线程执行tryLock(加时间ts),去拿锁,阻塞等待超过ts后就不等待了,即中断等待,干点别的事情。

线程唤醒机制:
Synchronized中的唤醒是随机唤醒,唤醒正在等待此锁的线程队列中的某个线程
Lock可以绑定多个条件,实现分组唤醒,精确唤醒正在等待此锁的线程队列中的某个线程

锁的类型:
Synchronized 是可重入,等待不可中断,不可判断,非公平锁;
Lock 锁则是 可重入,可判断, 等待可中断,可公平可不公平锁

锁的性能:Synchronized 适用于少量同步的情况下,性能开销比较大。
Lock 锁适用于大量同步阶段:Lock 锁可以提高多个线程进行读的效率(使用 readWriteLock)

在竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,
【但是在资源竞争很激烈(很多个线程竞争同一个资源)的情况下,
Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;】


15.JDK锁总结
锁设计出来目的就是对资源的访问加以控制,就两种读锁和写锁 。
至于“共享,排他(有的叫独占),可重入,不可重入,公平,非公平,自旋,非自旋”都是对这两种锁获取特点的描述。
共享和排他,公平和非公平讨论的是多个线程获取锁的问题,
可重入和不可重入,自旋讨论的是一个线程它本身获取锁的问题。
共享和独占:共享是指该锁可以被多个线程所持有,
多个线程都能同获得该锁,而不必等到持有锁的线程释放该锁。
独占是指该锁任何时刻只能被一个线程所持有,其它线程则无法获得,
除非已持有锁的线程释放了该锁。
synchronized锁,ReentrantLock锁,ReentrantReadWriteLock锁中的write lock写锁都是独占锁,
ReentrantReadWriteLock锁中的read lock读锁是共享锁。

公平和非公平:Threads acquire a fair lock in the order in which they requested it。
A nonfair lock permits barging:threads requesting a lock can jump ahead of the queue of waiting threads if the lock happens to be available when it is requested。
公平是指多个线程按照申请锁(调用get lock方法)的先后顺序来获取锁。
先申请的线程先取得锁,其他线程进入队列等待锁的释放,当锁释放后,在队头的线程被唤醒。
非公平是指多个线程获取的顺序并不是按照申请锁(调用get lock 方法)的顺序,
有可能后申请的线程比先申请的线程优先获取锁。
当一个线程想获取锁时,先试图插队,如果占用锁的线程释放了锁,下一个线程还没来得及拿锁,
那么当前线程就可以直接获得锁;
如果锁正在被其它线程占用,则排队,排队的时候就不能再试图获得锁了,
只能等到前面所有线程都执行完才能获得锁。
synchronized锁,ReentrantLock锁(默认),ReadWriteLock锁都是非公平锁。
ReentrantLock锁(true)是公平锁 。

可重入和不可重入:可重入就是说当前线程已经获得某个资源上的锁,然后执行的临界区代码里又有获取此锁需求,
可以再次获取该锁而不会出现死锁。
不可重入是说当前线程已经获得某个资源上的锁,然后执行的临界区代码里又有获取此锁需求,
想再次获取获取不到被阻塞在get lock那一行代码,多线程环境中容易造成死锁问题。
synchronized锁,ReentrantLock锁,ReentrantReadWriteLock锁都是可重入锁。

自旋,非自旋锁的实现方式上是不是使用了CAS原理

JDK中提供的工具锁基本上都是独占,非公平,可重入的锁 。
synchronized锁,ReentrantLock锁都是独占,可重入的锁,区别是使用方式上自动挡与手动挡的区别:
(1)synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。
ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。
(2)synchronized可重入,因为加锁和解锁自动进行。不必担心最后是否释放锁;
ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。


16.同步代码块
同步代码块即被锁包着的代码块, 
因为锁的作用,这部分代码是原子操作,一个线程执行完毕,另一个线程才能执行,不会让线程乱搞把数据搞的不一致了。 当前执行同步代码块的线程(Running态),可能还能还没执行完,cpu时间片就到期了,被操作系统切换出去,变成Ready就绪状态等待下一次调度。虽然当前线程没有cpu使用权了,但是,是它会一直【占用着锁】,而且那些没争抢到锁的线程可能处于Blocked阻塞状态,Waiting等待状态,Time-waiting超时等待状态, 视写代码时具体采用的锁类型而定。也就是说他们根本都没机会被操作系统调度切换进来拿到cpu时间片执行自己代码,直到当前持有锁的线程,重新被切换进去(Running态),把同步代码块执行完,把锁释放, 把这些等锁的线程中某个/某些被唤醒(线程状态被修改为Ready就绪态),这部分逻辑是在唤醒者线程中完成的,它有cpu使用权,然后被唤醒的线程重新状态流转Ready态-》Running态然后去重新竞争锁,拿到锁的线程可以执行同步代码块,没拿到锁的线程又回到了Blocked阻塞状态,Waiting等待状态,Time-waiting超时等待状态,周而复始。


17.cpu资源 与 锁资源 
Ready就绪态的线程,被操作系统调度,有了cpu使用权,进入Running态可以执行代码了,如果遇到了同步代码,还必须拿到对应的锁才能往下执行,拿不到锁,当前线程就立即放弃CPU使用权,暂时停止运行(挂起),不论手里的时间片有没有到期,并进入Blocked阻塞状态,Waiting等待状态,Time-waiting超时等待状态,视写代码时具体采用的锁类型而定。操作系统会将cpu重新分配给当前就绪态的某个线程使用,提提高cpu的利用率哈。

拿到了锁的Running态的线程,可以执行同步代码块,如果代码很长,还没执行完,时间片到期了,操作系统该做线程切换还是会把当前线程切换出去(Ready就绪态)。但是当前线程依旧持有锁资源,如果操作系统此时把当前线程的就绪态的锁竞争线程切换进去(进入Running态),有了cpu使用权,准备执行同步代码时,拿不到锁,锁在别人手里,竞争线程就会立即放弃CPU使用权,暂时停止运行(挂起),并进入Blocked阻塞状态,Waiting等待状态,Time-waiting超时等待状态,等待被有锁的那个就绪态Ready线程切换进去把同步代码执行完毕,把锁释放,唤醒他们,让他们重新状态流转Ready态-》Running态然后去重新竞争锁,周而复始。

18.死锁
死锁是这样一种情形:
多个线程同时被阻塞,等待自己想要的某个资源被释放,由于目标资源无法释放,所以线程会无限期的阻塞,程序也就不能正常终止。(表现形式是cpu占有率很高,但是却无法正常服务)。

一篇文章带你搞懂Java多线程宇宙_第7张图片

示例代码:

 一篇文章带你搞懂Java多线程宇宙_第8张图片

java 死锁产生的四个必要条件:
1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:线程B占有线程A想要的资源,线程C占有线程B想要的资源,线程A占有线程C的资源。这样就形成了一个等待环路。
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

解决办法:
1)线程一次性申请自己所需要的所有资源

一篇文章带你搞懂Java多线程宇宙_第9张图片

 2)占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

 一篇文章带你搞懂Java多线程宇宙_第10张图片

3)线程都按某一顺序去申请资源,按照反序去释放资源。

一篇文章带你搞懂Java多线程宇宙_第11张图片

19.一个JVM系统的CPU偏高一般就是以下几个原因:

1.代码中存在死循环
2.JVM频繁GC
3.加密和解密的逻辑
4.正则表达式的处理
5.频繁地线程上下文切换

20. 多线程代码执行发生数据不一致三大Bug源头(可见性、原子性、有序性):

你写的多线程代码放在机器上跑,【偶尔会出现】代码跑出的结果跟预期不符合(数据不一致)根本原因计算机的底层机制(系统/硬件)运行机制导致的一些问题你没有规避。
譬如你用多个线程去执行相同的一段功能代码(给计数器累加),线程A执行代码中间会被【操作系统】切换出去另一个线程B切换进来执行,过一会儿A又切换回来继续执行,就有可能出现数据处理混乱情况。你希望要是这段代码A能一次性(原子操作)执行完就好了,就不会出现问题了。高级语言语句/语句块可不是原子操作,怎么破? JAVA提供了同步机制(锁),你写代码时用synchronized 把想原子化的代码包起来,解决了这种可能会发生的数据不一致问题。
再譬如你有俩线程操作共享变量,一个线程A执行修改了共享变量的值(新值),另一个线程B有用这个共享变量最新的值做一些操作的需求,结果发现操作的还是旧值,结果跟结果预期不符合,你希望一个线程A修改的变量值能被另一个线程B立刻看见就好了,就不会有问题了。之所以会有这种现象是因为现在硬件已经是【多核CPU】时代了,每个核心有自己的缓存,线程执行代码时访问的都是内存变量在缓存的拷贝,跑在这个核心的线程感知不到另一个核心上线程对数据的修改。
JAVA提供了可见性机制,你写代码时用Volatile修饰共享变量 ,它可以保证当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。简单操作的背后是底层操作系统的缓存一致性协议和总线锁的支持。
还譬如有一段功能代码(单例模式饿汉式的)结果就是我现在多个线程去执行这段代码时,
偶尔出现有的线程返回的实例为null值,跟预期不一样啊,扒一下【汇编代码】,发现其中片段 instance=newSingleton(),你写的时候是按一种顺序写的
(指令 1:分配一块内存 M;指令 2:在内存 M 上初始化 Singleton 对象;指令 3:然后 M 的地址赋值给 instance 变量),
但是在用当前OS平台的JDK【编译器】编译后你发现代码指令顺序给你调整了
(指令 1:分配一块内存 M;指令 2:将 M 的地址赋值给 instance 变量;指令 3:然后在内存 M 上初始化 Singleton 对象),编译器做优化了,把我的指令顺序都改了??【瞎几把优化】。
JAVA提供了禁止编译器做指令重排的机制,你写代码时用Volatile修饰共享变量,编译时会自动插入指令屏障指令保证代码执行顺序按你代码写的来。
这些由于软硬件运行机制导致的多线程代码执行结果不符合预期的问题被抽象称为可见性、原子性、有序性问题,是多线程代码数据不一致问题bug源头

坑一:CPU缓存导致的可见性问题
坑二:线程切换导致的原子性问题
坑三:编译优化带来的有序性问题

JMM(java内存模型)就是对这种软硬件运行机制、问题及解决方案的抽象描述。因果关系理顺:   先有软硬件运行机制导致的跑多线程代码出现的问题,再有Java提供的解决方案(volatile、synchronized、final、concurren包)让你写代码时【按需使用】规避这些问题。

ps: 初学多线程时,大篇幅讲锁啊,其实讨论的都是原子性问题没解决好导致的数据不一致问题,
不知道还有可见性问题,顺序性问题导致的跑多线程代码时数据不一致。

三大问题复现示例代码

#可见性问题

public class App {
    boolean  flag;
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
    public boolean getFlag() {
        return flag;
    }
    public static void main(String[] args) throws Exception {

        App app = new App();
        app.setFlag(false);
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);//注意:睡觉的目的是希望让线程t醒来就绪后被调度到别的核心上,否则就有可能复现不了,线程t跟main在一个核上跑
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                app.setFlag(true);
                System.out.println(Thread.currentThread().getName()+"线程把共享变量app的flag值改为了"+app.getFlag());
            }
        });
        t.start();
        while (true){
            if(app.getFlag()){
                System.out.println("我看到别的线程改动的值了:"+app.getFlag());
                break;
            }
        }
    }
}
#原子性问题

public class App {
    public static int count = 0;
    public static void main(String[] args) throws Exception {
        Thread add = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    count += 1;
                }
            }
        });
        Thread dec = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    count -= 1;
                }
            }
        });
        add.start();
        dec.start();
        add.join();
        dec.join();
        System.out.println("count="+count);

    }
}
#顺序性问题

public class App {
    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                Single single = Single.getSingle();
                System.out.println(Thread.currentThread().getName()+"获取了单例"+single.toString());
                if(single==null){
                    System.out.println(Thread.currentThread().getName()+"拿到了null");
                }

            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                Single single = Single.getSingle();
                System.out.println(Thread.currentThread().getName()+"获取了单例"+single.toString());
                if(single==null){
                    System.out.println(Thread.currentThread().getName()+"拿到了null");
                }

            }
        });

        t1.start();
        t2.start();
    }
}

class Single{
    private Single() {
        System.out.println(Thread.currentThread().getName()+"调用了构造方法");
    }
    private static Single single = null ;//懒汉

    public static Single getSingle() {
        if (single == null) {
            synchronized (Single.class) {
                if (single == null) {
                    single = new Single();
                }
            }
        }
        return single;
    }
}

Q&A:synchronized能解决可见性问题么?                                                                                            改造一下那个volatile代码,事实证明可以原理是:JVM关于synchronized的两条规定

  • 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。
  • 线程解锁前,必须把共享变量的最新值刷新到主内存中。

#synchronized关键字解决可见性问题
public class App {
    boolean  flag;
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
    public boolean getFlag() {
        return flag;
    }
    public static void main(String[] args) throws Exception {

        App app = new App();
        app.setFlag(false);
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                app.setFlag(true);
                System.out.println(Thread.currentThread().getName()+"线程把共享变量app的flag值改为了"+app.getFlag());
            }
        });
        t.start();
        while (true){
            synchronized (app){//synchronized关键字可以解决可见性问题
                if(app.getFlag()){
                    System.out.println("我看到别的线程改动的值了:"+app.getFlag());
                    break;
                }
            }

        }
    }
}

参考:Java多线程中解决内存可见性问题的两种方法:synchronized、volatile 及CAS算法

21. 内存模型与JAVA内存模型的定义:

内存模型就相当于是在多核时代下硬件的使用说明书(描述该平台采用的CPU架构,缓存体系,内存访问方式,缓存一致性策略,指令重排序策略,内存屏障指令),只要按照说明书来编写多线程程序,那不管底层如何乱序,如何优化,都不会影响你代码的正确性。不同硬件平台的内存模型,描述的内存访问情况,缓存一致方式,指令乱序情况,及禁止乱序的方式都完全不一样,它们只适用于自己平台的指令集。Java为了屏蔽硬件和操作系统访问内存的各种差异,提出了「Java内存模型」的规范,并且Java虚拟机实现了这种规范(即Java内存模型是虚拟机这个硬件的使用说明书),保证了Java程序在各种平台下对内存的访问都能得到一致效果。

22.ThreadLocal工具

需求背景:
Q&A: 如何在一个线程内传递状态(上下文)? 让线程处理流程中很深层次调用的方法中能访问到它或者让线程在处理流程中的任何节点调用的方法中都能访问到它?
方式一:给线程执行到的每个方法增加一个context入参,这种方式比较麻烦也不优雅,如果到目标调用方法中间还有很多调用方法,一个一个加很繁琐,有些节点方法中根本就不需要用到这个参数显得很突兀。
方式二:将状态信息(上下文)数据放在当前线程对应的那个Thread对象里,每次你就能根据方法自身需要,像调用Thread.currentThread().getName()那样Thread.currentThread().getxxContext()获取上下文数据。
刚好Thread类中定义了这样的成员变量:
ThreadLocal.ThreadLocalMap threadLocals = null;//线程独享

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//InheritableThreadLocal主要用于子线程创建时,需要自动继承父线程的ThreadLocal变量,方便必要信息的进一步传递。

问题是Thread类中没提供这个成员变量的set和get方法啊?ThreadLocal提供了。
ThreadLocal类作用就是将上下文变量跟某个线程绑定。
绑定方式:在线程执行的某个方法或代码块中set(xxx),在此位置之后所有的流程方法中都能通过get()访问到它了。
static ThreadLocal 
userInfoContextThreadLocal = new ThreadLocal<>();

static ThreadLocal 
taskInfoContextThreadLocal = new ThreadLocal<>();


铺垫的知识
1)三个概念:
内存泄露存在某个/某些实例对象,已经不再使用了,但是还被引用着,导致GC回收不了,空间浪费。这种情况都是因为代码设计缺陷原因导致的。如果这种不可回收的实例对象随着时间累计的越来越多,最终会导致OOM。

引用=特殊的变量,存的是地址
强引用
:我们平时用到的引用99.999%都是强引用(定义xx类型的引用变量指向某个xx类型实例),
强引用指向的对象不会被GC回收,即使内存不够。如果想要取消一个对象的强引用,将该引用赋值为null即可。                                                                                                                                    软引用:  用SoftReference包装类型创建的引用叫软引用,被软引用指向的对象平时不会被GC回收,但当内存不足时,就会回收该对象。      
弱引用用WeakReference包装类型创建的引用叫弱引用,GC回收的时候,JVM不管内存是否充足都会回收的这一类引用指向的对象,我们使用java.lang.ref.WeakReference给对象创建一个弱引用。
如:WeakReference weakReference = new WeakReference(new Student("sunch"));

FinalReference 强引用,如果一个对象具有强引用,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
SoftReference  软引用,如果一个对象只具有软引用,如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。
WeakReference  弱引用,如果一个对象只具有弱引用, 在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

2)发生内存泄漏的根本原因是:
由于从thread实例到value实例之间的Thread -> ThreadLocalMap -> entry -> value ->value实例这条引用链是强引用链(强引用组成的链条),也就是说entry上的value所引用的实例的生命周期和线程的生命周期一样,如果线程thread实例一直不执行结束被GC回收销毁(如线程池中的线程复用场景),导致value实例一直回收不了。如果像这样的线程有很多,那么这样的value实例也会越来越多,占着内存,导致内存泄露。

3)解决方式:
threadlocal设计者把key设置成了弱引用类型的key,由于从thread实例到value实例之间的Thread -> ThreadLocalMap -> entry ->key->threadlocal实例,最后一环是弱引用,如果创建threadlocal实例的那个线程觉得threadlocal实例用不上了,主动将栈中threadlocal实例的强引用置为null或者线程执行结束,栈销毁。现在threadlocal实例没有外部强引用,只剩下key这个弱引用,垃圾回收线程会回收掉threadlocal实例,并将所有的弱引用key置为null,既然key用不上了,value实例也就没有必要存在了,
threadlocal提供了set(this,value)和get(this)方法中,在index定位+线性探测过程中发现有key=null的,全都把value置为null,防止内存泄漏。【重点】
当然最佳实践是:
在使用threadlocal绑定上下文变量到thread实例完成一些些需求时,线程执行的方法/代码中不再需要上下文变量了,自己主动调用threadlocal.实例remove(),销毁value实例。

#threadLocalA 865977613
#threadLocalB -1788458156

线程0:
threadLocalMap:
  index=4,entry(-1788458156,bbb0)
  index=13,entry(865977613,aaa0)
线程1:
threadLocalMap:   
  index=4,entry(-1788458156,bbb1)
  index=13,entry(865977613,aaa1)
 

set方法流程: 
Thread类成员变量threadLocalMap仅仅是个数组entry[k,v] table,其中k是threadLocal实例的地址(referent),v是set的值,插入时,如果是第一次插入,会把这个table数组给new出来,【初始容量是16】,【阈值是16*2/3 = 11】后续插入(key,v)时:
1)先定位int i = key.threadLocalHashCode & (table.length - 1);
2)判断table[i]位置上是不是空,是空直接new一个entry插入,table[i] = new entry(key,v)
3)不是空,判断key是不是相等,相等则value覆盖oldvalue,不相等移动到下一个坐标table[i+1]去重复做23操作直到找个位置插进去 (解决HASH冲突的方式,线性探测再散列)。

  

你可能感兴趣的:(码公子,java)