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();
@Override
public 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();
@Override
public 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{
@Override
public 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 {
//把吃的总数-1
Desk.count--;
//如果有就开吃
System.out.println("正在吃面条,还能吃"+Desk.count+"碗");
//吃完之后唤醒厨师继续做
Desk.lock.notifyAll();
//修改桌子的状态
Desk.foodFlag=0;
}
}
}
}
}
}
Cook类
package waitandnotify;
public class Cook extends Thread{
@Override
public 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 {
@Override
public 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());
}
}
只有三个线程
线程池主要核心原理
- 创建一个池子,池子中是空的
- 提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可
- 但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待
自定义线程池
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,//核心线程数量,能小于0
6,//最大线程数,不能小于0,最大数量>=核心线程数量
60,//空闲线程最大存活时间
TimeUnit.SECONDS,//时间单位秒
new ArrayBlockingQueue<>(3),//任务队列
Executors.defaultThreadFactory(),//创建线程工厂
new ThreadPoolExecutor.AbortPolicy()//任务的拒绝策略
);
}
}
不断的提交任务,会有以下三个临界点:
- 当核心线程满时,再提交任务就会排队
- 当核心线程满,队伍满时,会创建临时线程
- 当核心线程满,队伍满,临时线程满时,会触发任务拒绝策略
任务拒绝策略
ThreadPoolExecutor.AbortPolicy 丢弃任务并抛出ReiectedExecutionException异常
ThreadPoolExecutor.DiscardPolicy 丢弃任务,但是不抛出异常 这是不推荐的做法
ThreadPoolExecutor.DiscardoldestPolicy 抛弃队列中等待最久的任务 然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy 调用任务的run()方法绕过线程池直接执行
最大并行数
多线程的最大并行数理想情况下应为CPU核心数的两倍,即2N,在IO密集型运算中;而在CPU密集型运算中,则建议设置为CPU核心数加一,即N+1 。
多线程技术是现代计算机编程中不可或缺的一部分,尤其是在处理高并发、高性能需求时,合理设置多线程的最大并行数能显著提高程序的效率和稳定性。这涉及到两种主要的任务类型:IO密集型和CPU密集型。对于IO密集型任务,由于大量的时间花在等待IO操作(如网络请求或数据库查询)上,因此可以设置更多的线程以保持CPU的高效利用。相反,CPU密集型任务则主要集中在处理器运算上,过多的线程反而可能导致频繁的上下文切换,降低效率。
这一期就到这里啦
努力遇见更好的自己!!!