【Java从入门到大牛】多线程

🔥 本文由 程序喵正在路上 原创,CSDN首发!
💖 系列专栏:Java从入门到大牛
🌠 首发时间:2023年11月18日
🦋 欢迎关注🖱点赞👍收藏🌟留言🐾

目录

  • 多线程的创建
    • 方式一:继承Thread类
    • 方式二:实现Runnable接口
    • 方式三:实现Callable接口
  • Thread的常见方法
  • 线程安全
    • 什么是线程安全问题
    • 用程序模拟线程安全问题
  • 线程同步
    • 认识线程同步
    • 方式一:同步代码块
    • 方式二:同步方法
    • 方式三:Lock锁
  • 线程通信【了解】
  • 线程池
    • 认识线程池
    • 创建线程池
    • 线程池处理Runnable任务
    • 线程池处理Callable任务
    • Executors工具类实现线程池
  • 其它细节知识:并发、并行
  • 其它细节知识:线程的生命周期
  • 乐观锁
  • 多线程练习题

多线程的创建

方式一:继承Thread类

什么是线程?

线程(Thread)是一个程序内的一条执行流程,比如常见的 main 方法就是一个线程

public static void main(String[] args) {// 代码...for(int i = 0; i <10; i++) {System.out.println(i);}//...
}

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

什么是多线程?

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

多线程的用处

在生活中,有很多多线程应用的例子,比如,12306网站同时有多位用户在抢票,那么网站就需要提供多条线程来为多位用户服务;在使用百度网盘时,用户可能同时需要上传和下载资源,这也需要用到多线程技术;再比如消息通信、淘宝、京东系统都离不开多线程技术

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

Java 是通过 java.lang.Thread 类的对象来代表线程的

多线程的创建方式一:继承Thread类

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

代码实现

MyThread类

/*** 1. 让子类继承Thread线程类*/
public class MyThread extends Thread {@Override   // 2. 必须重写Thread类的run方法public void run() {// 这里用来描述线程要执行的任务for (int i = 1; i <= 5; i++) {System.out.println("子线程MyThread输出:" + i);}}
}

测试类

/*** 目标:掌握线程的创建方式一:继承Thread类*/
public class ThreadTest1 {// main方法是由一条默认的主线程负责执行的public static void main(String[] args) {// 3. 创建MyThread线程类的对象代表一个线程Thread t = new MyThread();  // 多态写法// 4. 启动线程,必须是调用start方法t.start();  // 启动后,现在就存在两个线程:main线程和t线程for (int i = 1; i <= 5; i++) {System.out.println("主线程main输出:" + i);}}
}

因为有两个线程,线程执行的顺序也不确定,所有每次执行结果都可能不同

在这里插入图片描述

方法一优缺点:

  • 优点:编码简单
  • 缺点:线程类已经继承 Thread 类,所以无法再继承其他类,不利于功能的扩展

多线程的注意事项

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

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

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

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

方式二:实现Runnable接口

多线程的创建方式二:实现Runnable接口

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

  2. 创建 MyRunnable 任务对象

  3. 把 MyRunnable 任务对象交给 Thread 处理
    在这里插入图片描述

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

代码实现

MyRunnable类

/*** 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.把任务对象交给一个线程对象处理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接口

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

怎么解决这个问题?

  • JDK5.0 提供了 Callable 接口和 FutureTask 来实现
  • 这种方式最大的优点就是可以返回线程执行完毕后的结果

多线程的第三种创建方式:利用Callable接口和FutureTask类来实现

1、创建任务对象

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

2、把线程任务对象交给 Thread 对象

3、调用 Thread 对象的 start 方法启动线程

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

FutureTask的API

在这里插入图片描述
代码实现

MyCallable类

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

在这里插入图片描述

线程创建方式三的优缺点

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

Thread的常见方法

Thread 提供了很多与线程操作相关的方法
在这里插入图片描述

应用演示

MyThread类

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

测试类1

/*** 目标:掌握Thread的常用方法*/
public class ThreadTest1 {public static void main(String[] args) {Thread t1 = new MyThread("1号线程");t1.start();System.out.println(t1.getName());   // Thread-0Thread t2 = new MyThread("2号线程");t2.start();System.out.println(t2.getName());   // Thread-1//获取主线程对象的名字Thread m = Thread.currentThread();m.setName("main主线程");System.out.println(m.getName());for (int i = 1; i <= 5; i++) {System.out.println(m.getName() + "输出:" + i);}}
}

在这里插入图片描述

测试类2

/*** 目标:掌握sleep方法,join方法的作用*/
public class ThreadTest2 {public static void main(String[] args) throws Exception {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();}
}

在这里插入图片描述

Thread其他方法的说明

Thread 类还提供了诸如:yield、interrupt、守护线程、线程优先级等线程的控制方法,在开发中很少使用,在这里就不一一列出了

线程安全

什么是线程安全问题

什么是线程安全问题?

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

取钱的线程安全问题

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

有可能会出现2人都成功取钱10万元的结果

用程序模拟线程安全问题

需求

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

分析

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

代码实现

账户类

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 + "来取钱:余额不足~");}}// get、set方法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(); // 小红}
}

在这里插入图片描述

线程同步

认识线程同步

线程同步

解决线程安全问题的方案

线程同步的思想

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

线程同步的常见方案

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

方式一:同步代码块

同步代码块

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

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

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

同步锁的注意事项

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

同步代码块的实现

IDEA里加同步锁的快捷方式:鼠标选中要上锁的核心代码,按住 Ctrl+Alt+T,然后选择第9项 synchronized 即可

依旧是前面那个取钱的例子,我们只需要将账户类稍微修改一下即可,代码如下

public class Account {private String cardId; // 卡号private double money; // 余额// ...// 取钱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)对象作为锁对象

方式二:同步方法

同步方法

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

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

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

同步方法底层原理

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

同步方法代码实现

账户类修改如下

public class Account {private String cardId; // 卡号private double money; // 余额// ...// 同步方法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 + "来取钱:余额不足~");}}// ...
}

方式三:Lock锁

Lock锁

  • Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大

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

    在这里插入图片描述

Lock的常用方法
在这里插入图片描述

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 void drawMoney(double money) {// 先搞清楚是谁来取钱?String name = Thread.currentThread().getName();// 为了防止try里面的代码块出现异常而导致无法解锁,// 我们需要对其进行异常处理,并将解锁操作放到finally里面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(); // 解锁}}// ...
}

线程通信【了解】

什么是线程通信?

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

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

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

Object类的等待和唤醒方法
在这里插入图片描述

注意:上述方法应该使用当前同步锁对象进行调用

为了更好地理解生产者与消费者模型,我们来看一个例子:

  • 3个生产者线程,负责生产包子,每个线程每次只能生产1个包子放在桌子上
  • 2个消费者线程负责吃包子,每人每次只能从桌子上拿1个包子吃
  • 同一时刻,只能有一个生产者生产包子,或只能有一个消费者吃包子

代码实现

桌子类

import java.util.ArrayList;
import java.util.List;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();}}
}

测试类

/*** 目标:了解一下线程通信*/
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();}
}

在这里插入图片描述

线程池

认识线程池

什么是线程池?

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

不使用线程池的问题

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

线程池的工作原理
在这里插入图片描述

创建线程池

Java中谁代表线程池?

JDK5.0 起提供了代表线程池的接口:ExecutorService

如何得到线程池对象?

  • 方式一:使用 ExecutorService 的实现类 ThreadPoolExecutor 自创建一个线程池对象
  • 方式二:使用 Executors(线程池的工具类)调用方法返回不同特点的线程池对象

ThreadPoolExecutor的构造器

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,RejectedExecutionHandler handler) 
  • 参数一:corePoolSize : 指定线程池的核心线程的数量
  • 参数二:maximumPoolSize:指定线程池的最大线程数量
  • 参数三:keepAliveTime :指定临时线程的存活时间
  • 参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)
  • 参数五:workQueue:指定线程池的任务队列
  • 参数六:threadFactory:指定线程池的线程工厂
  • 参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)

创建一个线程池

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

线程池的注意事项

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

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

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

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

线程池处理Runnable任务

ExecutorService的常用方法
在这里插入图片描述

新任务拒绝策略

在这里插入图片描述

代码实现

MyRunnable类

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

测试类

import java.util.concurrent.*;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(); // 立即关闭线程池!不管任务是否执行完毕!}
}

在这里插入图片描述

线程池处理Callable任务

ExecutorService的常用方法
在这里插入图片描述

代码实现

MyCallable类

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 Thread.currentThread().getName() + "求出了1-" + n + "的和是:" + sum;}
}

测试类

import java.util.concurrent.*;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));System.out.println(f1.get());System.out.println(f2.get());System.out.println(f3.get());System.out.println(f4.get());}
}

在这里插入图片描述

Executors工具类实现线程池

什么是Executors

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

在这里插入图片描述

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

Executors使用可能存在的陷阱

大型并发系统环境中,使用Executors如果不注意可能会出现系统风险。

在这里插入图片描述

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

进程

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

并发的含义

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

并行的理解

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

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

什么是线程的生命周期

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

Java线程的状态

  • Java中总共定义了6种状态
  • 6种状态都定义在Thread类的内部枚举类中
    在这里插入图片描述

线程的6种状态互相转换

在这里插入图片描述

线程的6种状态总结

在这里插入图片描述

乐观锁

前面我们学到的,都是悲观锁,就是一上来就加锁,没有安全感,每次只能一个线程进入访问完毕后,再解锁,线程安全,但是性能较差

什么是乐观锁

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

例子

有1个变量,100个线程,每个线程都要对其加100次

不加锁

MyRunnable.java

public class MyRunnable implements Runnable{private int cnt;     // 记录浏览人数@Overridepublic void run() {// 加100次for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + " cnt --------> " + (++cnt));}}
}

Test1.java

public class Test1 {public static void main(String[] args) {// 需求:1个变量,100个线程,每个线程对其加100次Runnable target = new MyRunnable();for (int i = 0; i < 100; i++) {new Thread(target).start();}}
}

加的顺序不一定是一个一个地加,还有可能出现加不到10000的情况,因为有可能在同一时刻两个线程同时抢到了执行权,对同一个数字进行加1,结果相同,但是少了一次加法

在这里插入图片描述

加悲观锁

MyRunnable.java

public class MyRunnable implements Runnable{private int cnt;     // 记录浏览人数@Overridepublic void run() {// 加100次for (int i = 0; i < 100; i++) {synchronized(this) {System.out.println(Thread.currentThread().getName() + " cnt --------> " + (++cnt));}}}
}

没有问题

在这里插入图片描述

加乐观锁

加乐观锁后,当有两个线程同时抢到执行权时,一个线程在对变量修改完成后,要赋值回去的时候会进行判断,若变量已经被修改了,则这一轮修改作废,重新进行一轮修改

MyRunnable2.java

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

Test2.java

public class Test2 {public static void main(String[] args) {// 需求:1个变量,100个线程,每个线程对其加100次Runnable target = new MyRunnable2();for (int i = 0; i < 100; i++) {new Thread(target).start();}}
}

在这里插入图片描述

多线程练习题

需求描述

有100份礼品,小红、小明两人同时发送,当剩下的礼品数小于10份的时候则不再送出,利用多线程模拟该过程并将线程的名称打印出来,并最后在控制台分别打印小红、小明分别送出了多少份礼物

代码

SendThread.java

import java.util.List;
import java.util.Random;// 发送礼品
public class SendThread extends Thread {private List<String> gift;private int cnt;    // 计数public SendThread(List<String> gift, String name) {super(name);this.gift = gift;}@Overridepublic void run() {// 发礼品// 注意:锁必须唯一Random r = new Random();String name = Thread.currentThread().getName();while (true) {synchronized(gift) {if (gift.size() < 10) {break;}String rs = gift.remove(r.nextInt(gift.size()));System.out.println(name + "发出了:" + rs);cnt++;}}}public int getCnt() {return cnt;}
}

Demo.java

import java.util.ArrayList;
import java.util.List;
import java.util.Random;public class Demo1 {public static void main(String[] args) throws Exception {// 创建100份礼品List<String> gift = new ArrayList<>();String[] names = {"口红", "鲜花", "包包", "剃须刀", "皮带", "手表"};Random r = new Random();    // 随机分配礼品名称for (int i = 0; i < 100; i++) {gift.add(names[r.nextInt(names.length)] + (i+1));}System.out.println(gift);// 定义线程类,创建线程对象,去集合中拿礼品给别人SendThread xh = new SendThread(gift, "小红");xh.start();SendThread xm = new SendThread(gift, "小明");xm.start();xh.join();xm.join();System.out.println("小红送出了" + xh.getCnt() + "份礼物");System.out.println("小明送出了" + xm.getCnt() + "份礼物");}
}

在这里插入图片描述

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

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

相关文章

Adversarial Attacks on Neural Networks for Graph Data

Adversarial Attacks on Neural Networks for Graph Data----《针对图数据的神经网络的对抗攻击》 论文提出了两个问题&#xff1a; 1、属性图的深度学习模型容易受攻击吗&#xff1f; 2、他们的结果可靠吗&#xff1f; 回答这两个问题需要考虑到GNN的特性&#xff1a; ①关…

6.8完全二叉树的节点个数(LC222-E)

算法&#xff1a; 如果不考虑完全二叉树的特性&#xff0c;直接把完全二叉树当作普通二叉树求节点数&#xff0c;其实也很简单。 递归法&#xff1a; 用什么顺序遍历都可以。 比如后序遍历&#xff08;LRV&#xff09;&#xff1a;不断遍历左右子树的节点数&#xff0c;最后…

Windows11 python3.12 安装pyqt6 pyqt6-tools

Windows11 python3.12 安装pyqt6比较容易&#xff0c;但pyqt6-tools一直安装不上去。出错信息如下&#xff1a; (venv) PS D:\python_project\pyqt6> pip install pyqt6-tools Collecting pyqt6-toolsUsing cached pyqt6_tools-6.4.2.3.3-py3-none-any.whl (29 kB) Collec…

VS中修改解决方案名称和项目名称

如何修改visual studio2019中的项目名 - 知乎 (zhihu.com) 查了很多&#xff0c;还是这个可行。虽然文中说不是最简单的&#xff0c;但在所查找资料中是可行且最简单的。 要点主要是&#xff1a; 1、比如我们复制一个解决方案&#xff0c;最好是带代码哈&#xff0c;也就是添…

面向对象成员之属性

属性:通过方法改造出来 # 1.编写时 # 方法上方写property # 方法参数:只有一个self # 2.使用时:无需加括号 对象.方法 # 3.应用场景:对于简单的方法,当无需传参且有返回值时,可以使用 property class Foo(object):def _init_(self):...propertydef start(self):return 1pr…

【Linux网络编程】高级I/O

目录 五种I/O模型 阻塞和非阻塞 非阻塞I/O I/O多路复用之Select、Poll、与Epoll 本文目的是深入浅出理解高级I/O相关的知识&#xff0c;结尾附上代码加深理解相关知识。 五种I/O模型 1.阻塞I/O&#xff1a;在内核将数据准备好之前&#xff0c;系统调用会一直等待。所有的套…

CTFHub | Cookie注入,UA注入,Refer注入,过滤空格(利用hackbar插件)

Cookie注入 Cookie 注入原理 Cookie 注入的原理也和其他注入一样&#xff0c;只不过是将提交的参数以 Cookie 方式提交&#xff0c;而一般的注入是使用 GET 或者 POST 方式提交&#xff0c;GET 方式提交就是直接在网址后面加上需要注入的语句&#xff0c;POST 方式则是通过表单…

不允许你还没有了解哈希表、哈希桶、哈希冲突的解决,如何避免冲突

✏️✏️✏️今天给各位带来的是哈希桶、哈希冲突方面的知识。 清风的CSDN博客 &#x1f61b;&#x1f61b;&#x1f61b;希望我的文章能对你有所帮助&#xff0c;有不足的地方还请各位看官多多指教&#xff0c;大家一起学习交流&#xff01; 动动你们发财的小手&#xff0c;点…

springboot项目yml文件中使用${}配置

1、传统写法 &#xff08;1&#xff09;配置服务启动端口 # 服务端口 server:port: 9898 &#xff08;2&#xff09;使用idea启动 &#xff08;3&#xff09;使用jar包启动 2、使用${}写法 格式&#xff1a;${自定义参数名:默认值} 作用&#xff1a; 项目启动时动态配置变量…

社区无人零售:投资新热点,创业新机遇

社区无人零售是一个备受关注的创业项目&#xff0c;被视为投资的“爆点”。与其他国家相比&#xff0c;无人零售在国内市场远未达到饱和&#xff0c;因此成为了当下的新风口。今天&#xff0c;我将详细分析这个创业项目&#xff0c;以帮助感兴趣的朋友们了解更多。 首先&#x…

邀请报名|11月24日阿里云原生 Serverless 技术实践营 深圳站

活动简介 “阿里云云原生 Serverless 技术实践营 ” 是一场以 Serverless 为主题的开发者活动&#xff0c;活动受众以关注 Serverless 技术的开发者、企业决策人、云原生领域创业者为主&#xff0c;活动形式为演讲、动手实操&#xff0c;让开发者通过一个下午的时间增进对 Ser…

动 态 规 划

一、&#xff08;what&#xff1f;&#xff09; 二、&#xff08;why&#xff1f;&#xff09; 三、&#xff08;how&#xff1f;&#xff09; 四、典型例题分析&#xff1a; 例题1&#xff1a;神奇的兔子序列 输入&#xff1a;月数 输出&#xff1a;兔子数 思路&#xff1…