java开发离不开多线程,以前都是零零碎碎的知识点,今天要把这些知识点整合起来,构建java多线程知识体系
java多线程概述
进程和线程
-
进程:进程是资源分配的最小单位,进程的本质是一个正在执行的程序,程序运行时系统会创建一个进程,并且给每个进程分配独立的内存地址空间保证每个进程地址不会相互干扰。同时,在 CPU对进程做时间片的切换时,保证进程切换过程中仍然要从进程切换之前运行的位置处开始执行。所以进程通常还会包含程序计数器、堆栈指针。
有了进程以后,可以让操作系统从宏观层面实现多应用并 发。而并发的实现是通过 CPU 时间片不端切换执行的。对于单核 CPU 来说,在任意一个时刻只会有一个进程在被 CPU 调度 -
线程:线程是CPU调度的最小单位,举个形象的例子:进程好比是csdn写博客的应用程序,进程里面可以包含多个线程,就比如我写博客的时候,写的文本需要实时的转换成markdown文档(一个线程),文档也需要实时的保存(另一个线程)。这两个线程是互相不会依赖的线程,这两个线程就是并发运行的。如果没有多线程,那就会阻塞,文档保存的时候,不可以编辑。文档编辑的时候不可以保存。。。
在知乎上看到一个更好的比喻
链接
做个简单的比喻:进程=火车,线程=车厢
- 线程在进程下行进(单纯的车厢无法运行)
- 一个进程可以包含多个线程(一辆火车可以有多个车厢)
- 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
- 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
- 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
- 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
- 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
- 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-“互斥锁”
- 进程使用的内存地址可以限定使用量(比如火车上的餐厅,最多只允许多少人进入,如果满了需要在门口等,等有人出来了才能进去)-“信号量”
问?有了进程以后,为什么还会发明线程呢?
- 在多核 CPU 中,利用多线程可以实现真正意义上的并行执行
- 在一个应用进程中,会存在多个同时执行的任务,如果其中一个任务被阻塞,将会引起不依赖该任务的任务也被阻塞。通过对不同任务创建不同的线程去处理,可以提升程序处理的实时性
- 线程可以认为是轻量级的进程,所以线程的创建、销毁比进程更快
并发和并行
- 并发:单核心CPU运行多线程就是并发,并发在微观来说是CPU在同一时刻只能有一个任务在执行
- 并行:多核心CPU运行多线程程序就是并行,并行在微观来说是CPU在同一时刻有多个任务在执行
多线程的创建方式
- 继承 Thread 类创建线程
- 实现 Runnable 接口
- 使用 ExecutorService、实现Callable、 Future接口; 实现带返回结果的多线程。
- 线程池
继承 Thread 类创建线程
继承Thread方法其实和实现Runnable差不多,Thread 类本质上是实现了 Runnable 接口的一个实例。代表一个线程的实例。启动线程的唯一方法就是通过 Thread类的 start()实例方法。start()方法是一个 native 方法,它会启动一个新线程,并执行 run()方法。这种方式实现多线程很简单,通过自己的类直接 extend Thread,并复写 run()方法,就可以启动新线程并执行自己定义的 run()方法。
package com.xhc.test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TestApplication extends Thread{
public static void main(String[] args) throws ClassNotFoundException {
TestApplication t = new TestApplication();
t.start();
System.out.println("--我是主线程--");
}
@Override
public void run() {
for(int i = 0 ; i < 50 ; i++){
System.out.println(i);
}
super.run();
}
}
实现Runnable接口
package com.xhc.test.thread.demo;
public class RunnableDemo implements Runnable {
private String name = "";
public RunnableDemo(String name){
this.name = name;
}
public static void main(String[] args) {
RunnableDemo r = new RunnableDemo("ThreadA");
Thread t = new Thread(r);
t.start();
System.out.println("---我是主线程--");
}
@Override
public void run() {
for(int i=0; i < 50 ; i++){
System.out.println("--"+i);
}
}
}
使用 ExecutorService、Callable、 Future 实现带返回结果的多线程。
package com.xhc.test.thread.demo;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
* @author xuehuichen
* @version V1.0
* @Package com.xhc.test.thread.demo
*/
public class CallableDemo implements Callable<String> {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建固定大小的线程池
ExecutorService executorService = Executors.newFixedThreadPool(1);
CallableDemo callableDemo = new CallableDemo();
Future<String> future = executorService.submit(callableDemo);
System.out.println(future.get());
executorService.shutdown();
}
@Override
public String call() throws Exception {
int a = 1;
int b = 2;
System.out.println(a + b);
return "执行结果:" + (a + b);
}
}
线程池(以后学)
线程的生命周期
- NEW:初始状态,线程被构建,但是还没有调用 start 方法
- RUNNABLED:运行状态,JAVA 线程把操作系统中的 就绪 和 运行 两种状态统一称为“运行中”
- BLOCKED:阻塞状态,表示线程进入等待状态,也就是线程因为某种原因放弃了 CPU 使用权,阻塞也分为几种情况
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么 jvm 会把当前的线程 放入到 锁池 中
- 等待阻塞:运行的线程执行 wait 方法,jvm 会把当前线程放入到等待队列
- 其他阻塞:运行的线程执行 Thread.sleep 或者 t.join 方法,或者发出了 I/O 请求时,JVM 会把当前线程设置 为阻塞状态,当 sleep 结束、join 线程终止、io 处理完毕则线程恢复
- WAITING:等待状态
- TIME_WAITING:超时等待状态,超时以后自动返回
- TERMINATED:终止状态,表示当前线程执行完毕
线程的生命周期
java中的线程状态和操作系统中的线程状态
java中的线程状态(6种)
- 新建 NEW
- 运行状态 RUNNING
- WATING
- TIMEWATING
- BLOCKED
- 终止状态 TERMINAED
操作系统中的线程状态(5种)
操作系统中一个线程真实存在状态只有5种
- new
- ready
- running
- waiting
- dead
小知识点:
wait() 和 notify() 方法这一对方法却必须在 synchronized 方法或块中调用,理由很简单,只有在synchronized方法或块中当前线程才占有锁,才有锁可以释放。
由于notify()只是唤醒一个线程,但我们由不能确定具体唤醒的是哪一个线程,也许我们需要唤醒的线程不能够被唤醒,因此在实际使用时,一般都用notifyAll()方法,唤醒有所线程
线程 等待(wait) 和 线程阻塞(blocked) 的区别:两者都表示线程当前暂停执行的状态,而两者的区别,基本可以理解为:进入 waiting 状态是线程主动的,而进入 blocked 状态是被动的。更进一步的说,进入 blocked 状态是在同步(synchronized)代码之外,而进入 waiting 状态是在同步代码之内(然后马上退出同步)。
线程的启动
调用 start()方法去启动一个线程,当 run 方法中的代码执行完毕 以后,线程的生命周期也将终止。调用 start 方法的语义是当前线程告诉 JVM,启动调用 start 方法的线程。
注意:为什么启动线程用start()方法而不用run()方法?因为如果调用run()方法相当于调用了一个普通的示例方法,并没有启动线程。
线程启动是调用了本地方法(跟操作系统有关)
线程的终止
stop()
stop() 方法已经过期了,不建议使用,因为stop 方法在结束一个线程时并不会保证线程的资源正常释放,因此会导致程序可能出现一些不确定的状态。
要优雅的去中断一个线程,在线程中提供了一个 interrupt 方法
package com.xhc.test.thread.demo;
import java.util.concurrent.TimeUnit;
/**
* @author xuehuichen
* @version V1.0
* @Package com.xhc.test.thread.demo
*/
public class InterruptDemo {
private static int i;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
//默认情况下 isInterrupted 返回 false、通过 thread.interrupt 变成了 true
while (!Thread.currentThread().isInterrupted()) {
System.out.println("------"+Thread.currentThread().isInterrupted());
i++;
}
System.out.println("Num:" + i);
}, "interruptDemo");
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt(); //加和不加的效果
}
}
这种通过标识位或者中断操作的方式能够使线程在终止时 有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅
Thread.interrupted() 线程的复位
isInterrupted 默认值为false。
package com.xhc.test.thread.demo;
import java.util.concurrent.TimeUnit;
/**
* @author xuehuichen
* @version V1.0
* @Package com.xhc.test.thread.demo
*/
public class InterruptDemoTwo {
private static int i;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("before:" + Thread.currentThread().isInterrupted());
Thread.interrupted(); //对线程进行复位,isInterrupted 由 true 变成 false
System.out.println("after:" + Thread.currentThread().isInterrupted());
}
}
}, "interruptDemo");
thread.start();
TimeUnit.SECONDS.sleep(1);
thread.interrupt();
}
}
抛出 InterruptedException 进行复位
除了通过 Thread.interrupted 方法对线程中断标识进行复位以外,还有一种被动复位的场景,就是对抛出 InterruptedException异常的方法, 在 InterruptedException 抛出之前,JVM 会先把线程的中断标识位清除(将true置为false),然后才会抛出 InterruptedException,这个时候如果调用 isInterrupted 方法,将会返回 false。
复位的解释:其他线程只有把中断标志设置为true的权利, 被中断线程接受不接受这个中止标志去中止自己,完全看这个线程的实现
深入理解interrupt() 中断流程
thread.interrupt()方法实际就是设置一个 interrupted 状态标识为 true、并且通过 ParkEvent 的 unpark 方法来唤醒线程。
- 对于 synchronized 阻塞的线程,被唤醒以后会继续尝试获取锁,如果失败仍然可能被 park
- 在调用 ParkEvent 的 park 方法之前,会先判断线程的中断状态,如果为 true,会清除当前线程的中断标识
- Object.wait 、 Thread.sleep 、 Thread.join 会抛出InterruptedException
为什么 Object.wait、 Thread.sleep 和 Thread.join 都 会 抛 出 InterruptedException?
这几个方法有一个共同点,都是属于阻塞的方法
而阻塞方法的释放会取决于一些外部的事件,但是阻塞方法可能因为等不到外部的触发事件而导致无法终止,所以,它允许一个线程请求自己来停止它正在做的事情。当一个方法抛出 InterruptedException 时,它是在告诉调用者如果执行该方法的线程被中断,它会尝试停止正在做的事情并且通过抛出 InterruptedException 表示提前返回。
所以,这个异常的意思是表示一个阻塞被其他线程中断了。 然后,由于线程调用了 interrupt()中断方法,那么
Object.wait、Thread.sleep 等被阻塞的线程被唤醒以后会通过 is_interrupted 方法判断中断标识的状态变化,如果发现中断标识为 true,则先清除中断标识,然后抛出 InterruptedException
需要注意的是,InterruptedException 异常的抛出并不意味着线程必须终止,而是提醒当前线程有中断的操作发生,至于接下来怎么处理取决于线程本身,比如
- 直接捕获异常不做任何处理
- 将异常往外抛出
- 停止当前线程,并打印异常信息
如何让线程顺序执行
Thread.join()
Thread.join(),他的实现原理其实就是利用wait()、notify();它的作用其实就是让线程的执行结果对后续线程的访问可见。
代码示例:
public class JoinDemo {
private static int i=10;
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
i=30;
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
//Thread.sleep(?); //你怎么知道多久能够执行完毕?
//t线程中的执行结果对于main线程可见.
t.join(); //Happens-Before模型 |
//我希望t线程的执行结果可见
System.out.println("i:"+i);
}
}
ThreadLocal
线程隔离机制。
ThreadLocal实际上一种线程隔离机制,也是为了保证在多线程环境下对于共享变量的访问的安全性。
public class ThreadLocalDemo {
// private static int num=0;
static ThreadLocal<Integer> local=new ThreadLocal<Integer>(){
protected Integer initialValue(){
return 0; //初始化一个值
}
};
static ThreadLocal<Integer> local1=new ThreadLocal<Integer>(){
protected Integer initialValue(){
return 0; //初始化一个值
}
};
public static void main(String[] args) {
Thread[] thread=new Thread[5];
for (int i=0;i<5;i++){
thread[i]=new Thread(()->{
local1.set(10);
int num=local.get(); //获得的值都是0
local.set(num+=5); //设置到local中 thread[0] ->thread[1] ->
System.out.println(Thread.currentThread().getName()+"-"+num);
local.remove();
});
}
for (int i = 0; i < 5; i++) {
thread[i].start();
}
}
}
ThreadLocal原理
- ThreadLocalMap
下一篇
多线程学习(二)之多线程的锁