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

java——多线程基础

目录

  • 线程的概述
  • 多线程的创建
    • 方式一:继承Thread类
    • 方式二:实现Runnable接口
    • 方式三:利用Callable接口、FutureTask类来实现。
    • Thread常用的方法
  • 线程安全问题
    • 线程安全问题概述
    • 线程安全问题案例
      • 取钱案例描述
      • 模拟代码如下:
      • 执行结果
  • 线程同步
    • 概述
    • 线程同步的常见方案
      • 1. 同步代码块
      • 2. 同步方法
      • 3. Lock锁
  • 线程通信
    • 概述
    • 线程通信案例
    • 案例代码实现
  • 线程池
    • 线程池概述
    • 线程池创建
    • 线程池执行Runnable任务
      • 代码案例
    • 线程池执行Callable任务
      • 代码案例
      • 核心线程数量到底应该配置多少呢?
  • 线程池工具类(Executors)
  • 并发、并行和生命周期
    • 并发和并行
      • 1. 什么是进程、线程?
      • 2. 什么是并发?
      • 3. 什么是并行?
      • 4. 多线程到底是并发还是并行呢?
    • 线程的生命周期
  • 乐观锁与悲观锁

线程的概述

什么是线程?
线程(Thread)是一个程序内部的一条执行流程。
程序中如果只有一条执行流程,那这个程序就是单线程的程序

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

如何在程序中创建出多条线程?
Java是通过java.lang.Thread 类的对象来代表线程的。

多线程的创建

方式一:继承Thread类

  1. 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
  2. 创建MyThread类的对象
  3. 调用线程对象的start()方法启动线程(启动后还是执行run方法的)

示例代码如下:
主线程类:

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);}}
}

子线程类:

/*** 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,无法继承其他类,不利于功能的扩展。

多线程的注意事项

1、启动线程必须是调用start方法,不是调用run方法。
直接调用run方法会当成普通方法执行,此时相当于还是单线程执行。
只有调用start方法才是启动一个新的线程执行。
2、不要把主线程任务放在启动子线程之前。
这样主线程一直是先跑完的,相当于是一个单线程的效果了。

方式二:实现Runnable接口

  1. 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
  2. 创建MyRunnable任务对象
  3. 把MyRunnable任务对象交给Thread处理。
  4. 调用线程对象的start()方法启动线程
    20240224-021236-Ye.png

示例代码如下:
定义一个任务类

/*** 1、定义一个任务类,实现Runnable接口*/
public class MyRunnable implements Runnable{// 2、重写runnable的run方法@Overridepublic void run() {// 线程要执行的任务。for (int i = 1; i <= 5; i++) {System.out.println("子线程输出 ===》" + i);}}
}

主线程类

/*** 多线程的创建方式二:实现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);}}
}

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

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

  1. 可以创建Runnable的匿名内部类对象。
  2. 再交给Thread线程对象。
  3. 再调用线程对象的start()启动线程。

代码示例:

/*** 多线程创建方式二的匿名内部类写法。*/
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);}}
}

方式三:利用Callable接口、FutureTask类来实现。

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

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

方式三 实现步骤:

  1. 创建任务对象
    定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
    把Callable类型的对象封装成FutureTask(线程任务对象)。
  2. 把线程任务对象交给Thread对象。
  3. 调用Thread对象的start方法启动线程。
  4. 线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。

示例代码如下:
创建任务对象类

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;}
}

主线程类

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);}
}

FutureTask的API
20240224-023728-f1.png

方式三的优缺点
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果:
缺点:编码复杂一点。线程创建方式三的优缺点
优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。
缺点:编码复杂一点。

Thread常用的方法

Thread提供了很多与线程操作相关的方法
20240224-024029-q8.png

示例代码:
子线程

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

主方法

/*** Thread的常用方法。*/
public class ThreadTest1 {public static void main(String[] args) {Thread t1 = new MyThread("1号线程");// t1.setName("1号线程");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方法的作用

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. 存在多个线程在同时执行
  2. 同时访问一个共享资源
  3. 存在修改该共享资源

线程安全问题案例

取钱案例描述

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

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

出现线程安全问题的步骤:
20240224-141227-FK.png

模拟代码如下:

先定义一个共享的账户类

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;}
}

在定义一个取钱的线程类

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);}
}

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

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(); // 小红}
}

执行结果

某个执行结果:

小明来取钱100000.0成功!
小红来取钱100000.0成功!
小红来取钱后,余额剩余:-100000.0
小明来取钱后,余额剩余:0.0

线程同步

概述

线程同步解决线程安全问题的方案。

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

线程同步的常见方案

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

Java提供了三种方案:

  1. 同步代码块
  2. 同步方法
  3. Lock锁

1. 同步代码块

作用:把访问共享资源的核心代码给上锁,以此保证线程安全。

synchronized(同步锁){访问共享资源的核心代码
}

原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。
同步锁的注意事项
对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。

代码示例
在共享账户类里使用同步代码块,来解决前面代码里面的线程安全问题。我们只需要修改Account类中的代码即可。

// 小明 小红线程同时过来的
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 + "来取钱:余额不足~");}}
}

执行结果:

小明来取钱100000.0成功!
小明来取钱后,余额剩余:0.0
小红来取钱:余额不足~

锁对象如何选择的问题

1. 建议把共享资源作为锁对象, 不要将随便无关的对象当做锁对象
我们把锁改为"锁" 这样一个字符串也行 因为这个资源在内存中永远只有一份
所以各个线程需要去竞争 但是这样好不好? 明显不行 万一有另外俩个人再创了一个账户 那就变成了四个人竞争一把锁了
2. 对于实例方法,建议使用this作为锁对象
3. 对于静态方法,建议把类的字节码(类名.class)当做锁对象这里是Account.class

2. 同步方法

同步方法,就是把整个方法给锁住,一个线程调用这个方法,另一个线程调用的时候就执行不了,只有等上一个线程调用结束,下一个线程调用才能继续执行,同样是修改Account类中的代码即可。

修饰符 synchronized 返回值类型 方法名称(形参列表){操作共享资源的代码
}

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

同步方法底层原理

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

示例代码如下:

// 同步方法
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 + "来取钱:余额不足~");}
}

同步代码块和同步方法区别

1.不存在哪个好与不好,只是一个锁住的范围大,一个范围小
其中锁的范围小一点 性能稍微好一点 可以提前加载那些公共区域的代码 但是提升的性能对于现在的计算机来说可以忽略不计
反而同步方法的可读性要好一些
2.同步方法是将方法中所有的代码锁住
3.同步代码块是将方法中的部分代码锁住

3. Lock锁

Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。
Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。
20240224-145419-li.png

Lock锁是JDK5版本专门提供的一种锁对象,通过这个锁对象的方法来达到加锁,和释放锁的目的,使用起来更加灵活。格式如下

1.首先在成员变量位置,需要创建一个Lock接口的实现类对象(这个对象就是锁对象)private final Lock lk = new ReentrantLock();
2.在需要上锁的地方加入下面的代码lk.lock(); // 加锁//...中间是被锁住的代码...lk.unlock(); // 解锁

使用Lock锁改写前面DrawThread中取钱的方法,代码如下

// 创建了一个锁对象
//因为俩个线程公用一个账户 所以建立一个实例变量作为锁是可以的
//用final修饰更专业 防止二次赋值
private final Lock lk = new ReentrantLock();public void drawMoney(double money) {// 先搞清楚是谁来取钱?String name = Thread.currentThread().getName();try {//用try cath finally写更专业 因为你不能保证被锁的代码没有bug 有bug也要及时解锁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(); // 解锁}}
}

运行程序结果,观察线程安全问题已解决。

注意事项:

  1. lock锁需要使用final修饰更专业 防止二次赋值
    private final Lock lk = new ReentrantLock();

  2. 加锁和解锁时用try cath finally写更专业 因为你不能保证被锁的代码没有bug 有bug也要及时解锁

线程通信

概述

什么是线程通信?
当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺。

线程通信的常见模型(生产者与消费者模型)

  1. 生产者线程负责生产数据
  2. 消费者线程负责消费生产者生产的数据。
  3. 注意:生产者生产完数据应该等待自己,通知消费者消费;消费者消费完数据也应该等待自己,再通知生产者生产!

线程通信案例

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

案例的思路:

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

20240224-151402-IS.png

注意:上述方法应该使用当前同步锁对象进行调用。
释放当前锁对象时,必须先唤醒其他线程,再释放自己所占锁

案例代码实现

按照上面分析的思路和java Object提供的api写代码。先写桌子类,代码如下

public class Desk {private List<String> list = new ArrayList<>();// 放1个包子的方法// 厨师1 厨师2 厨师3//实例方法默认用this作为锁 所以可以保证锁住5个线程 它们公用一个桌子对象//锁也是可以跨方法的public 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();//必须用当前同步锁对象进行调用 否则会出bugthis.wait();//因为只有锁对象知道当前谁占据着它 谁需要等待}else {// 有包子了,不做了。// 唤醒别人, 等待自己this.notifyAll();//注意!!!notifyAll()和wait()位置不能调换this.wait();//你如果先wait了 你让自己等待了 那你还怎么唤醒别人}} catch (Exception e) {//拦截sleep异常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做了一个肉包子~~
吃货1吃了:厨师3做的肉包子
厨师1做了一个肉包子~~
吃货1吃了:厨师1做的肉包子
厨师3做了一个肉包子~~
吃货1吃了:厨师3做的肉包子
厨师1做了一个肉包子~~
吃货2吃了:厨师1做的肉包子
//不终止则一直运行下去 可以发现没有出现线程安全问题 

线程池

线程池概述

  1. 什么是线程池?
    线程池就是一个可以复用线程的技术。

  2. 不使用线程池的问题:
    用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的,而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能。

  3. 线程池解决的问题:
    使用线程池,就可以解决上面的问题。线程池内部会有一个容器,存储几个核心线程,假设有3个核心线程,这3个核心线程可以处理3个任务。
    但是任务总有被执行完的时候,假设第1个线程的任务执行完了,那么第1个线程就空闲下来了,有新的任务时,空闲下来的第1个线程可以去执行其他任务。依此内推,这3个线程可以不断的复用,也可以执行很多个任务。
    所以,线程池就是一个线程复用技术,它可以提高线程的利用率。

线程池创建

在JDK5版本中提供了代表线程池的接口ExecutorService,而这个接口下有一个实现类叫ThreadPoolExecutor类,使用ThreadPoolExecutor类就可以用来创建线程池对象。下面是它的构造器,参数比较多

20240224-154547-S2.png

用这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. 什么时候开始拒绝新的任务?
    核心线程和临时线程都在忙、任务队列也满了、新任务过来时才会开始拒绝任务。

线程池执行Runnable任务

创建好线程池之后,接下来我们就可以使用线程池执行任务了。
线程池执行的任务可以有两种,一种是Runnable任务;一种是callable任务。
下面的execute方法可以用来执行Runnable任务。

20240224-155432-D2.png
20240224-160537-Gc.png

代码案例

先准备一个线程任务类

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个参数理解。

public class ThreadPoolTest1 {public static void main(String[] args) {// 1、通过ThreadPoolExecutor创建一个线程池对象。ExecutorService pool = new ThreadPoolExecutor(3, 5, 8,TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),new ThreadPoolExecutor.CallerRunsPolicy());Runnable target = new MyRunnable();pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!pool.execute(target); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!pool.execute(target);pool.execute(target);pool.execute(target);pool.execute(target);// 到了临时线程的创建时机了pool.execute(target);pool.execute(target);// 到了新任务的拒绝时机了!pool.execute(target);// pool.shutdown(); // 等着线程池的任务全部执行完毕后,再关闭线程池// pool.shutdownNow(); // 立即关闭线程池!不管任务是否执行完毕!}
}

执行结果:

pool-1-thread-5 ==> 输出666~~
main ==> 输出666~~
pool-1-thread-1 ==> 输出666~~
pool-1-thread-3 ==> 输出666~~
pool-1-thread-4 ==> 输出666~~
pool-1-thread-2 ==> 输出666~~
//其中123是核心线程执行的 45是临时线程执行的
//注意程序还是一直运行的 线程池不会自动关闭 设计出来就是一直服务的
//main输出是因为使用了CallerRunsPolicy()拒绝策略 新来的要拒绝的任务由主线程main执行了

线程池执行Callable任务

Callable任务相对于Runnable任务来说,就是多了一个返回值。
执行Callable任务需要用到上面ExecutorService的submit方法

代码案例

先准备一个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());}
}

某次执行后,结果如下所示

pool-1-thread-1求出了1-100的和是:5050
pool-1-thread-2求出了1-200的和是:20100
pool-1-thread-3求出了1-300的和是:45150
pool-1-thread-3求出了1-400的和是:80200

核心线程数量到底应该配置多少呢?

根据经验法则,大致参考以下原则:

  1. 如果是计算密集型的任务:核心线程数量 = CPU的核数 + 1
  2. 如果是IO密集型的任务:核心线程数量 = CPU核数 * 2

CPU核数查看,这个cpu是16核。
20240224-162721-sw.png

线程池工具类(Executors)

Executors是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。

20240224-162047-Pf.png

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

测试代码:

public class ThreadPoolTest3 {public static void main(String[] args) throws Exception {// 1、通过Executors创建一个线程池对象。ExecutorService pool = Executors.newFixedThreadPool(17);// 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开发手册》提供的强制规范要求,在大型并发系统环境中容易出bug。
20240224-162619-Io.png

并发、并行和生命周期

并发和并行

1. 什么是进程、线程?

  1. 正常运行的程序(软件)就是一个独立的进程
  2. 线程是属于进程,一个进程中包含多个线程
  3. 进程中的线程其实并发和并行同时存在

可以打开系统的任务管理器看看(快捷键:Ctrl+Shfit+Esc),自己的电脑上目前有哪些进程。
20240224-163444-Vo.png

2. 什么是并发?

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

简单记:并发就是多条线程交替执行

3. 什么是并行?

并行指的是,多个线程同时被CPU调度执行。如下图所示,多个CPU核心在执行多条线程

20240224-163545-35.png

4. 多线程到底是并发还是并行呢?

其实多个线程在我们的电脑上执行,并发和并行是同时存在的。

线程的生命周期

在Thread类中有一个嵌套的枚举类叫Thread.Status,这里面定义了线程的6中状态。如下图所示
20240224-164432-Ts.png

NEW: 新建状态,线程还没有启动
RUNNABLE: 可以运行状态,线程调用了start()方法后处于这个状态
BLOCKED: 锁阻塞状态,没有获取到锁处于这个状态
WAITING: 无限等待状态,线程执行时被调用了wait方法处于这个状态
TIMED_WAITING: 计时等待状态,线程执行时被调用了sleep(毫秒)或者wait(毫秒)方法处于这个状态
TERMINATED: 终止状态, 线程执行完毕或者遇到异常时,处于这个状态。

这几种状态之间切换关系如下图所示

20240224-164913-Fh.png

线程的六种状态的总结
20240224-164948-tM.png

乐观锁与悲观锁

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

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

下面举例说明,先写一个没有锁的多线程场景:

public static void main(String[] args) throws Exception {//需求:1个静态变量,100个线程,每个线程对其加100次 最终值为10000Runnable target = new MyRunnable();for (int i = 1; i <= 100; i++) {new Thread(target).start();}}
public class MyRunnable implements Runnable{private int count;//用实例变量代替静态变量 反正线程都是公用一个任务对象的 所以是可以的@Overridepublic void run() {//100次for (int i = 0; i < 100; i++) {System.out.println("count--------->"+(++count));}}
}

某次运行结果:

...
count--------->9989
count--------->9990
count--------->9991
count--------->9992
count--------->9993
count--------->9994
count--------->9995
count--------->9996
count--------->9997
count--------->9998
count--------->9999
//虽然概率比较小 还是出现了一次线程安全问题
//有一次增值计算重叠了 没有加到10000

乐观锁解决:

首先解释原理,安全问题来自于,比如当count是10时,俩个线程几乎同时进入,将其值修改成11,于是便发生了安全问题,少加了一次。乐观锁采用CAS算法(可以自己进入count.incrementAndGet()源码看看),在加之前就记录了count的原来的值,比如当线程进入时记录count是10,然后将其加到11准备写入时,发现count已经变成11了,于是会将这次修改写入作废,重复上述过程,重新加一次。

代码如下:

public class MyRunnable implements Runnable{//整数修改的乐观锁:用java的原子类实现的private AtomicInteger count = new AtomicInteger();@Overridepublic void run() {//100次for (int i = 0; i < 100; i++) {System.out.println("count--------->"+(count.incrementAndGet()));}}
}

执行结果可以发现没有线程安全问题。

相关文章:

java——多线程基础

目录 线程的概述多线程的创建方式一&#xff1a;继承Thread类方式二&#xff1a;实现Runnable接口方式三&#xff1a;利用Callable接口、FutureTask类来实现。Thread常用的方法 线程安全问题线程安全问题概述线程安全问题案例取钱案例描述模拟代码如下&#xff1a;执行结果 线程…...

Python服务器监测测试策略与工具:确保应用的高可用性!

在构建高可用性的应用程序时&#xff0c;服务器监测测试是至关重要的一环。Python作为一种强大的编程语言&#xff0c;提供了丰富的工具和库来帮助我们进行服务器监测测试。本文将介绍一些关键的策略和工具&#xff0c;帮助你确保应用的高可用性。 1. 监测策略的制定&#xff…...

Spring Security源码学习

Spring Security本质是一个过滤器链 过滤器链本质是责任链设计模型 1. HttpSecurity 【第五篇】深入理解HttpSecurity的设计-腾讯云开发者社区-腾讯云 在以前spring security也是采用xml配置的方式&#xff0c;在<http>标签中配置http请求相关的配置&#xff0c;如用户…...

大数据面试总结三

1、hdfs作为分布式存储系统&#xff0c;底层的实现的方式&#xff08;可能不正确&#xff09; 1、底层是一个分布式存储的&#xff0c;底层会将数据进行切分多个block块&#xff08;128M&#xff09;&#xff0c;并存储在不同的节点上面&#xff0c;这种分布式方式有助于提高数…...

AI赚钱套路总结和教程

最近李一舟和Sora 很火&#xff0c;作为第一批使用Sora赚钱的男人&#xff0c;一个清华学美术的跟人讲AI&#xff0c;信的人太多了&#xff0c;钱太好赚了。3年时间&#xff0c;李一舟仅通过卖课就赚了1.75亿元&#xff0c;其中《每个人的人工智能课》收入2786万元&#xff0c;…...

Linux安装jdk、tomcat、MySQL离线安装与启动

一、JDK和Tomcat的安装 1.JDK安装 直接上传到Linux服务器的&#xff0c;上传jdk、tomcat安装包 解压JDK安装包 //解压jdk tar -zxvf jdk-8u151-linux-x64.tar.gz 置环境变量(JAVA_HOME和PATH) vim /etc/profile 在文件末尾添加以下内容&#xff1a; //java environment expo…...

Python爬虫-使用代理伪装IP

爬虫系列&#xff1a;http://t.csdnimg.cn/WfCSx 前言 我们在做爬虫的过程中经常会遇到这样的情况&#xff0c;最初爬虫正常运行&#xff0c;正常抓取数据&#xff0c;一切看起来都是那么的美好&#xff0c;然而一杯茶的功夫可能就会出现错误&#xff0c;比如 403 Forbidden&…...

Typora结合PicGo + 使用Github搭建个人免费图床

文章目录 一、国内图床比较二、使用Github搭建图床三、PicGo整合Github图床1、下载并安装PicGo2、设置图床3、整合jsDelivr具体配置介绍 4、测试5、附录 四、Typora整合PicGo实现自动上传 每次写博客时&#xff0c;我都会习惯在Typora写好&#xff0c;然后再复制粘贴到对应的网…...

【Redis】redis简介与安装

Redis 简介 Redis 是完全开源的&#xff0c;遵守 BSD 协议&#xff08;Berkeley Software Distribution 意思是"伯克利软件发行版&#xff09;&#xff0c;是一个高性能的 key-value 数据库。具有以下几个比较明显的特点&#xff1a; 性能极高 – Redis能读的速度可以达…...

【xss跨站漏洞】xss漏洞利用工具beef的安装

安装环境 阿里云服务器&#xff0c;centos8.2系统&#xff0c;docker docker安装 前提用root用户 安装docker yum install docker 重启docker systemctl restart docker beef安装 安装beef docker pull janes/beef 绑定到3000端口 docker run --rm -p 3000:3000 janes/beef …...

编程笔记 html5cssjs 086 JavaScript 内置对象

编程笔记 html5&css&js 086 JavaScript 内置对象 一、Object二、Array三、String四、Number五、Math六、Date七、RegExp八、Function九、示例小结 JavaScript 内置对象是 JavaScript 语言本身定义的一系列预定义的对象&#xff0c;这些对象在全局作用域中可以直接使用&…...

AttributeError: ‘DataFrame‘ object has no attribute ‘set_value‘怎么修改问题的解决

在jupyternotebook中运行&#xff1a; def remplacement_df_keywords(df, dico_remplacement, roots False):df_new df.copy(deep True)for index, row in df_new.iterrows():chaine row[plot_keywords]if pd.isnull(chaine): continuenouvelle_liste []for s in chaine.…...

Jmeter内置变量 vars 和props的使用详解

JMeter是一个功能强大的负载测试工具&#xff0c;它提供了许多有用的内置变量来支持测试过程。其中最常用的变量是 vars 和 props。 vars 变量 vars 变量是线程本地变量&#xff0c;它们只能在同一线程组内的所有线程中使用&#xff08;线程组内不同线程之间变量不共享&#…...

c#高级-正则表达式

正则表达式是由普通字符和元字符&#xff08;特殊符号&#xff09;组成的文字形式 应用场景 1.用于验证输入的邮箱是否合法。 2.用于验证输入的电话号码是否合法。 3.用于验证输入的身份证号码是否合法。等等 正则表达式常用的限定符总结&#xff1a; 几种常用的正则简写表达式…...

说说UE5中的几种字符串类

在Unreal Engine 5 (UE5) 的C中&#xff0c;与字符串相关的类主要包括&#xff1a; FString&#xff1a; Unreal Engine中用于处理字符串的主要类&#xff0c;提供了丰富的字符串操作方法和功能。 FText&#xff1a; 用于表示本地化文本的类&#xff0c;可以包含多种语言的文本…...

(done) 如何判断一个矩阵是否可逆?

参考视频&#xff1a;https://www.bilibili.com/video/BV15H4y1y737/?spm_id_from333.337.search-card.all.click&vd_source7a1a0bc74158c6993c7355c5490fc600 这个视频里还暗含了一些引理 1.若 AX XB 且 X 和 A,B 同阶可逆&#xff0c;那么 A 和 B 相似。原因&#xff1…...

洗眼镜用的超声波清洗机哪一家更好一点?好用超声波清洗机排名

在我们日常生活中&#xff0c;眼镜、首饰、手表等细小物件的清洁一直是一个让人头疼的问题。传统的清洁方法不仅耗时耗力&#xff0c;还可能因为不当的操作而损伤到这些精细的物品。那么&#xff0c;有没有一种既快捷又安全的清洁方式呢&#xff1f;答案就是使用超声波清洗机。…...

(二十二)Flask之上下文管理第三篇【收尾—讲一讲g】

目录: 每篇前言:g到底是什么?生命周期在请求周期内保持数据需要注意的是:拓展—面向对象的私有字段深入讲解一下那句:每篇前言: 🏆🏆作者介绍:【孤寒者】—CSDN全栈领域优质创作者、HDZ核心组成员、华为云享专家Python全栈领域博主、CSDN原力计划作者🔥🔥本文已…...

五种多目标优化算法(MOGWO、MOJS、NSWOA、MOPSO、MOAHA)性能对比,包含6种评价指标,9个测试函数(提供MATLAB代码)

一、5种多目标优化算法简介 1.1MOGWO 1.2MOJS 1.3NSWOA 1.4MOPSO 1.5MOAHA 二、5种多目标优化算法性能对比 为了测试5种算法的性能将其求解9个多目标测试函数&#xff08;zdt1、zdt2 、zdt3、 zdt4、 zdt6 、Schaffer、 Kursawe 、Viennet2、 Viennet3&#xff09;&#xff0…...

istio实战:springboot项目在istio中服务调用

目录 一、前言二、准备工作三、问题排查四、总结参考资料 一、前言 在经过前面几天k8s和Istio的安装之后&#xff0c;开始进入最核心的阶段。微服务在抛弃传统的服务注册和服务发现之后&#xff0c;是怎么在istio怎么做服务间的调用的呢&#xff1f;本次实战花费了我2-3天的时…...

云计算——弹性云计算器(ECS)

弹性云服务器&#xff1a;ECS 概述 云计算重构了ICT系统&#xff0c;云计算平台厂商推出使得厂家能够主要关注应用管理而非平台管理的云平台&#xff0c;包含如下主要概念。 ECS&#xff08;Elastic Cloud Server&#xff09;&#xff1a;即弹性云服务器&#xff0c;是云计算…...

uniapp中使用aixos 报错

问题&#xff1a; 在uniapp中使用aixos&#xff0c;运行后报如下错误&#xff1a; AxiosError: There is no suitable adapter to dispatch the request since : - adapter xhr is not supported by the environment - adapter http is not available in the build 解决方案&…...

ios苹果系统,js 滑动屏幕、锚定无效

现象&#xff1a;window.addEventListener监听touch无效&#xff0c;划不动屏幕&#xff0c;但是代码逻辑都有执行到。 scrollIntoView也无效。 原因&#xff1a;这是因为 iOS 的触摸事件处理机制和 touch-action: none 的设置有关。ios有太多得交互动作&#xff0c;从而会影响…...

基于 TAPD 进行项目管理

起因 自己写了个小工具&#xff0c;仓库用的Github。之前在用markdown进行需求管理&#xff0c;现在随着功能的增加&#xff0c;感觉有点难以管理了&#xff0c;所以用TAPD这个工具进行需求、Bug管理。 操作流程 注册 TAPD&#xff0c;需要提供一个企业名新建一个项目&#…...

C/C++ 中附加包含目录、附加库目录与附加依赖项详解

在 C/C 编程的编译和链接过程中&#xff0c;附加包含目录、附加库目录和附加依赖项是三个至关重要的设置&#xff0c;它们相互配合&#xff0c;确保程序能够正确引用外部资源并顺利构建。虽然在学习过程中&#xff0c;这些概念容易让人混淆&#xff0c;但深入理解它们的作用和联…...

【Android】Android 开发 ADB 常用指令

查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...

TSN交换机正在重构工业网络,PROFINET和EtherCAT会被取代吗?

在工业自动化持续演进的今天&#xff0c;通信网络的角色正变得愈发关键。 2025年6月6日&#xff0c;为期三天的华南国际工业博览会在深圳国际会展中心&#xff08;宝安&#xff09;圆满落幕。作为国内工业通信领域的技术型企业&#xff0c;光路科技&#xff08;Fiberroad&…...

嵌入式学习之系统编程(九)OSI模型、TCP/IP模型、UDP协议网络相关编程(6.3)

目录 一、网络编程--OSI模型 二、网络编程--TCP/IP模型 三、网络接口 四、UDP网络相关编程及主要函数 ​编辑​编辑 UDP的特征 socke函数 bind函数 recvfrom函数&#xff08;接收函数&#xff09; sendto函数&#xff08;发送函数&#xff09; 五、网络编程之 UDP 用…...

TJCTF 2025

还以为是天津的。这个比较容易&#xff0c;虽然绕了点弯&#xff0c;可还是把CP AK了&#xff0c;不过我会的别人也会&#xff0c;还是没啥名次。记录一下吧。 Crypto bacon-bits with open(flag.txt) as f: flag f.read().strip() with open(text.txt) as t: text t.read…...

DeepSeek越强,Kimi越慌?

被DeepSeek吊打的Kimi&#xff0c;还有多少人在用&#xff1f; 去年&#xff0c;月之暗面创始人杨植麟别提有多风光了。90后清华学霸&#xff0c;国产大模型六小虎之一&#xff0c;手握十几亿美金的融资。旗下的AI助手Kimi烧钱如流水&#xff0c;单月光是投流就花费2个亿。 疯…...