多线程(Lock锁,死锁,等待唤醒机制,阻塞队列,线程池)

2024-06-21 00:44

本文主要是介绍多线程(Lock锁,死锁,等待唤醒机制,阻塞队列,线程池),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Lock锁

虽然我们可以理解同步代码块和同步方法的锁对象问题但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作Lock中提供了获得锁和释放锁的方法
void lock():获得锁void unlock():释放锁
即手动上锁、手动释放锁
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化ReentrantLock的构造方法
ReentrantLock():创建一个ReentrantLock的实例


例子:

package Threadmethod;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class MyThread1 extends Thread{static int jicket=0;static Lock lock=new ReentrantLock();@Overridepublic void run() {while (true){lock.lock();try {if(jicket==100){break;}else {Thread.sleep(10);jicket++;System.out.println(getName()+ "正在卖第"+jicket+"张票");}} catch (InterruptedException e) {throw new RuntimeException(e);} finally {lock.unlock();}}}
}

测试类 

package Threadmethod;public class ThreadDemo8 {public static void main(String[] args) {MyThread1 t1=new MyThread1();MyThread1 t2=new MyThread1();MyThread1 t3=new MyThread1();t1.setName("窗口一");t2.setName("窗口二");t3.setName("窗口三");t1.start();t2.start();t3.start();}
}

死锁

死锁是指在多个进程或线程中,每个进程或线程因为等待另一个进程或线程所拥有的资源而进入无限等待的状态,导致系统无法继续执行下去的情况。死锁通常发生在以下情况下:

1. 互斥:多个进程或线程同时只能持有一个资源,如果一个进程或线程占用了一个资源,其他进程或线程就无法访问该资源。

2. 请求与保持:进程或线程在等待其他进程或线程所拥有的资源时,继续持有已经占用的资源。

3. 不可抢占:已经占用了某个资源的进程或线程不能被其他进程或线程抢占。

4. 循环等待:多个进程或线程形成一个循环等待资源的关系,每个进程或线程都在等待下一个进程或线程所拥有的资源。

死锁的解决方法包括:

1. 预防:通过破坏四个必要条件中的一个或多个条件来预防死锁的发生。

2. 避免:在资源分配的时候,使用一种资源分配算法来避免可能引发死锁的情况。

3. 检测与恢复:周期性地检测系统中是否存在死锁,如果发现死锁,则采取恢复策略(如抢占资源或终止进程)解除死锁。

4. 忽略:有些系统选择忽略死锁的发生,因为死锁发生的概率较低,解除死锁所需的系统开销较大。

package Thread1;public class MyThread extends Thread {static   Object objA = new Object();static Object objB = new Object();@Overridepublic void run() {while (true) {if ("线程A".equals(getName())) {synchronized (objA) {System.out.println("线程A拿到了A锁,准备拿锁");//A锁要拿到B锁才能继续synchronized (objB) {System.out.println("线程A拿到了B锁,顺利执行完一轮");}}}else if ("线程B".equals(getName())) {if ("线程B".equals(getName())) {synchronized (objB) {System.out.println("线程B拿到了B锁,准备拿A锁");//B锁要拿到A锁才能继续synchronized (objA) {System.out.println("线程B拿到了A锁,顺利执行完一轮");}}}}}}
}

这就是死锁, A锁要拿到B锁才能继续,B锁要拿到A锁才能继续

等待唤醒机制

等待唤醒机制(Wait/Notify Mechanism)是指线程之间的一种协作机制,用于解决多线程并发执行中的同步与通信问题。

在等待唤醒机制中,一个线程可以调用wait()方法进入等待状态,同时释放对象锁;而另一个线程则可以调用notify()或者notifyAll()方法来唤醒处于等待状态的线程,并使其进入就绪状态,以便于执行。

等待唤醒机制本质上是基于对象的监视器(Monitor)实现的。每个对象都有一个与之关联的监视器,用于控制对该对象的访问。当一个线程调用了该对象的wait()方法后,该线程就会释放该对象的监视器,并进入等待队列,直到被其他线程调用notify()或者notifyAll()方法来唤醒。

等待唤醒机制常用于生产者-消费者模型、读写锁模型等场景。通过等待唤醒机制,线程之间可以协调合作,实现数据的安全共享与交换。

void wait()        当前线程等待,直到被其他线程唤醒
void notify()        随机唤醒单个线程
void notifyAll()        唤醒所有线程 

写个例子

Desk类:

package waitandnotify;public class Desk {//是否有面条 0:没有  1:有public static int foodFlag=0;//总个数public static int count=10;//锁对象public static Object lock=new Object();}

Foodie类

package waitandnotify;public class Foodie extends Thread{@Overridepublic void run() {///循环while(true){// 同步代码块synchronized (Desk.lock){//   判断共享数据是否到了末尾(到了末尾)if (Desk.count==0){break;}else{// 判断共享数据是否到了末尾(没有到末尾,执行核心逻辑)//先判断桌子上是否有面条if (Desk.foodFlag==0){//如果没有就等待try {Desk.lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}else {//把吃的总数-1Desk.count--;//如果有就开吃System.out.println("正在吃面条,还能吃"+Desk.count+"碗");//吃完之后唤醒厨师继续做Desk.lock.notifyAll();//修改桌子的状态Desk.foodFlag=0;}}}}}
}

Cook类

package waitandnotify;public class Cook extends Thread{@Overridepublic void run() {while (true){synchronized (Desk.lock){if(Desk.count==0){break;}else {//判断桌子上是否有食物if(Desk.foodFlag==1) {// 如果有,就等待try {Desk.lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}else {// 如果没有,就制作食物System.out.println("厨师做了一碗面条");// 修改桌子上的食物状态Desk.foodFlag=1;//叫醒消费者开吃Desk.lock.notifyAll();}}}}}
}

测试类

package waitandnotify;public class ThreadDemo {public static void main(String[] args) {Cook c=new Cook();Foodie f=new Foodie();c.setName("厨师");f.setName("吃货");c.start();f.start();}
}

阻塞队列 

阻塞队列(Blocking Queue)是一种特殊的队列,其在插入和删除元素时,当队列已满或为空时会阻塞等待。阻塞队列常用于多线程编程中,用于实现线程间的安全通信。

阻塞队列的主要特点是:当队列为空时,从队列中取出元素的操作将会被阻塞,直到队列中有新的元素被插入;当队列已满时,将元素插入队列的操作将会被阻塞,直到队列中有空位。

阻塞队列有多种实现方式,常见的有:
1. ArrayBlockingQueue:基于数组实现的有界阻塞队列,按照 FIFO(先进先出)的顺序对元素进行存取。
2. LinkedBlockingQueue:基于链表实现的可选有界或无界阻塞队列,按照 FIFO 的顺序对元素进行存取。
3. SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等待一个相应的删除操作,反之亦然。
4. PriorityBlockingQueue:基于优先级堆实现的阻塞队列,元素按照优先级进行存取。

阻塞队列的使用可以简化多线程编程中的线程同步操作,保证线程安全,并提供了一种有效的方式来实现生产者-消费者模型。

 线程的状态

线程池

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

public static ExecutorService newCachedThreadPool()创建一个没有上限的线程池

MyRunnable类:

package threadpool;public class MyRunnable implements Runnable {@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName()+"-----"+i);}}
}

测试:

package threadpool;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class ThreadPoolDemo {public static void main(String[] args) {//获取线程池对象ExecutorService pool1 = Executors.newCachedThreadPool();//提交任务pool1.submit(new MyRunnable());pool1.submit(new MyRunnable());pool1.submit(new MyRunnable());pool1.submit(new MyRunnable());pool1.submit(new MyRunnable());pool1.submit(new MyRunnable());}
}


可以看到有6个线程

public static ExecutorService newFixedThreadPool (int nThreads)创建有上限的线程池

测试

package threadpool;import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class ThreadPoolDemo2 {public static void main(String[] args) {//获取线程池对象ExecutorService pool1 = Executors.newFixedThreadPool(3);//提交任务pool1.submit(new MyRunnable());pool1.submit(new MyRunnable());pool1.submit(new MyRunnable());pool1.submit(new MyRunnable());pool1.submit(new MyRunnable());pool1.submit(new MyRunnable());}
}

只有三个线程

线程池主要核心原理

  1. 创建一个池子,池子中是空的
  2. 提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可
  3. 但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待

自定义线程池

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(核心线程数量,最大线程数量,空闲线程最大存活时间,时间的单位,任务队列,创建线程工厂,任务的拒绝策略);

参数一:核心线程数量        不能小于θ        
参数二:最大线程数        不能小于等于0,最大数量 >= 核心线程数量
参数三:空闲线程最大存活时间                不能小于θ
参数四:时间单位        用TimeUnit指定
参数五:任务队列                不能为null
参数六:创建线程工厂        不能为null
参数七:任务的拒绝策略        不能为null

package threadpool;import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class ThreadPoolDemo3 {public static void main(String[] args) {ThreadPoolExecutor pool=new ThreadPoolExecutor(3,//核心线程数量,能小于06,//最大线程数,不能小于0,最大数量>=核心线程数量60,//空闲线程最大存活时间TimeUnit.SECONDS,//时间单位秒new ArrayBlockingQueue<>(3),//任务队列Executors.defaultThreadFactory(),//创建线程工厂new ThreadPoolExecutor.AbortPolicy()//任务的拒绝策略);}
}

不断的提交任务,会有以下三个临界点:

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

任务拒绝策略

ThreadPoolExecutor.AbortPolicy        丢弃任务并抛出ReiectedExecutionException异常 
ThreadPoolExecutor.DiscardPolicy        丢弃任务,但是不抛出异常 这是不推荐的做法
ThreadPoolExecutor.DiscardoldestPolicy  抛弃队列中等待最久的任务 然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy        调用任务的run()方法绕过线程池直接执行 

最大并行数

多线程的最大并行数理想情况下应为CPU核心数的两倍,即2N,在IO密集型运算中;而在CPU密集型运算中,则建议设置为CPU核心数加一,即N+1

多线程技术是现代计算机编程中不可或缺的一部分,尤其是在处理高并发、高性能需求时,合理设置多线程的最大并行数能显著提高程序的效率和稳定性。这涉及到两种主要的任务类型:IO密集型和CPU密集型。对于IO密集型任务,由于大量的时间花在等待IO操作(如网络请求或数据库查询)上,因此可以设置更多的线程以保持CPU的高效利用。相反,CPU密集型任务则主要集中在处理器运算上,过多的线程反而可能导致频繁的上下文切换,降低效率。


这一期就到这里啦

努力遇见更好的自己!!!

这篇关于多线程(Lock锁,死锁,等待唤醒机制,阻塞队列,线程池)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1079728

相关文章

Redis客户端连接机制的实现方案

《Redis客户端连接机制的实现方案》本文主要介绍了Redis客户端连接机制的实现方案,包括事件驱动模型、非阻塞I/O处理、连接池应用及配置优化,具有一定的参考价值,感兴趣的可以了解一下... 目录1. Redis连接模型概述2. 连接建立过程详解2.1 连php接初始化流程2.2 关键配置参数3. 最大连

Spring Security 单点登录与自动登录机制的实现原理

《SpringSecurity单点登录与自动登录机制的实现原理》本文探讨SpringSecurity实现单点登录(SSO)与自动登录机制,涵盖JWT跨系统认证、RememberMe持久化Token... 目录一、核心概念解析1.1 单点登录(SSO)1.2 自动登录(Remember Me)二、代码分析三、

C#中lock关键字的使用小结

《C#中lock关键字的使用小结》在C#中,lock关键字用于确保当一个线程位于给定实例的代码块中时,其他线程无法访问同一实例的该代码块,下面就来介绍一下lock关键字的使用... 目录使用方式工作原理注意事项示例代码为什么不能lock值类型在C#中,lock关键字用于确保当一个线程位于给定实例的代码块中时

RabbitMQ消费端单线程与多线程案例讲解

《RabbitMQ消费端单线程与多线程案例讲解》文章解析RabbitMQ消费端单线程与多线程处理机制,说明concurrency控制消费者数量,max-concurrency控制最大线程数,prefe... 目录 一、基础概念详细解释:举个例子:✅ 单消费者 + 单线程消费❌ 单消费者 + 多线程消费❌ 多

Go语言并发之通知退出机制的实现

《Go语言并发之通知退出机制的实现》本文主要介绍了Go语言并发之通知退出机制的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录1、通知退出机制1.1 进程/main函数退出1.2 通过channel退出1.3 通过cont

Spring Boot 中的默认异常处理机制及执行流程

《SpringBoot中的默认异常处理机制及执行流程》SpringBoot内置BasicErrorController,自动处理异常并生成HTML/JSON响应,支持自定义错误路径、配置及扩展,如... 目录Spring Boot 异常处理机制详解默认错误页面功能自动异常转换机制错误属性配置选项默认错误处理

Java中的xxl-job调度器线程池工作机制

《Java中的xxl-job调度器线程池工作机制》xxl-job通过快慢线程池分离短时与长时任务,动态降级超时任务至慢池,结合异步触发和资源隔离机制,提升高频调度的性能与稳定性,支撑高并发场景下的可靠... 目录⚙️ 一、调度器线程池的核心设计 二、线程池的工作流程 三、线程池配置参数与优化 四、总结:线程

WinForm跨线程访问UI及UI卡死的解决方案

《WinForm跨线程访问UI及UI卡死的解决方案》在WinForm开发过程中,跨线程访问UI控件和界面卡死是常见的技术难题,由于Windows窗体应用程序的UI控件默认只能在主线程(UI线程)上操作... 目录前言正文案例1:直接线程操作(无UI访问)案例2:BeginInvoke访问UI(错误用法)案例

Linux线程之线程的创建、属性、回收、退出、取消方式

《Linux线程之线程的创建、属性、回收、退出、取消方式》文章总结了线程管理核心知识:线程号唯一、创建方式、属性设置(如分离状态与栈大小)、回收机制(join/detach)、退出方法(返回/pthr... 目录1. 线程号2. 线程的创建3. 线程属性4. 线程的回收5. 线程的退出6. 线程的取消7.

Linux下进程的CPU配置与线程绑定过程

《Linux下进程的CPU配置与线程绑定过程》本文介绍Linux系统中基于进程和线程的CPU配置方法,通过taskset命令和pthread库调整亲和力,将进程/线程绑定到特定CPU核心以优化资源分配... 目录1 基于进程的CPU配置1.1 对CPU亲和力的配置1.2 绑定进程到指定CPU核上运行2 基于