学习完Java基础后,让我们开始踏上进阶之旅,一起感受Java并发编程的魅力。
今天是第一篇内容——并发编程线程基础。本篇内容将介绍进程与线程的概念和区别、线程创建和运行的几种方式、线程通知与等待、线程常用方法、守护线程和用户线程、ThreadLocal的使用与原理等内容。
01
进程与线程
进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。
进程和线程的区别:
1. 进程拥有独立的地址空间,进程之间的内存不共享;线程共享同一进程的地址空间,同一进程内的多个线程可以直接访问共享数据;更具体来说,进程拥有自己的堆空间,线程共享进程的堆空间;
2. 进程和线程都有自己的栈空间,线程栈主要用来存储线程的局部变量,是私有的;
3. 进程有自己的方法区,存放JVM加载的类、常量及静态变量,由线程共享;
4. 线程有自己的程序计数器,用来记录线程当前要执行的指令地址;
5. 每个进程都有自己的一组资源(如内存、文件描述符、信号量等),这些资源对其他进程不可见。线程共享进程资源。
6.进程是资源分配和调度的基本单位,线程是 CPU 分配的基本单位。
在Java中,当我们启动main函数时其实就启动了一个JVM的进程,而main函数所在的线程就是这个进程中的一个线程,也称主线程。
02
Java线程创建与运行
Java线程的创建有三种方式,继承Thread类、实现Runnable接口、以及实现Callable接口和FutureTask的方式。
继承Thread类
public class ThreadTest {
// 方式1: 继承Thread类
/*
* 优点:run方法内获取当前线程直接使用this即可
* 缺点:
* 1. Java不支持多继承,继承了Thread类就不能再继承其他类
* 2. 无法复用,当多个线程执行一样的任务时,需要多份任务代码
* */
// 1.创建一个类并继承Thread类
public static class MyThread extends Thread {
// 2. 重写run方法
@Override
public void run() {
System.out.println("a child thread...");
}
}
public static void main(String[] args) {
// 创建线程
MyThread myThread = new MyThread();
// 启动线程
myThread.start();
}
}
实现Runnable接口
// 方式2: 实现Runnable接口
/*
* 缺点:任务没有返回值
* */
// 1. 实现Runnable接口
public class RunnableTask implements Runnable {
// 2. 重写run方法
@Override
public void run() {
System.out.println("a child thread...");
}
}
public class ThreadTest02 {
public static void main(String[] args) {
// 2. 实例化RunnableTask对象
RunnableTask runnableTask = new RunnableTask();
// 3. 创建Thread对象
Thread thread = new Thread(runnableTask);
// 4. 启动线程
thread.start();
// 5. 可以复用
new Thread(runnableTask).start();
new Thread(runnableTask).start();
}
}
实现Callable接口+FutureTask方式
public class ThreadTest03 {
// 方式3: 实现Callable接口
/*
* 优点:
* 1. 任务有返回值
* 2. 线程可以复用
* */
// 1. 实现Callable接口
public static class CallerTask implements Callable<String> {
// 2. 重写call方法
@Override
public String call() throws Exception {
System.out.println("a child thread say hello...");
return "hello";
}
}
public static void main(String[] args) {
// 3. 创建FutrueTask对象, 传入Callable对象(构造函数为CallerTask实例)
FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
// 4. 使用futureTask对象作为任务创建线程
Thread thread = new Thread(futureTask);
// 5. 启动线程
thread.start();
try {
// 6.等待线程执行完毕并获取线程执行结果
String result = futureTask.get();
System.out.println("result is: " + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
03
线程通知与等待
线程有很多状态,包括就绪、运行、阻塞等,如果一个线程在运行时调用了一个共享变量的wait方法,该调用线程就会被阻塞;如果调用了notify方法,阻塞的线程就会被唤醒,下面来看这两种方法的具体使用方式。
wait()函数
当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生下面几件事情之一才返回: (1)其他线程调用了该共享对象的notify()或者notifyAll()方法; (2)其他线程调用了该线程的interrupt()方法,该线程抛出 InterruptedException异常返回。
另外需要注意的是,如果调用 wait()方法的线程没有事先获取该对象的监视器锁,则 调用 wait()方法时调用线程会抛出 IllegalMonitorStateException异常。
public class ThreadTest04 {
// 创建资源
public static volatile Object resourceA = new Object();
public static volatile Object resourceB = new Object();
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
try {
// 获取资源A的监视器锁
//synchronized 同步代码块,用于获取某个对象(共享资源)的监视器锁
synchronized (resourceA) {
System.out.println("threadA get resourceA lock");
// 获取资源B的监视器锁
synchronized (resourceB) {
System.out.println("threadA get resourceB lock");
// 线程A阻塞,并释放资源A的监视器锁
System.out.println("threadA release resourceA lock");
resourceA.wait(); // 资源A阻塞自己则线程A释放到获取的资源A的锁
}
}
} catch (InterruptedException e) {
System.out.println("threadA is interrupted...");;
}
}
});
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
synchronized (resourceA) {
System.out.println("threadB get resourceA lock");
System.out.println("threadB try get resourceB lock");
synchronized (resourceB) {
System.out.println("threadB get resourceB lock");
System.out.println("threadB release resourceA lock");
resourceA.wait();
}
}
} catch (InterruptedException e) {
System.out.println("threadB is interrupted...");;
}
}
});
threadA.start();
threadB.start();
// 等待两个线程执行结束
threadA.join();
threadB.join();
System.out.println("main end...");
}
}
如上代码中,在main函数里面启动了线程 A 和线程 B,为了让线程A先获取到锁,这里让线程B先休眠了1s,线程A先后获取到共享变量resourceA和共享变量resourceB上的锁,然后调用了 resourceA的wait()方法阻塞自己,阻塞自己后线程A释放掉获取的 resourceA 上的锁。
线程 B 休眠结束后会首先尝试获取 resourceA 上的锁,如果当时线程 A 还没有调用 wait()方法释放该锁,那么线程B会被阻塞, 当线程A释放了 resourceA上的锁后,线程 B 就会获取 到 resourceA 上的锁, 然后尝试获取 resourceB 上的锁 。由于线程 A 调用的是 resourceA上的 wait()方法,所以线程A挂起自己后并没有释放获取到的 resourceB上的锁, 所以线程 B 尝试获取 resourceB 上的锁时会被阻塞 。
这就证明了当线程调用共享对象的 wait()方法时,当前线程只会释放当前共享对象的锁,当前线程持有的其他共享对象的监视器锁并不会被释放 。
public class ThreadTest05 {
static Object obj = new Object();
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("threadA is start...");
// 阻塞当前线程
synchronized (obj) {
obj.wait();
}
System.out.println("threadA is end...");
} catch (InterruptedException e) {
System.out.println("threadA is interrupted...");;
}
}
});
threadA.start();
Thread.sleep(1000);
System.out.println("start interrupt threadA");
threadA.interrupt();
System.out.println("end interrupt threadA");
System.out.println("main end...");
}
}
threadA调用共享对象 obj 的 wait()方法后阻塞挂起了自己,然后 主线程在休 眠 1s 后中断了 threadA 线程,中断后 threadA 在 obj.wait()处抛出 java.lang. InterruptedException 异常而返回并终止。
notify()函数
一个线程调用共享对象的 notify()方法后,会唤醒一个在该共享变量上调用 wait系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
此外,被唤醒的线程不能马上从 wait 方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回也就是唤醒它的线程释放了共享变量上的监视器锁后,被唤醒的线程也不一定会获取到共享对象的监视器锁,这是因为该线程还需要和其他线程一起竞争该锁, 只有该线程竞争到了共享变量的监视器锁后才可以继续执行。
类似 wait 系列方法,只有当前线程获取到了共享变量的监视器锁后,才可以调用共享变量的 notify() 方法,否则会抛出illegalMonitorStateException 异常。
notifyAll() 方法则会唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。
public class TestNotify {
// 创建资源
private static volatile Object resourceA = new Object();
public static void main(String[] args) throws InterruptedException {
// 创建线程
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
//获取资源A的监视器锁
synchronized (resourceA) {
System.out.println("threadA get resourceA lock");
try {
System.out.println("threadA begin wait");
resourceA.wait();
System.out.println("threadA end wait");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
// 创建线程B
Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA) {
System.out.println("threadB get resourceA lock");
try {
System.out.println("threadB begin wait");
resourceA.wait();
System.out.println("threadB end wait");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread threadC = new Thread(new Runnable() {
@Override
public void run() {
synchronized (resourceA) {
System.out.println("threadC begin notify");
// 只会唤醒一个线程
resourceA.notify();
}
}
});
threadA.start();
threadB.start();
// 让线程A和线程B都执行到wait 方法后在调用C的notify
Thread.sleep(1000);
threadC.start();
// 等待所有线程结束
threadA.join();
threadB.join();
threadC.join();
System.out.println("main over");
}
}
notify()只会唤醒A或B中的一个线程,notifyAll则会唤醒 resourceA 的等待集合里面的所有线程。
在共享变量上调用 notifyAll()方法只会唤醒调用这个方法前调用了 wait系列函数而被放入共享变量等待集合里面的线程。如果调用 notifyAll()方法后一个线程调用了该共享变量的 wait() 方法而被放入阻塞集合, 则该线程是不会被唤醒的。
04
线程常用方法
join():等待所有线程执行完成
sleep():让线程休眠,不参与CPU的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。
yield():让出CPU
void interrupt()方法: 中断线程, 例如,当线程 A 运行时,线程 B 可以调用线程 A 的 interrupt() 方法来设置线程 A 的中断标志为 true 并立即返回。设置标志仅仅是设置标志 , 线程 A 实际并没有被中断, 它会继续往 下执行 。如果线程 A 因为调用了 wait系列函数、 join方法或者sleep方法而被阻塞挂起,这时候若线程B调用线程 A 的 interrupt() 方法,线程 A 会在调用这些方法 的地方抛 出 InterruptedException 异 常而返回。
boolean isInterrupted()方法: 检测当前线程是否被中断,如果是返回 true, 否返回false。
boolean interrupted()方法: 检测当前线程(如果在主线程中调用,则指的是主线程)是否被中 断 , 如果是返回 true,否 false。与 islnterrupted不同的是,该方法如果发现当前线程被中断, 则会清除 中断标志,并且该方法是 static 方法 , 可 以 通过 Thread 类直接调用。
public class InterruptTest implements Runnable {
public void run() {
try {
// do something
while(!Thread.currentThread().isInterrupted()) {
// do something
}
} catch (InterruptedException e) {
System.out.println("thread is interrupted...");
} finally {
// clean up, if needed
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 当线程被中断,则退出循环
while(!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread() + " is running...");
}
}
});
thread.start();
Thread.sleep(1000);
System.out.println("main thread interrupt thread");
thread.interrupt();
// 等待子线程执行完毕
thread.join();
System.out.println("main is over...");
}
public class InterruptTest02 {
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
for (;;){
}
}
});
threadA.start();
threadA.interrupt(); //为线程A设置中断标志
System.out.println("threadA.isInterrupted(): " + threadA.isInterrupted()); // true // 获取中断标志且不重置
System.out.println("threadA.interrupted(): " + threadA.interrupted()); // false // currentThread().isInterrupted(true); 重置当前线程(主线程)的中断标志
System.out.println("Thread.interrupted(): " + Thread.interrupted()); // false // currentThread().isInterrupted(true); 重置当前线程(主线程)的中断标志
threadA.join();
}
}
05
守护线程和用户线程
main函数所在的线程就是用户线程,JVM内部也有很多守护线程,如垃圾回收线程。
守护线程和用户线程的区别是:当最后一个非守护线程结束时,JVM会正常退出,而不管当前是否有守护线程。
Thread daemondThread = new Thread(new Runnable() {
@Override
public void run() {
}
});
daemondThread.setDaemon(true); // 设置为守护线程
daemondThread.start();
当父线程结束后,子线程还是可以继续存在的,也就是子线程的生命周期并不受父线程的影响。这也说明了在用户线程还存在的情况下 NM 进程并不会终止。
在启动线程前将线程设置为守护线程,如果当前进程中不存在用户线程,但是还存在正在执行任务的守护线程,则主线程不等守护线程,运行完毕就会结束JVM进程。main线程运行结束后,JVM会自动启动一个叫作DestroyJavaVM的线程, 该线程会等待所有用户线程结束后终止 JVM 进程。
06
ThreadLocal
ThreadLocal 是 JDK 包提供的,它提供了线程本地变量,也就是如果你创建了 一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。创建一个ThreadLocal变量后,每个线程都会复制一个变量到自己的本地内存。
public class ThreadLocalTest {
static ThreadLocal<String> localVariable = new ThreadLocal<>();
static void print(String str) {
System.out.println(str + ":" + localVariable.get());
localVariable.remove();
}
public static void main(String[] args) {
Thread threadOne = new Thread(new Runnable(){
@Override
public void run() {
localVariable.set("threadOne local variable");
print("threadOne");
System.out.println("threadOne remove after:" + localVariable.get());
}
});
Thread threadTwo = new Thread(new Runnable(){
@Override
public void run() {
localVariable.set("threadTwo local variable");
print("threadTwo");
System.out.println("threadTwo remove after:" + localVariable.get());
}
});
threadOne.start();
threadTwo.start();
}
}
07
InheritableThreadLocal
InheritableThreadLocal 继承自 ThreadLocal, 其提供了一个特性,就是让子线程可以访问在父线程中设置的本地变量 。
在 InheritableThreadLocal 的世界里,变量 inheritableThreadLocals替 代了 threadLocals 。
public class TestThreadLocal {
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
threadLocal.set("hello world");
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread:" + threadLocal.get()); // thread:null
}
});
thread.start();
System.out.println("main:" + threadLocal.get()); // main:hello world
}
}
子线程拿不到父线程的ThreadLocal。如果改成InheritableThreadLocal就可以访问父线程的变量了。
public static ThreadLocal<String> threadLocal
= new InheritableThreadLocal<>();
那么在什么情况下需要子线程可以获取父线程的ThreadLocal变量呢?情况还是蛮多的,比如子线程需要使用存放在threadlocal变量中的用户登录信息,再比如一些中间件需要把统一的id追踪的整个调用链路记录下来。其实子线程使用父线程中的 threadlocal方法有多种方式, 比如创建线程时传入父线程中的变量,并将其复制到子线程中,或者在父线程中构造一个 map 作为参数传递给子线程,但是这些都改变了我们的使用习惯,所以在这些情况下 InheritabIeThreadLocal 就显得比较有用 。