一、进程和线程
进程是资源分配的最小单位,可以视为程序的一个实例,进程是用来加载指令,管理内存、管理IO的。
一个进程可以分为多个线程,一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给cpu执行,java中,线程作为最小调度单位,进程作为资源分配的最小单位。
线程上下文切换场景:
1、线程cpu时间片用完;
2、垃圾回收;
3、有更高优先级线程需要运行;
4、线程自动调用sleep, yield, wait, join, park, synchronized, lock等方法;
当contect switch发生时,需要有操作系统保存当前线程的状态,并恢复另一线程的状态,Java中对应的概念就是程序计数器,它的作用是记住下一条jvm指令的执行地址,是线程私有的。(状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等,context switch频繁切换会影响性能)
二、java多线程中常见方法
sleep()
1、调用sleep会让线程从Running进入Timed Waiting状态(阻塞);
2、其他线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException;
3、睡眠结束后的线程未必会立刻得到执行(就绪状态,争抢cpu时间片);
4、建议用TimeUnit的sleep代替Thread的sleep来获得更好的可读性;
yield()
1、调用yield会让当前线程从Running进入Runnable就绪状态,然后调度执行其他线程;
2、具体的实现依赖于操作系统的执行调度器;
join()
作用:保持线程间同步状态,分为有时间限制的等待和无时间限制的等待(即join方法是否传参,例:join(3000)),有时间限制的等待,若提前返回(线程执行完毕),则不会继续等待(以实际线程结束的时间为准);
isInterrupted()
作用:判断线程是否被打断 (注:调用后不会清除打断标记);
interrupted()
作用:判断是否被打断(注:调用后会清除打断标记) ;
stop() ,suspend() , resume()
stop():停止线程运行;
suspend():挂起(暂停)线程运行;
resume() :恢复线程运行;
这些方法均不推荐使用,原因:已过时,容易破坏同步代码块,造成线程死锁。
setPriority()
用于设置线程优先级, 范围1~10,默认优先级为5,越大优先级越高;
1、线程优先级会提示(hint)调度优先调度该线程,但它仅仅是一个提示,调度器可以忽略它;
2、如果cpu比较忙,那么优先级高的线程会获得更多的时间片,但cpu闲时,优先级几乎没作用;
防止CPU占用100%
在没有利用cpu来计算时,可以使用yield或sleep来让出cpu的使用权给其他程序,也可以使用wait或条件变量达到类似的效果;
1、不同的是,后两种(wait,条件变量)都需要加锁,并且需要相应的唤醒操作,一般适用于要进行同步的场景;
2、sleep适用于无需锁同步的场景;
setDaemon(true)
作用:将一个线程设为守护线程。
默认情况下,Java进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
常见的守护线程:
1、垃圾回收线程就是一种守护线程;
2、tomcat中的Acceptor和Poller线程都是守护线程,所以Tomcat接收到 shutdown命令后,不会等待它们处理完请求;
三、两阶段终止模式
Two Phase Termination:在一个线程T1中“优雅”的终止线程2,这里的优雅是给T2一个料理后事的机会。
错误思路:
1、使用线程对象的stop()方法停止线程。stop方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其他线程将永远无法获取锁;
2、使用System.exit(int)方法停止线程。目的仅是停止一个线程,但这种做法会让整个程序都停止;
正确思路(代码如下):
class TwoPhaseTermination{
private Thread monitor;
// 启动监控线程
public void start(){
monitor = new Thread(() ->{
while(true){
Thread current = Thread.currentThread();
if(current.isInterrupted()){
System.out.println("料理后事");
break;
}
try {
Thread.sleep(1000);// 情况1
System.out.println("执行监控记录");// 情况2
} catch (InterruptedException e) {
e.printStackTrace();
// 在情况1被打断
// 重新设置打断标记
current.interrupt();
}
}
});
monitor.start();
}
// 停止监控线程
public void stop(){
monitor.interrupt();
}
四、线程状态
五种状态(操作系统)
1、初始状态:仅是在语言层面创建了线程对象,还未与操作系统线程关联;
2、可运行状态:(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由CPU调度执行;
3、运行状态:指获取了CPU时间片运行中的状态,当cpu时间片用完,就会从运行状态切换至可运行状态,会导致线程的上下文切换;
4、阻塞状态:如果调用了阻塞API,如BIO读写文件,这时线程实际不会用到cpu,会导致线程的上下文切换,进入阻塞状态,等bio操作完毕,就由操作系统唤醒阻塞的进程,切换至可运行状态,与可运行状态的区别是,对阻塞状态的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们;
5、终止状态:表示线程已经执行完毕,生命周期已经结束,不会再转换为其他状态;
java中,线程有6中状态
1、NEW:线程刚被创建,还未调用start()方法;
2、RUNNABLE:涵盖了操作系统的可运行、运行;
3、BLOCKED:阻塞状态;
4、WAITING:没时间的等待;
5、TIMED_WAITING: 有时间的等待;
6、TERMINATED:线程终止;
五、保证线程安全
造成线程不安全的原因:存在临界区和竞态条件;
临界区:一段代码块内如果对共享变量同时存在读和写,则将这块代码称为临界区;
竞态条件:多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测;
解决临界区的竞态条件:
1、阻塞式解决方案:synchronized,Lock
2、非阻塞式解决方案:原子变量
synchronized实际是使用对象锁保证临界区代码的原子性。
class Test{
public synchronized void test(){
}
}
// 等价于
class Test{
public void test(){
synchronized (this){
}
}
}
......
class Test{
public synchronized static void test(){
}
}
// 等价于
class Test{
public static void test(){
synchronized (Test.class){
}
}
}
常见的线程安全类
String
Integer
StringBuffer
Random
Vector
HashTable
juc下的类
线程安全是指:多个线程调用它们同一个实例的某个方法时,是线程安全,也可以理解为它们的每个方法是原子的,但注意它们多个方法组合不是原子的;
String、Integer等都是不可变类,因为其内部的状态是不可改变的,因此它们的方法都是线程安全的;



