1. 线程和进程
- 进程是代码在数据集合上的一次运行活动, 是系统进行资源分配和调度的基本单位。
- 线程则是进程的一个执行路径, 一个进程中至少有一个线程,进程中的线程共享进程的资源。线程是CPU 分配的基本单位。
- Java中,多个线程共享进程的堆和方法区资源,每个线程有自己的程序计数器和栈区域。
-
程序计数器:
(1) 记录了该线程让出CPU时的执行地址,待再次分配到时间片时线程就可以从计数器指定的地址继续执行。
(2) 只有执行的是Java 代码时记录的才是下一条指令的地址,而对于native方法记录的是undefined 地址。 -
栈资源:
(1) 存储该线程的局部变量
(2) 存放线程的调用栈帧 -
堆:
(1) 堆里面主要存放使用new 操作创建的对象实例
(2) 堆是被进程中的所有线程共享的 - 方法区用来存放JVM加载的类、常量及静态变量等信息,是线程共享的。
2. 线程创建
- 继承Thread 类并重写run方法
(1) 创建完thread对象后该线程并没有被启动执行,调用了start 方法后才真正启动了线程。
(2) start 方法后线程处于就绪状态,等待获取CPU 资源。
(3) 在run()方法内可以使用this获取当前线程。
//创建线程
MyThread thread= new MyThread();
// 启动线程
thread .start();
- 实现Runnable接口的run 方法
(1) 多个线程可共用一个task 代码逻辑。
(2) 在实现接口的同时可以继承其他类。
RunableTask task =new RunableTask();
new Thread(task).start() ;
new Thread(task).start() ;
- 使用FutureTask 方式
(1) 实现Callable 接口的call()方法。
(2) 可以拿到任务的返回结果。
static class CallerTask implements Callable<String>{
@Override
public String call() throws Exception{}
}
//创建异步任务
FutureTask<String> futureTask =new FutureTask<>(new CallerTask()) ;
//启动线程
new Thread(futureTask).start () ;
try {
//等待任务执行完毕,并返回结果
String result = futureTask.get ();
} catch (ExecutionException e) {}
3. 线程通知与等待
-
wait()函数
当一个线程调用一个共享变量的wait()方法时,该调用线程会被阻塞挂起,直到发生以下情况才返回:
(1) 其他线程调用了该共享对象的notify()或者notifyAll()方法;
(2) 其他线程调用了该线程的interrupt()方法,该线程抛出InterruptedException异常返回。调用wait()方法的线程需要先获取该对象的监视器锁,否则会抛出IllegalMonitorStateException异常
通过以下方式获取监视器锁:
(1) 执行synchronized 同步代码块时, 使用该共享变量作为参数。
(2) 调用该共享变量的方法,并且该方法使用了synchronized 修饰。
4. 线程睡眠
- sleep方法:
(1) 当一个执行中的线程调用了Thread 的sleep方法后,会暂时让出指定时间的执行权,不参与CPU的调度
(2) 该线程拥有的监视器资源,比如锁还是持有不会让出。
(3) 睡眠时间到了后该线程处于就绪状态,等待获取cpu资源。
(4) 在睡眠期间其他线程调用了该线程的interrupt方法中断了该线程,则该线程会在调用sleep方法的地方抛出IntermptedException异常而返回。 - yield方法:
(1) 线程调用yield 方法时,是在暗示线程调度器当前线程请求让出自己的CPU 使用,让线程调度器现在就可以进行下一轮的线程调度,但是线程调度器可以无条件忽略这个暗示。
(2) 调用yield 方法时后,当前线程会让出CPU 使用权,然后处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。
5. 线程中断
Java 中的线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行, 而是被中断的线程根据中断状态自行处理。
interrupt方法:中断线程
(1) 线程B可以调用线程A的interrupt方法来设置线程A 的中断标志为true并立即返回。
(2) 如果线程A 因为调用了wait 系列函数、join 方法或者sleep 方法而被阻塞挂起,会在调用这些方法的地方抛出InterruptedException 异常而返回。线程上下文切换时机有:
(1) 当前线程的CPU 时间片使用完处于就绪状态时
(2) 当前线程被其他线程中断时
6. 线程死锁
死锁是指两个或两个以上的线程在执行过程中,因争夺对方已经持有的资源而造成的互相等待的现象。
死锁的产生必须具备四个条件:
互斥条件:该资源同时只由一个线程占用。
请求并持有条件: 指一个线程己经持有了至少一个资源, 但又提出了新的资源请求,而新资源己被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
不可剥夺条件: 指线程获取到的资源在自己使用完之前不能被其他线程抢占。
环路等待条件:发生死锁时, 必然存在一个线程-资源的环形链。避免死锁:
(1) 只有请求并持有和环路等待条件是可以被破坏的。
(2) 造成死锁的原因其实和申请资源的顺序有很大关系, 使用资源申请的有序性原则就可以避免死锁。
(3) 资源申请的有序性:假如线程A 和线程B 都需要资源1, 2, 3, ... , n 时,对资源进行排序,线程A 和线程B 只有在获取了资源n-1 时才能去获取资源n 。
6. 守护线程与用户线程
- Java 中的线程分为两类,daemon 线程(守护线程〉和user 线程(用户线程)。
- 守护线程
(1) 在JVM启动的同时,启动了好多守护线程, 比如垃圾回收线程
(2) 守护线程是否结束并不影响JVM的退出,只要用户线程都退出了,就会终止JVM进程。如果你希望在主线程结束后JVM进程马上结束,那么在创建线程时可以将其设置为守护线程。
(3) 设置守护线程: thread.setDaemon(true) ;
(4) main线程运行结束后, JVM会自动启动一个叫作DestroyJava VM 的线程, 该线程会等待所有用户线程结束后终止JVM进程。
(5) 在默认情况下, tomcat的接受线程和处理线程都是守护线程, 这意味着当tomcat收到shutdown 命令后并且没有其他用户线程存在的情况下tomcat 进程会马上消亡,而不会等待处理线程处理完当前的请求。
7. ThreadLocal
- 提供了线程本地变量,如果创建了一个ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的一个本地副本。
- 使用
//创建ThreadLocal 变量
ThreadLocal<String> localVariable = new ThreadLocal<> () ;
//在线程1中
localVariable.set("threadOne local variable");
//在线程2中
localVariable.set("threadTwo local variable");
//在某个线程中获取变量
localVariable.get()
原理
(1) 在每个线程内部都有一个名为threadLocals 的成员变量, 该变量的类型为HashMap,用于存储线程本地变量。key 为我们定义的ThreadLocal变量的this引用, value 则为我们使用set 方法设置的值。
(2) ThreadLocal只是相当于一个工具类,它通过set 方法把value 值放入调用线程的threadLocals 里面并存放起来, 当调用线程调用它的get 方法时,再从当前线程的threadLocals 变量里面将其拿出来使用。
(3) 为了防止内存溢出,在使用完后,记得通过ThreadLocal的remove方法移除变量。
(4) 同一个ThreadLocal 变量在父线程中被设置值后, 在子线程中是获取不到的。InheritableThreadLocal: 让子线程能访问到父线程中的值
(1) InheritabIeThreadLocal 继承了ThreadLocal,set和get操作的是该线程的成员变量inheritableThreadLocals。
(2) 在创建线程时,会判断当前线程(父线程)中的inheritableThreadLocals变量是否为空,如果不为空,会把父线程的inheritableThreadLocals 成员变量的值复制到子线程的inheritableThreadLocals变量中。这样子线程就拥有了一份父线程的可继承的变量的副本。
(3) 父线程和子线程的相互影响关系可以查看https://blog.csdn.net/v123411739/article/details/79117430
对于可变对象:父线程初始化, 因为Thread Construct浅拷贝, 共用索引, 子线程修改父线程跟着变; 父线程不初始化, 子线程初始化, 无Thread Construct浅拷贝, 子线程和父线程都是单独引用, 不同对象, 子线程修改父线程不跟着变。
对于不可变对象:不可变对象由于每次都是新对象, 所以无论父线程初始化与否,子线程和父线程都互不影响。