前言
JUC并发编程是Java程序猿必备的知识技能,只有深入理解并发过程中的一些原则、概念以及相应源码原理才能更好的理解软件开发的流程。在这篇文章中荔枝会梳理并发编程的基础,整理有关Java线程以及线程死锁的知识,希望能够帮助到有需要的小伙伴~~~
文章目录
前言
一、基本概念
1.1 什么是线程
1.2 常见的三种创建线程的方式
1.3 共享变量的wait、notify、notifyAll
1.4 线程的join()、sleep()和yeild()
1.5 线程中断
二、线程死锁
2.1 线程死锁产生的四种条件
2.2 避免死锁的有效措施
2.3 守护线程和用户线程
三、ThreadLocal
3.1 threadLocals
3.2 ThreadLocal不可继承问题的解决 —— InheritableThreadLocal类
总结
一、基本概念
1.1 什么是线程
线程是运行在进程上的单元,我们知道操作系统是将资源分配到进程上的,而线程是CPU资源分配的基本单位,或者说是占用CPU执行的基本单位更为贴切,线程栈中存放的是该线程的局部变量。进程中除了线程之外还提供了一块共享区域:堆和方法区。堆是进程中最大的一块内存,堆中存放的是所有的被new操作创建的对象示例,而方法区中则存放所有被JVM加载的类、常量和静态变量。
1.2 常见的三种创建线程的方式
常见的开启线程的方式主要有三种:继承Thread类并重写run方法、实现Runnable接口并实现run方法、实现Callable接口并实现call()方法
继承Thread类并重写run方法
public static class MyThread extends Thread{
@Override
public void run() {
System.out.println("overwite the thread");
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
实现Runnable接口
public static class MyThread implements Runnable{
@Override
public void run() {
System.out.println("overwrite Runnable");
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
new Thread(myThread).start();
}
实现Callable接口
public static class MyThread implements Callable {
@Override
public String call() throws Exception {
System.out.println("overwrite callable");
return "thread";
}
}
public static void main(String[] args) throws InterruptedException{
//创建异步任务
FutureTask<String> futureTask = new FutureTask<>(new MyThread());
//启动线程
new Thread(futureTask).start();
try{
String result = futureTask.get();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
三种线程创建方式的区别:
通过继承的方式优点:方便传参,在需要获取当前线程时使用this,而无需使用Thread.currentThread()。通过实现接口方式创建线程优点:由于Java语言不支持多继承,所以直接通过继承的方式还存在一定的局限性;
三种线程创建方式唯一一个有返回值的就是实现Callable接口并使用FutureTask创建异步任务,Callable接口中的call方法是一个有返回值的方法,可以通过FutureTask对象的get方法来获得线程返回结果。
1.3 共享变量的wait、notify、notifyAll
wait()
当一个获得共享变量的监视器锁的线程调用该变量的wait(),该线程会被阻塞挂起,同时会释放当前共享变量的监视器锁,想要结束阻塞挂起状态一般可以通过其他线程在调用wait()的线程挂起后调用该共享变量地notify() | notifyAll()方法来完成线程唤醒,也可以直接中断该线程异常返回。当然了,wait()方法中还可以提供了一个超时参数,没有在指定时间被唤醒就会返回。
public final void wait(long timeout, int nanos) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos > 0) {
timeout++;
}
wait(timeout);
}
wait(long timeout, int nanos)方法的底层也是调用一个wait(long timeout),只有nanos>0才能自增timeout。
虚假唤醒
线程的虚假唤醒指的是一个线程在没有被其它线程notify() | notifyAll() | 被中断 | 等待超时,就直接从挂起状态变为可运行状态(被唤醒)。避免虚假唤醒的方式仅需要使用一个循环不断地去获取是否达到了可唤醒线程地条件。
notify()
前面已经提到了,线程调用共享变量的notify()方法就可以将调用变量wait()方法阻塞的线程唤醒,需要注意的是即使唤醒该线程也不一定能直接进入执行状态,而是要与其它的线程一起竞争该共享变量的锁资源。
notifyAll()
区别于notify()只能唤醒一个线程,notifyAll可以直接唤醒该共享变量上由于调用wait方法挂起的所有线程。
1.4 线程的join()、sleep()和yeild()
sleep
阻塞挂起指定时长,结束后会退化成一个线程就绪状态,等待下一次的CPU调度。
package Thread;
import jdk.nashorn.internal.runtime.regexp.joni.exception.InternalException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadTest {
//创建一个资源独占锁
private static final Lock lock = new ReentrantLock();
//线程竞争锁的测试
public static void main(String[] args) throws InterruptedException{
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
//给线程的资源上锁
lock.lock();
try{
System.out.println("线程被激活:"+Thread.currentThread());
System.out.println("threadA is sleep");
Thread.sleep(3000);
System.out.println("threadA is awake");
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
//给线程的资源上锁
lock.lock();
try{
System.out.println("线程被激活:"+Thread.currentThread());
System.out.println("threadB is sleep");
Thread.sleep(3000);
System.out.println("threadB is awake");
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
});
//启动线程
threadA.start();
threadB.start();
}
}
运行结果
这里我们测试一下,如果给B不加锁资源的话是否B还需要等待A释放锁,结果自然是否定的。因为此时二者竞争的就不是一个锁资源辽!
需要注意的是:线程调用sleep后是不会让出锁资源的,而仅仅是在指定时间不参与线程的调度!
join()
join()是由Thread类提供的一个无参返回方法,主要功能是阻塞主线程,等待调用线程执行完成一起将结果返回。
yeild()
正常情况下,一个线程只有将分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度。但是如果一个线程调用yeild()方法,就相当于该线程告诉线程调度器他将让出CPU的执行权,即使按照时间片轮转机制还剩余一部分时间片。此时该线程就会由执行状态变为就绪状态,线程调度器会重新从线程就绪队列里面获取一个线程优先级最高的线程。
调用yeild方法的时候,线程并不会被阻塞挂起,而是让出时间片后处于一种就绪状态。
1.5 线程中断
Java中的线程中断是一种线程间的协作模式,通过设置线程中断的方式并不能直接终止线程,而是被中端的线程根据中断状态自行决定。
interrupt()
中断线程,这里需要注意的是调用该方法并不是实际意义上的中断线程,而是通过修改中断标志来告诉线程我现在尝试中断,但具体的决定权属于被中断的线程。
isInterrupted()
判断当前调用方法实例对象的线程是否被中断,不清除线程中断的标志
Interrupted()
判断当前执行线程是否被中断,清除中断标志。需要注意的是,interrupted()内部调用的是获取当前线程和的中断标志,而不是调用interrupted()的实例对象的中断标志。
线程中断的应用场景
在某些场景下,线程为了等待一些特定条件到来时,一般会调用sleep、wait系例函数、或者join来阻塞挂起当前线程。但如果此时线程在挂起的时候已经满足执行条件,此时我们可以直接中断线程强制抛出一个InterruptedException异常并使其处于激活状态,线程恢复并继续在新一轮CPU调度中继续向下执行。
线程的上下文切换
线程的上下文切换指的是:CPU的执行从一个线程调度到另一个线程的过程。一个典型的场景就是:当前线程使用完时间片后,就会处于就绪状态并让出CPU资源给其它的线程占用。
二、线程死锁
2.1 线程死锁产生的四种条件
资源互斥条件
线程堆已经获取到的资源进行排他性使用,即该资源同时只有一个线程占用。只有等到占用资源的线程释放出资源后,其它的资源请求者才能获取。
请求并持有条件
线程A已经持有至少一个资源,但A又提出了新的资源请求,那么A就会一直保持着原来持有资源的占用权。如果请求的资源已被其它的资源所占用,A就会一直阻塞着,但是不会释放当前持有的资源。
不可剥夺条件
线程获取到的资源在自己使用完主动释放前,其它的线程不能抢占。
环路等待原则
形成死锁的线程必定存在着一个线程-资源的环形链。
2.2 避免死锁的有效措施
在操作系统的学习中我们清除,形成死锁的四个条件中只有请求并持有和环路等待条件是可以被破坏的。形成死锁其实与不同线程间资源申请的顺序有关系,要避免死锁的出现我们必须保证资源申请的有序性原则。
什么是资源申请的有序性呢?
比如A和B线程同时申请诸如1,2,3,4的资源,那么我们要保证A,B线程在申请资源的时候是按照同一个顺序来申请资源的。不管A还是B先开始申请资源,其余的线程都会阻塞直至开始申请资源的线程执行完成释放出自己所拥有的资源。
2.3 守护线程和用户线程
守护线程和用户线程的概念区分需要基于JVM,具体的区别就是用户线程如果全部结束后JVM会退出,而守护线程是否结束并不影响JVM的退出。
设置线程为守护线程
thread.setDaemon(true);
而在Tomcat中,NIO实现的NioEndpoint中会开启一组接受线程来接受用户的连接请求、一组处理线程来负责具体处理用户请求,而这些线程其实都是守护线程!
三、ThreadLocal
ThreadLocal是一种在线程上维护本地变量的技术,它提供了线程的本地变量,可以将共享变量拷贝一份到线程分配到的内存空间中,虽然它的提出其实并不是用来解决多线程同时访问共享变量的线程安全问题,线程安全问题一般可以通过Java中的锁机制来解决,但是这里通过ThreadLocal提供了另外一种思路。
static ThreadLocal<String> localVariable = new ThreadLocal<>();
通过ThreadLocal创建一个本地变量localVariable ,当线程设置localVariable中的值,其实设置的是该线程的本地内存中的一个副本。值得注意的是,每个线程的本地变量并不是放在ThreadLocal实例中的,而是放在调用线程的threadLocals变量中。
3.1 threadLocals
每个线程内部都有的成员变量,类型为HashMap,其中key是我们定义的ThreadLocal变量的this引用,value通过set方法设置的值。每个线程的本地变量都存放在自己的内存变量threadLocals中!
本地变量的生命周期
只要调用线程不终止,该本地变量就会一直存放在threadLocals变量中,这就会带来内存溢出的隐患
调用线程清除本地变量
可以通过调用ThreadLocal变量里面的remove()方法。
3.2 ThreadLocal不可继承问题的解决 —— InheritableThreadLocal类
要想父线程本地变量可以被子线程继承使用,我们就需要借助InheritableThreadLocal类。InheritableThreadLocal继承自ThreadLocal类并重写了createMap()、getMap()、childValue()三个方法。具体的使用声明倒没有什么区别:
static InheritableThreadLocal<String> localVariable= new InheritableThreadLocal<>();
通过查看Thread源码,我们知道它的底层逻辑实现通过重写ThreadLocal类的creatMap和getMap方法让本地变量保存到了具体线程的inheritableThreadLocals成员变量中,线程在通过InheritableThreadLocal实例的set和get的时候就会创建当前线程的inheritableThreadLocals,并在创建子线程的构造函数中将父线程的inheritableThreadLocals成员变量复制一份到子线程中。下面是该类重写的三个方法:
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
/**
* Computes the child's initial value for this inheritable thread-local
* variable as a function of the parent's value at the time the child
* thread is created. This method is called from within the parent
* thread before the child is started.
* <p>
* This method merely returns its input argument, and should be overridden
* if a different behavior is desired.
*
* @param parentValue the parent thread's value
* @return the child thread's initial value
*/
protected T childValue(T parentValue) {
return parentValue;
}
/**
* Get the map associated with a ThreadLocal.
*
* @param t the current thread
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
* Create the map associated with a ThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the table.
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
从Thread类中的源码中我们可以看到,Thread类在构造方法的时候就会调用init方法,而在init方法中,我们不仅获取当前父线程,通过判断inheritableThreadLocals非空,在createInheritedMap中传入父线程的inheritableThreadLocals,复制到一个ThreadLocalMap对象中
总结
在这篇文章中,荔枝主要梳理了有关Java线程的相关知识,包括线程创建、几种线程的状态、线程死锁产生和避免、线程中断等等。也从源码角度深入地探究了ThreadLocal的原理及其应用场景,在后续的文章中荔枝也会持续输出,深入学习JUC并发编程并持续输出高质量的知识博文,希望大家能喜欢哈哈哈哈哈。
今朝已然成为过去,明日依然向往未来!我是荔枝,在技术成长之路上与您相伴~~~
如果博文对您有帮助的话,可以给荔枝一键三连嘿,您的支持和鼓励是荔枝最大的动力!
如果博文内容有误,也欢迎各位大佬在下方评论区批评指正!!!