JAVA开发学习-day10
1. Lock锁对象
Lock接口时Java提供的一种线程同步机制,它允许线程以排他性的方式访问共享资源。与synchronized关键字不同,Lock接口提供了更灵活的锁定和解锁操作,以及更多的控制选项。
Lock接口主要的实现类时ReentrantLock,它是一种可重入锁,意味着同一个线程可以多次获取同一把锁,而不会发生死锁。除了ReentrantLock,JAVA还提供了其他类型的锁,如ReentrantReadWriteLock等。
创建Lock对象
Lock lock = new ReentrantLock();
在需要进行同步的代码块中可以通过lock方法来上锁,执行完同步的代码块后调用unlock方法来解锁。也可以使用tryLock方法,返回值为布尔值,如果返回值为true则代表上锁成功,返回值为false则表示上锁失败。
public class EasyThreadB {
Lock lock = new ReentrantLock();
//锁对象 Lock
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(Thread.currentThread().getName() + "未成功加锁,去执行别的代码");
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();
}
}
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() + "读锁---加锁成功");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "方法结束");
lock.unlock();
}
public static void methodWrite(){
System.out.println(Thread.currentThread().getName() + "进入写方法");
Lock lock = rrwl.writeLock();
lock.lock();
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();
}
public static void main(String[] args) {
Runnable run = EasyThreadC::method;
Runnable runWrite = EasyThreadC::methodWrite;
Thread a = new Thread(runWrite);
Thread b = new Thread(runWrite);
Thread c = new Thread(runWrite);
a.start();
b.start();
c.start();
Thread d = new Thread(run);
Thread f = new Thread(run);
Thread g = new Thread(run);
d.start();
f.start();
g.start();
System.out.println("main线程结束");
}
}
在读取数据的方法中使用读锁,修改数据的方法中使用写锁,可以减少被阻塞的线程,提高程序的效率。
如下面的例子所示
public class EasyThreadE {
public static void main(String[] args) {
EasyList list = new EasyList();
Runnable runSize = ()->{
list.size();
};
Runnable runGet = ()->{
list.get(0);
};
Runnable runAdd = ()->{
list.add(12);
};
list.add(1);
Thread a = new Thread(runSize);
Thread b = new Thread(runGet);
Thread c = new Thread(runAdd);
a.start();
b.start();
c.start();
}
}
class EasyList{
private int[] value = new int[20];
private int size = 0;
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public int size(){
Lock readLock = rwLock.readLock();
readLock.lock();
System.out.println(Thread.currentThread().getName() + "进入size方法");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "结束size方法");
readLock.unlock();
return size;
}
public int get(int index){
Lock readLock = rwLock.readLock();
readLock.lock();
System.out.println(Thread.currentThread().getName() + "进入get方法");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if(index < 0 && index >= size){
throw new IndexOutOfBoundsException("index is" + index);
}
System.out.println(Thread.currentThread().getName() + "结束get方法");
readLock.unlock();
return value[index];
}
public boolean add(int item){
Lock writeLock = rwLock.writeLock();
writeLock.lock();
System.out.println(Thread.currentThread().getName() + "进入add方法");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if(size >= value.length){
return false;
}
value[size++] = item;
System.out.println(Thread.currentThread().getName() + "结束add方法");
writeLock.unlock();
return true;
}
}
1.1 线程通讯(wait, notify, notifyAll)
由于线程之间是抢占式执行的,不能确定线程的执行顺序,在实际开发过程中我们希望控制线程的执行顺序,从而实现线程的通讯,这就用到了wait,notify,notifyAll方法。
- wait: 使线程进入等待状态,并进入锁对象的等待池
- notify: 随机唤醒等待池中等待的一个线程,唤醒后的线程会继续运行wait后的代码
- notifyAll: 唤醒等待池中的所有线程
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 {
Thread.sleep(1000);
try {
System.out.println(Thread.currentThread().getName() + "线程进入等待状态");
OBJ.wait(); //使执行到该行代码的线程进入等待状态 (等待池)
System.out.println(Thread.currentThread().getName() + "线程被唤醒,重新运行");
} catch (InterruptedException e) {
e.printStackTrace();
}
} 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);
Thread b = new Thread(run);
Thread c = new Thread(run);
Thread d = new Thread(run);
Thread e = new Thread(run);
a.start();
b.start();
c.start();
d.start();
e.start();
}
}
wait,notify,notifiyAll方法为什么要用在synchronized代码块中?
wait,notify,notifiyAll方法需要一个线程在具有某个对象的监视锁时才能调用,这个监视锁通常是通过synchronized关键字获得的,如果不在synchronized代码块中调用wait,notify,notifiyAll方法会报异常。wait方法会使线程休眠并释放对象锁,等待其他线程
调用notify或notifyAll唤醒;如果wait,notify,notifiyAll方法不在synchronized代码块中等待,唤醒的语义就变得模糊,其他线程肯无法正常的释放或获取锁。
1.2 死锁
死锁描述的是这样一种情况:多个进程/线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释
放。由于进程/线程被无限期地阻塞,因此程序不可能正常终止。
死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程
就会互相等待而进入死锁状态。
产生死锁必须具备以下四个条件:
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才
释放资源。 - 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
注意,只有四个条件同时成立时,死锁才会出现。
2. 线程池
池就是为了资源的重复利用,减少内存占用,提高性能。我们可以使用线程池来实现线程的创建,管理和销毁工作。
线程池有以下三个优点:
-
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
-
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
-
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池的真正实现类是ThreadPoolExecutor,其构造方法的参数有7个:
- corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
- maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
- keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
- unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
- workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
- threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
- handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。
public ThreadPoolExecutor(int corePoolSize,
//核心线程数(必须)
int maximumPoolSize,
//最大线程数(必须)
long keepAliveTime,
//存活时间(必须)
TimeUnit unit,
//存活时间的时间单位(必须)
BlockingQueue<Runnable> workQueue,
//工作队列(必须)
ThreadFactory threadFactory,
//创建线程的工厂(非必须)
RejectedExecutionHandler handler)
//工作队列满后和达到最大线程数后的回绝策略(非必须)
通过调用ThreadPoolExecutor对象的executor方法来调用线程执行任务
public class EasyExecutors {
//线程池是为了完成线程创建和管理,销毁工作
public static void main(String[] args) throws ExecutionException, InterruptedException {
BlockingQueue queue = new ArrayBlockingQueue(12);
ThreadPoolExecutor tpe = new ThreadPoolExecutor(
5,
10,
10,
TimeUnit.SECONDS,
queue,
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
//线程任务 Runnable
Runnable run = EasyExecutors::method;
tpe.execute(run);
//线程池对象需要关闭
tpe.shutdown();
}
public static void method(){
System.out.println(Thread.currentThread().getName() + "执行代码");
}
}
除了实现Runnable接口中的run方法定义线程要执行的任务,还可以实现Callable接口的call方法来定义线程要执行的任务。
比起Runnable,Callable中的方法可以有返回值,可以抛出异常,也支持泛型返回值
运行Callable任务会返回一个Future对象,通过Future对象可以了解任务执行情况
public interface Future<V> {
//cancel:取消任务的执行
boolean cancel(boolean mayInterruptIfRunning);
//isCancelled:如果此任务在正常完成之前被取消,则返回true。
boolean isCancelled();
//isDone:如果此任务完成,则返回true。
boolean isDone();
//get:等待任务完成,然后返回其结果。是一个阻塞方法
V get() throws InterruptedException, ExecutionException;
//get(long timeout, TimeUnit unit):等待任务完成,然后返回其结果。
//如果在指定时间内,还没获取到结果,就直接返回null。
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
/*cancel:用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。
*参数mayInterruptIfRunning表示是否取消正在执行却没有执行完毕的任务,如果设置true,
*则表示可以取消正在执行过程中的任务。
*如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,
*若mayInterruptIfRunning设置为false,不会取消任务,返回false;如果任务还没有执行则无论*mayInterruptIfRunning为true还是false,肯定返回true。
*如果任务已经完成则无论mayInterruptIfRunning为true还是false,一定会返回false;*/
线程池运行Callable对象需要使用submit方法,submit也可以执行Runnable对象设定的线程任务
public class EasyExecutors {
//线程池是为了完成线程创建和管理,销毁工作
public static void main(String[] args) throws ExecutionException, InterruptedException {
BlockingQueue queue = new ArrayBlockingQueue(12);
ThreadPoolExecutor tpe = new ThreadPoolExecutor(
5,
10,
10,
TimeUnit.SECONDS,
queue,
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);
//f.cancel(true);
System.out.println(f.get()); //会等待线程执行完毕
//tpe.submit(run);
//线程池对象需要关闭
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(1000);
return "callResult";
}
}
2.1 线程池的工作原理
线程池的工作原理
- <1>池中是否有空闲的线程,如果有让该线程执行该任务
- <2>如果没有空闲的线程,判断池中的线程数量有没有达到核心线程数
- <3>没有达到核心线程数,创建新的线程,执行新的任务。如果已经达到,就将任务放入工作队列
- <4>如果工作队列已经填满,就判断是否达到最大线程数,如果没有就创建新的线程
- <5>如果已经达到最大线程数,工作队列也已经填满,没有空闲的线程,就执行回绝策略
线程池中的线程达到或超过核心线程数,超出的数量会根据存活时间进行销毁,直到线程数量为核心线程数
线程数小于等于核心线程数时,线程不会消亡
2.2 线程池的回绝策略(handler)
ThreadPoolExecutor类为我们提供了四种回绝策略,分别为AbortPolicy,CallerRunsPolicy,DiscardOldestPolicy,DiscardPolicy。
(默认策略)AbortPolicy 放弃该任务,并抛出一个异常RejectedExecutionException
CallerRunsPolicy 调用者执行,让传递任务的线程执行超出的任务
DiscardOldestPolicy 放弃队列中时间最长的任务,不会抛出异常
DiscardPolicy 直接放弃任务,不会抛出异常
2.3 功能线程池
Executors封装好了 4 种常见的功能线程池,如下:
- 定长线程池(FixedThreadPool)
- 定时线程池(ScheduledThreadPool)
- 可缓存线程池(CachedThreadPool)
- 单线程化线程池(SingleThreadExecutor)
定长线程池(FixedThreadPool)
定长线程池只有核心线程,线程数量固定,任务执行完成后立即回收线程,可以用于控制线程的最大并发数
// 1. 创建定长线程池对象 & 设置线程池线程数量固定为3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
fixedThreadPool.execute(task);
定时线程池(ScheduledThreadPool)
核心线程数量固定,非核心线程数量无限,任务执行完成后闲置10ms回收线程,适用于定时或周期性任务
// 1. 创建 定时线程池对象 & 设置线程池线程数量固定为5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务
可缓存线程池(CachedThreadPool)
核心线程数量为0,非核心线程数量无限,执行任务后闲置60s后回收,适用于数量大,耗时小的任务
// 1. 创建可缓存线程池对象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
cachedThreadPool.execute(task);
单线程化线程池(SingleThreadExecutor)
核心线程数量为1.非核心线程数量为0,执行任务后立即回收,可以保障任务的顺序执行,适用于数据库操作,文件操作等。
// 1. 创建单线程化线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
singleThreadExecutor.execute(task);
java枚举类学习:https://blog.csdn.net/weixin_36755535/article/details/130831969
线程池参考: https://blog.csdn.net/u013541140/article/details/95225769
Callable参考:https://blog.csdn.net/m0_61160520/article/details/131636644