【Java】多线程Thread类
1. 进程与线程
进程与线程的基本认识
进程(Process):进程是程序的一次动态执行过程,它经历了从代码加载、执行、到执行完毕的一个完整过程;同时也是并发执行的程序在执行过程中分配和管理资源的基本单位,竞争计算机系统资源的基本单位。
线程(Thread):线程可以理解为进程中的执行的一段程序片段,是进程的一个执行单元,是进程内可调度实体,是比进程更小的独立运行的基本单位,线程也被称为轻量级进程。
即使多进程可以提高硬件资源的利用率,但是进程的启动和销毁需要消耗大量的系统性能,从而导致程序的执行性能下降。所以为了进一步提升并发操作的处理能力,在进程的基础上又划分了多线程概念。
2. 多线程实现
Thread类实现多线程
Thread类的方法
1.通过继承Thread类,并重写父类的run()方法实现
public void run(){ }
定义线程类:
class MyThread extends Thread{@Overridepublic void run() {while(true){System.out.println("hello Thread");try{Thread.sleep(1000);}catch(InterruptedException e){throw new RuntimeException(e);}}}
}
public class Demo1 {public static void main(String[] args)throws InterruptedException {Thread t=new MyThread();t.start();//创建一个线程//t.run();while(true){System.out.println("hello main");Thread.sleep(1000);}//两个循环在同时进行}
}
Runnable接口实现多线程
使用Thread类的确可以实现多线程,但是也容易发现它的缺陷:面向对象的单继承局限,因此才采用Runnable接口来实现多线程。
该接口的定义如下(以下并不需要在代码中实现):
class MyRunnable implements Runnable{@Overridepublic void run(){while(true){System.out.println("hello main");try{Thread.sleep(1000);}catch(InterruptedException e){throw new RuntimeException(e);}}}
}
public class Demo2 {public static void main(String[] args)throws Exception{Runnable runnable=new MyRunnable();Thread t=new Thread(runnable);t.start();while(true){System.out.println("hello main");Thread.sleep(1000);}}
}
Thread(String name)后边两种方法,我们可以给线程对象命名。
默认情况下,线程对象名字是Thread-N,N是从0开始增加。
3.Thread常见方法
start()
start() 启动一个线程。
start()方法可以开启一个线程,线程创建好后是不会开始执行的,要使用start来开启。
只有当start(),操作系统内核才会创建一个真真正正的线程PCB,然后去调用run()方法,此时操作系统层面才会真的创建一个线程。
sleep()
使用sleep时会有一个中断异常(非受查异常),需要我们手动抛出
join()
join(): 等待一个线程;
join(t)表示等待一个线程t毫秒。
因为我们线程是随即调度的过程,线程的执行顺便我们无法把控,但在一些场景下我们需要有明确的线程程序顺序,等待线程,就是可以控制两个线程的结束顺序。
比如在main方法中有两个线程t1、t2,在主线程中有t1.join() ,那么主线程就要阻塞 等待t1线程执行完了才能在执行。如果在t1线程中有t2.join(),那么t1线程就要阻塞 等待t2线程执行完了才能执行任务。
如果去掉join()方法后,主线程与Thread线程抢占式执行!
public class Demo12 {public static void main(String[] args)throws Exception {Thread t=new Thread(()->{for(int i=0;i<3000;i++){System.out.println("hello thread");try{Thread.sleep(1000);}catch(InterruptedException e) {throw new RuntimeException(e);}}System.out.println("线程结束");});t.start();t.join(3000);//等待线程t执行完毕System.out.println("main 线程结束");}
}
中断线程
线程中断: 我们这里的中断,并不是线程立即就结束了,而是通知该线程,你应该结束了,但实际是否结束,取决于线程里具体的代码
package Thread;public class Demo3 {public static boolean flag=true;public static void main(String[] args)throws InterruptedException{Thread t1=new Thread(()->{while(flag){System.out.println("hello Thread");try{Thread.sleep(1000);}catch(InterruptedException e){e.printStackTrace();}}});t1.start();Thread.sleep(3000);flag=false;}
}
flag 变量的作用是作为线程执行的控制开关,用于安全地终止 t1 线程的循环执行。
flag
变量是主线程与 t1
线程之间的一个通信媒介,通过修改它的值,主线程可以安全地通知 t1
线程“该停止工作了”,避免了线程的无限循环。如果没有 flag
,t1
线程的 while(true)
循环将无法终止,导致线程永久运行
即使 t1
线程因 sleep
进入阻塞状态,只要 sleep
时间结束,线程恢复运行后会立即检查 flag
,从而响应终止信号。
4.Thread类的常见属性
getId():
ID是线程的唯一标识,不同的线程不重复
package Thread;class MyThread1 extends Thread{@Overridepublic void run(){System.out.println("hello word");}
}
public class getIdtest {public static void main(String[] args) {Thread t=new MyThread1();System.out.println(t.getId());}
}
getName:
名称是我们构造方法设置的名称,如果没有设置,那就是默认名。
public class getNametest {public static void main(String[] args) {Thread t=new Thread();System.out.println(t.getName());//因为我们没有命名,所以使用的是默认名。}
}
getState():
状态:
getState()方法可以获取当前Thread对象的状态,这是Java层面定义的线程状态,要注意和PCB上的区分。
线程有6种状态:NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED。
NEW表示创建好了一个Java线程对象,安排好了任务,只是还没有启动,没有调用start方法之前是不会创建PCB的,和PCB没有关系。
RUNNABLE是运行状态或就绪状态,在操作系统层面已经有了对应的PCB。
BLOCKED是阻塞状态的一种,在等待锁
WAITING表示一直没有时间限制的等待,一直等直到被唤醒。
TIMED_WAITING指定了等待时间的阻塞状态,过时就继续执行任务。
TERMINATED是执行结束,PCB已经销毁,Java层面的线程对象还在。
查看Thread对象的所有状态:
package Thread;public class Demo14 {public static void main(String[] args) {for(Thread.State state: Thread.State.values()){System.out.println(state);}}
}
getPriority:
可以获取当前线程的优先级,但没有太大的用,实际还得取决于操作系统内核的具体调度。
package Thread;public class test {public static void main(String[] args) {Thread t=new Thread();System.out.println(t.getPriority());}
}
isDaemon():
是否为守护线程(也叫后台线程), 简单的理解一下,我们手动创建的线程都是前台线程,JVM自带的线程都是后台线程,我们也可以通过setDaemon()方法将前台线程设置为后台线程。
public static void main(String[] args) {Thread t=new MyThread2();System.out.println(t.isDaemon());}
isAlive():
线程的run方法是否执行结束。
package Thread;
class MyThread2 extends Thread{@Overridepublic void run(){System.out.println("hello thread1");}
}
public class Demo4 {public static void main(String[] args) {Thread t=new MyThread2();System.out.println(t.isAlive());t.start();System.out.println(t.isAlive());}}
interrupt()方法
第一种解释:
- 发送信号:通知线程“需要中断当前操作”,而非强制杀死线程。
- 打破阻塞:中断
sleep
/wait
等阻塞方法,让线程从阻塞中唤醒并处理中断。 - 协作终止:线程通过检查中断状态(
isInterrupted()
)或处理InterruptedException
,决定是否结束,体现“谁运行谁控制”的设计原则
在 Demo3
中,正是由于 t1
线程内部通过 while (!isInterrupted())
主动响应中断信号,主线程的 t1.interrupt()
才能安全、优雅地终止线程。
public static void main(String[] args)throws InterruptedException{Thread t1=new Thread(()->{while(!Thread.currentThread().isInterrupted()){System.out.println("hello Thread");try{Thread.sleep(1000);}catch(InterruptedException e){e.printStackTrace();}}});t1.start();Thread.sleep(3000);t1.interrupt();}
第二个解释:
1.把线程内部的标志位设置为true
2.如果线程正在sleep,会触发异常,将sleep唤醒。
sleep唤醒的同时,会清空标志位,将刚才设置的标志位在设置为false(这就导致,我们的循环会继续执行).
如果我们想去中断,我们可以在异常里加一个break。
2.3 多线程运行状态
任意线程具有5种基本的状态:
1.创建状态
实现Runnable接口和继承Thread类创建一个线程对象后,该线程对象处于创建状态,此时它已经有了内存空间和其它资源,但他还是处于不可运行的。
2.就绪状态
新建线程对象后,调用该线程的start方法启动该线程。启动后,进入线程队列排队,由CPU调度服务。
3.运行状态
就绪状态的线程获得处理器的资源时,线程就进入了运行状态,此时将自动调用run方法。
4.阻塞状态
正在运行的线程在某些特殊情况下,如:当前线程调用sleep、suspend、wait等方法时,运行在当前线程里的其它线程调用join方法时,以及等待用户输入的时候。只有当引起阻塞原因消失后,线程才能进入就绪状态。
5.终止状态
当线程run方法运行结束后,或者主线程的main()方法结束后,线程才能处于终止状态,线程一旦死亡就不能复生。