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

2023-12-13 05:23:02

多线程相关的三组概念

程序和进程
  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方法,定义线程的运行内容
 ? ?@Override
 ? ?public 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() {
 ? ? ? ? ? ?@Override
 ? ? ? ? ? ?public void run() {
 ? ? ? ? ? ? ? ?for(int i = 1; i <= 100; i++) {
 ? ? ? ? ? ? ? ? ? ?System.out.println(i + "...extends");
 ? ? ? ? ? ? ?  }
 ? ? ? ? ?  }
 ? ? ?  }.start();
?
 ? ? ? ?//第二条线程打印1-100,使用实现的方式
 ? ? ? ?new Thread(new Runnable() {
 ? ? ? ? ? ?@Override
 ? ? ? ? ? ?public 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关键字找到getName
 ? ? ? ? ? ?public void run() {
 ? ? ? ? ? ? ? ?System.out.println("name:"+this.getName() );
 ? ? ? ? ?  }
 ? ? ?  };
 ? ? ? ?thread.setName("线程0");
 ? ? ? ?thread.start();
 ? ? ? ?Thread thread1 = new Thread() {
 ? ? ? ? ? ?@Override
 ? ? ? ? ? ?public 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关键字找到getName
 ? ? ? ? ? ?public void run() {
 ? ? ? ? ? ? ? ?System.out.println("name:"+this.getName() );
 ? ? ? ? ?  }
 ? ? ?  };
 ? ? ? ?thread.start();
?
 ? ? ? ?new Thread(new Task(),"线程1") {
 ? ? ? ? ? ?@Override
 ? ? ? ? ? ?public void run() {
 ? ? ? ? ? ? ? ?System.out.println("name:"+this.getName() );
 ? ? ? ? ?  }
 ? ? ?  }.start();
 ?  }
}
class Task implements Runnable{
 ? ?@Override
 ? ?public void run() {
 ?  }
}
获取当前线程对象

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

代码示例:
/**
 * 获取当前线程对象
 */
public class Simple06 {
 public static void main(String[] args) {
 ? ? ? ?Thread thread = new Thread("线程0") {
 ? ? ? ? ? ?@Override
 ? ? ? ? ? ?// 继承方式 可以加this关键字找到getName
 ? ? ? ? ? ?public 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{
 ? ?@Override
 ? ?public 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 {
     ? ?@Override
     ? ?public 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() {
 ? ? ? ? ? ?@Override
 ? ? ? ? ? ?public void run() {
 ? ? ? ? ? ? ? ?while (true) {
 ? ? ? ? ? ? ? ? ? ?p.print1();
 ? ? ? ? ? ? ?  }
 ? ? ? ? ?  }
 ? ? ?  };
?
 ? ? ? ?Thread t2 = new Thread() {
 ? ? ? ? ? ?@Override
 ? ? ? ? ? ?public 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() {
 ? ? ? ? ? ?@Override
 ? ? ? ? ? ?public void run() {
 ? ? ? ? ? ? ? ?while (true) {
 ? ? ? ? ? ? ? ? ? ?p.print1();
 ? ? ? ? ? ? ?  }
 ? ? ? ? ?  }
 ? ? ?  };
?
 ? ? ? ?Thread t2 = new Thread() {
 ? ? ? ? ? ?@Override
 ? ? ? ? ? ?public 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() {
 ? ? ? ? ? ?@Override
 ? ? ? ? ? ?public void run() {
 ? ? ? ? ? ? ? ?while (true) {
 ? ? ? ? ? ? ? ? ? ?p.print1();
 ? ? ? ? ? ? ?  }
 ? ? ? ? ?  }
 ? ? ?  };
?
 ? ? ? ?Thread t2 = new Thread() {
 ? ? ? ? ? ?@Override
 ? ? ? ? ? ?public 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.class
 ? ? ? ? ? ?synchronized (Tickets.class) {
 ? ? ? ? ? ? ? ?if (tickets <= 0) {
 ? ? ? ? ? ? ? ? ? ?break;
 ? ? ? ? ? ? ?  }
 ? ? ? ? ? ? ? ?try {
 ? ? ? ? ? ? ? ? ? ?Thread.sleep(10);
 ? ? ? ? ? ? ?  } catch (InterruptedException e) {
 ? ? ? ? ? ? ? ? ? ?e.printStackTrace();
 ? ? ? ? ? ? ?  }
 ? ? ? ? ? ? ? ?//线程1等待 线程2等待
 ? ? ? ? ? ? ? ?//线程3 把ticket-- 变成0  这时候cpu切换到线程1  线程1执行--操作变为-1
 ? ? ? ? ? ? ? ?tickets--;
 ? ? ? ? ? ? ? ?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() {
 ? ? ? ? ? ? ? ?//没有获取到锁的线程对象的状态是:BLOCKED
 ? ? ? ? ? ? ? ?synchronized ("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());//NEW
 ? ? ? ?t1.start();
 ? ? ? ?System.out.println("B:" + t1.getState());//RUNNABLE
 ? ? ? ?Thread.sleep(10);//0.01
 ? ? ? ?System.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() {
 ? ? ? ? ? ?@Override
 ? ? ? ? ? ?public 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() {
 ? ? ? ? ? ?@Override
 ? ? ? ? ? ?public 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() {
 ? ? ? ? ? ?@Override
 ? ? ? ? ? ?public 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的代码注释上 就不报错了
 ?  }
}
?

文章来源:https://blog.csdn.net/2301_77351901/article/details/134898972
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。