【黑马java基础】多线程

什么是线程?

线程(Thread)是一个程序内部的一条执行流程。

在这里插入图片描述

这个是一条执行流程,虽然有循环,但是最后只有一条流程往前推进,所以视为一条。

程序中如果只有一条执行流程,那这个程序就是单线程的程序。

程序是指令序列,这些指令可以让CPU完成指定的任务。*.java程序经过编译后形成 *.class文件,在Windows中启动一个JVM虚拟机具体的任务相当于创建了一个进程,在虚拟机中加载class文件并运行,在class文件中通过执行创建新线程的代码来执行。

多线程是什么?

多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)。

例如:消息通信、淘宝、京东系统都离不开多线程技术。

1、多线程的创建

如何在程序中创建出多条线程?

Java是通过java.lang.Thread 类的对象来代表线程的。 用以下3个方式来创建多线程:

1.1 方式一:继承Thread类

实现步骤

①定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法

②创建MyThread类的对象

③调用线程对象的start()方法启动线程(启动后还是执行run方法的)

package com.itheima.d1_create_thread;/*** 目标:掌握线程的创建方式一:继承Thread类*/
public class ThreadTest1 {// main方法是由一条默认的主线程负责执行。public static void main(String[] args) {// 3、创建MyThread线程类的对象代表一个线程Thread t = new MyThread();// 4、启动线程(自动执行run方法的)t.start();  // main线程 t线程for (int i = 1; i <= 5; i++) {System.out.println("主线程main输出:" + i);}}
}
package com.itheima.d1_create_thread;/*** 1、让子类继承Thread线程类。*/
public class MyThread extends Thread{// 2、必须重写Thread类的run方法@Overridepublic void run() {// 描述线程的执行任务。for (int i = 1; i <= 5; i++) {System.out.println("子线程MyThread输出:" + i);}}
}

每次调用的结果不一样,随机生成:

在这里插入图片描述

继承Thread类优缺点:

优点:编码简单

缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。(单继承)

多线程的注意事项

1、启动线程必须是调用start方法,不是调用run方法。

  • 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
  • 只有调用start方法才是启动一个新的线程执行。

在这里插入图片描述

2、不要把主线程任务放在启动子线程之前。

  • 这样主线程一直是先跑完的,相当于是一个单线程的效果了。

1.2 方式二:实现Runnable接口

实现步骤

①定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法

②创建MyRunnable任务对象

③把MyRunnable任务对象交给Thread处理。

Thread类提供的构造器说明
public Thread(Runnable target)封装Runnable对象成为线程对象

④调用线程对象的start()方法启动线程

代码如下:

package com.itheima.d1_create_thread;/*** 1、定义一个任务类,实现Runnable接口*/
public class MyRunnable implements Runnable{// 2、重写runnable的run方法@Overridepublic void run() {// 线程要执行的任务。for (int i = 1; i <= 5; i++) {System.out.println("子线程输出 ===》" + i);}}
}
package com.itheima.d1_create_thread;/*** 目标:掌握多线程的创建方式二:实现Runnable接口。*/
public class ThreadTest2 {public static void main(String[] args) {// 3、创建任务对象。Runnable target = new MyRunnable();// 4、把任务对象交给一个线程对象处理。//  public Thread(Runnable target)new Thread(target).start();for (int i = 1; i <= 5; i++) {System.out.println("主线程main输出 ===》" + i);}}
}

其中,任务对象没有start方法,因为任务对象不是线程对象,start方法是线程对象才有的。所以需要把任务对象交给线程对象去处理。

在这里插入图片描述

实现Runnable接口优缺点:

  • 优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。
  • 缺点:需要多一个Runnable对象。

线程创建方式二:匿名内部类的写法

①可以创建Runnable的匿名内部类对象。

②再交给Thread线程对象。

③再调用线程对象的start()启动线程。

代码如下:

package com.itheima.d1_create_thread;/*** 目标:掌握多线程创建方式二的匿名内部类写法。*/
public class ThreadTest2_2 {public static void main(String[] args) {// 1、直接创建Runnable接口的匿名内部类形式(任务对象)Runnable target = new Runnable() {@Overridepublic void run() {for (int i = 1; i <= 5; i++) {System.out.println("子线程1输出:" + i);}}};new Thread(target).start();// 简化形式1:new Thread(new Runnable() {@Overridepublic void run() {for (int i = 1; i <= 5; i++) {System.out.println("子线程2输出:" + i);}}}).start();// 简化形式2:new Thread(() -> {for (int i = 1; i <= 5; i++) {System.out.println("子线程3输出:" + i);}}).start();for (int i = 1; i <= 5; i++) {System.out.println("主线程main输出:" + i);}}
}

1.3 方式三:实现Callable接口

在前面学的两种线程创建方式都存在一个问题:假如线程执行完毕后有一些数据需要返回,它们重写的run方法均不能直接返回结果。

那么如何解决这个问题呢?

  • JDK 5.0提供了Callable接口和FutureTask类来实现(多线程的第三种创建方式)。
  • 这种方式最大的优点:可以返回线程执行完毕后的结果。

步骤:

①创建任务对象

  • 定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
  • 把Callable类型的对象封装成FutureTask(线程任务对象)。

②把线程任务对象交给Thread对象。

③调用Thread对象的start方法启动线程。

④线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。

FutureTask的API

FutureTask提供的构造器说明
public FutureTask<>(Callable call)把Callable对象封装成FutureTask对象。
FutureTask提供的方法说明
public V get() throws Exception获取线程执行call方法返回的结果。
package com.itheima.d1_create_thread;import java.util.concurrent.Callable;/*** 1、让这个类实现Callable接口*/
public class MyCallable implements Callable<String> {private int n;public MyCallable(int n) {this.n = n;}// 2、重写call方法@Overridepublic String call() throws Exception {// 描述线程的任务,返回线程执行返回后的结果。// 需求:求1-n的和返回。int sum = 0;for (int i = 1; i <= n; i++) {sum += i;}return "线程求出了1-" + n + "的和是:" + sum;}
}
package com.itheima.d1_create_thread;import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;/*** 目标:掌握线程的创建方式三:实现Callable接口。*/
public class ThreadTest3 {public static void main(String[] args) throws Exception {// 3、创建一个Callable的对象Callable<String> call = new MyCallable(100);// 4、把Callable的对象封装成一个FutureTask对象(任务对象)// 未来任务对象的作用?// 1、是一个任务对象,实现了Runnable对象.// 2、可以在线程执行完毕之后,用未来任务对象调用get方法获取线程执行完毕后的结果。FutureTask<String> f1  = new FutureTask<>(call);// 5、把任务对象交给一个Thread对象new Thread(f1).start();Callable<String> call2 = new MyCallable(200);FutureTask<String> f2  = new FutureTask<>(call2);new Thread(f2).start();// 6、获取线程执行完毕后返回的结果。// 注意:如果执行到这儿,假如上面的线程还没有执行完毕// 这里的代码会暂停,等待上面线程执行完毕后才会获取结果。String rs = f1.get();System.out.println(rs);String rs2 = f2.get();System.out.println(rs2);}
}

线程创建方式三的优缺点

优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。

缺点:编码复杂一点。

1.4 对比三种线程的创建方式,以及不同点

方式优点缺点
继承Thread类编程比较简单,可以直接使用Thread类中的方法扩展性较差,不能再继承其他的类,不能返回线程执行的结果
实现Runnable接口扩展性强,实现该接口的同时还可以继承其他的类。编程相对复杂,不能返回线程执行的结果
实现Callable接口扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果编程相对复杂

2、Thread的常用方法

Thread提供了很多与线程操作相关的方法

Thread提供的常用方法说明
public void run()线程的任务方法
public void start()启动线程
public String getName()获取当前线程的名称,线程名称默认是Thread-索引
public void setName(String name)为线程设置名称
public static Thread currentThread()获取当前执行的线程对象
public static void sleep(long time)让当前执行的线程休眠多少毫秒后,再继续执行
public final void join()…让当前调用这个方法的线程先执行完
Thread提供的常见构造器说明
public Thread(String name)可以为当前线程指定名称
public Thread(Runnable target)封装Runnable对象成为线程对象
public Thread(Runnable target, String name)封装Runnable对象成为线程对象,并指定线程名称

getName(),setName(),currentThread()代码如下:

public class MyThread extends Thread{public MyThread(String name){super(name); //1.执行父类Thread(String name)构造器,为当前线程设置名字}@Overridepublic void run() {//2.currentThread() 哪个线程执行它,它就会得到哪个线程对象。Thread t = Thread.currentThread();for (int i = 1; i <= 3; i++) {//3.getName() 获取线程名称System.out.println(t.getName() + "输出:" + i);}}
}

在测试类中,创建线程对象,并启动线程

public class ThreadTest1 {public static void main(String[] args) {Thread t1 = new MyThread();t1.setName(String name) //设置线程名称;t1.start();System.out.println(t1.getName());  //Thread-0Thread t2 = new MyThread("2号线程");// t2.setName("2号线程");t2.start();System.out.println(t2.getName()); // Thread-1// 主线程对象的名字// 哪个线程执行它,它就会得到哪个线程对象。Thread m = Thread.currentThread();m.setName("最牛的线程");System.out.println(m.getName()); // mainfor (int i = 1; i <= 5; i++) {System.out.println(m.getName() + "线程输出:" + i);}}
}

执行上面代码,效果如下,每条线程都有自己的名字了:

在这里插入图片描述

sleep(),join()应用实例如下:

package com.itheima.d2_thread_api;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;/*** 目标:掌握sleep方法,join方法的作用。*/
public class ThreadTest2 {public static void main(String[] args) throws Exception {System.out.println(Runtime.getRuntime().availableProcessors());for (int i = 1; i <= 5; i++) {System.out.println(i);// 休眠5sif(i == 3){// 会让当前执行的线程暂停5秒,再继续执行// 项目经理让我加上这行代码,如果用户交钱了,我就注释掉!Thread.sleep(5000);}}// join方法作用:让当前调用这个方法的线程先执行完。Thread t1 = new MyThread("1号线程");t1.start();t1.join();Thread t2 = new MyThread("2号线程");t2.start();t2.join();Thread t3 = new MyThread("3号线程");t3.start();t3.join();}
}

执行效果如下:

在这里插入图片描述

前几行(1-5)是每隔5秒输出一个

后面几行(1号线程输出-3号线程输出)是等每个线程输出完才会往下继续执行,不像之前是随机执行。

Thread其他方法的说明:

Thread类还提供了诸如:yield, interrupt, 守护线程,线程优先级等线程的控制方法,在开发中很少用。

3、线程安全

3.1 什么是线程安全问题

线程安全问题指的是,多个线程同时操作同一个共享资源的时候,可能会出现业务安全问题。

比如:取钱的线程安全问题(场景:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,如果小明和小红同时来取钱,并且2人各自都在取钱10万元,可能会出现什么问题呢?)

在这里插入图片描述

①小明线程步骤一先判断余额是否够,够

②小红线程步骤一先判断余额是否够,够

③因为小明线程判断够,所以银行吐出10万

④银行账户更新为0元

⑤因为小红线程判断够,所以银行吐出10万

⑥银行账户更新为-10万元

结果:2人都取钱10万,银行亏了10万。

以上取钱案例中的问题,就是线程安全问题的一种体现。

线程安全问题出现的原因?

  • 存在多个线程在同时执行
  • 同时访问一个共享资源
  • 存在修改该共享资源

3.2 用程序模拟线程安全问题

需求:
小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,模拟2人同时去取钱10万。

分析:
①:需要提供一个账户类,接着创建一个账户对象代表2个人的共享账户。
②:需要定义一个线程类(用于创建两个线程,分别代表小明和小红)。
③:创建2个线程,传入同一个账户对象给2个线程处理。
④:启动2个线程,同时去同一个账户对象中取钱10万。

先定义一个共享的账户类

package com.itheima.d3_thread_safe;public class Account {private String cardId; // 卡号private double money; // 余额。public Account() {}public Account(String cardId, double money) {this.cardId = cardId;this.money = money;}// 小明 小红同时过来的public void drawMoney(double money) {// 先搞清楚是谁来取钱?String name = Thread.currentThread().getName();// 1、判断余额是否足够if(this.money >= money){System.out.println(name + "来取钱" + money + "成功!");this.money -= money;System.out.println(name + "来取钱后,余额剩余:" + this.money);}else {System.out.println(name + "来取钱:余额不足~");}}public String getCardId() {return cardId;}public void setCardId(String cardId) {this.cardId = cardId;}public double getMoney() {return money;}public void setMoney(double money) {this.money = money;}}

再定义一个取钱的线程类

package com.itheima.d3_thread_safe;public class DrawThread extends Thread{private Account acc;public DrawThread(Account acc, String name){super(name);this.acc = acc;}@Overridepublic void run() {// 取钱(小明,小红)acc.drawMoney(100000);}
}

最后,再写一个测试类,在测试类中创建两个线程对象

package com.itheima.d3_thread_safe;/*** 目标:模拟线程安全问题。*/
public class ThreadTest {public static void main(String[] args) {// 1、创建一个账户对象,代表两个人的共享账户。Account acc = new Account("ICBC-110", 100000);// 2、创建两个线程,分别代表小明 小红,再去同一个账户对象中取钱10万。new DrawThread(acc, "小明").start(); // 小明new DrawThread(acc, "小红").start(); // 小红}
}

最后结果:两个人都取了10万元,余额为-10万元

在这里插入图片描述

4、线程同步

4.1 认识线程同步

所谓线程同步就是解决线程安全问题的方案。

线程同步的思想就是让多个线程实现先后依次访问共享资源,这样就解决了安全问题。

线程同步的常见方案

加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。

谁先进来,锁对象就分配给谁,如果是同时,锁对象有相应的机制,分配给其中的一个人。此时是小红线程先拿到锁对象

在这里插入图片描述

然后拿走10万元,余额变成0元

在这里插入图片描述

小红任务离开。自动解锁。小明竞争到锁

在这里插入图片描述

小明加锁,进入账户,发现余额不足

在这里插入图片描述

对于加锁的实现,有很多方案,比如以下三种:

4.2 方式一:同步代码块

同步代码块就是把访问共享资源的核心代码给上锁,以此保证线程安全

在这里插入图片描述

这个同步锁就是一个java对象来代表一把锁。

它的原理是每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行

注意:对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。

把核心代码块也就是Accout.java中对于钱的操作加锁,代码实现如下:

// 小明 小红线程同时过来的public void drawMoney(double money) {// 先搞清楚是谁来取钱?String name = Thread.currentThread().getName();// 1、判断余额是否足够synchronized ("黑马") {  //此时加锁了if(this.money >= money){System.out.println(name + "来取钱" + money + "成功!");this.money -= money;System.out.println(name + "来取钱后,余额剩余:" + this.money);}else {System.out.println(name + "来取钱:余额不足~");}}}

现在问题解决了,加了锁,小红小明只有一人可以对账户里的钱进行操作,但是如果说还有两个人有另一个共享的账号呢?

“黑马”是用双引号括起来的,整个计算机中只有一份,所以锁住小红或者小明的同时也锁住了另两个人,那么怎么办呢?

同步锁选择使用this关键字,this正好代表共享资源!

// 小明 小红线程同时过来的public void drawMoney(double money) {// 先搞清楚是谁来取钱?String name = Thread.currentThread().getName();// 1、判断余额是否足够// this正好代表共享资源!synchronized (this) {if(this.money >= money){System.out.println(name + "来取钱" + money + "成功!");this.money -= money;System.out.println(name + "来取钱后,余额剩余:" + this.money);}else {System.out.println(name + "来取钱:余额不足~");}}}

那么如果是静态方法呢?

官方建议,静态方法建议使用字节码(类名.class)对象作为锁对象。

public static void test(){synchronized (Account.class){}}

总结一下:

  • 锁对象随便选择一个唯一的对象好不好呢?

    不好,因为会影响其他无关线程的执行。

  • 锁对象的使用规范

    建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象。
    对于静态方法建议使用字节码(类名.class)对象作为锁对象。

4.3 方式二:同步方法

同步方法就是把访问共享资源的核心方法给上锁,以此保证线程安全

在这里插入图片描述

原理也是每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

代码如下:

// 小明 小红线程同时过来的// 同步方法public synchronized void drawMoney(double money) {// 先搞清楚是谁来取钱?String name = Thread.currentThread().getName();// 1、判断余额是否足够if(this.money >= money){System.out.println(name + "来取钱" + money + "成功!");this.money -= money;System.out.println(name + "来取钱后,余额剩余:" + this.money);}else {System.out.println(name + "来取钱:余额不足~");}}

同步方法底层原理

  • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
  • 如果方法是实例方法:同步方法默认用this作为的锁对象。
  • 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。

那么有个问题,是同步代码块好还是同步方法好一点?

分两个方面比较:

范围上:同步代码块锁的范围更小,同步方法锁的范围更大。范围小的性能更好(因为大家可以先把不是带锁的代码同步执行完,速度更快)

可读性:同步方法更好。

4.4 方法三:Lock锁

以上两个办法都可以保证线程安全,他们共同的特点都是提供一个所谓的对象来代表锁,然后程序就会自动的帮助加锁解锁,以便保证线程安全。还有最后一个办法,Lock锁。

Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。也就是可以自己创建一个锁对象,然后new一个锁对象,进行手工的加锁和解锁。

Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。

构造器说明
public ReentrantLock()获得Lock锁的实现类对象

Lock常用方法:

方法名称说明
void lock()获得锁
void unlock()释放锁

代码如下:

package com.itheima.d6_synchronized_lock;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class Account {private String cardId; // 卡号private double money; // 余额。// 创建了一个锁对象private final Lock lk = new ReentrantLock();public Account() {}public Account(String cardId, double money) {this.cardId = cardId;this.money = money;}// 小明 小红线程同时过来的public void drawMoney(double money) {// 先搞清楚是谁来取钱?String name = Thread.currentThread().getName();try {lk.lock(); // 这个位置加锁// 1、判断余额是否足够if(this.money >= money){System.out.println(name + "来取钱" + money + "成功!");this.money -= money;System.out.println(name + "来取钱后,余额剩余:" + this.money);}else {System.out.println(name + "来取钱:余额不足~");}} catch (Exception e) {e.printStackTrace();} finally {lk.unlock(); // 这个位置解锁}}public String getCardId() {return cardId;}public void setCardId(String cardId) {this.cardId = cardId;}public double getMoney() {return money;}public void setMoney(double money) {this.money = money;}
}

注意:

  1. 在创建锁对象的时候,建议在前面加一个final修饰,作用是lk这个锁之后是不能被替换的
  2. 在之后的编程中,如果加锁部分有bug,跳出了程序,那就意味着此时这个线程只加锁,但没有解锁,那会导致其他线程也进不来。所以加锁部分最好加个trycatchfianlly,然后把解锁操作放在finally中

5、线程通信[了解]

首先,什么是线程通信呢?

  • 当多个线程共同操作共享资源时,线程间通过某种方式互相告知自己的状态,以相互协调,避免无效的资源挣抢。

线程通信的常见模式:是生产者与消费者模型

  • 生产者线程负责生成数据
  • 消费者线程负责消费生产者生成的数据
  • 注意:生产者生产完数据后应该让自己等待,通知其他消费者消费;消费者消费完数据之后应该让自己等待,同时通知生产者生成。

比如下面案例中,有3个厨师(生产者线程),两个顾客(消费者线程)。

在这里插入图片描述

接下来,我们先分析一下完成这个案例的思路

1.先确定在这个案例中,什么是共享数据?
答:这里案例中桌子是共享数据,因为厨师和顾客都需要对桌子上的包子进行操作。2.再确定有哪几条线程?哪个是生产者,哪个是消费者?
答:厨师是生产者线程,3条生产者线程; 顾客是消费者线程,2条消费者线程3.什么时候将哪一个线程设置成什么状态生产者线程(厨师)放包子:1)先判断是否有包子2)没有包子时,厨师开始做包子, 做完之后把别人唤醒,然后让自己等待3)有包子时,不做包子了,直接唤醒别人、然后让自己等待消费者线程(顾客)吃包子:1)先判断是否有包子2)有包子时,顾客开始吃包子, 吃完之后把别人唤醒,然后让自己等待3)没有包子时,不吃包子了,直接唤醒别人、然后让自己等待

按照上面分析的思路写代码。先写桌子类,代码如下

public class Desk {private List<String> list = new ArrayList<>();// 放1个包子的方法// 厨师1 厨师2 厨师3public synchronized void put() {try {String name = Thread.currentThread().getName();// 判断是否有包子。if(list.size() == 0){list.add(name + "做的肉包子");System.out.println(name + "做了一个肉包子~~");Thread.sleep(2000);// 唤醒别人, 等待自己//这里要注意顺序,一定是先唤醒别人,再等待自己this.notifyAll();this.wait();}else {// 有包子了,不做了。// 唤醒别人, 等待自己this.notifyAll();this.wait();}} catch (Exception e) {e.printStackTrace();}}// 吃货1 吃货2public synchronized void get() {try {String name = Thread.currentThread().getName();if(list.size() == 1){// 有包子,吃了System.out.println(name  + "吃了:" + list.get(0));list.clear();Thread.sleep(1000);this.notifyAll();this.wait();}else {// 没有包子this.notifyAll();this.wait();}} catch (Exception e) {e.printStackTrace();}}
}

再写测试类,在测试类中,创建3个厨师线程对象,再创建2个顾客对象,并启动所有线程

public class ThreadTest {public static void main(String[] args) {//   需求:3个生产者线程,负责生产包子,每个线程每次只能生产1个包子放在桌子上//    2个消费者线程负责吃包子,每人每次只能从桌子上拿1个包子吃。Desk desk  = new Desk();// 创建3个生产者线程(3个厨师)new Thread(() -> {while (true) {desk.put();}}, "厨师1").start();new Thread(() -> {while (true) {desk.put();}}, "厨师2").start();new Thread(() -> {while (true) {desk.put();}}, "厨师3").start();// 创建2个消费者线程(2个吃货)new Thread(() -> {while (true) {desk.get();}}, "吃货1").start();new Thread(() -> {while (true) {desk.get();}}, "吃货2").start();}
}

执行上面代码,运行结果如下:发现多个线程相互协调执行,避免无效的资源争抢。

厨师1做了一个肉包子~~
吃货2吃了:厨师1做的肉包子
厨师3做了一个肉包子~~
吃货2吃了:厨师3做的肉包子
厨师1做了一个肉包子~~
吃货1吃了:厨师1做的肉包子
厨师2做了一个肉包子~~
吃货2吃了:厨师2做的肉包子
厨师3做了一个肉包子~~
吃货1吃了:厨师3做的肉包子

6、线程池

线程池就是一个可以复用线程的技术

要理解什么是线程复用技术,我们先得看一下不使用线程池会有什么问题,理解了这些问题之后,我们在解释线程复用同学们就好理解了。

假设:用户每次发起一个请求给后台,后台就创建一个新的线程来处理,下次新的任务过来肯定也会创建新的线程,如果用户量非常大,创建的线程也讲越来越多。然而,创建线程是开销很大的,并且请求过多时,会严重影响系统性能。

而使用线程池,就可以解决上面的问题。如下图所示,线程池内部会有一个容器,存储几个核心线程,假设有3个核心线程(人),这3个核心线程可以处理3个任务(圆点)。

在这里插入图片描述

但是任务总有被执行完的时候,假设第1个线程的任务执行完了,那么第1个线程就空闲下来了,有新的任务时,空闲下来的第1个线程可以去执行其他任务。依此内推,这3个线程可以不断的复用,也可以执行很多个任务。

在这里插入图片描述

工作线程(WorkThread)或者核心线程,这些是固定数量可以重复利用的。下面的区域是任务队列(WorkQueue),任务队列就是放线程需要处理的任务的,里面的任务实际是一个个对象,他们必须实现Runnable或者Callable任务接口才能成为任务对象扔到任务队列,然后被线程处理。

线程池可以控制线程的数量,然后重复利用线程来处理任务。同时它也可以控制任务的数量,可以把任务暂时缓存起来。因为线程池既可以控制线程的数量,又可以控制任务的数量,所以它不会因为线程过多,或者任务过多而导致把系统资源耗尽而引起系统耗尽的风险,提高系统的工作性能。

在这里插入图片描述

所以,线程池就是一个线程复用技术,它可以提高线程的利用率。

6.1 如何创建线程池?

在JDK5版本中提供了代表线程池的接口ExecutorService

如何得到线程池对象呢,有以下两个方式:

方式一:ExecutorService实现类ThreadPoolExecutor

使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。

在这里插入图片描述

在这里插入图片描述

用这7个参数的构造器来创建线程池的对象。代码如下

ExecutorService pool = new ThreadPoolExecutor(3,	//核心线程数有3个5,  //最大线程数有5个。   临时线程数=最大线程数-核心线程数=5-3=28,	//临时线程存活的时间8秒。 意思是临时线程8秒没有任务执行,就会被销毁掉。TimeUnit.SECONDS,//时间单位(秒)new ArrayBlockingQueue<>(4), //任务阻塞队列,没有来得及执行的任务在,任务队列中等待Executors.defaultThreadFactory(), //用于创建线程的工厂对象new ThreadPoolExecutor.CallerRunsPolicy() //拒绝策略
);

关于线程池,我们需要注意下面的两个问题:

1、临时线程什么时候创建?

新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。

2、什么时候会开始拒绝新任务?

核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。

方式二:Executors线程池的工具类

使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。

跳转

6.2 线程池处理Runnable任务

ExecutorService的常用方法

方法名称说明
void execute(Runnable command)执行 Runnable 任务
Future submit(Callable task)执行 Callable 任务,返回未来任务对象,用于获取线程返回的结果
void shutdown()等全部任务执行完毕后,再关闭线程池!
List shutdownNow()立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务

先准备一个线程任务类

public class MyRunnable implements Runnable{@Overridepublic void run() {// 任务是干啥的?System.out.println(Thread.currentThread().getName() + " ==> 输出666~~");//为了模拟线程一直在执行,这里睡久一点try {Thread.sleep(Integer.MAX_VALUE);} catch (InterruptedException e) {e.printStackTrace();}}
}

下面是执行Runnable任务的代码,注意阅读注释,对照着前面的7个参数理解。

ExecutorService pool = new ThreadPoolExecutor(3,	//核心线程数有3个5,  //最大线程数有5个。   临时线程数=最大线程数-核心线程数=5-3=28,	//临时线程存活的时间8秒。 意思是临时线程8秒没有任务执行,就会被销毁掉。TimeUnit.SECONDS,//时间单位(秒)new ArrayBlockingQueue<>(4), //任务阻塞队列,没有来得及执行的任务在,任务队列中等待Executors.defaultThreadFactory(), //用于创建线程的工厂对象new ThreadPoolExecutor.CallerRunsPolicy() //拒绝策略
);Runnable target = new MyRunnable();
pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
//下面4个任务在任务队列里排队
pool.execute(target);
pool.execute(target);
pool.execute(target);
pool.execute(target);//下面2个任务,会被临时线程的创建时机了
pool.execute(target);
pool.execute(target);
// 到了新任务的拒绝时机了!
pool.execute(target);

执行上面的代码,结果输出如下:

在这里插入图片描述

接着看最后一个参数:新任务的拒绝策略

策略详解
ThreadPoolExecutor.AbortPolicy丢弃任务并抛出RejectedExecutionException异常。是默认的策略
ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常 这是不推荐的做法
ThreadPoolExecutor.DiscardOldestPolicy抛弃队列中等待最久的任务 然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy由主线程负责调用任务的run()方法从而绕过线程池直接执行

6.3 线程池处理Callable任务

ExecutorService的常用方法

方法名称说明
void execute(Runnable command)执行 Runnable 任务
Future submit(Callable task)✅执行 Callable 任务,返回未来任务对象,用于获取线程返回的结果
void shutdown()等全部任务执行完毕后,再关闭线程池!
List shutdownNow()立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务

先准备一个Callable线程任务

public class MyCallable implements Callable<String> {private int n;public MyCallable(int n) {this.n = n;}// 2、重写call方法@Overridepublic String call() throws Exception {// 描述线程的任务,返回线程执行返回后的结果。// 需求:求1-n的和返回。int sum = 0;for (int i = 1; i <= n; i++) {sum += i;}return Thread.currentThread().getName() + "求出了1-" + n + "的和是:" + sum;}
}

再准备一个测试类,在测试类中创建线程池,并执行callable任务。

public class ThreadPoolTest2 {public static void main(String[] args) throws Exception {// 1、通过ThreadPoolExecutor创建一个线程池对象。ExecutorService pool = new ThreadPoolExecutor(3,5,8,TimeUnit.SECONDS, new ArrayBlockingQueue<>(4),Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy());// 2、使用线程处理Callable任务。Future<String> f1 = pool.submit(new MyCallable(100));Future<String> f2 = pool.submit(new MyCallable(200));Future<String> f3 = pool.submit(new MyCallable(300));Future<String> f4 = pool.submit(new MyCallable(400));// 3、执行完Callable任务后,需要获取返回结果。System.out.println(f1.get());System.out.println(f2.get());System.out.println(f3.get());System.out.println(f4.get());}
}

执行后,结果如下图所示

在这里插入图片描述

(没听懂)

6.4 Executors工具类实现线程池

Java为开发者提供了一个创建线程池的工具类,叫做Executors,它提供了方法可以创建各种不能特点的线程池。

方法名称说明
public static ExecutorService newFixedThreadPool(int nThreads)创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
public static ExecutorService newSingleThreadExecutor()创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。
public static ExecutorService newCachedThreadPool()线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了60s则会被回收掉。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。

注意 :这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。

创建固定线程数量的线程池代码如下。这几个方法用得不多,所以这里不做过多演示,了解一下就行了。

public class ThreadPoolTest3 {public static void main(String[] args) throws Exception {// 1、通过Executors创建一个线程池对象。ExecutorService pool = Executors.newFixedThreadPool(17);// 老师:核心线程数量到底配置多少呢???// 计算密集型的任务:核心线程数量 = CPU的核数 + 1// IO密集型的任务:核心线程数量 = CPU核数 * 2// 2、使用线程处理Callable任务。Future<String> f1 = pool.submit(new MyCallable(100));Future<String> f2 = pool.submit(new MyCallable(200));Future<String> f3 = pool.submit(new MyCallable(300));Future<String> f4 = pool.submit(new MyCallable(400));System.out.println(f1.get());System.out.println(f2.get());System.out.println(f3.get());System.out.println(f4.get());}
}

Executors创建线程池这么好用,为什么不推荐同学们使用呢?原因在这里:看下图,这是《阿里巴巴Java开发手册》提供的强制规范要求。

在这里插入图片描述

7、其它细节知识:并发、并行

进程就是:

  • 正在运行的程序(软件)就是一个独立的进程。
  • 线程是属于进程的,一个进程中可以同时运行很多个线程。
  • 进程中的多个线程其实是并发和并行执行的。

在这里插入图片描述

大块的就是进程,下拉里是线程

并发:

进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。

并行:

在同一个时刻上,同时有多个线程在被CPU调度执行。

在这里插入图片描述

多线程是并发和并行同时进行的。

8、其它细节知识:线程的生命周期

线程的生命周期:

  • 也就是线程从生到死的过程中,经历的各种状态及状态转换。
  • 理解线程这些状态有利于提升并发编程的理解能力。

Java线程的状态:Java总共定义了6种状态,6种状态都定义在Thread类嵌套的内部枚举类中(Thread.State)。

在这里插入图片描述

线程的6种状态互相转换

在这里插入图片描述

线程的6种状态总结

线程状态说明
NEW(新建)线程刚被创建,但是并未启动。
Runnable(可运行)线程已经调用了start(),等待CPU调度
Blocked(锁阻塞)线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态
Waiting(无限等待)一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒
Timed Waiting(计时等待)同waiting状态,有几个方法(sleep,wait)有超时参数,调用他们将进入Timed Waiting状态。
Teminated(终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

9、悲观锁与乐观锁

要求又要线程安全,又要同时执行,应该怎么处理?

悲观锁:一上来就加锁,没有安全感。每次只能一个线程进入访问完毕后再解锁。线程安全,性能较差

乐观锁:一开始不上锁,认为是没有问题的,大家一起跑,等要出现线程安全的时候才开始控制。线程安全,性能较好。

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

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

相关文章

Ubuntu20.04安装Elasticsearch

简介 ELK&#xff08;Elasticsearch, Logstash, Kibana&#xff09;是一套开源的日志管理和分析工具&#xff0c;用于收集、存储、分析和可视化日志数据。以下是如何在Ubuntu服务器上安装和配置ELK堆栈以便发送和分析日志信息的步骤。 安装Elasticsearch 首先&#xff0c;安…

【系统架构设计师】十八、架构设计实践(信息系统架构设计理论与实践2)

目录 四、企业信息系统的总体框架 4.1 战略系统 4.2 业务系统 4.3 应用系统 4.4 企业信息基础设施 4.5 业务流程重组BPR 4.6 业务流程管理BPM 五、信息系统架构设计方法 5.1 行业标准的体系架构框架 5.2 架构开发方法 5.3 信息化总体架构方法 5.4 信息化建设生命周…

防火墙——网络环境支持

目录 网络环境支持 防火墙的组网 web连接上防火墙 web管理口 让防火墙接到网络环境中 ​编辑 管理员用户管理 缺省管理员 接口 配置一个普通接口 创建安全区域 路由模式 透明模式 混合模式 防火墙的安全策略 防火墙转发流程 与传统包过滤的区别 创建安全策略 …

DDoS攻击:威胁与防护策略

DDoS&#xff08;分布式拒绝服务&#xff09;攻击是网络安全领域的一大挑战&#xff0c;对企业造成严重的影响。本文将深入探讨DDoS攻击的原理和防护方法。 DDoS攻击的原理 DDoS攻击通过大量请求&#xff0c;使目标系统无法响应正常请求。攻击者利用多台计算机发送大量请求&am…

气膜羽毛球馆的维护和运营成本解析—轻空间

随着人们对健康生活方式的追求不断增加&#xff0c;羽毛球这项运动也愈发受到欢迎。然而&#xff0c;传统的羽毛球馆往往存在建设周期长、成本高、维护复杂等问题。气膜羽毛球馆作为一种新型的运动场馆解决方案&#xff0c;因其快速搭建、环保节能、舒适环境等优势而逐渐被广泛…

跨平台桌面应用程序框架Electron

用于构建跨平台桌面应用程序的框架。Electron 由 GitHub 开发&#xff0c;它允许开发者使用 Web 技术&#xff08;如 HTML、CSS 和 JavaScript&#xff09;来创建桌面软件。Electron 基于 Node.js 和 Chromium&#xff0c;因此可以提供丰富的功能和性能。 Electron 的主要优点…

LabVIEW和IQ测试仪进行WiFi测试

介绍一个使用LabVIEW和LitePoint IQxel-MW IQ测试仪进行WiFi测试的系统。包括具体的硬件型号、如何实现通讯、开发中需要注意的事项以及实现的功能。 使用的硬件​ IQ测试仪型号: LitePoint IQxel-MW 电脑: 配置高效的台式机或笔记本电脑 路由器: 支持802.11ax (Wi-Fi 6) 的…

C语言 | Leetcode C语言题解之第282题给表达式添加运算符

题目&#xff1a; 题解&#xff1a; #define MAX_COUNT 10000 // 解的个数足够大 #define NUM_COUNT 100 // 操作数的个数足够大 long long num[NUM_COUNT] {0};long long calc(char *a) { // 计算表达式a的值// 将数字和符号&#xff0c;入栈memset(num, 0, sizeof(num));in…

2024大家都想掌握的4种PDF翻译技巧

借着互联网的东风现在全球化的交流越发频繁&#xff0c;很多时候都会遇到跨语言交流的问题。外语不好的小伙伴阅读外国文献的时候应该都很头疼吧&#xff0c;这时候pdf翻译成中文的工具就可以解决这个问题啦。 1.福昕翻译 直通车&#xff1a;https://fanyi.pdf365.cn/ 这个…

PSINS工具箱函数介绍——insplot

insplot是一个绘图命令,用于将avp数据绘制出来 本文所述的代码需要基于PSINS工具箱,工具箱的讲解: PSINS初学指导基于PSINS的相关程序设计(付费专题)使用方法 此函数使用起来也很简单,直接后面加avp即可,如: insplot(avp);其中,avp为: 每行表示一个时间1~3列为姿态…

量化交易策略解读

光大证券-20190606-重构情绪体系&#xff0c;探知市场温度——市场情绪体系系列报告之二.pdf 市场情绪与股市择时体系研究 市场情绪的重要性 市场情绪反映了投资者心理状态的集体体现&#xff0c;对市场走势有同步或滞后的影响&#xff0c;并在某些情况下预示市场转折点。 择…

一键解锁:科研服务器性能匹配秘籍,选择性能精准匹配科研任务和计算需求的服务器

一键解锁&#xff1a;科研服务器性能匹配秘籍 HPC科研工作站服务器集群细分领域迷途小书童 专注于HPC科研服务器细分领域kyfwq001 &#x1f3af;在当今科技飞速发展的时代&#xff0c;科研工作对计算资源的需求日益增长&#x1f61c;。选择性能精准匹配科研任务和计算需求的服…

集合的面试题和五种集合的详细讲解

20240724 一、面试题节选二、来自于b站人人都是程序员的视频截图 &#xff08;感谢人人都是程序员大佬的视频&#xff0c;针对于个人复习。&#xff09; 一、面试题节选 二、来自于b站人人都是程序员的视频截图 hashmap&#xff1a; 唯一的缺点&#xff0c;无序&#xf…

maven项目容器化运行之3-优雅的利用Jenkins和maven使用docker插件调用远程docker构建服务并在1Panel中运行

一.背景 在《maven项目容器化运行之1》中&#xff0c;我们开启了1Panel环境中docker构建服务给到了局域网。在《maven项目容器化运行之2》中&#xff0c;我们基本实现了maven工程创建、远程调用docker构建镜像、在1Panel选择镜像运行容器三大步骤。 但是&#xff0c;存在一个问…

昇思25天学习打卡营第23天 | CycleGAN图像风格迁移互换

昇思25天学习打卡营第23天 | CycleGAN图像风格迁移互换 文章目录 昇思25天学习打卡营第23天 | CycleGAN图像风格迁移互换CycleGAN模型模型结构循环一致损失函数 数据集数据下载创建数据集 网络构建生成器判别器损失函数和优化器前向计算梯度计算与反向传播 总结打卡 CycleGAN模…

【办公软件】Office 2019以上版本PPT 做平滑切换

Office2019以上版本可以在切页面时做平滑切换&#xff0c;做到一些简单的动画效果。如下在快捷菜单栏中的切换里选择平滑。 比如&#xff0c;在两页PPT中&#xff0c;使用同一个形状对象&#xff0c;修改了大小和颜色。 选择切换为平滑后&#xff0c;可以完成如下的动画显示。 …

java-poi实现excel自定义注解生成数据并导出

因为项目很多地方需要使用导出数据excel的功能&#xff0c;所以开发了一个简易的统一生成导出方法。 依赖 <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>4.0.1</version…

【TortoiseGit】合并单个commit(提交)到指定分支上

0、前言 当我们用Git的时候经常用到多个分支&#xff0c;会经常有如下情况&#xff1a;一个dev分支下面会有多个test分支&#xff0c;而每个test分支由不同的开发者。而我们会有这样的需求&#xff1a; 当某个test分支完成了相应功能验证&#xff0c;就要把成功验证的功能代码…

智能卡芯片载带条带AOI外观检测设备及系统

智能卡及其芯片载带简介 我国智能卡产业的发展始于1993年的“金卡工程”&#xff0c;它是一项把货币电子化&#xff0c;运用芯片技术来搭载电子货币应用&#xff0c;运用互联网技术建立从发行到受理的电子货币系统&#xff0c;以提高社会运作效率&#xff0c;方便人们工作生活为…

Mac m1安装 MongoDB 7.0.12

一、下载MongoDB MongoDB 社区版官网下载 二、安装配置MongoDB 1.解压下载的压缩包文件&#xff0c;文件夹重命名为mongodb; 2.将重命名为mongodb的文件夹&#xff0c;放在/usr/local 目录下 3.在/usr/local/mongodb 目录下&#xff0c;新建data 和 log这两个文件夹&#…