1、线程的上下文切换
在多线程编程中,线程个数一般都大于CPU个数,而每个CPU同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的,CPU资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用CPU执行任务。当前线程使用完时间片后,就会处于就绪状态并让出CPU让其他线程占用,这就是上下文切换,从当前线程的上下文切换到了其他线程。那么就有一个问题,让出CPU的线程等下次轮到自己占有CPU时如何知道自己之前运行到哪里了?所以在切换线程上下文时需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场。
线程上下文切换时机有:当前线程的CPU时间片使用完处于就绪状态时,当前线程被其他线程中断时。
2、线程死锁
2.1、什么是线程死锁?
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去,如下图所示。
在图中,线程A已经持有了资源2,它同时还想申请资源1,线程B已经持有了资源1,它同时还想申请资源2,所以线程1和线程2就因为相互等待对方已经持有的资源,而进入了死锁状态。
那么为什么会产生死锁呢?学过操作系统的朋友应该都知道,死锁的产生必须具备以下四个条件。
-
互斥条件:指线程对已经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
-
请求并持有条件:指一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
-
不可剥夺条件:指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
-
环路等待条件:指在发生死锁时,必然存在一个线程一资源的环形链,即线程集合
{T0,T1,T2,…,Tn}中的T0正在等待一个T1占用的资源,T1正在等待T2占用的资源,……Tn正在等待已被T0占用的资源。
2.2、如何避免线程死锁?
要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,但是学过操作系统的小伙伴应该都知道,目前只有请求并持有和环路等待条件是可以被破坏的。
3、守护线程与用户线程
Java中的线程分为两类,分别为daemon线程(守护线程)和user线程(用户线程)。在JVM启动时会调用main函数,main函数所在的线程就是一个用户线程,其实在JVM内部同时还启动了好多守护线程,比如垃圾回收线程。
那么守护线程和用户线程有什么区别呢?
区别之一是当最后一个非守护线程结束时,JVM会正常退出,而不管当前是否有守护线程,也就是说守护线程是否结束并不影响JVM的退出。言外之意,只要有一个用户线程还没结束,正常情况下JVM就不会退出。
那么在Java中如何创建一个守护线程?代码如下。
public static void main(String[]args){
Thread daemonThread=new Thread(new Runnable(){
public void run(){
});
//设置为守护线程
daemonThread.setDaemon(true);
daemonThread.start();
}
只需要设置线程的daemon参数为true即可。
4、ThreadLocal
多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时。为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步,如下图所示。
ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个ThreadLocal变量后,每个线程都会复制一个变量到自己的本地内存,如下图所示。
public class ThreadLocal01 {
static ThreadLocal<String> localVariable = new ThreadLocal<>();
//定义print函数
static void print(String str){
//打印当前线程本地内存中localVariable变量的值
System.out.println(str + ":" + localVariable.get());
//清除当前线程本地内存中的localVariable变量
localVariable.remove();
}
public static void main(String[] args) {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
//设置线程A中本地变量localVariable的值
localVariable.set("threadA local variable");
print("threadA");
System.out.println("threadA remove after" + ":" +localVariable.get());
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
//设置线程A中本地变量localVariable的值
localVariable.set("threadB local variable");
print("threadB");
System.out.println("threadB remove after" + ":" +localVariable.get());
}
});
threadA.start();
threadB.start();
}
}
运行结果如下:
threadA:threadA local variable
threadB:threadB local variable
threadA remove after:null
threadB remove after:null
ThreadLocal的实现原理:
由该图可知,Thread类中有一个threadLocals和一个inheritable ThreadLocals,它们都是ThreadLocalMap类型的变量,而ThreadLocalMap是一个定制化的Hashmap。在默认情况下,每个线程中的这两个变量都为null,只有当前线程第一次调用ThreadLocal的 set或者get方法时才会创建它们。其实每个线程的本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量里面。也就是说,ThreadLocal类型的本地变量存放在具体的线程内存空间中。ThreadLocal就是一个工具壳,它通过set方法把value值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的get方法时,再从当前线程的threadLocals变量里面将其拿出来使用。如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的threadLocals变量里面,所以当不需要使用本地变量时可以通过调用ThreadLocal变量的remove方法,从当前线程的threadLocals里面删除该本地变量。
另外,Thread 里面的 threadLocals 为何被设计为map 结构?很明显是因为每个线程可以关联多个ThreadLocal变量。
总结:如下图所示,在每个线程内部都有一个名为threadLocals的成员变量,该变量的类型为HashMap,其中key为我们定义的ThreadLocal变量的this引用,value则为我们使用set方法设置的值。每个线程的本地变量存放在线程自己的内存变量threadLocals中,如果当前线程一直不消亡,那么这些本地变量会一直存在,所以可能会造成内存溢出,因此使用完毕后要记得调用ThreadLocal的 remove 方法删除对应线程的threadLocals中的本地变量。
同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。这应该是正常现象,因为在子线程thread 里面调用get方法时当前线程为thread线程,而这里调用set方法设置线程变量的是main线程,两者是不同的线程,自然子线程访问时返回null。那么有没有办法让子线程能访问到父线程中的值?答案是有。
为了解决这个的问题,InheritableThreadLocal应运而生。InheritableThreadLocal继承自ThreadLocal,其提供了一个特性,就是让子线程可以访问在父线程中设置的本地变量。