Lock 接口
1. ReentrantLock
可重入锁:什么是 “可重入”,可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。
public class TestLock2 { public static void main(String[] args) { Ticket ticket = new Ticket(); new Thread(()-> { for (int i=0;i<20;i++) { ticket.sale(); } },"A").start(); new Thread(()-> { for (int i = 0; i < 20; i++) { ticket.sale(); } },"B").start(); new Thread(()-> { for (int i = 0; i < 20; i++) { ticket.sale(); } },"C").start(); } } class Ticket{ private Integer number=30; private final ReentrantLock lock = new ReentrantLock(); public void sale() { lock.lock(); try { if (number>0) { System.out.println(Thread.currentThread().getName() + "卖出了第" + number + "张票,余票" + --number + "张"); } else { System.out.println("票已卖完"); } }finally { lock.unlock(); } } }
Lock 与 synchronized 的不同:
-
Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现;
-
synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock( ) 去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;
-
Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能响应中断;
-
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到;
-
Lock 可以提高多个线程进行读操作的效率。
虚假唤醒
多线程环境的编程中,我们经常遇到让多个线程等待在一个条件上,等到这个条件成立的时候我们再去唤醒这些线程,让它们接着往下执行代码的场景。假如某一时刻条件成立,所有的线程都被唤醒了,然后去竞争锁,因为同一时刻只会有一个线程能拿到锁,其他的线程都会阻塞到锁上无法往下执行,等到成功争抢到锁的线程消费完条件,释放了锁,后面的线程继续运行,拿到锁时这个条件很可能已经不满足了,这个时候线程应该继续在这个条件上阻塞下去,而不应该继续执行,如果继续执行了,就说发生了虚假唤醒。
if(条件) { this.wait() } //if中的条件不满足,于是线程进入wait等待状态,但其他线程执行notifyAll()使此线程被唤醒,由于wait()方法“在哪里等待就在哪里继续”,于是跳过了if判断,即使条件并不满足,也继续执行
条件判断需要放到while循环中,防止虚假唤醒
Lock 方法线程间通信
condition.await( ) 和 condition.signalAll( ) 方法
public class TestLock3 { public static void main(String[] args) { Share share = new Share(); new Thread(()-> { for (int i=0;i<10;i++) { share.incr(); } },"A").start(); new Thread(()-> { for (int i=0;i<10;i++) { share.incr(); } },"B").start(); new Thread(()-> { for (int i=0;i<10;i++) { share.decr(); } },"C").start(); new Thread(()-> { for (int i=0;i<10;i++) { share.decr(); } },"D").start(); } } class Share{ private Integer number = 0; private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void incr() { lock.lock(); try { while (number!=0) { condition.await(); } number++; System.out.println(Thread.currentThread().getName()+"::"+number); condition.signalAll(); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } } public void decr() { lock.lock(); try { while (number!=1) { condition.await(); } number--; System.out.println(Thread.currentThread().getName()+"::"+number); condition.signalAll(); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { lock.unlock(); } } }
线程间定制化通信
使用 condition.signal( ) 唤醒指定线程
class Share{ private Integer flag = 1; private Lock lock = new ReentrantLock(); private Condition a1 = lock.newCondition(); private Condition b1 = lock.newCondition(); private Condition c1 = lock.newCondition(); public void print5(Integer loop) throws InterruptedException { lock.lock(); try { while (flag != 1) { a1.await(); } for (int i=1;i<=5;i++) { System.out.println(Thread.currentThread().getName()+"打印了第"+i+"次::第"+loop+"轮"); } flag = 2; b1.signal(); } finally { lock.unlock(); } } public void print10(Integer loop) throws InterruptedException { lock.lock(); try { while (flag != 2) { b1.await(); } for (int i=1;i<=10;i++) { System.out.println(Thread.currentThread().getName()+"打印了第"+i+"次::第"+loop+"轮"); } flag = 3; c1.signal(); } finally { lock.unlock(); } } public void print15(Integer loop) throws InterruptedException { lock.lock(); try { while (flag != 3) { c1.await(); } for (int i=1;i<=15;i++) { System.out.println(Thread.currentThread().getName()+"打印了第"+i+"次::第"+loop+"轮"); } flag = 1; a1.signal(); } finally { lock.unlock(); } } } public class TestLock2 { public static void main(String[] args) { Share share = new Share(); new Thread(()->{ for (int i=1;i<=2;i++) { try { share.print5(i); } catch (InterruptedException e) { e.printStackTrace(); } } },"AA").start(); new Thread(()->{ for (int i=1;i<=2;i++) { try { share.print10(i); } catch (InterruptedException e) { e.printStackTrace(); } } },"BB").start(); new Thread(()->{ for (int i=1;i<=2;i++) { try { share.print15(i); } catch (InterruptedException e) { e.printStackTrace(); } } },"CC").start(); } }
2. 集合的线程安全
并发修改异常问题
public class ThreadDemo1 { public static void main(String[] args) { List<String> list = new ArrayList<>(); for (int i=0;i<30;i++) { new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,8)); System.out.println(list); }).start(); } } }
异常报错:java.util.ConcurrentModificationException
Vector 类
List<String> list = new Vector<>();
add( ) 方法添加了 synchronized 关键字
Collections 工具类
List<String> list = Collections.synchronizedList(new ArrayList<>());
CopyOnWriteArrayList 类
写时复制技术
List<String> list = new CopyOnWriteArrayList();
CopyOnWriteArraySet 类
解决 HashSet 线程不安全问题
Set<String> strings = new CopyOnWriteArraySet<>();
ConcurrentHashMap 类
解决 HashMap 线程不安全问题
Map<String, Object> hashMap = new ConcurrentHashMap<>();
3. 多线程锁
synchronized 实现同步的基础:Java中的每一个对象都可以作为锁 具体表现形式:
-
对于普通同步方法,锁的是当前实例对象;
-
对于静态方法,锁的是当前类的 Class 对象;
-
对于同步方法块,锁的是 synchronized 括号里配置的同步监视器对象;
公平锁和非公平锁
ReentrantLock 的实现是基于其内部类 FairSync(公平锁) 和 NonFairSync(非公平锁) 实现的。
-
公平锁:
-
公平和非公平锁的队列都基于锁内部维护的一个双向链表,表结点Node的值就是每一个请求当前锁的线程。公平锁则在于每次都是依次从队首取值。
-
锁的实现方式是基于如下几点:
-
表结点Node和状态state的volatile关键字。
-
sum.misc.Unsafe.compareAndSet的原子操作(见附录)。
-
-
非公平锁:
-
在等待锁的过程中, 如果有任意新的线程妄图获取锁,都是有很大的几率直接获取到锁的。
可重入锁
可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。 在 JAVA环境下 ReentrantLock 和synchronized 都是可重入锁。可重入锁最大的作用是避免死锁。
4. 死锁
死锁的概念
在许多应用中进程需要以独占的方式访问资源,当操作系统允许多个进程并发执行时可能会出现进程永远被阻塞现象,如两个进程分别等待对方所占的资源,于是两者都不能执行而处于永远等待状态,此现象称为死锁。
死锁产生的条件
-
互斥条件 临界资源是独占资源,进程应互斥且排他的使用这些资源。
-
占有和等待条件 进程在请求资源得不到满足而等待时,不释放已占有资源。
-
不剥夺条件 又称不可抢占,已获资源只能由进程自愿释放,不允许被其他进程剥夺。
-
循环等待条件 又称环路条件,存在循环等待链,其中,每个进程都在等待链中等待下一个进程所持有的资源,造成这组进程处于永远等待状态。
死锁只有在这四个条件同时满足时出现。
5. Callable 接口
public class CallableDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { FutureTask<Integer> futureTask1 = new FutureTask<>(new CallableTest()); FutureTask<Integer> futureTask2 = new FutureTask<>(()->{return 1024;}); new Thread(futureTask1,"AA").start(); new Thread(futureTask2,"BB").start(); System.out.println(futureTask1.get()); System.out.println(futureTask2.get()); } } class CallableTest implements Callable{ @Override public Integer call() throws Exception { return 200; } }
Callable 接口和 Runnable 接口
-
实现 Callable 接口方式有返回值,Runnable 接口没有;
-
实现 Callable 接口方式中的 call( ) 方法时若产生计算错误则会抛出异常,但 Runnable 接口不会;
-
Callable 接口实现方法名称是 call( ),Runnable 接口是 run( );
6. JUC 辅助类
CountDownLatch 减少计数
public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(10); for (int i=0;i<10;i++) { new Thread(()->{ System.out.println(Thread.currentThread().getName()+"离开了"); countDownLatch.countDown(); }).start(); } countDownLatch.await(); System.out.println("锁门了"); } }
CyclicBarrier 循环栅栏
CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集。当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将被重置以便下次使用。
public class CyclicBarrierDemo { private static final Integer NUMBER=7; public static void main(String[] args) { //创建CyclicBarrier对象 CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER,()->{ System.out.println("集齐7颗龙珠召唤神龙!"); }); //集齐7颗龙珠过程 for (int i=1;i<=7;i++) { new Thread(()->{ try { System.out.println(Thread.currentThread().getName()+"星龙珠"); cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } },String.valueOf(i)).start(); } } }
Semaphore 信号灯
-
Semaphore 是什么 Semaphore 通常我们叫它信号量, 可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。
可以把它简单的理解成我们停车场入口立着的那个显示屏,每有一辆车进入停车场显示屏就会显示剩余车位减1,每有一辆车从停车场出去,显示屏上显示的剩余车辆就会加1,当显示屏上的剩余车位为0时,停车场入口的栏杆就不会再打开,车辆就无法进入停车场了,直到有一辆车从停车场出去为止。
-
Semaphore 使用场景 通常用于那些资源有明确访问数量限制的场景,常用于限流 。
-
比如:数据库连接池,同时进行连接的线程有数量限制,连接不能超过一定的数量,当连接达到了限制数量后,后面的线程只能排队等前面的线程释放了数据库连接才能获得数据库连接。
-
比如:停车场场景,车位数量有限,同时只能容纳多少台车,车位满了之后只有等里面的车离开停车场外面的车才可以进入。
public class SemaphoreDemo { //6辆汽车,停3个车位 public static void main(String[] args) { //创建Semaphore对象,设置许可数量 Semaphore semaphore = new Semaphore(3); //模拟6辆汽车 for (int i=1;i<=6;i++) { new Thread(()->{ try { //抢占 semaphore.acquire(); System.out.println(Thread.currentThread().getName()+"抢占到了车位"); //随机等待1~5秒 TimeUnit.SECONDS.sleep(new Random().nextInt(5)); System.out.println(Thread.currentThread().getName()+"离开了车位"); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); } },String.valueOf(i)).start(); } } }
7. ThreadPool 线程池
线程池介绍
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够充分保证内核的充分利用,还能够防止过分调度。 Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor 这几个类
线程池使用方式
-
一池N线程:Executors.newFixedThreadPool(int)
-
一个任务一个任务的执行,一池一线程:Executors.newSingleThreadExecutor();
-
线程池根据需求创建线程,可扩容:Executors.newCachedThreadPool();
ThreadPoolExecutor 参数
1. int corePoolSize //核心线程数量 2. int maximumPoolSize //最大线程数量 3. long keepAliveTime //线程存活时间 4. TimeUnit unit //存活时间单位 5. BlockingQueue<Runnable> workQueue //阻塞队列 6. ThreadFactory threadFactory //线程工厂 7. RejectedExecutionHandler handler //拒绝策略
如何配置线程池
-
CPU密集型任务 尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换
-
IO密集型任务 可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间
-
混合型任务 可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效 因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。 因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失
拒绝策略
rejectedExectutionHandler参数字段用于配置绝策略,常用拒绝策略如下:
-
AbortPolicy:用于被拒绝任务的处理程序,它将抛出RejectedExecutionException
-
CallerRunsPolicy:用于被拒绝任务的处理程序,它直接在execute方法的调用线程中运行被拒绝的任务。
-
DiscardOldestPolicy:用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试execute。
-
DiscardPolicy:用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。
8. 自定义线程池
new ThreadPoolExecutor() 创建
为什么不允许使用 Executors. 的方式创建线程池? 阿里巴巴 Java 开发手册中明确提到: 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors 返回的线程池对象的弊端如下: 1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM(java.lang.OutOfMemoryError)。 2)CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
new ThreadPoolTaskExecutor() 创建
Spring更加推荐我们开发者使用ThreadPoolTaskExecutor类来创建线程池,其本质是对java.util.concurrent.ThreadPoolExecutor的包装。 这个类则是spring包下的,是Spring为我们开发者提供的线程池类,这里重点讲解这个类的用法。 Spring提供了xml给我们配置ThreadPoolTaskExecutor线程池,但是现在普遍都在用SpringBoot开发项目,所以直接上yaml或者properties配置即可,或者也可以使用@Configuration配置也行。
ThreadPoolTaskExecutor配置:
-
application.properties
# 核心线程池数 spring.task.execution.pool.core-size=5 # 最大线程池数 spring.task.execution.pool.max-size=10 # 任务队列的容量 spring.task.execution.pool.queue-capacity=5 # 非核心线程的存活时间 spring.task.execution.pool.keep-alive=60 # 线程池的前缀名称 spring.task.execution.thread-name-prefix=god-jiang-task-
-
AsyncScheduledTaskConfig.java
@Configuration public class AsyncScheduledTaskConfig { @Value("${spring.task.execution.pool.core-size}") private int corePoolSize; @Value("${spring.task.execution.pool.max-size}") private int maxPoolSize; @Value("${spring.task.execution.pool.queue-capacity}") private int queueCapacity; @Value("${spring.task.execution.thread-name-prefix}") private String namePrefix; @Value("${spring.task.execution.pool.keep-alive}") private int keepAliveSeconds; @Bean public Executor myAsync() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); //最大线程数 executor.setMaxPoolSize(maxPoolSize); //核心线程数 executor.setCorePoolSize(corePoolSize); //任务队列的大小 executor.setQueueCapacity(queueCapacity); //线程前缀名 executor.setThreadNamePrefix(namePrefix); //线程存活时间 executor.setKeepAliveSeconds(keepAliveSeconds); /** * 拒绝处理策略 * CallerRunsPolicy():交由调用方线程运行,比如 main 线程。 * AbortPolicy():直接抛出异常。 * DiscardPolicy():直接丢弃。 * DiscardOldestPolicy():丢弃队列中最老的任务。 */ executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); //线程初始化 executor.initialize(); return executor; } }
在方法上添加@Async注解,然后还需要在@SpringBootApplication启动类或者@Configuration注解类上 添加注解@EnableAsync启动多线程注解,@Async就会对标注的方法开启异步多线程调用,注意,这个方法的类一定要交给Spring容器来管理
另外需要注意的是:关于注解失效需要注意以下几点
-
注解的方法必须是public方法
-
方法一定要从另一个类中调用,也就是从类的外部调用,类的内部调用是无效的,因为@Transactional和@Async注解的实现都是基于Spring的AOP,而AOP的实现是基于动态代理模式实现的。那么注解失效的原因就很明显了,有可能因为调用方法的是对象本身而不是代理对象,因为没有经过Spring容器。
-
异步方法使用注解@Async的返回值只能为void或者Future