线程
守护线程
用户线程:用户自己创建的线程(新建Thread线程的操作)
* 守护线程: 运行在后台,比如垃圾回收(GC)
//创建用户线程和守护线程
public static void main(String[] args) {
Thread t1 = new Thread(()->{
System.out.println(Thread.currentThread().getName()+"::"+Thread.currentThread().isDaemon());//false表示他是一个用户线程
while (true){
}
},"aa");
//将aa设置成守护线程,所以main线程结束后,作为守护线程的aa也结束运行,而用户线程的aa则会继续运行
t1.setDaemon(true);
t1.start();
//此时主线程已经结束,但用户线程还在继续运行
System.out.println(Thread.currentThread().getName()+" over");
}
创建线程的三种方式
/**
* 创建线程的三种方法
* Thread Runnable Callable
*/
static class CreateThread{
public static void main(String[] args) throws ExecutionException, InterruptedException {
/**
* 方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
* 用 Runnable 更容易与线程池等高级 API 配合
* 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
*
* FutureTask可以利用Future接口中的run方法返回结果
*/
//方法1 Thread类创建
Thread thread = new Thread("t1"){
//创建了一个Thread的子类对象,并重写父类的run方法。
@Override
public void run() {
for (int i = 0; i < 100000; i++) {
System.out.println(Thread.currentThread().getName()+":这是Thread1");
}
}
};
//方法2 使用Runnable接口配合Thread
Thread thread2 = new Thread(()->{
//在源码里,Thread会检查参数Runnable对象是否被赋值,如果没有赋值,那就调用Runnable接口实现的run方法
for (int i = 0; i < 100000; i++) {
System.out.println(Thread.currentThread().getName()+":这是Thread2");
}
},"t2");
//方法三,FutureTask 配合 Thread
FutureTask<String> task = new FutureTask<String>(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("running......");
TimeUnit.SECONDS.sleep(2);
return "执行完成";
}
});
Thread thread3 = new Thread(task,"FTask1");
//thread3.start();
//get方法在call方法执行完成前都会是阻塞状态,只有全部的代码执行完后get才能够返回返回值
//System.out.println(task.get());
}
线程运行的原理
栈帧
/**
* 线程运行的原理
* 我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟
* 机就会为其分配一块栈内存。
* 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
* 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
*/
static class TestFrames{
/**
* 线程启动后,Jvm就会分配一块栈内存
* 每执行一个方法,都会产生一个新的栈帧内存
* method1,2方法执行完后,内存就会被释放
* @param args
*/
public static void main(String[] args) {
method1(10);
}
private static void method1(int x){
int y = x+3;
Object m = method2();
System.out.println(m);
}
private static Object method2(){
Object n = new Object();
return n;
}
}
这个是线程创建后的操作
首先Main主线程会被分配一块独立的栈空间。并为main栈帧中的局部变量args在堆中创建一个newString[]空间。程序计数器运行到一个方法,都会为方法的局部变量开辟一块空间,然后确定方法的返回地址。当方法运行完后便会释放掉栈帧的内存空间。
多线程下
/**
* 多线程
*/
static class TestFramesMulti{
/**
* 我们在刚才的运行方式新增一个线程t1
*
* 从debug中可以看出,每个线程都会被开辟一块独立的虚拟机栈,有自己的栈帧,它们之间互不干扰。
* @param args
*/
public static void main(String[] args) {
Thread thread = new Thread("t1"){
@Override
public void run() {
method1(20);
}
};
thread.start();
method1(10);
}
private static void method1(int x){
int y = x+3;
Object m = method2();
System.out.println(m);
}
private static Object method2(){
Object n = new Object();
return n;
}
}
由图中可以看到,创建新线程的t1倍分配了一块区别于main线程的独立的栈空间。
线程的上下文切换
/**
* 线程的上下文切换
* 线程会因为一些原因停止执行而执行另一个线程的代码:
* 1.时间片用完
* 2.垃圾回收会暂停所有的工作线程,导致上下文切换
* 3.被高优先级的线程抢占
* 4.线程自己调用了sleep,yield,wait,join等方法
* 当上下文切换发生时,操作系统会保存当前运行的状态并恢复另外一个线程的状态。在Java中就是程序计数器。
* 它的作用时记住下一条JVM指令的执行地址,是线程私有的。
* 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
* Context Switch 频繁发生会影响性能
*/
start和run
start是线程的启动方法,而run只是线程启动后执行的方法
如果只是调用run方法,那就相当于执行了一个普通方法
static class TestCurrentMethod{
public static void main(String[] args) {
Thread t1 = new Thread("t1") {
@Override
public void run() {
System.out.println("执行run方法的线程:"+Thread.currentThread().getName());
//这里可以证明一下上面的打印其实是main方法打印的
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//如果只是调用run方法,那其实就是调取了一个普通方法
t1.run();
/**
* 执行结果:
* 执行run方法的线程:main
* 结束运行
*/
System.out.println("结束运行");
}
}
start不能被调用多次,否则会报IllegalThreadException
sleep和yield
sleep
- 1. 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 2. 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 3. 睡眠结束后的线程未必会立刻得到执行
- 4. 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
yield
- 1. 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
- 2. 具体的实现依赖于操作系统的任务调度器
调用Sleep后进程的状态(对应sleep.1)
public static void TestSleep(){
Thread t1 = new Thread("t1"){
@Override
public void run() {
System.out.println("1.t1的状态是:"+Thread.currentThread().getState());
try {
Thread.sleep(3000);
System.out.println("2.t1的状态是:"+Thread.currentThread().getState());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
System.out.println("3.t1的状态是:"+t1.getState());
t1.start();
System.out.println("4.t1的状态是:"+t1.getState());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("5.t1的状态是:"+t1.getState());
}
3.t1的状态是:NEW
4.t1的状态是:RUNNABLE
1.t1的状态是:RUNNABLE
5.t1的状态是:TIMED_WAITING
2.t1的状态是:RUNNABLE
sleep.2
public static void TestInterrupter(){
Thread t1 = new Thread("t1"){
@Override
public void run() {
System.out.println("进入睡眠咯");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
System.out.println("起床气了起床气了");
e.printStackTrace();
}
}
};
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("现在准备打断t1");
t1.interrupt();
}
进入睡眠咯
现在准备打断t1
起床气了起床气了
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at hwh.JUC.Day01.Note01$TestSleepAndYield$2.run(Note01.java:238)
Process finished with exit code 0
线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
public static void TestYield(){
Runnable task1 = () -> {
int count = 0;
for (;;) {
System.out.println("---->1 " + count++);
}
};
Runnable task2 = () -> {
int count = 0;
for (;;) {
Thread.yield();
System.out.println(" ---->2 " + count++);
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
实际执行上,t1打印的数字更高。
join 方法详解
为什么需要 join
下面的代码执行,打印
r
是什么?
static int r = 0;
public static void main (String[]args) throws InterruptedException {
test1();
}
private static void test1 () throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
r = 10;
});
t1.start();
log.debug("结果为:{}", r);
log.debug("结束");
}
分析
因为主线程和线程
t1
是并行执行的,
t1
线程需要
1
秒之后才能算出
r=10
而主线程一开始就要打印
r
的结果,所以只能打印出
r=0
解决方法
用
sleep
行不行?为什么?
用
join
,加在
t1.start()
之后即可
在主线程里调用t1的join方法,主线程就不会立刻结束而是等待t1线程结束。所以会打印出r=10
等待多个结果
- 以调用方角度来讲,如果需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
调用join后实现了main和t1的线程同步
private static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
Thread t2 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
r2 = 20;
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
long end = System.currentTimeMillis();
System.out.println("r1 = "+r1+"||r2="+r2+"||time-"+(end-start));
}
在这种复杂的调用里,多个线程通过join方法实现了线程同步
也可以给join加上参数,使其只等待参数值的时间。
Interrupt方法详解
Interrupt可以用来打断处于阻塞状态的线程(sleep,join,wait)
private static void testInterrupt(){
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
t1.start();
System.out.println("Interrupter start");
t1.interrupt();
try {
//线程处于sleep,wait,join 当线程被打断后,打断标记会被置为false
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("isInterrupted="+t1.isInterrupted());
}
打断正在运行的线程
private static void testRunningInterrupt(){
Thread t1 = new Thread(() -> {
while (true){
//可以利用打断状态来主动停止线程
if(Thread.currentThread().isInterrupted()){
System.out.println("被打断了");
break;
}
}
});
t1.start();
System.out.println("Interrupter start");
t1.interrupt();
try {
//线程处于sleep,wait,join 当线程被打断后,打断标记会被置为false
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果只是这样打断,isInterrupted的值会是真,但t1线程 不会停止运行
System.out.println("isInterrupted="+t1.isInterrupted());
}
两阶段终止模式
/** * 两阶段终止模式 * 在一个多线程案例中,如何让t1线程优雅的停止t2,这个优雅指的是给t2一个料理后事的机会. * * 错误思路:利用stop方法停止运行 * 如果强制杀死线程,如果该线程锁住了共享资源,那么整个线程就没有机会再释放锁。 */
/**
* 两阶段终止模式
* 在一个多线程案例中,如何让t1线程优雅的停止t2,这个优雅指的是给t2一个料理后事的机会.
*
* 错误思路:利用stop方法停止运行
* 如果强制杀死线程,如果该线程锁住了共享资源,那么整个线程就没有机会再释放锁。
*/
static class Test3{
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination twoPhaseTermination = new TwoPhaseTermination();
twoPhaseTermination.start();
Thread.sleep(3500);
twoPhaseTermination.stop();
}
static class TwoPhaseTermination{
private Thread monitor;
//启动监控线程
public void start(){
monitor = new Thread(()->{
while (true){
Thread current = Thread.currentThread();
if (current.isInterrupted()){
System.out.println("料理后事咯");
break;
}else {
try {
TimeUnit.SECONDS.sleep(1); //被打断情况1
System.out.println("执行监控操作"); //被打断情况2
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("正在Sleep中被打断");
current.interrupt();
}
}
}
});
monitor.start();
}
//停止监控线程
public void stop(){
monitor.interrupt();
}
}
}
后续会有voliate关键字来优化这段代码
线程的五种状态(操作系统层面)
- 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 【运行状态】指获取了 CPU 时间片运行中的状态 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
- 【阻塞状态】如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
- 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
线程的六种状态(JavaAPI层面)
- NEW 线程刚被创建,但是还没有调用 start() 方法
- RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的
- 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
- BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述
- TERMINATED 当线程代码运行结束