如何应对Android面试官->线程与进程、手写ThreadLocal
基础概念
什么是进程和线程?
进程:一个正在运行的应用程序;操作系统管理的最小单元(操作系统进行资源分配的最小单位),一个进程至少一个线程,或者多个线程;进程与进程之间是相互独立的;
资源:运行一个程序需要 CPU、内存空间,磁盘IO,这些都属于运行一个程序所需要的资源;
线程:进程的运行方法;CPU 调度的最小单元,如果一个进程还有一个线程没有杀掉,那么进程就还是处于存活状态(线程依附与进程而存在);线程之间是可以共享资源的,包括CPU、内存空间,磁盘IO;
CPU核心数和线程数的关系?
早期一个核心一个线程,后期超线程技术之后 一个核心两个线程;
什么是多核?早期的计算机芯片上面只能放一个逻辑核心,随着摩尔定律的失效,大家发现在CPU上在缩短这个宽度3纳米,在往小缩短就要受到量子物理的约束,此时提高晶体管的密度是不可行的了;那么把多个物理核心集成到一块芯片上,也就是说在一块物理芯片上会有多块处理器,这就是所谓的CPU多核;
为什么CPU的核心数限制,但是我们在启动一个应用程序之后 启动了很多线程(超过了线程数)但是我们的程序还是能正常执行?
CPU时间片轮转机制?
CPU 运行过程中分时间的机制;
CPU 调度,会把 1秒钟 分成好多小的单位,假设每个单位都是 1 纳秒,所有线程都会去操作系统抢占这个 1纳秒,谁抢到了谁就去执行,假设有三个线程 A B C,A 抢到了这一纳秒的时间,那么就去运行 A 线程的代码,当运行到A线程的某个代码位置的时候,这 1纳秒 用完了,就要释放 CPU 出来,然后和其他线程继续去抢第二个 1纳秒,假如第二个时间片被其他线程抢到了,就执行其他线程,依次往后所有线程继续抢,当 A 再次抢到的时候,要接着 A 执行到的地方继续执行,谁来记录 A 执行到了什么地方,就由程序计数器来负责;
并发、并行?
并行:同时有多个线程在执行,可以同时运行的任务数;
并发:既有前台 又有后台,交替执行;总结的同一个时间点的吞吐量(讨论并发的时候不能脱离时间单位,否则没有意义);
举例子:假设咖啡机在 1分钟 之内,有四个人拿到了咖啡,那么咖啡机在一分钟内的并发量就是 4
综合来说:并发就是说我们的应用能交替的执行不同的任务,只不过 CPU 或者操作系统使用了不同的技术,使人以不可察觉的速度在运行这些任务。看起来好像达到了同时执行的效果,但其实不是;
线程存在并行和并发 协程只有并发没有并行;
高并发编程的意义,好处是什么?
平衡使用我们的线程,不要 CPU 过累;
Java程序默认是多线程还是单线程?
Java 程序默认是多线程的;执行一个 Java 程序,可能会有 6 个 也可能会有 8 个;
public class OnlyMain {
public static void main(String[] args) {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "], "
+ threadInfo.getThreadName());
}
}
}
多次执行,会开启 6 - 8 个线程;
为什么要用线程池来管理线程,是否可以无限制的创建线程?
Linux 下一个进程所能开的线程数最多 1000 个;Windows 下一个进程所能开的线程数最多 2000个;
那么线程创建会分配栈空间,缺省是 1MB,只单纯的 new 1000 个线程,所占用的内存就达到了 1G;
资源回收的动作放在 finalize 方法中为什么会不靠谱?
因为很有可能不会被执行,因为执行这个方法是一个单独的 Finalizer 线程,这个线程是守护线程,当主线程一旦退出,这个线程就跟着退出了,那么这个方法很有可能来不及调用,这个线程就结束了,相关的资源释放工作还没做完;
Thread
线程实现有几种方式?
两种方式,一是 extends Thread,一是 implements Runnable,实现 Callable 接口严格意义上来说不是
源码中的注释 【There are two ways to create a new thread of execution】说明只有两种方式来创建线程;
继承 Thread 类
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
}
}
PrimeThread pThread = new PrimeThread(1000L);
pThread.start();
实现 Runnable 接口
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
}
}
PrimeRun pRun = new PrimeRun(1000L);
Thread thread = new Thread(pRun);
thread.start();
实现 Callable 接口 本质上还是实现了 Runnable 接口,所以它其实可以和 Runnable 归位一类
public class ThreadCallable implements Callable<String> {
@Override
public String call() throws Exception {
return null;
}
}
public static void main(String[] args) {
ThreadCallable threadCallable = new ThreadCallable();
FutureTask<String> futureTask = new FutureTask<>(threadCallable);
new Thread(futureTask).start();
}
本质上:Callable 的实例会交给 FutureTask,而 FutureTask 实现了 RunnableFuture 接口,而 RunnableFuture 接口继承自 Runnable 接口;
所以:实现线程的方式只有两种
继承 Thread 和 实现 Runnable 的区别 ?
Thread 是对线程的抽象,Runnable 是对任务(业务逻辑)的抽象;
线程的安全终止
Thread 源码中有一个 stop 方法,但是这个 stop 方法被 JDK 打上了一个废弃的注解 @Deprecated
Thread 源码中还有一个 destory 方法,但是这个 destory 方法被 JDK 打上了一个废弃的注解 @Deprecated
Thread 源码中有一个 suspend 方法,但是这个 suspend 方法被 JDK 打上了一个废弃的注解@Deprecated
JDK并不建议使用这几个方法,这些方法带有很高的强制性;例如 suspend 方法,会让线程发生一次上下文切换,从当前的一个可运行状态切换到一个挂起状态,但是 suspend 被调用后,相关的线程并不会释放锁,而是占着这个资源进入一个睡眠状态,这样就很容易发生死锁的问题;同样的 stop 在终结一个线程的时候,比较野蛮会强制把当前线程一把干掉,不关心当前线程有没有释放资源;
例如:开启一个线程,写一个10K的文件,当写到4K的时候,调用了 stop,那么这个线程就会被野蛮停掉,那么这个文件就是缺省的,不能被使用的;
所以说 stop、suspend、destory等 为什么不建议使用?因为它可能导致线程所占用的资源不会释放;
Thread 中提供了比较和谐的方式,thread.interrupt() 发起停止信号,和 isInterrupted() 接收停止信号结合使用;
Thread 源码中,有 4 个关于 interrupt 的方法
interrupt 对线程进行终断,但并不是真真正正的终止一个线程,而是发给线程的一个终断标志位,当调用这个方法的时候,其实就是给线程打了一个招呼,不代表这个线程就要立即停止工作,而且这个线程完全可以不理会这个终断请求(也就是说 JDK 中,线程是协作式的,而不是所谓的抢占式);
isInterrupted 判断当前线程是否被终断;
public static class UseThread extends Thread {
public UseThread(String name) {
super(name);
}
@Override
public void run() {
super.run();
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " interrupt flag = " + isInterrupted());
while (!isInterrupted()) {
// while (true) {
// while (Thread.interrupted())
System.out.println("thread is running");
System.out.println(threadName + " inner interrupt flag = " + isInterrupted());
}
System.out.println(threadName + " interrupt flag = " + isInterrupted());
}
}
public static void main(String[] args) throws InterruptedException {
UseThread useThread = new UseThread("endThread");
useThread.start();
Thread.sleep(10);
useThread.interrupt(); // 终断线程,设置一个标志位为 true
}
当调用了 interrupt 的时候,如果不用 isInterrupted 接收的话,也就是 while(true) 放开,那么线程还是会一直执行
interrupted 这个方法是 Thread 的一个静态方法,判断当前线程是否被终断,与 isInterrupted 在使用上有区别,区别如下:
Thread.interrupted 会将是否终断的标志位清除,如果设置成了 true,那么它就会重置回 false
那么,在 Runnable 中 如何终断线程呢?
通过 Thread.currentThread().isInterrupted();
public class ThreadRunnable implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + " interrupt flag = " + Thread.currentThread().isInterrupted());
while (!Thread.currentThread().isInterrupted()) {
System.out.println("thread is running");
System.out.println(threadName + " interrupt flag = " + Thread.currentThread().isInterrupted());
}
System.out.println(threadName + " interrupt flag = " + Thread.currentThread().isInterrupted());
}
}
Thread.sleep() 方法 会抛出一个 InterruptedException,那么当我们 catch 住这个异常之后,获取当前终断标志位的时候 是 true 还是 false ?
public static class HasInterruptException extends Thread {
@Override
public void run() {
super.run();
while (!isInterrupted()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "in InterruptedException interrupt flag is " + isInterrupted());
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "I am extends Thread");
}
System.out.println(Thread.currentThread().getName() + " interrupt flag is" + isInterrupted());
}
}
结果是 false ,也就是说线程抛出这个异常的同时,把标志位又重置成了 false;
所以 需要我们在 catch 的时候,手动的调用一下 interrupt() 方法;
public static class HasInterruptException extends Thread {
@Override
public void run() {
super.run();
while (!isInterrupted()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "in InterruptedException interrupt flag is " + isInterrupted());
interrupt();
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "I am extends Thread");
}
System.out.println(Thread.currentThread().getName() + " interrupt flag is" + isInterrupted());
}
}
所以:JDK 针对 sleep、wait 等增加异常抛出的原因是什么?有什么好处?
修改终断标志位为 false,可以让我们及时的释放资源,并手动的调用 interrupt 来终断线程,防止死锁的发生;
run 和 start 的区别?
run 是方法,和线程没有任何关系,start 会走底层,最终调度到 run 方法;run方法一般是业务实现的方法,可以多次调用,也可以直接通过声明的 thread 对象来调用 run 方法;
如果我们直接调用 run 方法的时候 会发生了什么?
public class ThreadRun {
public static void main(String[] args) {
ThreadRunTest runTest = new ThreadRunTest();
runTest.run();
}
static class ThreadRunTest extends Thread {
@Override
public void run() {
super.run();
int i = 90;
while (i > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("CurrentThread: " + Thread.currentThread().getName() + ", and i = " + (i--));
}
}
}
}
直接调用 run 方法,当前逻辑会在主线程执行,不会在子线程执行,只有调用 start 方法才会在子线程中执行;
所以:start 才是真正意义上的启动了一个线程并和操作系统里面的线程进行一个挂钩。run 本质上就是普通类里的一个普通方法而已。对于 new 出来的一个线程 start 只能调用一次,多次调用就会抛出异常;
调用 start0 方法 真真正正的开始执行线程,调度到 native 层;
start 方法调用两次会抛出异常;通过 threadStatus != 0 的判断,会抛出 IllegalThreadStateException 异常;
线程状态(生命周期)
新建(初始):new 一个新的线程对象,就称为线程的初始,但是初始不代表这个线程就开始执行了,只有调用了 start 方法之后才会进入运行态;
运行态:
- 就绪(可运行状态)
- CPU 时间片用完了或者被操作系统剥夺了或者自动放弃了,线程就会出于就绪中,等待下次的时间片;
- 调用 join 获得执行权,通过系统调度进入运行中;
- 运行中
-
拿到了时间片,被分配了 cpu,就处于运行中;
-
调用 yield 就会进入就绪状态;
- yield 方法 让当前线程让出 cpu 的执行权(仅仅只是让出 cpu 执行权,但是不会让出锁),当前线程就会由运行态进入可运行状态(就绪);
- join 方法 让cpu放弃当前正在执行的线程,转而来执行调用 join 方法的线程;
等待态:
- 等待
- 线程调用了 wait、join、sleep、LockSupport.park() 方法,进入等待态;
- 当线程调用了 notify 或者 notifyAll、LockSupport.unpark(Thread) 之后,线程就会重新进入运行态;
- 等待超时
- 线程调用 wait(long)、sleep(long)、 join(long)、LockSupport.parkNanos()、LockSupport.parkUntil() 方法,传入时间,当到达这个时间阈值之后,就是等待超时态;
- 超时之后,或者当线程调用了 notify、notifyAll、LockSupport.unpark(Thread) 之后,线程就会重新进入运行态;
阻塞态:
- 线程调用了synchronized 修饰的代码或者代码块,如果没有拿到锁,就会处于阻塞状态。只有 synchronized 修饰的才会进入阻塞状态,其他都不是,例如:调用 Lock,它底层实现就是调用了LockSupport,所以调用 lock 是进入了等待或者等待超时;
- 获取到锁之后,线程就会重新进入运行态(属于被迫进入);
执行完成(终止)态:
- run方法跑完了,就会进入完成态;
- 调用了 stop 方法,强制关闭,也会进入完成态
- 调用了 setDeamon 方法,将当前线程变成守护线程,当进程中的所有非守护线程都执行完毕之后,那么守护线程就会跟着死亡进入完成态;
如何控制线程的执行顺序?
join 关键方法,join 的意思是使得 cpu 放弃当前线程的执行,并返回对应的线程,例如 调用 A 线程的 join 方法,则主线程放弃 cpu 控制权,并返回 A 线程继续执行,直到 A 线程执行完毕,A 执行完毕释放之后主线程继续执行调用 b.start();
ThreadJoinTest a = new ThreadJoinTest("A");
ThreadJoinTest b = new ThreadJoinTest("B");
a.start();
a.join();
b.start();
// 会在 a 执行完毕之后 才执行 b,一定要注意这个调用顺序
能不能控制线程的优先级?
不会考虑,setPriority() 方法能不能发挥作用完全由操作系统来决定,因为线程的优先级很依赖于系统的平台,所以这个优先级无法对号入座,无法做到你想象中的优先级,属于不稳定,有风险;因为某些开源框架,也不可能依赖线程优先级来设置自己想要的优先级顺序,这个是不可靠的;
守护线程
先来看一组示例
上面这些除了 main(主线程) 之外都可以认为是守护线程,守护线程也包括GC;守护线程其实是一种支持类的线程,主要是对程序后台做一些调度、支持性的工作(比如内存的回收等等);
当我们启动了一个进程的时候,这个进程中会通过 new Thread 启动了很多的线程,那么这些通过 new Thread 启动的线程,如果不设置 setDameon 都是用户线程(非守护线程),对于 JDK 内部启动的线程或者通过参数配置(setDameon)启动的线程都是守护线程;
在一个进程里面,如果所有的用户线程(非守护线程)都结束之后,这个进程也就跟着停止了,那么所有的守护线程也就结束了;
public class DaemonThread {
public static void main(String[] args) throws InterruptedException {
UseThread useThread = new UseThread();
useThread.setDaemon(true);
useThread.start();
Thread.sleep(5);
}
static class UseThread extends Thread {
@Override
public void run() {
super.run();
try {
while(!isInterrupted()) {
System.out.println(Thread.currentThread().getName() + " extends Thread");
}
} finally {
// 守护线程的 finally 不一定起作用
System.out.println("finally");
}
}
}
}
执行之后,因为 UseThread 被设置成了守护线程,当我们的主线程执行完毕(Thread.sleep(5) 执行之后)之后,这个 UseThread 就会停止执行,不需要我们调用 useThread.interrupt();
但是,通过 Console 中的打印结果来看,finally 并没有执行;
也就是说我们不能通过 finally 代码块来确保执行关闭、清理资源;finally 能否执行完全看操作系统的调度;
用户线程不用担心,finally 一定会执行;
能不能指定 CPU 去执行某个线程?
不能。Java 做不到,唯一能够去干预的就是 C 语言调用内核 API 去指定才行;
多线程间数据如何共享(锁)
多个线程之间访问同一个对象,如果我们不做操作会发生什么?
public class SyncThread {
private int count = 0;
public void addCount() {
count++;
}
public static void main(String[] args) throws InterruptedException {
SyncThread syncThread = new SyncThread();
AddCount addCount1 = new AddCount(syncThread);
AddCount addCount2 = new AddCount(syncThread);
addCount1.start();
addCount2.start();
Thread.sleep(5);
System.out.println(syncThread.count);
}
static class AddCount extends Thread {
private SyncThread simpleOper;
public AddCount(SyncThread simpleOper) {
this.simpleOper = simpleOper;
}
@Override
public void run() {
super.run();
for (int i = 0; i < 10000; i++) {
simpleOper.addCount();
}
}
}
}
执行多次,都不是想要的结果(20000),多线程操作同一个全局变量,导致结果不正确;
还有如下示例代码:
public class GpsEngine{
private static GpsEngine gpsEngine;
public static GpsEngine getGpsEngine() {
if(gpsEngine == null) {
// 时间片轮转机制当Thread-0执行到这里的时候,执行结束,CPU 执行器被操作系统调度器给释放了,那么 Thread-0 就会暂停到这里,当 Thread-0
// 再次抢到的时候,由程序计数器告知,继续执行,那么就又会执行一次对象的创建,违背了单例的初衷(一个实例)
// 但是 Thread-1 执行到创建对象的这行代码 才执行结束,CPU 执行器被操作系统调度器释放。
gpsEngine = new GpsEngine();
}
return gpsEngine;
}
}
// 上述代码线程不安全,如果多个线程同时获取GpsEngine实例,就会出问题
多线程创建单例问题,不符合了单例的一个对象原则;
如何解决?加锁!
synchronized // 内置锁。保证在某一个时刻只有一个线程访问这个变量
对象锁
synchronized 作用在方法上,把这个方法变成同步方法
public synchronized void addCount() {
count++;
}
synchronized 作用在代码块上,把这段代码变成同步代码块
Object object = new Object();
public void addCount1() {
synchronized (object) {
count ++;
}
}
这些也都可以叫作 对象锁,锁的作用是在同一个对象上;
如果锁在两个对象上,则这个就会失效;
类锁
synchronized 作用在静态方法上,会被叫作 类锁(class对象锁)
public static synchronized void addCount() {
count++;
}
每一个类在进行类加载的时候,都会在 JVM 中有一个 class 对象,static 方法上进行加锁的时候,意味着加锁的是一个 class 对象,本质上还是一个对象锁。这个对象就是每个类在虚拟机中所拥有的唯一的一个 class 对象;
类锁和对象锁之间也是互相不干扰的,因为不是同一个对象;
但是:
这种情况下是可以并行的,因为是两个不同的锁对象,一个是 class 对象,一个是 object 对象;
死锁
死锁是指两个或者两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或者系统产生了死锁 ;
产生死锁的原因:
- 多个操作者(M >= 2)争夺多个资源(N >= 2),N <= M;
- 争夺资源的顺序不对;
- 解决方案:顺序争夺资源;
- 拿到资源不放手;
- 解决方案:使用尝试拿锁的机制 tryLock()、资源使用完毕之后放手-> lock() unlock();
死锁代码示例:
public class DeadLockThread {
private static Object obj1 = new Object();
private static Object obj2 = new Object();
public static void a() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized(obj1) {
System.out.println(threadName + " get obj1");
Thread.sleep(100);
synchronized(obj2) {
System.out.println(threadName + " get obj2");
}
}
}
public static void b() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized(obj2) {
System.out.println(threadName + " get obj2");
Thread.sleep(100);
synchronized(obj1) {
System.out.println(threadName + " get obj1");
}
}
}
static class Kobe extends Thread {
private String name;
public Kobe(String name) {
this.name = name;
}
@Override
public void run() {
Thread.currentThread().setName(name);
try {
a();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread.currentThread().setName("James");
Kobe kobe = new Kobe("Kobe");
kobe.start();
try {
b();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
从打印结果来看,发生了死锁,如果没有发生死锁的话,则日志应该都能输出;程序一旦发生死锁是没有办法恢复的,只能重启程序;
解决方案代码示例:
public class TryLockThread {
private static Lock obj1 = new ReentrantLock();
private static Lock obj2 = new ReentrantLock();
public static void a() throws InterruptedException {
String threadName = Thread.currentThread().getName();
Random random = new Random();
while (true) {
if (obj1.tryLock()) {
System.out.println(threadName + " get obj1");
try {
if (obj2.tryLock()) {
try {
System.out.println(threadName + " get obj2");
System.out.println("a to b do work ... ");
break;
} finally {
obj2.unlock();
}
}
} finally {
obj1.unlock();
}
}
// 这里休眠是为了:减少拿锁的过程,也就是减少活锁产生的时间
Thread.sleep(random.nextInt(3));
}
}
public static void b() throws InterruptedException {
String threadName = Thread.currentThread().getName();
Random random = new Random();
while (true) {
if (obj2.tryLock()) {
System.out.println(threadName + " get obj1");
try {
if (obj1.tryLock()) {
try {
System.out.println(threadName + " get obj2");
System.out.println("b to a do work ... ");
break;
} finally {
obj1.unlock();
}
}
} finally {
obj2.unlock();
}
}
// 这里休眠是为了:减少拿锁的过程,也就是减少活锁产生的时间
Thread.sleep(random.nextInt(3));
}
}
static class Kobe extends Thread {
private String name;
public Kobe(String name) {
this.name = name;
}
@Override
public void run() {
Thread.currentThread().setName(name);
try {
a();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread.currentThread().setName("James");
Kobe kobe = new Kobe("Kobe");
kobe.start();
try {
b();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
没有发生死锁的问题;
显示锁
本质可以自己控制程序的锁定和解锁;
Lock 所有继承 lock 接口的锁都是显示锁;
// ReentrantLock 可重入锁: 就是在递归的时候 可以反复的拿锁;
Lock lock = new ReentrantLock();
lock.lock(); // 加锁
try{
}finally{
lock.unlock(); // 解锁
}
// 这两个方法是必须要实现的
所以:可重入锁也是显示锁;
示例代码:
public class LockDemo{
private Lock lock = ReentrantLock();
private int count = 0;
public void incr() {
lock.lock();
try {
count ++;
} finally {
lock.unlock();
incr();
}
}
}
wait、notify
notify
唤醒 wait(); 被冻结的线程,如果没有冻结的线程,没有任何关系,java 默认不会报错,默认会不处理;
wait
当前自己线程冻结,释放 CPU 执行资格,让出 CPU 执行权,让 CPU 去执行其他线程;
示例代码:
public class WaitNotifyTest {
static class Res {
public int id;
public String name;
private boolean flag = false;
// 生产
public synchronized void in() {
if(!flag) {
id += 1;
System.out.println("produce: " + id);
flag = true;
notify();
try {
wait();
} catch (Exception e) {
//
}
}
}
// 消费
public synchronized void out() {
if(flag) {
id -= 1;
System.out.println("consumer: " + id);
flag = false;
notify();
try {
wait();
} catch (Exception e) {
//
}
}
}
}
class ProduceThread extends Thread {
private Res mRes;
public ProduceThread(Res res){
this.mRes = res;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
mRes.in();
}
}
}
class ConsumerThread extends Thread {
private Res mRes;
public ProduceThread(Res res){
this.mRes = res;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
mRes.out();
}
}
}
public class Test {
public static void main(String[] args) {
Res res = new Res();
ProduceThread pt = new ProduceThread(res);
ConsumerThread ct = new ConsumerThread(res);
pt.start();
ct.start();
}
}
}
wait 和 notify 只有操作同一把锁的时候才生效,如何确定是同一把锁,由 main 函数中的两个 thread 操作的是同一个对象来确定 这两个线程操作的是同一个锁,那么 notify 唤醒的是操作这个对象的所有线程,wait 是当前线程哪个线程调用,哪个线程就会释放 CPU 执行权;
wait 和 notify 在使用的时候必须要被 synchronized 包裹着,否则会报错;
wait、sleep
wait 和 sleep 是都会让出 CPU 执行权的,但是 sleep 不会释放锁,wait 会释放锁;
sleep 可以在任何地方使用,而 wait 只能在同步方法或者同步块中使用;
try {
Thread.sleep(1000);
} catch(Exception e) {
//
}
从锁的状态来区分,可以有多少种锁,以及它们的优缺点?
- 无锁状态;
- 不锁住资源,多个线程中只有一个线程能修改成功,其他线程会重试;
- 偏向锁
-
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获取锁的代价更低,引入来偏向锁;
-
拿锁的过程中 判断对象头中的线程 ID 是不是自己,是的话 就不进行自旋操作;
- 轻量级锁
-
指的就是那些没有被挂起或者阻塞的线程通过 CAS 操作来加锁和解锁;
-
自旋锁;
-
适应性自旋锁;
-
控制自旋次数的锁,在 JDK1.5 的时候 默认定义为 10次,JDK1.6 之后改为了无固定的值,由虚拟机自行判定;
-
超过自旋时间之后,就会膨胀为重量级锁;
- 重量级锁
- 没有拿到锁的线程都是要被阻塞或者挂起;
线程本地变量 ThreadLocal
线程隔离,保证线程独有的属性。ThreadLocal 可以让每个线程拥有一个属于自己的变量副本,不会和其他线程的变量副本冲突,实现了线程的数据隔离;
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();
}
set实现
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap 实现
每一个 ThradLocaMap 都持有一个 Entry[]数组,数组中的每个 Entry 实例持有两个变量;
- ThreadLocal<?> k
- Object v
为什么是 Entry[]?
变量副本保存在线程自己的内部,每个线程又可以拥有多个ThreadLocal变量副本,为了保证线程变量副本的唯一,所以内部就用了一个数组来保存这多个 ThreadLocal 变量;
手写 ThreadLocal 核心实现
public class CustomThreadLoacl<T> {
// 存放变量副本的map容器,以 Thread 为 key,变量副本为 value
private Map<Thread, T> theadMap = new HashMap<Thread, T>();
public syncorhized T get() {
return theadMap.get(Thread.currentThread());
}
public synchroized set(T t) {
theadMap.set(Thread.currentThread(), t);
}
}
简历润色
简历上可写:深度理解Java多线程、线程安全、并发编程,可手写ThreadLocal的核心实现;
下一章预告
带你玩转CAS原理;
欢迎三连
来都来了,点个关注、点个赞吧~~
转载自:https://juejin.cn/post/7310024732354265125