JUC相关知识整理
视频链接:狂神说JUC:https://www.bilibili.com/video/BV1B7411L7tE
Synchronized 与 Lock 区别
- Synchronized是内置的java关键字,Lock是一个java类
- Synchronized无法判断获取锁的状态,Lock可以判断是否获取到了锁
- Synchronized会自动释放锁,Lock必须手动释放锁,如果不释放锁,会出现死锁
- Synchronized 线程1(获取锁)、线程2(等待), Lock锁不一定等待,可以通过tryLock() 尝试获取锁
- Synchronized是可重入锁,不可以中断,是非公平锁;Lock也是可重入锁,可以中断,默认是非公平锁,也可以设置成公平锁(自由度更高)
- Synchronized 适合锁少量代码同步问题;Lock适合锁大量的同步代码
8锁现象 弄懂锁是什么,怎么判断锁的是谁
- 永远知道什么是锁,锁的是谁? synchronized锁的,要么是对象,要么是Class(模板,全局唯一)
- 静态方法会在类加载的时候初始化
- 如果两个线程中,两个对象,调用的都是普通同步方法,那么不需要相互等待。因为是两把锁,互不影响
- 如果两个线程中,一个对象,调用的一个是普通同步方法,一个是普通非同步方法。(非同步方法不用等待同步方法的锁释放),互不影响
- 如果两个线程中,调用的都是静态同步方法,那么锁的都是Class模板(全局唯一),不管调用方法的是一个对象,还是两个对象。都是同一把锁(Class模板)
- 如果两个线程中,一个对象/两个对象,分别调一个静态同步方法,一个普通同步方法。那么相当于是两把锁,互不影响
Callable
- 可以有返回值
- 可以抛出异常
- 方法不同 call()
- 通过FutureTask作为适配类,中间转一道,来创建线程。
- futureTask.get() 获取到的结果可能会阻塞(线程执行耗时操作,需要等待执行完,才能拿到结果)
- futureTask.get() 获取到的结果会被缓存,以提高效率(两个线程执行,call()方法只执行一次)
学习方法
- 先会用
- 货比三家,寻找其他解决方案
- 分析源码,知其所以然
JUC中三大常用辅助类
CountDownLatch -- (减法计数器)
原理:每次线程调用countDown()数量-1,假设计数器变为0,countDownLatch.await()就会被唤醒,继续执行
-
new CountDownLatch(6); // 初始化线程计数器总数
-
countDownLatch.countDown(); // 计数器减1
-
countDownLatch.await(); // 等待计数器归零,然后向下执行
/** * @author lixing * @date 2022-03-27 15:04 * @description 减法计数器 为了保证多少个线程执行完毕后,才执行后续操作 (场景:多个学生都完成任务,回家了,才关闭教室门) */ public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(6); // 总数是6 for(int i=1; i<=6; i++){ new Thread(()->{ System.out.println(Thread.currentThread().getName()+" go out"); countDownLatch.countDown(); // -1 }, String.valueOf(i)).start(); } countDownLatch.await(); // 等待计数器归零,然后向下执行 System.out.println("close door"); } }
CyclicBarrier -- (加法计数器)
原理:执行完指定个数的线程后,才会执行初始化定义的另一个线程
-
new CyclicBarrier(7, ()->{System.out.println("召唤神龙成功!");}); // 初始化定义线程计数到达7个,才执行指定线程
-
cyclicBarrier.await(); // 每次执行一个线程后执行等待,线程计数累加到7个,会执行指定线程
/** * @author lixing * @date 2022-03-27 15:17 * @description 加法计数器 */ public class CyclicBarrierDemo { public static void main(String[] args) { // 集齐七颗龙珠召唤神龙 CyclicBarrier cyclicBarrier = new CyclicBarrier(7, ()->{ System.out.println("召唤神龙成功!"); }); // 代表需要执行完7个线程,才会执行第二个参数的线程“召唤神龙” for(int i=1; i<=7; i++){ final int temp = i; new Thread(()->{ System.out.println(Thread.currentThread().getName()+"收集"+temp+"颗龙珠"); try { cyclicBarrier.await(); } catch (Exception e) { e.printStackTrace(); } }).start(); } } }
Semaphore -- (信号量)
原理:初始通过信号量定义共享资源的个数,通过acquire()和release()方法实现多个共享资源的互斥使用。常用场景如:抢车位、并发限流
-
semaphore.acquire(); // 获得,如果已经满了,则等待被释放为止。
-
semaphore.release(); // 释放,会将当前信号量释放 +1,然后唤醒等待的线程。
/** * @author lixing * @date 2022-03-27 15:33 * @description 信号量 场景:抢车位! 6个车 3个车位 、 限流! */ public class SemaphoreDemo { public static void main(String[] args) { // 信号量 => 停车位3个 线程数 => 车辆数6个 Semaphore semaphore = new Semaphore(3); for(int i=1; i<=6; i++){ new Thread(()->{ try { semaphore.acquire(); // acquire() 得到 System.out.println(Thread.currentThread().getName()+"抢到车位"); TimeUnit.SECONDS.sleep(2); System.out.println(Thread.currentThread().getName()+"离开车位"); } catch (InterruptedException e) { e.printStackTrace(); }finally { semaphore.release(); // release() 释放 } }, String.valueOf(i)).start(); } } }
ReadWriteLock -- 读写锁
概念:更细粒度的锁,针对于写操作,一次只允许一个线程占有;针对于读操作,可以同时多个线程占有
/**
* @author lixing
* @date 2022-03-27 15:56
* @description 读写锁 ReadWriteLock
* 读-读 可以共存
* 读-写 不能共存
* 写-写 不能共存
* 独占锁(写锁)一次只能被一个线程占有
* 共享锁(读锁)多个线程可以同时占有
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCacheLock myCacheLock = new MyCacheLock();
// 写入
for(int i=1; i<=5; i++){
final int temp = i;
new Thread(()->{
myCacheLock.put(temp+"", temp+"");
}, String.valueOf(i)).start();
}
// 读取
for(int i=1; i<=5; i++){
final int temp = i;
new Thread(()->{
myCacheLock.get(temp+"");
}, String.valueOf(i)).start();
}
}
}
// 自定义缓存(加锁的)
class MyCacheLock{
private volatile Map<String, Object> map = new HashMap<>();
// 读写锁:更加细粒度的控制
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 存,写入的时候,只希望同时只有一个线程写
public void put(String key, Object value){
readWriteLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"写入"+key);
map.put(key, value);
System.out.println(Thread.currentThread().getName()+"写入ok");
}catch (Exception e){
e.printStackTrace();
}finally {
readWriteLock.writeLock().unlock();
}
}
// 取,读,所有人都可以读
public void get(String key){
readWriteLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"读取"+key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"读取ok");
}catch (Exception e){
e.printStackTrace();
}finally {
readWriteLock.readLock().unlock();
}
}
}
BlockingQueue 阻塞队列
-
写入:如果队列满了,就必须阻塞等待
-
读取:如果队列是空的,必须阻塞等待生产
-
阻塞队列有四组API [添加、移除、检测队首元素]
- 抛出异常:add()、remove()、element()
- 有返回值,不抛出异常:offer()、poll()、peek()
- 阻塞等待:put()、take()
- 超时等待:offer(,,)、poll(,)
SynchronousQueue 同步队列
没有容量,进去一个元素,必须等待取出来,才能再往里放入一个元素
- SynchronousQueue和其他的BlockingQueue不一样,它不存储元素,put了一个元素,必须从里面take出来,否则不能再put进去值
线程池
3大方法、7大参数、4种拒绝策略
池化技术
- 程序运行的本质:占用系统的资源。 优化资源的使用 => 池化技术(线程池、连接池、内存池、对象池...)
- 池化技术:事先准备好一些资源,有人要用,就来拿,用完后还回来
- 线程池的好处: 线程复用、可以控制最大并发数、管理线程
- 降低资源消耗
- 提高响应速度
- 方便管理
线程池三大方法
- newSingleThreadExecutor(); // 单个线程
- newFixedThreadPool(5); // 创建一个固定大小的线程池
- newCachedThreadPool(); // 缓冲线程池,可伸缩,遇强则强,遇弱则弱
线程池七大参数
源码分析
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, // 约等于21亿
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
// 本质:ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
- corePoolSize:核心线程大小
- maximumPoolSize:最大线程大小
- keepAliveTime:空闲线程超时时间,超时了没有人调用就会被释放
- timeUnit:超时时间单位
- workQueue:阻塞队列
- threadFactory:线程工厂,创建线程的,一般用默认值
- handler:拒绝策略
阿里java开发手册中【强制】不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学能更加明确线程池的运行规则,规避资源耗尽的风险。 Executors返回的线程池对象的弊端如下:
- FixedThreadPool 和 SingleThreadPool:允许的任务请求队列长度为Integer.MAX_VALUE,约等于21亿,可能会堆积大量的请求,从而导致OOM。
- CachedThreadPool 和 ScheduledThreadPool:允许创建的最大线程数为Integer.MAX_VALUE,约等于21亿,可能会创建大量的线程,从而导致OOM。
线程池四种拒绝策略
- ThreadPoolExecutor.AbortPolicy():丢弃任务,拒绝执行,并抛出RejectedExecutionException异常
- ThreadPoolExecutor.CallerRunsPolicy():由调用者(调用的线程)去执行
- ThreadPoolExecutor.DiscardPolicy():丢弃任务,不抛出异常
- ThreadPoolExecutor.DiscardOldestPolicy():抛弃最早进入队列的任务,然后将当前任务入队,再次执行
最大线程数如何设置
CPU密集型、IO密集型(调优)
- CPU密集型:几核,就是几,可以保持cpu的效率最高 (并行执行)
- Runtime.getRuntime().availableProcessors() -- 获取当前运行服务器的CPU核数
- IO密集型:判断程序中十分耗IO资源的线程,只要大于这个线程数就可以了,一般 2*n,保证处理IO的线程阻塞时,其他线程可以正常执行
四大函数式接口
lambda表达式、链式编程、函数式接口、Stream流式计算
1.函数式接口:只有一个方法的接口 Function,有输入,有返回,类型可自定义
// 函数式接口Function 源码 ,输入参数类型T,返回参数类型R
@FunctionalInterface
public interface Function<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
}
// 超级多FunctionalInterface
// 简化编程模型,在新版本的框架底层大量应用
// 例如:foreach(消费则类的函数式接口)
public static void main(String[] args) {
// Function function = new Function<String, String>() {
// @Override
// public String apply(String str) {
// return str;
// }
// };
Function function = (str) -> {return str;};
System.out.println(function.apply("123"));
}
2.断定型接口:有一个输入参数,返回值只能是布尔值。
public static void main(String[] args) {
// 判断字符串是否为空
// Predicate<String> predicate = new Predicate<String>() {
// @Override
// public boolean test(String str) {
// return str.isEmpty();
// }
// };
Predicate<String> predicate = (str)->{return str.isEmpty();};
System.out.println(predicate.test("adf"));
}
3.消费型接口:只有输入,没有返回值
public static void main(String[] args) {
// Consumer<String> consumer = new Consumer<String>() {
// @Override
// public void accept(String str) {
// System.out.println(str);
// }
// };
Consumer<String> consumer = (str)->{
System.out.println(str);
};
consumer.accept("abc");
}
4.供给型接口:没有输入,有返回值
public static void main(String[] args) {
// Supplier<Integer> supplier = new Supplier<Integer>() {
// @Override
// public Integer get() {
// return 1024;
// }
// };
Supplier<Integer> supplier = ()->{return 1024;};
System.out.println(supplier.get());
}
Stream流式计算 (必须掌握)
什么是流式计算
- 大数据:存储+计算
- 集合、MySQL的本质就是存储东西的,计算都应该交给流来操作。
/*
* 题目要求:一分钟之内完成此题,只能用一行代码实现
* 现在只有5个用户。,筛选:
* 1. ID必须是偶数
* 2. 年龄大于23岁
* 3. 用户名转为大写字母
* 4. 用户名字母倒着排序
* 5. 只输出一个用户
*/
public static void main(String[] args) {
User u1 = new User(1, "a", 21);
User u2 = new User(2, "b", 22);
User u3 = new User(3, "c", 23);
User u4 = new User(4, "d", 24);
User u5 = new User(5, "e", 25);
// 集合就是存储
List<User> list = Arrays.asList(u1, u2, u3, u4, u5);
// 计算交给Stream流
// lambda表达式、链式编程、函数式接口、Stream流式计算
list.stream()
.filter(u->{return u.getId()%2==0;}) // 过滤出ID为偶数的
.filter(u->{return u.getAge()>23;}) // 过滤出年龄大于23
.map(u->{return u.getName().toUpperCase();}) // 名字转换为大写字母
.sorted((o1, o2)->{return o2.compareTo(o1);}) // 用户字母倒序
.limit(1)
.forEach(System.out::println);
}
ForkJoin
什么是ForkJoin
-
ForkJoin是在JDK1.7中提出的,对于大数据量,将任务拆成多个子任务,放入到队列中,通过多线程并行执行,将计算结果累加返回,提高效率。
ForkJoin特性:工作窃取
-
里面维护的是双端队列,一个线程的队列任务执行完了,会去窃取另一个线程上的任务来执行,以提升执行效率
如何使用forkJoin
-
new 一个 forkjoinpool,用它来执行
-
计算任务 forkjoinpool.execute(ForkJoinTask task)
-
计算类要继承 ForkJoinTask
public class ForkJoinDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { test1(); // 时间:272 test2(); // 时间:166 test3(); // 时间:150 } // 常规计算方式 public static void test1(){ long sum = 0; long start = System.currentTimeMillis(); for(long i=1; i<=10_0000_0000; i++){ sum += i; } long end = System.currentTimeMillis(); System.out.println("sum="+sum+" 时间:"+(end-start)); } // forkjoin方式 public static void test2() throws ExecutionException, InterruptedException { long start = System.currentTimeMillis(); ForkJoinPool forkJoinPool = new ForkJoinPool(); ForkJoinTask<Long> task = new ForkJoinDemo1(0, 10_0000_0000); ForkJoinTask<Long> submit = forkJoinPool.submit(task); long sum = submit.get(); // 获取线程执行结果的时候,会阻塞等待 long end = System.currentTimeMillis(); System.out.println("sum="+sum+" 时间:"+(end-start)); } // stream并行流 public static void test3(){ long start = System.currentTimeMillis(); long sum = LongStream.range(0, 10_0000_0000).parallel().reduce(0, Long::sum); long end = System.currentTimeMillis(); System.out.println("sum="+sum+" 时间:"+(end-start)); } } class ForkJoinDemo1 extends RecursiveTask<Long>{ private long start; private long end; private long temp = 10000; // 临界值1w,超过1w就采用ForkJoin的方式执行 public ForkJoinDemo1(long start, long end){ this.start = start; this.end = end; } @Override protected Long compute() { if(end-start < temp){ // 小于临界值,采用常规方法计算 long sum = 0; for(long i=start; i<=end; i++){ sum+=i; } return sum; }else{ // 超过临界值,采用forkJoin方式计算 long middle = (start+end)/2; // 中间值 ForkJoinDemo1 task1 = new ForkJoinDemo1(start, middle); task1.fork(); // 拆分任务,把任务压入线程队列 ForkJoinDemo1 task2 = new ForkJoinDemo1(middle+1, end); task2.fork(); // 拆分任务,把任务压入线程队列 return task1.join() + task2.join(); // 将求和结果累加 } } }
JMM
请谈谈你对volatile的理解
- volatile是jvm提供的轻量级同步机制
- 保证可见性
- 禁止指令重排
- 不保证原子性
什么是JMM
-
JMM:java内存模型
-
关于JMM的一些同步的约定:
- 线程解锁前,必须把共享变量立刻刷回主存
- 线程加锁前,必须读取主存中最新的值到工作内存中
- 加锁和解锁是同一把锁
-
线程 工作内存 主存
-
8种操作(四组操作)
- lock(锁定):把主存中一个变量标识为线程独占状态
- unlock(解锁):把一个处于锁定状态的变量释放出来,释放后的变量才能被其他线程锁定
- read(读取):把一个变量从主存中读出,传输到线程的工作内存中
- load(载入):把read操作的变量放入到线程的工作内存中
- use(使用):把工作内存中的变量传输给执行引擎
- assign(赋值):把一个从执行引擎中接收到的值放入工作内存的变量副本中
- store(存储):把工作内存中一个变量的值传输到主内存中
- write(写入):把从工作内存传过来的变量值放到主存的变量中
volatile
- 保证可见性
public class VolatileDemo {
// 不加volatile,程序会死循环
// 加了volatile,可以保证可见性
private volatile static int num = 0;
/**
* 线程1和线程2都读取了主存中的num变量的值,其实是都拷贝了一份num=0到自己线程的工作内存中。
* 1秒后线程1将num改成1,并写入主存中,但是线程2没有读取主存中更新的值,所以一直在循环体中
**/
public static void main(String[] args) throws InterruptedException {
// 主线程(线程1)
// 线程2 对主内存的变化不可见,加了volatile后,num的值在主存中发生了变化,线程2中可以获取到最新的值
new Thread(()->{
while(num == 0){
}
}).start();
TimeUnit.SECONDS.sleep(1);
num = 1;
System.out.println(num);
}
}
- 不保证原子性
-
原子性:不可分割
/** * @author lixing * @date 2022-03-29 21:19 * @description 不保证原子性,并发下 num++的操作可能被其他线程抢占 */ public class VolatileDemo2 { private volatile static int num = 0; public static void add(){ num++; // 不是原子性操作 } public static void main(String[] args) { // 理论上结果应该是20000 for (int i = 1; i <= 20; i++) { new Thread(()->{ for (int j = 0; j < 1000; j++) add(); } }).start(); } while(Thread.activeCount() > 2){ // main gc Thread.yield(); } System.out.println(Thread.currentThread().getName() + " " + num); // 很难输入 20000 } }
如何在不用lock和synchronized的情况下,保证原子性
- 使用原子类 -- 这些类的底层都直接和操作系统挂钩,在内存中修改值。
// 原子类的Integer
private volatile static AtomicInteger num = new AtomicInteger(0);
public static void add(){
num.getAndIncrement(); // AtomicInteger +1的方法 CAS
}
- 禁止指令重排
什么是指令重排
- 你写的程序,计算机并不是按照你写的那样去执行的
- 源代码 --> 编译器优化的重排 ---> 指令并行也可能会重排 ---> 内存系统也会重排 ---> 执行
- volatile修饰后,会在指令的上面和下面添加内存屏障,防止上面的指令和下面的指令交换位置
深入理解CAS
什么是CAS
-
CAS(ComparAndSet):比较并交换,比较当前工作内存中的值和主内存中的值,如果这个值是期望的,就执行操作,如果不是,则一直循环。
-
缺点:
- 循环会耗时
- 一次只能保证一个共享变量的原子性
- 存在ABA问题
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2022);
// 期望、更新
// public final boolean compareAndSet(int expect, int update)
// 如果期望的值达到了,就更新;否则,不更新 CAS是CPU的并发原语
atomicInteger.compareAndSet(2022, 2023);
System.out.println(atomicInteger.get()); // 2023
}
ABA问题
- 一个线程t1拿到了内存值为1,另一个线程t2也拿到了内存值为1,然后t2将1改为2,又改回1;对于t1,它看到的内存值依旧是1,认为没有做任何修改。所以t1可以操作成功
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(2022);
// ---------- 线程t2 ----------
atomicInteger.compareAndSet(2022, 2023);
System.out.println(atomicInteger.get()); // 2023
atomicInteger.compareAndSet(2023, 2022);
System.out.println(atomicInteger.get()); // 2022
// ---------- 线程t1 ----------
atomicInteger.compareAndSet(2022, 8888);
System.out.println(atomicInteger.get()); // 8888
}
如何解决ABA问题 -- 引入原子引用 AtomicStampedReference,对应的思想:乐观锁
- 通过带版本号的原子操作
public static void main(String[] args) {
// 第二个参数,相当于版本号
// 注意:如果泛型是包装类型,注意对象的引用问题
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(34, 1);
// 让线程1先执行
new Thread(()->{
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("a1=>"+stamp); // a1=>1
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 模拟SQL中乐观锁的操作,更新操作的时候,将数据版本号+1
System.out.println(atomicStampedReference.compareAndSet(34, 35, atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1)); // true
System.out.println("a2=>"+atomicStampedReference.getStamp()); // a2=>2
System.out.println(atomicStampedReference.compareAndSet(35, 34, atomicStampedReference.getStamp(),
atomicStampedReference.getStamp() + 1)); // true
System.out.println("a3=>"+atomicStampedReference.getStamp()); // a3=>3
}, "a").start();
new Thread(()->{
int stamp = atomicStampedReference.getStamp(); // 获得版本号
System.out.println("b1=>"+stamp); // b1=>1
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 由于线程a执行了两次更新操作,版本号由1变成了3,与期望值不同了,所以这里修改失败。与乐观锁的原理相同
System.out.println(atomicStampedReference.compareAndSet(34, 88, stamp, stamp + 1)); // false
System.out.println("b2=>"+atomicStampedReference.getStamp()); // b2=>3
}, "b").start();
}
Unsafe类
// 源码
/**
* java无法操作内存,但是java可以通过native方法来调用c++,c++可以操作内存
* Unsafe类就相当于java的一个后门,可以通过这个类操作内存
**/
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset // 获取内存地址偏移值
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
自旋锁
自旋锁的底层就是采用CAS的原理实现的,只要未达到期望值,就一直循环判断(自旋),直到达到期望值,进行更新
// java原子类 AtomicInteger的+1操作,底层就是CAS原理,自旋锁
atomicInteger.getAndIncrement();
// 底层源码
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// var1 ==> this var2 ==> valueOffset var4 ==> 1public final int getAndAddInt(Object var1, long var2, int var4) { int var5; // 自旋锁 (基于CAS原理) do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
各种锁的理解
- 公平锁、非公平锁
- 公平锁:非常公平,不能插队,必须先来后到
- 非公平锁:非常不公平,可以插队(默认都是非公平锁,效率高)
public ReentrantLock(){
sync = new NonfairSync();
}
public ReentrantLock(boolean fair){
sync = fair ? new FairSync() : new NonfairSync();
}
- 可重入锁
- 某个线程获取到某个锁,可以再次获取锁,而不会出现死锁
public class LockDemo {
public static void main(String[] args) {
Phone5 phone5 = new Phone5();
new Thread(()->{phone5.sms();}, "a").start();
new Thread(()->{phone5.call();}, "b").start();
}
}
class Phone5{
Lock lock = new ReentrantLock();
public void sms(){
lock.lock(); // 细节问题:lock锁必须配对,否则会死在里面
try {
System.out.println(Thread.currentThread().getName()+" sms");
call(); // 这里也有锁
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void call(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+" call");
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
-
自旋锁: 基于CAS实现
-
死锁
/**
* @author lixing
* @date 2022-03-30 23:14
* @description 死锁 ==> 两个线程,一个占据资源1,一个占据资源2;现在他俩都想竞争对方持有的资源,造成一种相互等待的僵局
*/
public class DeadLockDemo {
private static final Object RESOURCE1 = "resource1"; // 资源1
private static final Object RESOURCE2 = "resource2"; // 资源2
public static void main(String[] args) {
new Thread(()->{
synchronized(RESOURCE1){
System.out.println(Thread.currentThread().getName()+"获取资源1");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (RESOURCE2){
System.out.println(Thread.currentThread().getName()+"获取资源2");
}
}
}, "t1").start();
new Thread(()->{
synchronized(RESOURCE2){
System.out.println(Thread.currentThread().getName()+"获取资源2");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (RESOURCE1){
System.out.println(Thread.currentThread().getName()+"获取资源1");
}
}
}, "t2").start();
}
}
如何解决死锁
- 使用
jps -l
定位进程号 - 使用
jstack 进程号
查看进程中的堆栈信息,分析死锁问题 - 比较low的方法:查看日志