当前位置: 首页 > news >正文

java多线程(常用方法、实现方式、线程安全问题、生命周期、线程池)

多线程相关的三组概念

程序和进程
  1. 程序(program):一个固定的运行逻辑和数据的集合,是一个静态的状态,一般存储在硬盘中。简单来说就是我们编写的代码

  2. 进程(process):一个正在运行的程序,一个程序的一次运行,是一个动态的概念般存储在内存中。 例如:command + option + esc,打开任务管理器可以看到所有进程

  3. 进程是正在运行的一个程序:

    程序是静态的:QQ这个程序,如果不运行就是存在磁盘中,不占内存,所以是静态的。

    进程是动态的:QQ这个程序,如果在运行就运行在内存中,占用内存,所以是动态的。

进程和线程
  1. 进程(process):一个正在运行的程序,一个程序的一次运行,是一个动态的概念存储在内存中。

    • 比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存空间,比如我们再启动迅雷,就会又启动一个进程,操作系统会给迅雷分配新的内存空间

    • 进程是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程,有它自身的产生,存在和消亡的过程

  2. 线程(thread):一条独立的执行路径。多线程,在执行某个程序的时候,该程序可以有多个子任务,每个线程都可以独立的完成其中一个任务。在各个子任务之间,没有什么依赖关系,可以单独执行。

    • 线程是由进程创建的,是进程的一个实体

    • 一个进程可以拥有多个线程

  3. 进程和线程的关系: 进程是用于分配资源的单位 一个进程中,可以有多条线程;但是一个进程中,至少有一条线程

    比如当我们启动迅雷时,就开启一个进程,使用迅雷时我们可以同时下载5个电影资源,每一个下载任务就是一个线程

    线程不会独立的分配资源,一个进程中的所有线程,共享同一个进程中的资源

并行和并发
  1. 并行(parallel):多个任务(进程、线程)同时运行。在某个确定的时刻,有多个任务在执行,多个cpu可以实现并行。 条件:有多个cpu,多核编程

  2. 并发(concurrent):多个任务(进程、线程)同时发起。不能同时执行的(只有一个cpu),只能是同时要求执行。就只能在某个时间片内,将多个任务都有过执行。一个cpu在不同的任务之间,来回切换,只不过每个任务耗费的时间比较短,cpu的切换速度比较快,所以可以让用户感觉就像多个任务在同时执行。并发就是同一时刻,多个任务交替执行,造成一种,“貌似同时”的错觉,简单来说,单核cpu实现的多任务就是并发

  3. 举例:

    1. 并发

    image-20230102225106897

    image-20230102225242835

    2.并行

    image-20230102225635068

一个人可以同时做多件事儿,就是并发

多个人可以同时做多件事儿,就是并行

并发是多个任务之间互相抢占资源

并行是多个任务之间不互相抢占资源

并发的关键是你有处理多个任务的能力,不一定要同时。 并行的关键是你有同时处理多个任务的能力。

最关键的点就是:是否是『同时』。

但是我们的电脑也会存在并发与并行同时存在的情况

并行和并发的图示
并发的本质:不同任务来回切换:

image-20230102180832171

并行和并发的比较:

image-20230102181327022

image-20230102181400200

image-20230102181418596

问&答:

问:既然只有一个cpu,还需要在不同的任务之间来回切换,那么效率到底是提升了还是降低了?

答:

1、提升了:整个系统的效率提升了(cpu的使用率提升了),对于某个具体的任务而言,效率是降低了

2、并发技术,解决的是不同的设备之间速率不同的问题 cpu: 10^-9秒 大概是千分之一毫秒 执行一次. 0.000000001 内存:10^-6秒 0.000001 磁盘:10^-3秒 0.001 人:10^0秒.

3、cpu 的效率非常高,给磁盘一个指令,让磁盘寻找某个字节,cpu就赶紧切换到其它的任务,执行其它的

计算、存储的操作。这样子就可以让计算机系统中的所有设备都运转起来,提升了cpu的使用率。绝不能出现

cpu这种昂贵的设备等待磁盘这种廉价设备的情况。

多线程的实现方式

多线程实现方式一:继承方式
  1. 继承Thread

  2. 步骤:

    1、定义一个类,继承Thread类

    2、重写自定义类中的run方法,用于定义新线程要运行的内容

3、创建自定义类型的对象

4、调用线程启动的方法:start()方法

代码示例:
/*** 多线程实现方式一:实现方式*/
public class Simple01 {public static void main(String[] args) {// 创建自定义类型的对象MyThread mt = new MyThread();// 调用对象的start方法,启动线程mt.start(); // 用主线程 调用自定义的线程
​// 让主线程(主方法本来就所在的线程)继续运行for (int i = 1; i <= 100; i++) {System.out.println(i);}// 现象是主线程内容和新线程内容交替执行,两条线程相互没有依赖关系}
​
}
​
// 定义一个类,继承Thread类
class MyThread extends Thread {//重写run方法,定义线程的运行内容@Overridepublic void run() {for (int i = 0; i <=100; i++) {System.err.println(i);}}
}
多线程实现方式二:实现方式
  1. 实现Runnable接口:

    Runnable接口的实现类对象,表示一个具体的任务,将来创建一个线程对象之后,让线程执行这个任务

  2. 步骤: 1、定义一个任务类,实现Runnable接口 2、重写任务类中的run方法,用于定义任务的内容 3、创建任务类对象,表示任务 4、创建一个Thread类型的对象,用于执行任务类对象 5、调用线程对象的start方法,开启新线程

代码示例:
/*** 多线程实现方式二:实现方式*/
public class Simple02 {public static void main(String[] args) {// 创建任务类对象MyTask myTask = new MyTask();// 创建线程对象,绑定要执行的任务Thread thread = new Thread(myTask);// 启动新线程thread.start();
​for (int i = 1; i <= 10000; i++) {System.out.println(i);}}
}
// 定义一个任务类
class MyTask  implements  Runnable{@Override// 重写run方法,定义任务的内容public void run() {for (int i = 1; i <= 10000; i++) {System.err.println(i);}}
}
两种方式的比较
  1. 代码复杂程度:

    继承Thread方式简单

    实现Runnable接口的方式比较复杂

  2. 设计:java中只支持单继承、不支持多继承

    • 继承方式:

      某个类继承了Thread类,那么就无法继承其他业务中需要的类型,就限制了我们的设计。所以扩展性较差。

    • 实现方式:

      某个类通过实现Runnable的方式完成了多线程的设计,仍然可以继承当前业务中的其它类型,扩展性较强。

  3. 灵活性:

    • 继承方式:

      将线程对象和任务内容绑定在了一起,耦合性较强、灵活性较差

    • 实现方式:

      将线程对象和任务对象分离,耦合性就降低,灵活性增强:同一个任务可以被多个线程对象执行,某个线程对象也可以执行其它的任务对象。并且将来还可以将任务类对象,提交到线程池中运行;任务类对象可以被不同线程运行,方便进行线程之间的数据交互。

使用匿名内部类创建线程对象
代码示例:
/*** 使用匿名内部类创建线程对象*/
public class Simple03 {public static void main(String[] args) {
​//第一条线程打印1-100,使用继承的方式new Thread() {@Overridepublic void run() {for(int i = 1; i <= 100; i++) {System.out.println(i + "...extends");}}}.start();
​//第二条线程打印1-100,使用实现的方式new Thread(new Runnable() {@Overridepublic void run() {for(int i = 1; i <= 100; i++) {System.out.println(i + "...implements" );}}}).start();}
}

Thread类中的常用方法

获取线程名称
  1. getName():获取线程的名称

  2. 注意事项: 1、如果没有给线程命名,那么线程的默认名字就是Thread-x,x是序号,从0开始

代码示例:
/*** 获取线程名称*/
public class Simple04 {public static void main(String[] args) {Thread thread = new Thread() {@Override// 继承方式 可以加this关键字找到getNamepublic void run() {System.out.println("name:"+this.getName() );}};thread.setName("线程0");thread.start();Thread thread1 = new Thread() {@Overridepublic void run() {System.out.println("name1:"+this.getName() );}};thread1.setName("线程1");thread1.start();}
}
设置线程名称
  1. setName(String name) 使用线程对象的引用,调用该方法给线程起名字

  2. 构造方法: Thread(String name):给线程通过构造方法起名字 Thread(Runnable target, String name):在给线程target对象赋值的同时,也给线程起名

  3. 注意事项: 线程设置名字,既可以在启动之前设置,也可以在启动之后设置

代码名称:
/*** 设置 线程名称*/
public class Simple05 {public static void main(String[] args) {Thread thread = new Thread("线程0") {@Override// 继承方式 可以加this关键字找到getNamepublic void run() {System.out.println("name:"+this.getName() );}};thread.start();
​new Thread(new Task(),"线程1") {@Overridepublic void run() {System.out.println("name:"+this.getName() );}}.start();}
}
class Task implements Runnable{@Overridepublic void run() {}
}
获取当前线程对象

1、作用: 某段代码只要是在执行,就一定是在某个方法中执行的,只要在某个方法中,代码就一定是被某个线程执行的。所以任意一句代码,都有执行这句代码的线程对象。 可以在任意位置,获取当前正在执行这段代码的线程对象 2、方法: Thread Thread.currentThread(); 返回当前正在执行这段代码的线程对象的引用 哪条线程在执行这段代码,返回的就是哪条线程

代码示例:
/*** 获取当前线程对象*/
public class Simple06 {public static void main(String[] args) {Thread thread = new Thread("线程0") {@Override// 继承方式 可以加this关键字找到getNamepublic void run() {System.out.println("name:"+this.getName() );}};thread.start();
//        System.out.println(thread.getName());
​new Thread(new Task(),"线程1").start();}
}
class Task implements Runnable{@Overridepublic void run() {System.out.println(Thread.currentThread());}
}
线程休眠
  1. Thread.sleep(long time) 让当前线程休眠 time:休眠的时间,单位是毫秒

  2. 作用: 当某段代码在某个位置需要休息的时候,就使用线程的休眠 无论哪个线程在运行这段代码,碰到sleep方法都需要停下休息

  3. 注意事项: 有一个异常,中断异常:InterruptedException 在普通方法中,可以声明抛出 在重写的run方法中,必须只能处理,不能声明

    代码示例:
    /*** 线程休眠*/
    public class Simple07 {public static void main(String[] args) throws InterruptedException {
    //        InterruptedException 中断异常
    //        System.out.println(1);
    //        Thread.sleep(1000);
    //        System.out.println(2);
    ​MyCountDown myCountDown = new MyCountDown();Thread thread = new Thread(myCountDown);thread.start();}
    }
    class MyCountDown implements Runnable {@Overridepublic void run() {// 在run方法中,异常只能处理,不能抛出for (int i = 5; i >= 1; i--) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(i);}}
    }
守护线程
  1. setDaemon(boolean flag):每条线程默认不是守护线程,只有设定了flag为true之后,该线程才变成守护线程

  2. isDaemon():判断某条线程是否是守护线程

  3. 守护线程的特点: 守护线程就是用于守护其它线程可以正常运行的线程,在为其它的核心线程准备良好的运行环境。

    如果非守护线程全部死亡,守护线程就没有存在的意义了,一段时间之后,虚拟机也一同结束。

    虚拟机结束了,所有线程都结束了。

  4. 别名: 后台线程

    好比前台的表演都是需要后台工作人员的准备支持,前台没有人表演了,那么后台工作人员就没有存在的意义了。

    在jvm中,gc就是守护线程,作用就是当用户自定义的线程以及主线程执行完毕后,gc才停止工作,

    不然的话,当主线程没有执行结束,gc就结束了,这样就会造成gc垃圾还没有清理完,主线程早就挂了。

    (注意:主线程不是守护线程,gc才是守护线程)。

    我们只需要了解什么是守护线程的概念,即可。

代码示例:
/*** 线程守护*/
public class Simple08 {public static void main(String[] args) {
​Thread thread = new Thread(() -> {for (int i = 0; i < 100; i++) {System.out.println(i+"...守护线程");}});// 设置为守护线程thread.setDaemon(true);System.out.println(thread.isDaemon());// 启动之后,系统中就有两条线程:主线程和守护线程thread.start();
​for (int i = 1; i <= 50; i++) {System.out.println(i + "...main");}//主线程运行完成之后,系统中就只剩下了守护线程//如果非守护线程全部死亡,守护线程就没有存在的意义了,//一段时间后,虚拟机也一同结束。//当主线程执行结束,jvm还没有立即结束,所以守护线程还会执行一段时间}
}

在实际开发中,守护线程(Daemon Thread)有以下主要作用:

  1. 后台任务执行:守护线程通常用于执行后台任务,这些任务不需要对用户可见,且不影响主线程的执行。比如,在 Web 服务器中,可以使用守护线程处理一些定时任务、日志记录等后台操作,而不会影响用户请求的处理。

  2. 资源回收:守护线程在 JVM 关闭时会自动终止,并且它们的执行不会阻止 JVM 退出。因此,守护线程通常用于执行资源回收工作,如垃圾回收器(Garbage Collector)线程就是守护线程,负责回收不再使用的对象。

  3. 定时任务:守护线程可以用于执行定时任务,如定时更新缓存、定时发送心跳包等。这样可以在后台自动执行这些任务,而不需要手动触发。

  4. 后台服务:某些后台服务可能需要一直运行,并随着应用的启动而启动。这些服务可以作为守护线程运行,以确保在应用退出时能够自动终止。

多线程中的线程安全问题

问题描述

某段代码在没有执行完成的时候,cpu就可能被其它线程抢走,结果导致当前代码中的一些数据发生错误

原因:没有保证某段代码的执行的完整性、原子性

希望:这段代码要么全都执行,要么全都没有执行

代码示例:
/*** 多线程中的线程安全问题* 模拟出票系统*/
public class Simple01 {public static void main(String[] args) {Ticket ticket = new Ticket(); // 堆中只有一个对象Thread thread1 = new Thread(ticket, "窗口1");Thread thread2 = new Thread(ticket, "窗口2");Thread thread3 = new Thread(ticket, "窗口3");thread1.start();thread2.start();thread3.start();}
}
​
class Ticket implements Runnable {
​private int ticktNum = 0;//票号
​// 线程售票public void run() {while (ticktNum < 10) { // 模拟售10张票// 1.模拟出票时间try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}// 2.打印进程号和票号String name = Thread.currentThread().getName();ticktNum = ++ticktNum;System.out.println("线程" + name + "售票:票号" + ticktNum);}// 出现的问题,某票号有可能被卖了多次// 不存在的票10号也有可能出现出现了// 出现不存在的票号原因:线程1,2在tickNum++ 时候被其他线程抢走了。倒置被累加多次
​}
}

多个线程对全局变量只读,不写,那么一般这个变量是安全的,

多个线程同时执行写的操作,那么就需要考虑线程同步的问题,否则会影响线程安全问题

综上所述,线程安全问题根本原因:

  1. 多个线程在操作共享的数据;

  2. 多个线程对共享数据有写的操作;

问题解决:

要解决以上线程问题,只要在某个线程修改共享资源的时候,其它线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

为了保证每个线程都能正常执行共享资源操作,Java引入了线程同步机制:

构造代码块

1) 同步代码块(synchronized)

2) 同步方法 (synchronized)

同步代码块
  1. 同步代码块: 使用一种格式,达到让某段代码执行的时候,cpu不要切换到影响当前代码的代码上去 这种格式,可以确保cpu在执行A线程的时候,不会切换到其它线程上去

  2. 使用格式 synchronized (锁对象) { 需要保证完整性、原子性的一段代码(需要同步的代码) }

  3. 使用同步代码块之后的效果: 当cpu想去执行同步代码块的时候,需要先获取到锁对象,获取之后就可以运行代码块中的内容;

    当cpu正在执行当前代码块内容时,cpu可以切换到其它代码,但是不能切换到具有相同锁对象上;

    当cpu执行完当前代码块中代码之后, 会释放锁对象,cpu就可以运行其它具有当前锁对象的同步代码块了;

代码示例:
/*** 同步代码块*/
public class Simple02 {public static void main(String[] args) {Printer p = new Printer();
​Thread t1 = new Thread() {@Overridepublic void run() {while (true) {p.print1();}}};
​Thread t2 = new Thread() {@Overridepublic void run() {while (true) {p.print2();}}};
​t1.start();t2.start();}
}
​
class Printer {Object object = new Object();public void print1() {// 线程1在获取了obj锁之后,线程2就无法获取obj锁,// 就只能等待线程1运行完这个方法,两条线程才能再次共同争夺锁对象synchronized (object) {System.out.print("码");System.out.print("上");System.out.print("未");System.out.println("来");}
​
​}
​public void print2() {synchronized (object) {System.err.print("线");System.err.print("程");System.err.println("锁");}
​
​}
}
// 不加锁的话,会出现:码上未来码   线程锁码 等数据不准确现象
// A线程执行print1完毕 释放obj对象,然后cpu被B线程抢去执行print2方法,
// B线程执行print2完毕,释放obj对象,然后cpu又被A或者B 线程抢去执行对应方法,
同步方法

1、同步代码块:在某段代码执行的时候,不希望cpu切换到其他影响当前线程的线程上去,就在这段代码上加上同步代码块,至于该方法中一些只读的成员变量就不需要放在同步代码块中了。

所以同步代码块中,只存放需要保证完整性、原子性的一段代码。

2、如果某个方法中,所有的代码都需要加上同步代码块,使用同步方法这种【简写格式】来替代同步代码块

3、同步方法的格式:

权限修饰符 [静态修饰符] synchronized 返回值类型 方法名称(参数列表) {

需要同步的方法体

}

4、同步方法的锁对象

如果是非静态的方法,同步方法的锁对象就是this,当前对象,哪个对象调用这个同步方法,这个同步方法使用的锁就是哪个对象

5、如果是静态的方法,同步方法的锁对象就是当前类的字节码对象,类名.class(在方法区的一个对象),哪个类在调用这个同步方法,这个同步方法使用的锁就是哪个类的字节码对象

代码示例:
/*** 同步方法*/
public class Simple03 {public static void main(String[] args) {Printer p = new Printer();Thread t1 = new Thread() {@Overridepublic void run() {while (true) {p.print1();}}};
​Thread t2 = new Thread() {@Overridepublic void run() {while (true) {p.print2();}}};
​t1.start();t2.start();}
}
​
class Printer {
​public static synchronized void print1() {System.out.print("码");System.out.print("上");System.out.print("未");System.out.println("来");}
​public static  synchronized void print2() {System.err.print("线");System.err.print("程");System.err.println("锁");}
}

6、特别注意,如果是静态方法,在静态方法内部使用同步代码块,同步代码块锁对象也是当前类的字节码对象

/*** 静态方法中使用同步代码块*/
public class Simple04 {public static void main(String[] args) {Printer p = new Printer();Thread t1 = new Thread() {@Overridepublic void run() {while (true) {p.print1();}}};
​Thread t2 = new Thread() {@Overridepublic void run() {while (true) {p.print2();}}};
​t1.start();t2.start();}
}
​
class Printer {public static void print1() {synchronized (Printer.class)  {System.out.print("码");System.out.print("上");System.out.print("未");System.out.println("来");}}public static void print2() {synchronized  (Printer.class)  {System.err.print("线");System.err.print("程");System.err.println("锁");}}
}
锁对象的说明
  1. 线程加上同步是为了让两条线程相互不要影响,如果相互影响,影响的是数据的正确性

  2. 使用锁对象,其实主要是为了锁数据。当某条线程在操作这个数据时,其它线程不能操作当前的数据。

  3. 如果两条线程要操作相同的数据,那么这两条线程就需要在操作数据的部分都加上同步代码块,并且使用相同的锁对象。

死锁
  1. A线程需要甲资源,同时拥有乙资源;B线程需要乙资源,同时拥有甲资源,两条线程都不肯释放自己拥有的资源,同时也需要其它的资源时,就都无法进行运行。形成“死锁”现象。

  2. 代码表现: 有了同步代码块的嵌套,就可能发生死锁。某条线程获取了外层锁对象A,需要内层锁对象B,等待;

    因为另一条线程此时已经获取了外层的锁对象B,需要内层的锁对象A,等待。两条线程就会形成死锁。

代码示例:
/*** 死锁*/
public class Simple05 {public static void main(String[] args) {
​Thread t1 = new Thread("苏格拉底") {public void run() {while (true) {synchronized ("筷子A") {System.out.println("苏格拉底争取到了筷子A,等待筷子B");
​synchronized ("筷子B") {System.out.println("苏格拉底获取了所有的筷子,狂吃狂吃");}}}}};
​Thread t2 = new Thread("柏拉图") {public void run() {while (true) {synchronized ("筷子B") {System.out.println("柏拉图争取到了筷子B,等待筷子A");synchronized ("筷子A") {System.out.println("柏拉图获取了所有的筷子,狂吃狂吃");}}}}};t1.start();t2.start();}
}
​
//  开发尽量避免锁的嵌套以此 来避免死锁现象发生。
// 公平锁 非公平锁  表锁 行锁 自旋锁
火车票案例
/*** 火车票案例*/
public class Simple06 {/*** 三个窗口,同时售卖100张火车票* 打印某个窗口卖出了1张票,还剩x张* 窗口是随机的** @param args*/public static void main(String[] args) {Tickets t1 = new Tickets();Tickets t2 = new Tickets();Tickets t3 = new Tickets();t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}
}
​
class Tickets extends Thread {
​//为了让多个线程多个对象 共享同一个数据 我们加上静态private static int tickets = 100;
​public void run() {while (true) {// 多个线程 使用同一个锁对象  使用xx.classsynchronized (Tickets.class) {if (tickets <= 0) {break;}try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}//线程1等待 线程2等待//线程3 把ticket-- 变成0  这时候cpu切换到线程1  线程1执行--操作变为-1tickets--;System.out.println(getName() + "卖出了1张票, 还剩" + tickets + "张票");}}}
}

线程生命周期

概述
  1. 线程是一个动态的概念,有创建的时候,也有运行和变化的时候,必须也就有消亡的时候,所以从生到死就是一个生命周期。在生命周期中,有各种各样的状态,这些状态可以相互转换。

  2. 状态罗列: 新建态:刚创建好对象的时候,刚new出来的时候 就绪态:线程准备好了所有运行的资源,只差cpu来临 运行态:cpu正在执行的线程的状态 阻塞态:线程主动休息、或者缺少一些运行的IO资源,即使cpu来临,也无法运行 死亡态:线程运行完成、出现异常、调用方法结束

状态转换图

image-20230103141234093

Java中线程状态的描述
  1. 线程状态只是理论上的状态,在计算机系统中有更加精确的描述,可以将各种不同的状态,封装成对象,这些对象可能和理论状态不完全相同。

  2. Java语言中,获取线程状态的方法: getState():返回当前线程对象的状态对象;返回值类型是Thread.State,Thread的内部类

  3. Thread.State:Java语言中,描述线程状态的类型 说明:由于状态时有限个的,所以该类的对象,不需要手动创建,直接在类中创建好了,获取即可,

    这种类型就是枚举类型。每个对象,称为枚举项。

    Thread.State中,枚举项:6个 NEW:新建态,没有开启线程 RUNNABLE:就绪态和运行态 BLOCKED:阻塞态 (等待锁、I\O,等待锁对象释放了,IO资源准备完毕了,就不阻塞了) WAITING:阻塞态 (调用了wait方法,等待其它线程唤醒的状态,没人喊它,它就一直阻塞) TIMED_WAITING:阻塞态(调用了有时间限制的wait方法、sleep方法) TERMINATED:死亡态

代码示例:
public class Simple01 {
​public static void main(String[] args) throws InterruptedException {//test1_new_timedWaiting_terminated_runnable();test2_blocked_TERMINATED();
​}
​//演示blocked阻塞状态public static void test2_blocked_TERMINATED() throws InterruptedException {
​Thread t1 = new Thread("t1线程") {public void run() {synchronized ("abc") {for (int i = 0; i <= 3; i++) {//打印数字,和线程名称System.out.println(i + "..." + getName());try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}}};Thread t2 = new Thread("t2线程") {public void run() {//没有获取到锁的线程对象的状态是:BLOCKEDsynchronized ("abc") {for (int i = 0; i <= 3; i++) {System.out.println(i + "..." + getName());try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}}};t1.start();t2.start();
​while (true) {System.out.println("t1...." + t1.getState());System.out.println("t2...." + t2.getState());Thread.sleep(50);//t1 和 t2 判断线程是否结束if (t1.getState().equals(Thread.State.TERMINATED) &&t2.getState().equals(Thread.State.TERMINATED)) {System.out.println("两条线程都结束了");break;}}}
​private static void test1_new_timedWaiting_runnable() throws InterruptedException {Thread t1 = new Thread() {public void run() {for (int i = 1; i < 100; i++) {System.out.println(i);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}
​}};System.out.println("A:" + t1.getState());//NEWt1.start();System.out.println("B:" + t1.getState());//RUNNABLEThread.sleep(10);//0.01System.out.println("C:" + t1.getState());//TIMED_WAITING}
}

线程池

概述
  1. 没有线程池的状态:

    当使用一条线程的时候,先将线程对象创建出来,启动线程,在运行过程中,正常完成任务之后,线程对象就结束了,就变成了垃圾对象,需要被垃圾回收器回收如果在系统中,大量的任务都是小任务,任务消耗时间较短、线程对象的创建和消亡耗费的时间比较多,

    结果:大部分的时间都浪费在了线程对象的创建和死亡上。

  2. 有线程池的状态 在没有任务的时候,先把线程对象准备好,存储到一个容器中,一旦有任务来的时候,就不需要创建

    对象,而是直接将对象获取出来执行任务如果任务破坏力较小,任务可以直接完成,这个线程对象不

    会进入死亡状态,而是被容器回收,继续活跃。如果任务破坏力较大,把线程任务搞死了,那么线程池

    也会继续提供下一个线程,继续完成这个任务。

线程池使用
  1. 步骤:获取线程池对象;创建任务类对象;将任务类对象提交到线程池中

  2. 获取线程池对象:

    工具类:

    //Executors:生成线程池的工具类,根据需求生成指定大小的线程池
    //ExecutorService  Executors.newSingleThreadExecutor():创建一个有单个线程的线程池
    //ExecutorService  Executors.newFixedThreadPool(int nThreads):创建一个指定线程数量的线程池    
  3. 创建任务类对象:Runnable的实现类对象,用于定义任务内容

  4. 将任务类对象提交到线程池中 ExecutorService:是一个接口,不需要手动创建这个接口的实现类对象,使用方法获取到的就是这个接口的实现类对象,就可以调用这个接口中的方法

  5. submit(Runnable r):可以将一个任务类对象,提交到线程池中,如果有空闲的线程,就可以马上运行这个任务,如果没有空闲线程,那么这个任务就需要等待。

  6. shutDown():结束线程池,已经提交的全部保证完成,在此代码后就不准继续提交任务了

  7. shutDownNow():结束线程池,已经开始运行的,保证完成,等待的任务不保证完成

但是已经提交的,还没有运行的,就不给运行了,作为返回值范围;对于没有提交的,不准提交。

代码示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
​
public class Simple02 {public static void main(String[] args) {//  准备一个线程池对象  1个//  ExecutorService es = Executors.newSingleThreadExecutor();//  准备一个线程池对象  2个ExecutorService es = Executors.newFixedThreadPool(2);Object obj = new Object();Runnable r1 = new Runnable() {@Overridepublic void run() {synchronized (obj) {for (int i = 1; i <= 10; i++) {System.out.println(i + "..." + Thread.currentThread().getName());try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}}}}};Runnable r2 = new Runnable() {@Overridepublic void run() {synchronized (obj) {for (int i = 11; i <= 20; i++) {System.out.println(i + "..." + Thread.currentThread().getName());try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}}}}};Runnable r3 = new Runnable() {@Overridepublic void run() {synchronized (obj) {for (int i = 21; i <= 30; i++) {System.out.println(i + "..." + Thread.currentThread().getName());try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}}}}};
​es.submit(r1);es.submit(r2);es.submit(r3);// 已经提交的全部保证完成,不准继续提交了
​//  es.shutdown();// 已经提交的 前三个任务 保证完成,在shutdown之后就没法添加任务了//  es.submit(r3);//  es.shutdownNow();//  shutDownNow():结束线程池,已经开始运行的,保证完成,等待的任务不保证完成//  另外结束线程池这句话遇到任务中有sleep执行的时候就会有问题,会抛异常//  可以把任务sleep的代码注释上 就不报错了}
}
​

相关文章:

java多线程(常用方法、实现方式、线程安全问题、生命周期、线程池)

多线程相关的三组概念 程序和进程 程序&#xff08;program&#xff09;&#xff1a;一个固定的运行逻辑和数据的集合&#xff0c;是一个静态的状态&#xff0c;一般存储在硬盘中。简单来说就是我们编写的代码 进程&#xff08;process&#xff09;&#xff1a;一个正在运行的…...

Day05 linux高级系统设计 - 管道

复制文件描述符 dup函数 作用&#xff1a; 文件描述符复制 语法&#xff1a; #include <unistd.h> int dup (int oldfd); 参数&#xff1a; 所需复制得文件描述符 返回值&#xff1a; 复制到的文件描述符 功能&#xff1a; 从文件描述符表中&#xff0c;找一个最小…...

低代码:美味膳食或垃圾食品?

一、什么是低代码 低代码是一种开发方法&#xff0c;通过可视化界面和少量的编码&#xff0c;使开发者能够快速构建应用程序。它的目标是提高开发效率、降低开发成本&#xff0c;并支持快速迭代和敏捷开发。 二、低代码的优缺点 低代码开发具有以下优点&#xff1a; 快速开…...

免费网页抓取工具大全【附下载和工具使用教程】

在当今信息爆炸的时代&#xff0c;获取准确而丰富的数据对于企业决策和个人研究至关重要。而网页抓取工具作为一种高效获取互联网数据的方式&#xff0c;正逐渐成为大家解决数据需求的得力助手。本文将深入探讨网页抓取工具的种类&#xff0c;并为大家提供简单实用的页面采集教…...

Leetcode 39 组合总和

题意理解&#xff1a; 一个 无重复元素 的整数数组 candidates 和一个目标整数 target 从candidates 取数字&#xff0c;使其和 target &#xff0c;有多少种组合&#xff08;candidates 中的 同一个 数字可以 无限制重复被选取&#xff09; 这道题和之前一道组合的区别&am…...

Windows下使用AndroidStudio及CMake编译Android可执行程序或静态库动态库

Windows下使用AndroidStudio及CMake编译Android可执行程序或静态库动态库 文章目录 Windows下使用AndroidStudio及CMake编译Android可执行程序或静态库动态库一、前言二、编译环境三、示例C/CPP程序1、总体工程结构2、示例代码3、CMakeLists.txt&#xff08;重要&#xff09;4、…...

MySQL七 | 存储引擎

目录 存储引擎 存储引擎特点 存储引擎选择 Innodb与MyISAM区别 存储引擎 默认存储引擎:InnoDB show engines;#展示当前数据库支持的存储引擎 存储引擎特点 特点InnoDBMyISAMMemory存储限制64TB有有事务安全支持--锁机制行锁表锁表锁Btree锁支持支持 支持 Hash索引--支…...

网上下载的pdf文件,为什么不能复制文字?

不知道大家有没有到过这种情况&#xff1f;在网上下载的PDF文件打开之后&#xff0c;发现选中文字之后无法复制。甚至其他功能也都无法使用&#xff0c;这是怎么回事&#xff1f;该怎么办&#xff1f; 当我们发现文件打开之后&#xff0c;编辑功能无法使用&#xff0c;很可能是…...

Linux下apisix离线安装教程

Linux下apisix离线安装教程 一、首先需要安装etcd&#xff1a;二、通过rpm离线安装apisix三、启动apisix四、安装apisix-dashboard1、安装2、更改dashboard登录账号名和密码3、运行 一、首先需要安装etcd&#xff1a; 解压缩etcd后执行以下命令&#xff1a; tar -xvf etcd-v3.…...

基于STM32 + DMA介绍,应用和步骤详解(ADC多通道)

前言 本篇博客主要学习了解DMA的工作原理和部分寄存器解析&#xff0c;针对ADC多通道来对代码部分&#xff0c;应用部分作详细讲解&#xff0c;掌握代码编程原理。本篇博客大部分是自己收集和整理&#xff0c;如有侵权请联系我删除。 本次博客开发板使用的是正点原子精英版&am…...

openGauss学习笔记-144 openGauss 数据库运维-例行维护-慢sql诊断

文章目录 openGauss学习笔记-144 openGauss 数据库运维-例行维护-慢sql诊断144.1 背景信息144.2 前提条件 openGauss学习笔记-144 openGauss 数据库运维-例行维护-慢sql诊断 144.1 背景信息 在SQL语句执行性能不符合预期时&#xff0c;可以查看SQL语句执行信息&#xff0c;便…...

计算机毕业设计springboot+ssm停车场车位预约系统java

管理员不可以注册账号 停车位包括车位所在楼层、车位编号、车位类型(全时间开放/高峰期开放)、预定状态等 用户预约时要求支付预约时间段的停车费用 违规行为&#xff1a;1.停车超过预约时间段 2.预约未使用 于系统的基本要求 &#xff08;1&#xff09;功能要求&am…...

打破常规思维:Scrapy处理豆瓣视频下载的方式

概述 Scrapy是一个强大的Python爬虫框架&#xff0c;它可以帮助我们快速地开发和部署各种类型的爬虫项目。Scrapy提供了许多方便的功能&#xff0c;例如请求调度、数据提取、数据存储、中间件、管道、信号等&#xff0c;让我们可以专注于业务逻辑&#xff0c;而不用担心底层的…...

系列学习前端之第 2 章:一文精通 HTML

全套学习 HTMLCSSJavaScript 代码和笔记请下载网盘的资料&#xff1a; 链接: https://pan.baidu.com/s/1-vY2anBdrsBSwDZfALZ6FQ 提取码: 6666 HTML 全称&#xff1a;HyperText Markup Language&#xff08;超文本标记语言&#xff09; 1、 HTML 标签 1. 标签又称元素&#…...

SCSS Module 这样处理配置和使用太赞了

SCSS Module 只是Scss和Css Module结合&#xff0c;可以利用SCSS对代码静态处理的能力&#xff0c;使得样式处理更强大一些&#xff0c;并不是什么新的东西&#xff0c;对比css-in-js和scoped&#xff0c;个人偏向喜欢Scss Module做样式隔离&#xff0c;先说一下优点&#xff1…...

【Unity动画】Unity 2D动画创建流程

本文以2D为案例&#xff0c;讲解Unity 播放动画的流程 准备和导入2D动画资源 外部导入序列帧生成的 Unity内部制作的 外部导入的3D动画 2.创建动画过程 打开时间轴Ctrl6 选中场景中的一个未来需要播放动画的物体 回到时间轴点击Create一个新动画片段 拖动2D动画资源放入…...

【算法每日一练]-图论(保姆级教程篇12 tarjan篇)#POJ3352道路建设 #POJ2553图的底部 #POJ1236校园网络 #缩点

目录&#xff1a; 今天知识点 加边使得无向图图变成双连通图 找出度为0的强连通分量 加边使得有向图变成强连通图 将有向图转成DAG图进行dp POJ3352&#xff1a;道路建设 思路&#xff1a; POJ2553&#xff1a;图的底部 思路&#xff1a; POJ1236校园网络 思路&#x…...

Python数据科学视频讲解:数据挖掘与建模的注意事项

1.7 数据挖掘与建模的注意事项 视频为《Python数据科学应用从入门到精通》张甜 杨维忠 清华大学出版社一书的随书赠送视频讲解1.7节内容。本书已正式出版上市&#xff0c;当当、京东、淘宝等平台热销中&#xff0c;搜索书名即可。内容涵盖数据科学应用的全流程&#xff0c;包括…...

unity | 动画模块之循环滚动选项框

一、作者的话 评论区有人问&#xff0c;有没有竖排循环轮播选项框&#xff0c;我就写了一个 二、效果动画 如果不是你们想要的&#xff0c;就省的你们继续往下看了 三、制作思路 把移动分成里面的方块&#xff0c;还有背景&#xff08;父物体&#xff09;&#xff0c;方块自…...

TinyMPC - CMU (卡耐基梅隆大学)开源的机器人 MPC 控制器

系列文章目录 CasADi - 最优控制开源 Python/MATLAB 库 文章目录 系列文章目录前言一、机器人硬件对比1.1 Teensy 上的微控制器基准测试1.2 机器人硬件1.3 BibTeX 二、求解器三、功能&#xff08;预期&#xff09;3.1 高效3.2 鲁棒3.3 可嵌入式3.4 最小依赖性3.5 高效热启动3.…...

基于FPGA的PID算法学习———实现PID比例控制算法

基于FPGA的PID算法学习 前言一、PID算法分析二、PID仿真分析1. PID代码2.PI代码3.P代码4.顶层5.测试文件6.仿真波形 总结 前言 学习内容&#xff1a;参考网站&#xff1a; PID算法控制 PID即&#xff1a;Proportional&#xff08;比例&#xff09;、Integral&#xff08;积分&…...

ubuntu搭建nfs服务centos挂载访问

在Ubuntu上设置NFS服务器 在Ubuntu上&#xff0c;你可以使用apt包管理器来安装NFS服务器。打开终端并运行&#xff1a; sudo apt update sudo apt install nfs-kernel-server创建共享目录 创建一个目录用于共享&#xff0c;例如/shared&#xff1a; sudo mkdir /shared sud…...

逻辑回归:给不确定性划界的分类大师

想象你是一名医生。面对患者的检查报告&#xff08;肿瘤大小、血液指标&#xff09;&#xff0c;你需要做出一个**决定性判断**&#xff1a;恶性还是良性&#xff1f;这种“非黑即白”的抉择&#xff0c;正是**逻辑回归&#xff08;Logistic Regression&#xff09;** 的战场&a…...

DAY 47

三、通道注意力 3.1 通道注意力的定义 # 新增&#xff1a;通道注意力模块&#xff08;SE模块&#xff09; class ChannelAttention(nn.Module):"""通道注意力模块(Squeeze-and-Excitation)"""def __init__(self, in_channels, reduction_rat…...

YSYX学习记录(八)

C语言&#xff0c;练习0&#xff1a; 先创建一个文件夹&#xff0c;我用的是物理机&#xff1a; 安装build-essential 练习1&#xff1a; 我注释掉了 #include <stdio.h> 出现下面错误 在你的文本编辑器中打开ex1文件&#xff0c;随机修改或删除一部分&#xff0c;之后…...

【第二十一章 SDIO接口(SDIO)】

第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...

MODBUS TCP转CANopen 技术赋能高效协同作业

在现代工业自动化领域&#xff0c;MODBUS TCP和CANopen两种通讯协议因其稳定性和高效性被广泛应用于各种设备和系统中。而随着科技的不断进步&#xff0c;这两种通讯协议也正在被逐步融合&#xff0c;形成了一种新型的通讯方式——开疆智能MODBUS TCP转CANopen网关KJ-TCPC-CANP…...

《基于Apache Flink的流处理》笔记

思维导图 1-3 章 4-7章 8-11 章 参考资料 源码&#xff1a; https://github.com/streaming-with-flink 博客 https://flink.apache.org/bloghttps://www.ververica.com/blog 聚会及会议 https://flink-forward.orghttps://www.meetup.com/topics/apache-flink https://n…...

Maven 概述、安装、配置、仓库、私服详解

目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...

使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台

🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...