锁
Lock lock = new ReentrantLock();创建锁对象
默认创建的是非公平锁,小括号内加true就是公平锁
lock.lock();//加锁
lock.tryLock();尝试加锁 成功返回true 失败返回false
加锁之后要用lock.unlock();方法解锁,否则程序可能会出现死锁状态。
一个线程获得了锁但没有释放,其他线程在试图获取相同的锁时可能会被阻塞。
public class EasyThreadB {
//锁对象 lock
Lock lock = new ReentrantLock();//创建锁对象
public void method() {
//lock.lock();//加锁
//lock.tryLock()尝试加锁 成功true 失败false
if (lock.tryLock()) {
System.out.println(Thread.currentThread().getName() + "进入方法");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "结束方法");
lock.unlock();//解锁
}else{
System.out.println("加锁未成功-----去执行别的代码");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
method();
}
}
public static void main(String[] args) {
Runnable run=new EasyThreadB()::method;
Thread a=new Thread(run);
Thread b=new Thread(run);
a.start();
b.start();
}
}
ReentrantLock和ReentrantReadWriteLock
ReentrantLock:是一种独占锁(也称为互斥锁),同一时刻只允许一个线程持有锁。这意味着当一个线程获得了 ReentrantLock 的锁之后,其他线程必须等待该线程释放锁才能获取锁。
ReentrantReadWriteLock不是锁,是读-写锁的容器。它将锁分为读锁和写锁两种。多个线程可以同时持有读锁,但是写锁是独占的。这意味着读锁之间不会互斥,但读锁与写锁之间是互斥的,即在写锁被持有期间,任何读锁的获取都会被阻塞。
读锁可以同时被多个线程持有,这意味着多个线程可以同时获得读锁而不互斥。这种情况下,多个线程可以并发地读取共享资源,而不会阻塞彼此。
写锁则是独占锁,同一时刻只能有一个线程持有写锁。当一个线程持有写锁时,其他线程无法同时持有写锁或者读锁,以确保写操作的原子性和一致性。
以下代码可以验证这一点,
public class EasyThreadC {
public static ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock();
public static ReentrantLock rl = new ReentrantLock();
public static void method() {
System.out.println(Thread.currentThread().getName() + "进入方法");
Lock lock = rrwl.readLock();
lock.lock();
System.out.println(Thread.currentThread().getName() + "加锁成功--读锁"+System.currentTimeMillis());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "方法结束"+System.currentTimeMillis());
lock.unlock();
}
public static void methodWrite() {
System.out.println(Thread.currentThread().getName() + "进入方法");
Lock lock = rrwl.writeLock();
lock.lock();
System.out.println(Thread.currentThread().getName() + "加锁成功--写锁"+System.currentTimeMillis());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "方法结束");
lock.unlock();
}
public static void main(String[] args) {
Runnable run = EasyThreadC::method;
Runnable runWrite = EasyThreadC::methodWrite;
Thread a = new Thread(run);
a.start();
....
此次省略部分进程创建代码,可以多复制几个上述代码
Thread f = new Thread(runWrite);
f.start();
...
此次省略部分进程创建代码,可以多复制几个上述代码
System.out.println("main线程结束----");
}
}
死锁
死锁(Deadlock)是指多个进程或线程在执行过程中,由于竞争资源而造成的一种互相等待的状态,导致它们都无法继续执行下去。在死锁状态下,每个进程或线程都在等待其他进程或线程释放它所需要的资源,同时又不释放自己已经占有的资源,使得所有相关的进程或线程都处于僵持状态,无法推进。
死锁的四个必要条件
互斥条件(Mutual Exclusion):至少有一个资源必须是不能共享的,即一次只能被一个进程或线程占用。
请求与保持条件(Hold and Wait):进程或线程至少已经占有一个资源,并且在请求其他资源时由于被其他进程或线程占有而等待。
不剥夺条件(No Preemption):资源只能由占有它的进程或线程释放,不能被系统强行剥夺。
循环等待条件(Circular Wait):存在一组进程或线程 {P1, P2, …, Pn},其中 P1 等待 P2 占有的资源,P2 等待 P3 占有的资源,依此类推,而 Pn 等待 P1 占有的资源,形成一个闭环。
当这四个条件同时满足时,就可能发生死锁。
如何避免死锁
破坏互斥条件:可以通过设计共享资源来避免互斥条件,或者使用可以共享的资源替代互斥资源。
破坏请求与保持条件:一次性获取所有需要的资源,而不是分批获取。
破坏不剥夺条件:允许系统剥夺某些资源。
破坏循环等待条件:对资源进行编号,并规定进程或线程只能按编号递增的顺序请求资源。
死锁是多线程和并发编程中常见的问题,解决死锁需要合理的资源分配策略和良好的设计思想,以确保系统在高并发和资源竞争的情况下能够稳定运行。
wait和sleep的区别
wait是Object中定义的方法,可以由锁对象调用,让执行到该代码的线程进入等待状态
sleep方法是Thread类中定义的静态方法,也可以让执行到该行代码的线程进入等待状态
区别:
1.sleep需要传入一个毫秒数,到达时间后会自动唤醒。wait不能自动唤醒,必须调用notify或notifyAll方法唤醒
2.sleep方法保持锁状态进入等待状态,wait方法解除锁状态,其他线程可以进入运行
进程被唤醒后变成就绪态
同时synchronized 关键字保证了同一时刻只有一个线程可以获取对象的锁,其他线程必须等待。
public class EasyThreadD {
public static final Object OBJ = new Object();
public static void method() {
System.out.println(Thread.currentThread().getName() + "进入方法");
synchronized (OBJ) {
OBJ.notify();//唤醒一条被该锁对象wait的线程
OBJ.notifyAll();//唤醒全部被锁对象wait的线程
System.out.println(Thread.currentThread().getName() + "进入同步代码块");
try {
try {
System.out.println(Thread.currentThread().getName() + "进入等待状态");
OBJ.wait();//让执行到该行代码的线程进入等待状态(等待池)
//wait状态被唤醒后进入就绪状态
System.out.println(Thread.currentThread().getName() + "重新运行");
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "结束同步代码块");
OBJ.notify();
}
}
public static void main(String[] args) {
Runnable run = EasyThreadD::method;
Thread a = new Thread(run);
a.start();
...
此次省略部分进程创建代码
}
}
线程池 池==重用
完成线程创建和管理,销毁工作
public class EasyExecutors {
public static void main(String[] args) throws ExecutionException, InterruptedException {
BlockingQueue qu=new ArrayBlockingQueue(12);
ThreadPoolExecutor tpe = new ThreadPoolExecutor(5,10,10,
TimeUnit.SECONDS,qu,Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
//线程任务:runnable,callable
Runnable run=EasyExecutors::method;
tpe.execute(run);
Callable<String> call=EasyExecutors::methodCall;
Future<String> f=tpe.submit(call);
//tpe.submit(run);
System.out.println(f.get());//会等待线程执行完毕
//关闭线程池对象
tpe.shutdown();
}
public static void method(){
System.out.println(Thread.currentThread().getName()+"执行代码");
}
public static String methodCall() throws InterruptedException {
System.out.println(Thread.currentThread().getName()+"执行代码call");
Thread.sleep(2000);
return "CallResult";
}
}
以上代码中使用了ThreadPoolExecutor池管理工具类
并使用了Runnable 和 Callable两种多线程任务执行方式
ThreadPoolExecutor
是 Java 中用于管理线程池的一个强大类,它提供了灵活的线程池管理功能,允许开发者在应用程序中高效地管理和复用线程。
线程池基本组成
ThreadPoolExecutor 类继承自 ExecutorService 接口,它通过一组线程来执行被提交的任务。线程池中的线程在任务执行完毕后不会销毁,而是被重用,这样可以减少线程创建和销毁的开销。
核心参数:
corePoolSize
线程池的核心线程数,即保持活动状态的线程数,即使线程处于空闲状态也不会被回收。
maximumPoolSize
线程池的最大线程数,包括核心线程数和额外创建的线程数。
keepAliveTime
当线程池中的线程数大于核心线程数时,多余的空闲线程的存活时间。这些线程会在空闲一定时间后被回收,直到线程池中的线程数不大于核心线程数。
TimeUnit unit
这个参数用于指定线程池中各种时间参数的时间单位,例如设置超时时间或者任务等待时间等。TimeUnit 是一个枚举类,包括 NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS 等选项,可以根据实际需要选择合适的时间单位。
BlockingQueue workQueue
这是一个阻塞队列,用于保存等待执行的任务。当线程池的线程数达到 corePoolSize 后,新提交的任务会被放入这个队列中等待执行。常用的实现类包括
ArrayBlockingQueue,
LinkedBlockingQueue,
PriorityBlockingQueue 等,每种实现有不同的特性和适用场景。
ThreadFactory threadFactory
ThreadFactory 用于创建新的线程。它是一个工厂接口,定义了如何创建线程的方法。通过自定义 ThreadFactory,可以指定线程的名称、优先级、是否为守护线程等。这样可以更好地管理线程,便于监控和调试。
RejectedExecutionHandler handler
当线程池无法执行新提交的任务时(通常是由于线程池已经关闭或者达到了最大线程数并且队列已满),RejectedExecutionHandler 定义了一种策略来处理这种情况。常用的策略包括:
ThreadPoolExecutor.AbortPolicy:直接抛出 RejectedExecutionException 异常。
ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)执行这个任务。
ThreadPoolExecutor.DiscardPolicy:直接丢弃这个任务,不做任何处理。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃最老的一个任务,尝试再次提交当前任务。
线程池的生命周期管理
ThreadPoolExecutor 提供了方法来启动、关闭和终止线程池。可以通过 shutdown() 和 shutdownNow() 方法来关闭线程池,保证已提交的任务都被执行完毕或者被中断。
线程池的工作原理
任务提交
客户端通过 ExecutorService 的 submit() 或 execute() 方法提交任务到线程池。
核心线程处理:
当任务被提交到线程池后,看池中是否有空闲的线程,如果有,让该线程执行任务
如果池中没有空闲的线程,判断池中的线程数量是否达到核心线程数
如果当前运行的线程数少于 corePoolSize(核心线程数),则创建新线程来处理任务。
任务队列管理:
如果当前线程数达到 corePoolSize,则新的任务会被放入 workQueue 中等待执行,直到队列满。不同的队列类型有不同的策略(如有界队列会拒绝新任务)。
创建新线程:
workQueue 队列(任务队列)已满之后再添加新的任务,判断当前线程数是否达到maximumPoolSize(最大线程数),如果没有,创建新的线程执行新的任务,直到填满最大线程数
最大线程数限制:
如果线程池中的线程数已经达到 maximumPoolSize,并且任务队列也满了,则根据定义的 handler 策略来处理新提交的任务(可能会抛出异常、运行在调用线程中、丢弃任务或者丢弃最旧的任务)。
空闲线程处理:
如果某个线程在指定的 keepAliveTime 内没有执行任务,并且当前线程数超过 corePoolSize,则将其终止,直到线程数不超过 corePoolSize。
任务执行:
线程池中的线程从 workQueue 中取出任务执行,直到线程池关闭或者任务执行出错。
关闭线程池:
当不再接受新的任务时,可以调用 shutdown() 或 shutdownNow() 方法来关闭线程池。
java中内置的线程池对象
Executors.newCachedThreadPool();
可以根据工作任务创建线程 ,如果没有空闲的线程就创建新的线程,线程存活时间60秒
Executors.newFixedThreadPool(10);
设定最大线程数量
Executors.newScheduledThreadPool(10);
提供定时运行的处理方案
Executors.newSingleThreadExecutor();
创建一个具有单个线程的线程池 保障任务队列完全按照顺序执行
Runnable 和 Callable
Runnable 接口
Runnable 接口是Java中定义多线程任务的基本方式。它是一个函数式接口,其中只有一个 run() 方法需要实现。通常通过实现 Runnable 接口来定义一个可以在多线程环境下执行的任务。
Callable 接口
Callable 接口与 Runnable 类似,但它允许任务返回一个结果,并且可以抛出一个检查异常。它是一个泛型接口,需要实现 call() 方法来执行任务,并且可以返回一个泛型类型的结果。
区别和适用场景
返回值:
Runnable 接口的 run() 方法没有返回值,通常用于执行没有返回结果的任务。
Callable 接口的 call() 方法可以返回一个结果,适用于需要获取任务执行结果的场景。
异常处理:
Runnable 接口的 run() 方法不能抛出已检查异常,只能捕获处理或者在方法内部处理异常。
Callable 接口的 call() 方法可以抛出检查异常,需要在调用时进行异常处理。
并发控制:
使用 Callable 结合 Future 接口可以更好地控制任务的执行状态和获取任务执行结果。
package com.easy725;
import java.util.concurrent.*;
public class EasyExcutorsA {
public static void main(String[] args) {
BlockingQueue queue=new ArrayBlockingQueue(12);
ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(5,8,10,
TimeUnit.SECONDS, queue,Executors.defaultThreadFactory(),new ThreadPoolExecutor.DiscardPolicy());
Runnable run=()->{
System.out.println(Thread.currentThread().getName()+"执行代码call");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"执行完毕");
};
for (int i = 0; i < 21; i++) {
threadPoolExecutor.execute(run);
}
threadPoolExecutor.shutdown();
}
}