一. 线程
1. 线程的引入
- 虽然进程已经可以解决并发编程这种问题,但是进程在频繁进行创建和销毁的时候,系统开销非常大,如果一个服务器向你发送多个请求,针对每一个请求,都需要创建一个进程来应答,每个进程都需要申请资源(从硬盘加载到内存)和释放资源, 导致了资源的浪费
- 服务器发送的多个请求,所需要的资源都是相同的,为了解决相同类型的问题而创建出多个进程,内存存放多个相同的资源,导致了内存资源的严重浪费
线程的出现解决‘分配资源’和”释放资源“带来的额外开销,同时还支持并发处理,被称为轻量级进程
2. 线程的概念
- 一个进程包含一个或者多个线程(线程是进程的更精细化分)
- 一个线程包含一个PCB(一个进程包含多个PCB)
- 一个进程里面的所有线程,共用一份资源
注意:多个线程的内存指针和文件描述符表都共用同一份(PCB中的内存指针都指向一个地址)
那么意味着创建第一个线程需要分配资源,后面的线程只需要使用第一个线程申请到的资源即可。
线程主要解决的问题:降低频繁申请资源和释放资源带来的开销(开销很小,并不是没有开销)
进程是资源分配的基本单位,线程是资源调度的基本单位
3. 线程和进程的区别
- 一个进程包含一个或者多个线程
- 每个线程都是单独的执行流,可以单独参与cpu的调度
- 一个进程分配一个资源,这个进程中的所有线程都共用这一个资源
- 进程是资源分配的基本单位,线程是资源调度的基本单位
- 进程和进程直接是相互独立的,彼此互不干扰
- 同一个进程中,线程和线程之间是相关的,一个线程抛出异常,可能会影响其他线程的工作(线程安全问题)
- 线程并不是越多越好,线程过少,资源利用率较低,线程过多,调度开销变大,要适中
四. 线程的创建
(1)继承Thread类
class MyThread extends Thread{
//run方法是线程的入口
@Override
public void run(){
System.out.println("hello World");
}
}
public class Demo_1 {
// main方法是进程的入口
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
// 调用start方法会调用系统API,在系统内核中创建出线程
// 意味着可以同时具备多个执行流
}
}
需要重写run方法
(2)实现Runnable接口
class MyThread3 implements Runnable{
@Override
public void run() {
while(true){
System.out.println("111");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demo_3 {
public static void main(String[] args) {
MyThread3 myThread3 = new MyThread3();
Thread t = new Thread(myThread3);
// Thread t = new Thread(new MyThread3());
t.start();
while(true){
System.out.println("222");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
这种写法灵活性更强,让线程和要执行的任务进行解耦合,如果那个线程想要实现这个操作,直接调用这个类即可
(3)使用匿名内部类
public class Demo_4 {
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run() {
while(true){
System.out.println("111");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
thread.start();
while(true){
System.out.println("222");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
没有具体的实例,不知道子类的名称
(4)采用匿名内部类创建Runnable类
public class Demo_5 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("111");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
while(true){
System.out.println("222");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
(5)采用lambda(推荐写法)
public class Demo_6 {
public static void main(String[] args) {
Thread t = new Thread( ()-> {//()内是形参列表
while(true){
System.out.println("111");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
while(true){
System.out.println("222");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
这里的()->括号内是形参类型
二. Thread类
在 Java 中,main函数是程序的入口点。当 Java 虚拟机(JVM)启动时,会创建一个主线程来执行main方法。
public static void main(String[] args) {
System.out.println("hello");
}
- 一个进程中至少包含一个线程,第一个线程被称为主线程
(1)初始Thread类
在 Java 中,Thread 类是用于创建和管理线程的类,它位于java.lang 包下,所以在使用的时候不需要导入 ,我们可以通过这个类,来创建一个进程
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
System.out.println("hello");
}
};
t.start();
System.out.println("222");
}
注意:
- run方法是线程的入口,想要线程实现的功能要写入run方法中
- Thread t = new Thread();是创建出一个实例,但是内核中还没有PCB,无法参与调度
- t.start()是启动线程,在内核中创建出PCB,参与调度
- 每一个线程都是独立的执行流,这两个代码是并发执行的
- 所有的进程地位都是平等的,进程的执行顺序是随机的(抢占式执行)
- 为什么先输出“222”,因为创建一个线程的开销很小,但并不是没有,会花费很少的一段时间,所有大概率会先输出主线程的内容
(2)常用构造方法
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
1. 创建线程对象
Thread thread = new Thread();
2. 使用 Runnable
创建一个新的线程对象,并指定一个实现了Runnable接口的任务。
class MyThread3 implements Runnable{
@Override
public void run() {
System.out.println("111");
}
}
public class Demo_2 {
public static void main(String[] args) {
MyThread3 myThread3 = new MyThread3();
Thread t = new Thread(myThread3);
}
}
3. 创建线程对象,并命名
Thread thread1 = new Thread("线程1");
注意:命名对线程不会起到影响,只是方便测试
4. 使用 Runnable 并命名
创建一个新的线程对象,并指定一个实现了Runnable接口的任务。
class MyThread3 implements Runnable{
@Override
public void run() {
System.out.println("111");
}
}
public class Demo_2 {
public static void main(String[] args) {
MyThread3 myThread3 = new MyThread3();
Thread t = new Thread(myThread3,"线程001");
}
}
(3)常用方法
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
- ID:jvm自动分配身份标识符,确保唯一性
- 名称:如果没有命名,会自己分配一个名称,用于区别
- 状态:常见有阻塞状态,运行状态,就绪状态等
- 优先级:可以设置优先级,对内核调度器的调度起到一些影响,主要还是随机调度
- 后台程序:不会阻止线程的结束
- 前台程序:会阻止线程的结束(一般我们创建的代码就是前台代码,会因为运行导致进程不能结束)
- 存活:创建出一个实例,不是存活,只有start后(在内核中创建出PCB可以进行调度),才是存活,线程执行完毕,内核中PCB被释放,即使实例还存在,也不是存活
public class Demo_9 {
public static void main(String[] args) {
Thread thread = new Thread(()->{
});
thread.start();
System.out.println(thread.getId());
System.out.println(thread.getName());
System.out.println(thread.getState());
System.out.println(thread.getPriority());
System.out.println(thread.isDaemon());
System.out.println(thread.isAlive());
System.out.println(thread.isInterrupted());
}
}
(4)启动线程
使用start方法,就是启动线程,在内核中创建出PCB可以进行调度
对于同一个Thread对象来说,start只能调用一次
start方法和run方法区别
start方法会创建一个新线程,run方法不会创建新线程,只是定义线程任务。
(5)终止线程
让run方法结束,线程任务完成就会终止线程,依赖run方法的代码逻辑
1. 通过共享的标记位
//通过设置一个变量,手动控制
public class Demo_7 {
//必须写外面,如果写在主线程里面,会发生报错,lambda只能接受不能被修改的值
static boolean flt = false;
//成员变量可以,变成了内部类访问外部类成员
public static void main1(String[] args) {
Thread t = new Thread(()->{
while(!flt){
System.out.println("这是一个正在运行的线程……");
}
System.out.println("线程运行结束……");
});
t.start();
// 主线程进行干扰
flt = true;
System.out.println("让线程结束……");
// 交换代码位置,可能会导致顺序错误
// 交换之后,可能会导致再次打印这是一个正在运行的线程
}
}
注意:共享的标志位要写在外面,因为不同的线程在不同的栈帧中运行,导致生命周期不一样,如果主线程栈帧销毁了,flt 变量不存在了,但是其他的线程的栈帧还在,想使用flt变量,会发生代码异常
通过改变flt的属性,来影响进程
2. 调用 interrupt() 方法
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
while (!Thread.currentThread().isInterrupted()) {//默认为false
System.out.println("这是一个正在运行的线程……");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// throw new RuntimeException(e);//报错并停止运行,后面代码不会执行
e.printStackTrace();//显示错误原因
break;//跳出循环,继续向下执行
}
}
System.out.println("线程执行完毕");
});
thread.start();
Thread.sleep(3000);
thread.interrupt();
System.out.println("让进程停下");
}
Thread.currentThread()获取当前的引用,isInterrupted()是判定标志位,interrupt()是修改标志位
注意:这里会发生异常,如果进程在处于休眠状态,interrupt()会将sleep提前唤醒,会执行异常处理(1.报错并停止运行 2. 提示错误原因继续向下运行 3.什么也不做继续向下运行)
其实还有一种强制终止线程,不管线程的死活,直接干掉他,但是java中不支持,因为如果线程执行了一半,直接干掉他,会产生一些垃圾数据(加载一半的错误信息等)
(6)等待线程
线程之间的执行顺序是完全随机的(随机调度,抢占式执行),我们也不清楚线程执行多长时间(如果知道执行时间,可以根据休眠,控制线程执行的顺序),但是我们可以使用join方法控制结束的顺序
方法 | 说明 |
---|---|
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |
public void join(long millis, int nanos) | 同理,但可以更高精度 |
注意:
- Join方法不是确定线程的执行顺序,而是确定线程的结束顺序
- 通过阻塞操作,让两个进程的结束时间产生了先后顺序
- 如果A线程中调用了Join方法,就是A线程进入阻塞状态
1. 死等
thread.join();
这种等待属于死等,只要这个进程不结束,就一直处于阻塞状态(死等)
2. 设置等待的时间
thread.join(100);
一般不会使用死等,万一代码出现故障,就会导致后面的代码不能工作(死机)
举例:使用多个进程并发进行一系列的计数
public class Demo_8 {
public static int result = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
for (int i = 0; i < 100; i++) {
result +=i;
}
});
thread.start();
// 解决方法1:
Thread.sleep(100);
// 弊端:如果不知道thread线程要执行多久,休眠0.1秒可能会少
// 解决方法2:
thread.join();
// 等thread线程执行完了,才会执行主线程
System.out.println("结果:"+result);
// 因为执行的顺序不确定
// 结果可能出现3种,1,结果为0,2.结果为中间数,3.结果为4950
}
}
- 休眠的方式,控制执行顺序(前提是知道执行时间,才能设置休眠时间)
- 死等的方式,等thread线程执行完了,才可以执行主线程的打印操作
(7)获取线程引用
Thread.currentThread()
这个方法返回当前线程对象的引用
如果是继承Thread,那么可以使用this拿到线程的实例
class MyThread extends Thread{
//run方法是线程的入口
@Override
public void run(){
System.out.println("hello World");
System.out.println(this.getId());
}
}
如果是实现接口或者lambda的方式,this没有用,因为在静态方法中,this和进程实例没有直接的关联,不能用于获取进程实例。
(8)休眠线程
方法 | 说明 |
sleep(long millis) | 使当前线程暂停执行指定的毫秒数 |
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
sleep方法:会让线程从就绪状态调度到阻塞状态,不参与调度,sleep结束,会从阻塞状态调度到就绪状态
三. 线程的状态
java中,线程的常见状态
- NEW:线程的实例创建成功,但是没有调用start方法在内核中创建线程
- TERMINATED:线程的实例还存在,但是线程已经执行完毕,内核中的PCB已销毁
- RUNNABLE:就绪状态,说明这个线程正在cpu上执行,或者准备就绪随时可以在CPU上执行
- WAITING:阻塞状态(Join或者wait),不带时间的阻塞(死等)
- TIMED_WAITING:阻塞状态,带时间的阻塞
- BLOCKED:由于锁竞争引起的阻塞
public class Demo_10 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
while(!Thread.currentThread().isInterrupted()){
System.out.println("111");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
break;
}
}
});
// 运行前
System.out.println(thread.getState());
thread.start();
// 运行后
System.out.println(thread.getState());
// 休眠后
Thread.sleep(3000);
System.out.println(thread.getState());
// 结束后
thread.interrupt();
thread.join();
System.out.println(thread.getState());
}
}
在线程的运行中,主要状态是:NEW --> RUNNABLE --> TERMINATED
会因为执行一些特殊的操作,进入阻塞状态,进入阻塞的方式不同,被唤醒的方式也可能不同。
点赞的宝子今晚自动触发「躺赢锦鲤」buff!