Java基础学习(十六)

news/2025/2/22 21:12:54/文章来源:https://www.cnblogs.com/victoria6013/p/18731410

Java基础学习(十六):多线程

目录
  • Java基础学习(十六):多线程
    • 概念
    • 多线程的实现方式
    • 常见成员方法
    • 线程安全问题
      • 同步代码块
      • 同步方法
      • Lock 锁
    • 生产者消费者模式(等待唤醒机制)
    • 线程池

本文为个人学习记录,内容学习自 黑马程序员


概念

  • 进程:程序的基本执行实体
  • 线程:操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位
  • 并发:在同一时刻,有多个指令在单个 CPU 上交替执行
  • 并行:在同一时刻,有多个指令在多个 CPU 上同时执行
  • JUC:java.util.concurrent 包的缩写

多线程的实现方式

在 Java 中,多线程有三种实现方式:

  1. 继承 Thread 类的方式进行实现

    步骤:① 自己定义一个类继承 Thread 类 ② 重写 run 方法 ③ 创建子类的对象 ④ 启动线程

    public class Test {public static void main(String[] args) {// 3.创建对象MyThread t = new MyThread();// 4.使用start方法启动线程t.start();}
    }// 1.继承Thread类
    class MyThread extends Thread {// 2.重写run方法@Overridepublic void run() {// 线程具体执行代码}
    }
    
  2. 实现 Runnable 接口的方式进行实现

    步骤:① 自己定义一个类实现 Runnable 接口 ② 重写 run 方法 ③ 创建自己类的对象 ④ 创建 Thread 类的对象 ⑤ 启动线程

    public class Test {public static void main(String[] args) {// 3.创建自己的类对象MyRun r = new MyRun();// 4.创建Thread类对象,并将自己的类作为参数Thread t = new Thread(r);// 5.使用start方法启动线程t.start();}
    }// 1.实现Runnable接口
    class MyRun implements Runnable {// 2.重写run方法@Overridepublic void run() {// 线程具体执行代码}
    }
    
  3. 利用 Callable 接口和 Future 接口方式实现

    特点:有返回值,即可以获取到多线程运行的结果

    步骤:① 创建一个类实现 Callable 接口 ② 重写 call 方法 ③ 创建自己类的对象 ④ 创建 FutureTask 类的对象 ⑤ 创建 Thread 类的对象 ⑥ 启动线程 ⑦ (可选)通过FutureTask类的get()方法得到返回值

    public class Test {public static void main(String[] args) throws ExecutionException, InterruptedException {// 3.创建自己类的对象MyCallable mc = new MyCallable();// 4.创建FutureTask类的对象,并将自己类的对象作为参数FutureTask<Integer> ft = new FutureTask<>(mc);// 5.创建Thread类的对象,并将FutureTask类的对象作为参数Thread t = new Thread(ft);// 6.启动线程t.start();// 7.(可选)通过FutureTask类的get()方法得到返回值Integer result = ft.get();}
    }// 1.实现Callable接口,泛型为返回值类型
    class MyCallable implements Callable<Integer> {// 2.重写call方法,方法体为要在线程中执行的内容,此处为求和计算,并将求和结果进行返回@Overridepublic Integer call() {int sum = 0;for (int i = 0; i < 10; i++) {sum += i;}return sum;}
    }
    

三种实现方式的对比:

优点 缺点
继承 Thread 类 编程简单,可以直接使用 Thread 类中的方法 无法再继承其他的类,扩展性差
实现 Runnable 接口 可以继承其他的类,扩展性较强 编程相对复杂,无法直接使用 Thread 类中的方法
实现 Callable 接口 可以继承其他的类,还能得到线程返回值,扩展性强 编程相对复杂,无法直接使用 Thread 类中的方法

常见成员方法

方法名 说明
String getName() 返回此线程的名称
void setName(String name) 设置线程的名字(构造方法也可以设置名字)
static Thread currentThread() 获取当前线程的对象
static void sleep(long time) 让线程休眠指定的时间,单位为毫秒
setPriority(int newPriority) 设置线程的优先级
final int getPriority() 获取线程的优先级
final void setDaemon(boolean on) 设置为守护线程
public static void yield() 出让线程/礼让线程
public void join() 插入线程/插队线程

注意事项:

  • 线程默认命名格式:Thread-X(X 为序号,从 0 开始,第一个线程为 Thread-0,第二个为 Thread-1,依此类推)

    public class Test {public static void main(String[] args) {MyThread mt = new MyThread();// 手动设置线程名字mt.setName("123");mt.start();}
    }class MyThread extends Thread {@Overridepublic void run() {// 获取线程名字System.out.println(getName());}
    }
    
  • 有两个方式设置线程名字:1. set() 方法 2. 构造方法

    但要注意:使用构造方法设置线程名字时,由于构造方法不能继承,因此需要在子类的构造方法中调用父类的构造方法

    class MyThread extends Thread {// 无参构造public MyThread() {}// 其中一个有参构造public MyThread(String name) {super(name);}@Overridepublic void run() {System.out.println(getName());}
    }
    
  • 哪条线程执行到 sleep() 方法,就会停留对应时间,只有等待时间到了之后才会继续执行后续代码

  • 线程的优先级:最小是 1,最大是 10,默认是 5

  • CPU 的调度方式分成抢占式调度和非抢占式调度,前者由多个线程随机抢占 CPU 使用权,后者由多个线程轮流使用 CPU,在 Java 中采用抢占式调度,优先级越高的线程抢占到 CPU 使用权的概率就越高。因此对于相同的任务,如果有两个线程同时执行该任务,优先级高的线程通常能更快地执行完任务

  • 守护线程:当所有的非守护线程执行完毕后,守护线程中的内容就算还没执行完,短时间内也会陆续终止

    public class Test {public static void main(String[] args) {MyThread1 mt1 = new MyThread1();MyThread2 mt2 = new MyThread2();// 将线程2设置为守护线程mt2.setDaemon(true);mt1.start();mt2.start();}
    }class MyThread1 extends Thread {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(getName() + "@" + i);}}
    }class MyThread2 extends Thread {@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(getName() + "@" + i);}}
    }
    
  • 出让线程:出让当前线程的 CPU 控制权,重新进行控制权的抢夺,可以使得 CPU 控制权的分布尽可能均匀

    public class Test {public static void main(String[] args) {MyThread t1 = new MyThread();MyThread t2 = new MyThread();t1.start();t2.start();}
    }class MyThread extends Thread {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(getName() + "@" + i);// 每执行完一次就出让控制权Thread.yield();}}
    }
    
  • 插入线程:可以将某一线程插入到当前线程之前,等待某一线程全部执行完毕后才继续执行当前线程

    public class Test {public static void main(String[] args) throws InterruptedException {MyThread t = new MyThread();t.start();// 将t线程插入到当前线程之前执行t.join();for (int i = 0; i < 10; i++) {System.out.println("main线程" + i);}}
    }class MyThread extends Thread {@Overridepublic void run() {for (int i = 0; i < 10; i++) {System.out.println(getName() + "@" + i);}}
    }
    
  • 线程的生命周期:

    图16-1


线程安全问题

由于多个线程的执行具有随机性,CPU 的控制权随时可能会被其他线程抢走,因此可能存在静态/共享数据操作到一半时控制权被抢走,导致效果有误的安全问题

同步代码块

  • 作用:把操作共享数据的代码锁起来,解决线程的安全问题

  • 格式:

    synchronized (锁) {操作共享数据的代码
    }
    
  • 特点:

    特点1:锁默认打开,有一个线程进去了,锁自动关闭

    特点2:锁里面的代码全部执行完毕,线程出来,锁自动打开

  • 示例:使用同步代码块实现多个窗口同时卖票

    public class Test {public static void main(String[] args) {MyThread t1 = new MyThread("窗口1");MyThread t2 = new MyThread("窗口2");MyThread t3 = new MyThread("窗口3");t1.start();t2.start();t3.start();}
    }class MyThread extends Thread {public MyThread() {}public MyThread(String name) {super(name);}static int ticket = 0;// 锁对象可以是任意的对象,但必须保证唯一,因此需要使用static修饰static Object lock = new Object();@Overridepublic void run() {while (true) {// 同步代码块synchronized (lock) {if (ticket < 100) {ticket++;System.out.println(getName() + "正在卖第" + ticket + "张票");} else {break;}}}}
    }
    

    细节1:要实现多个线程卖票,必须将同步代码块放在循环内,否则会出现只有一个线程将所有票卖完的情况

    细节2:锁对象必须是唯一的,如果锁对象不唯一就失去了上锁的功能了,通常使用 synchronized (MyThread.class) 作为锁对象

同步方法

  • 定义:把 synchronized 关键字加到方法上,称之为同步方法

  • 格式:

    修饰符 synchronized 返回值类型 方法名(方法参数) {...}
    
  • 特点:

    特点1:同步方法锁住方法里面的所有代码

    特点2:锁对象不能自己指定,对于非静态方法,锁对象为 this,对于静态方法,锁对象为当前类的字节码文件对象(例如 MyThread 类的字节码文件对象就为 MyThread.class)

  • 同步方法的写法:可以先写同步代码块,再将同步代码块抽取成同步方法(选中代码块再 Ctrl + Alt + M)

    public class Test {public static void main(String[] args) {MyRunnable mr = new MyRunnable();Thread t1 = new Thread(mr, "窗口1");Thread t2 = new Thread(mr, "窗口2");Thread t3 = new Thread(mr, "窗口3");t1.start();t2.start();t3.start();}
    }class MyRunnable implements Runnable {// 小细节:采用实现Runnable接口的方式实现多线程时,共享数据不需要使用static修饰,因为MyRunnable的对象mr是唯一的int ticket = 0;@Overridepublic void run() {while (true) {if (method()) break;}}private synchronized boolean method() {if (ticket == 100) {return true;} else {ticket++;System.out.println(Thread.currentThread().getName() + ": " + ticket);}return false;}
    }
    

Lock 锁

  • 作用:同步代码块和同步方法无法手动上锁和解锁,为了更清晰地表达如何上锁和解锁,JDK5 后提供了新的锁对象 Lock

  • Lock 实现了更广泛的锁定操作,提供了获得锁和释放锁的方法

    方法名 说明
    void lock() 手动上锁
    void unlock() 手动释放锁
  • Lock 是接口,不能直接实例化,一般采用它的实现类 ReentrantLock 来实例化

  • 注意:使用 Lock 锁手动操作锁时,一定要注意锁的释放,否则可能出现其他线程一直在等待解锁,导致程序无法终止

  • 示例:错误使用方法

    class MyThread extends Thread {static int ticket = 0;// 实例化对象static Lock lock = new ReentrantLock();@Overridepublic void run() {while (true) {lock.lock();if (ticket == 100) {// 执行break后直接退出循环,不会执行lock.unlock(),导致上锁了但未解锁break;} else {ticket++;System.out.println(getName() + " : " + ticket);}lock.unlock();}}
    }
    
  • 示例:正确使用方法,使用 try-catch-finally 语句确保锁的释放

    class MyThread extends Thread {static int ticket = 0;static Lock lock = new ReentrantLock();@Overridepublic void run() {while (true) {lock.lock();try {if (ticket == 100) {break;} else {ticket++;System.out.println(getName() + " : " + ticket);}} catch (Exception e) {e.printStackTrace();} finally {// finally语句块中的内容就算在try中退出了循环也会执行lock.unlock();}}}
    }
    

生产者消费者模式(等待唤醒机制)

  • 生产者消费者模式是一个十分经典的多线程协作的模式

  • 常见方法

    方法名 说明
    void wait() 当前线程等待,直到被其他线程唤醒
    void notify() 随机唤醒单个线程
    void notifyAll() 唤醒所有线程
  • 示例:生产者——厨师生产食物,消费者——顾客消费食物

    public class Test {public static void main(String[] args) {Cook cook = new Cook();Foodie foodie = new Foodie();cook.start();foodie.start();}
    }// 生产者——厨师
    class Cook extends Thread {@Overridepublic void run() {// 1.大循环while (true) {// 2.同步代码块synchronized (Desk.lock) {// 3.业务逻辑if (Desk.count == 0) {break;} else {if (Desk.foodFlag == 0) {System.out.println("做食物");Desk.foodFlag = 1;// 唤醒和这把锁绑定的所有线程Desk.lock.notifyAll();} else {try {// 让当前线程和锁绑定,等待被唤醒,释放锁Desk.lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}}}}}
    }// 消费者——顾客
    class Foodie extends Thread {@Overridepublic void run() {// 1.大循环while (true) {// 2.同步代码块synchronized (Desk.lock) {// 3.业务逻辑if (Desk.count == 0) {break;} else {if (Desk.foodFlag == 0) {try {// 让当前线程和锁绑定,等待被唤醒,释放锁Desk.lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}} else {System.out.println("吃食物");// 唤醒和这把锁绑定的所有线程Desk.lock.notifyAll();Desk.count--;Desk.foodFlag = 0;}}}}}
    }// 生产者和消费者以外的第三方,用于记录数据
    class Desk {// 记录当前是否有食物public static int foodFlag = 0;// 记录当前剩余食物(线程结束条件)public static int count = 10;// 锁对象public static Object lock = new Object();
    }
    
  • 阻塞队列实现等待唤醒机制

    • 阻塞队列一共实现了 4 个接口:Iterable,Collection,Queue,BlockingQueue,主要使用其中的两个实现类对象 ArrayBlockingQueue 和 LinkedBlockingQueue

    • ArrayBlockingQueue:底层由数组实现,有界

    • LinkedBlockingQueue:底层由链表实现,无界(不是真正的无界,最大值为 int 的最大值)

    • 示例:

      public class Test {public static void main(String[] args) {// 阻塞队列,生产者和消费者必须共用一个阻塞队列,此处定义队列容量为1,数据类型为StringArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);Cook cook = new Cook(queue);Foodie foodie = new Foodie(queue);cook.start();foodie.start();}
      }class Cook extends Thread {ArrayBlockingQueue<String> queue;public Cook(ArrayBlockingQueue<String> queue) {this.queue = queue;}@Overridepublic void run() {while (true) {// 使用queue.put()向阻塞队列中添加数据,如果队列已满则等待// 需要注意queue.put()的底层存在上锁和解锁,无需使用同步语句块包围try {queue.put("面条");} catch (InterruptedException e) {throw new RuntimeException(e);}}}
      }class Foodie extends Thread {ArrayBlockingQueue<String> queue;public Foodie(ArrayBlockingQueue<String> queue) {this.queue = queue;}@Overridepublic void run() {while (true) {// 使用queue.take()从阻塞队列中获取数据,如果队列为空则等待// 需要注意queue.take()的底层存在上锁和解锁,无需使用同步语句块包围try {String food = queue.take();System.out.println(food);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
      }
      
  • 线程的状态

    Java 中为线程规定了六种状态:新建状态(NEW),就绪状态(RUNNABLE),阻塞状态(BLOCKED),等待状态(WAITING),计时等待状态(TIMED_WAITING),结束状态(TERMINATED)

    如下图所示,但不包括运行状态,因为运行时的控制权已经交由操作系统了

    图16-2


线程池

  • 核心原理:①创建一个空的线程池 ②提交任务时,线程池会创建新的线程对象,任务执行完毕后线程归还给线程池,下一次提交任务时不需要创建新的线程,而是直接复用已有的线程 ③如果提交任务时,线程池中没有空闲线程,也无法再创建新的线程,就排队等待

  • 采用工具类创建线程池

    Executors:线程池的工具类,通过调用方法返回不同类型的线程池对象

    方法名 说明
    public static ExecutorService newCachedThreadPool() 创建一个没有上限的线程池(最大为 int 的大小)
    public static ExecutorService newFixedThreadPool(int nThreads) 创建一个有上限的线程池
    public class Test {public static void main(String[] args) {// 1.创建对象ExecutorService pool1 = Executors.newCachedThreadPool();// 2.提交任务(submit() 方法的参数可以是 Runnable 的实现类或者是 Callable 的实现类)pool1.submit(new MyRunnable());// 3.销毁对象pool1.shutdown();}
    }class MyRunnable implements Runnable {@Overridepublic void run() {// 线程具体代码}
    }
    
  • 自定义线程池

    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(可以有七个参数);
    /*
    参数一:核心线程数量					   不能小于0
    参数二:最大线程数量					   不能小于0,最大线程数量>=核心线程数量
    参数三:临时线程最大空闲时间(值)		  不能小于0
    参数四:临时线程最大空闲时间(单位)		  用TimeUnit指定
    参数五:任务队列						不能为null
    参数六:创建线程的方式					  不能为null
    参数七:要执行的任务过多时的任务拒绝策略	不能为null
    */
    

    向自定义线程池中提交任务时,存在以下三个临界点:

    1. 当核心线程满时,再提交任务就会排队
    2. 当核心线程满,队伍满时,再提交任务会创建临时线程
    3. 当核心线程满,队伍满,临时线程满时,再提交任务会触发任务拒绝策略

    举例:假设线程池的核心线程数为3,临时线程数为3,队列长度为3,那么当提交10个任务时,1、2、3号任务交由核心线程执行,4、5、6号任务进入队列等待,7、8、9号任务交由临时线程执行(只有核心线程分配完并且任务队列排满时才会创建临时线程),10号任务根据拒绝策略进行处理

    任务拒绝策略(静态内部类) 说明
    ThreadPoolExecutor.AbortPolicy 默认策略,丢弃任务并抛出异常
    ThreadPoolExecutor.DiscardPolicy 丢弃任务,但不抛出异常
    ThreadPoolExecutor.DiscardOldestPolicy 丢弃队列中最早的任务,将当前任务添加进队列中
    ThreadPoolExecutor.CallerRunsPolicy 调用任务的run()方法绕过线程池直接执行

    示例:

    public class Test {public static void main(String[] args) {// 1.创建对象ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3,										// 核心线程数量6,										// 最大线程数量60,										// 临时线程最大空闲时间(值)TimeUnit.SECONDS,						// 临时线程最大空闲时间(单位)new ArrayBlockingQueue<>(3),			// 任务队列Executors.defaultThreadFactory(),		// 创建线程的方式new ThreadPoolExecutor.AbortPolicy()	// 任务的拒绝策略);// 2.提交任务(submit() 方法的参数可以是 Runnable 的实现类或者是 Callable 的实现类)threadPoolExecutor.submit(new MyRunnable());// 3.销毁对象threadPoolExecutor.shutdown();}
    }
    
  • 线程池大小设置

    最大并行数:取决于 CPU 型号,4 核 8 线程的 CPU 的最大并行数为 8

    对于 CPU 密集型运算(CPU 运算比较多)的任务,一般设置线程池大小 = 最大并行数 + 1

    对于 I/O 密集型运算(文件读取比较多)的任务,一般设置线程池大小 = 最大并行数 * 期望 CPU 利用率 * (CPU 计算时间 + 等待时间) / CPU 计算时间

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/888176.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

AXI4-Stream Data FIFO(2.0)

IP核具体设置如下,数据宽度64bit,深度32,启用了包传输。打开Example Design三个IP核和两个AXI读写模块。 clk_wiz_0是mmcm IP核,提供工作时钟,proc_sys_reset_0是系统复位 IP核,提供复位信号,axis_data_fifo是本次的仿真IP 核。 axis_data_fifo_example_master是写模块,…

DeepSeek宣布下周开源5大项目,这才是真OpenAI!

近日,DeepSeek 团队宣布将在下周连续开源 5 个项目。这一举措不仅吸引了众多开发者的目光,还在技术社区引发了热烈讨论,不少网友甚至将 DeepSeek 誉为 “真正的 OpenAI”。一、DeepSeek开源计划详情(一)开源时间与方式DeepSeek团队决定开展“OpenSourceWeek”(开源周)活…

NOIP 2024 游记 | Loser.

Loser. p.s. 本文章比较废话()Day 1 早上 6:30 被拽起来了。吃饭。出门。坐地铁。到! 华科大门,比,华师气派多了诶诶。扯了个横幅,拍照。拍照。拍照。 诶诶,怎么 7:45 就拽着我们进去了。 进考场。我怎么是第一排第一个 /jk 看见了 @Hakureireimu_cjrljpx,但是他不认识…

利用 vscode 进行远程开发

打开远程项目路径在远程服务器上安装 vscode 插件 Go、GitLens、Git History、Git Graph、MarsCode AI对于本地已经安装的插件,点击Install In SSH:xxx就能把插件安装到远程服务器对于本地没有安装的插件,直接查找插件,选中安装,即是安装到远程服务器git 不高亮显示修改行从…

查看Oracle的数据库表命令

在 Oracle 里 “数据库” 的概念和其他数据库系统有所不同,Oracle 一个实例通常对应一个数据库,但可以包含多个不同的模式(类似于其他数据库系统中的用户数据库)1. 查看数据库名称你可以在 SQL*Plus 或者 SQL Developer 等工具中执行如下 SQL 语句:SELECT name FROM v$dat…

Linux 中declare命令详解

Linux 中declare命令001、普通测试[root@PC1 dir1]# ls [root@PC1 dir1]# echo $var1[root@PC1 dir1]# var1="hello world" [root@PC1 dir1]# echo $var1 hello world [root@PC1 dir1]# var1=100.55 [root@PC1 dir1]# echo $var1 100.55 [root@PC1 dir1]# var1=100 […

《软件开发与创新课程设计》第一次课后作业——对学生选课系统的改进

(1)博客介绍 本文的学生选课系统的源码来自于csdn的一篇博客当中。该系统的实现语言以C++为主,本文的主要内容围绕该系统进行分析,并针对系统的主要问题进行一些修改或重构。 本篇如有问题存在,请各位读者多多指正! (2)学生选课系统分析 源代码如下: 点击查看代码 #de…

pikachu unsafe Fileupload

在上传点上传非法文件,提示上传文件不符合要求,且BP没有新的数据包产生,判断为客户端检查禁用浏览器JavaScript后刷新网页,再次上传文件,提示上传成功,文件路径为uploads/test.phpedge: 设置->Cookie和网站权限->所有权限->Javascript->禁用 Chorme:设置-&g…

rust学习十九.1、模式匹配(match patterns)

本章节大概是书本上比较特殊一个,因为它没有什么复杂的内容,通篇主要讨论模式匹配的语法。 一、两个名词a.可反驳 - refutable 对某些可能的值进行匹配会失败的模式被称为是 可反驳的(refutable) let Some(x) = some_option_value;如果 some_option_value 的值是…

大对数电缆打线顺序

5种线缆主色:白色、红色、黑色、黄色、紫色 5种线缆配色:蓝色、橙色、绿色、棕色、灰色 25对电话电缆色谱线序表30对电话电缆色谱线序 这里要特别说明下:30对的电话电缆要注意了,30对通信电缆里有2种白色的主色,大于25对了就一定要看标识线了!!有一小把是用“白蓝"…

01-springsecurity数据库登录

01 - SpringSecurity实现数据库登录 环境: springboot 3.4.2, springsecurity 6.4.2, mybatis 3.0.4springsecurity中的UserDetails接口用于表示用户信息, 包含用户名、密码等信息。UserDetailsService接口用于加载用户信息, 里边就这一个方法 public interface UserDetailsSer…

【喜与悲】- 2025.2.22 晚

下图为《Balatro》中的一张小丑牌:【喜与悲】喜与悲可以重新触发所有打出的人头牌,是重新触发家族中的一员。但其特性也决定了其强度方差极大,有配合则强度很高,没有配合则纯浪费小丑位。但很少有小丑能与其配合,而能与其配合的小丑大多单独拎出来又不强。更多时候其几乎只…