概述
文章目录
- 前置要求
- 线程基础知识复习
- Futrue和Callable接口
- 从之前的FutureTask说
- 对Future的改进
- CompetableFuture和CompletionStage源码分别介绍
- 类架构说明
- 接口CompletionStage是什么
- 类CompletableFuture是什么
- 核心的四个静态方法,来创建一个异步操作
- 案例精讲-从电商网站的比价需求说
- 函数式编程已经成为主流
- 先说说join和get对比
- get()
- join()
- 发现了什么
- 大厂业务需求说明
- CompletableFutrue常用方法
- 获得结果和触发计算
- get()
- getNow()
- boolean complete(T value)
- 对计算结果进行处理
- thenApply
- handle
- 总结
- 对计算结果进行消费
- thenAccept
- thenRun(Runnable runnable)
- thenAccept(Consumer action)
- thenApply(Function fn)
- 对计算速度进行选用
- 对计算结果进行合并
- CompletableFutrue
- 说说Java"锁"事
- 大厂面试题复盘
- 从轻松的乐观锁和悲观锁开始
- 悲观锁
- 乐观锁
- 通过8种情况演示锁运行案例,看看我们到底锁的是什么
- 标准访问ab两个线程,请问先打印邮件还是短信
- sendEmail方法暂停3秒,请问先打印邮件还是短信
- 新增一个普通的hello方法,请问先打印邮件还是hello
- 有两部手机,请问先打印邮件还是短信
- 两个静态同步方法,同一部手机,请问先打印邮件还是短信
- 两个静态同步方法,2部手机,请问先打印邮件还是短信
- 一个静态同步方法,一个普通同步方法,同一部手机,请问先打印邮件还是短信
- 一个静态方法,一个同步方法,2部手机,请问先打印邮件还是短信
- 总结
- sync底层字节码
- 一定是一个enter两个exit吗,如果自定义异常呢?
- 反编译synchronized锁的是什么
- 为什么任何一个对象都可以成为一个锁
- 在HotSpot虚拟机中,monitor采用ObjectMonitor实现
- C++源码解读
- 公平锁和非公平锁
- 大致看下源码
- 为什么会有公平锁和非公平锁的设计,为什么默认非公平锁
- 使用公平锁会有什么问题
- 什么时候用公平,什么时候用非公平锁
- 可重入锁(又名递归锁)
- 说明
- 可重入锁四个字拆开来看
- synchronized的重入的实现机制:
- synchronized的代码演示
- ReentrantLock的代码演示
- 死锁及排查
- 死锁是什么
- 写个死锁看看
- 如何证明写的程序是死锁呢?
- 写锁(独占锁)/ 读锁(共享锁)
- 自旋锁SpinLock
- 无锁->独占锁->读写锁->邮戳锁
- 无锁->偏向锁->轻量锁->重量锁
- 其他细节
- LockSupport与线程中断
- 线程中断机制
- 从阿里蚂蚁金服面试题讲起
- 什么是中断
- 中断的相关API
- 面试题:如何使用中断标识停止线程?
- 通过volatile
- 通过AtomicBoolean
- 通过Thread自带的中断API方法实现
- 如何验证interrupt不是立即停止
- 既然不是立即停下来,那如果调用interrupt后加入一个sleep呢
- 静态方法Thead.interrupted()
- 总结
- LockSupport是什么
- 线程等待唤醒机制
- 3种让线程等待和唤醒的方法
- 使用Object中的wait()让线程等待,使用Object的notify()唤醒线程
- 使用JUC包中Condition的await()让线程等待,使用signal()唤醒线程
- LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
- Object类中的wait和notify方法实现线程等待和唤醒
- Condition接口中的await和signal方法实现线程的等待和唤醒
- Object和Condition使用的限制条件
- LockSupport类中的park等待和unpark唤醒
- park()
- unpark()
- Java内存模型值JMM
- 先从大厂面试题开始
- 计算器硬件存储体系
- Java内存模型Java Memory Model
- JMM规范下,三大特性
- JMM规范下,多线程对变量的读写过程
- JMM规范下,多线程先行发生原则之happens-before
- volatile与Java内存模型
- 被volatile修改的变量有2大特点
- 内存屏障(面试重点必须拿下)
- 先说生活case
- 是什么
- volatile凭什么可以保证可见性和有序性
- JVM中提供了四类内存屏障指令
- C++源码分析
- 四大内存屏障分别是什么意思
- volatile两个特性
- happens-before之volatile变量规则
- JMM将内存屏障插入策略分为4种
- 从i++的字节码角度说明,volatile不具有原子性
- 禁止重排序
- 如何正确使用volatile
- 最后的小总结
- CAS
- 没有CAS之前
- 是什么
- 说明,原理
- 硬件级别保证
- CASDemo代码
- CAS底层原理?如果知道,谈谈你对UnSafe类的理解
- 原子引用
- 自旋锁,借鉴CAS思想
- CAS缺点
- 原子操作类之18罗汉增强
- 分类
- 基本类型原子类
- 数组类型原子类
- 引用类型原子类
- AtomicReference
- AtomicStampReference
- AtomicMarkableReference
- 对象的属性修改原子类
- AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
- AtomicReferenceFieldUpdater
- 原子操作增强类原理深度解析
- 为什么LongAdder性能这么快?
- 聊聊ThreadLocal
- ThreadLocal简介
- 大厂面试题
- ThreadLocal中ThreadLcoalMap的数据结构和关系?
- ThreadLocal中的key是弱引用,这是为什么?
- ThreadLocal内存泄漏问题,你知道吗?
- Thread Local中最后为什么要加remove方法?
- 从阿里Thread Local规范开始
- 非线程安全的SimpleDateFormat
- ThreadLocal源码分析
- Thread、ThreadLocal、ThreadLocalMap 之间的关系
- ThreadLocal内存泄漏问题
- 强软弱虚4种引用?
- 什么是内存泄漏?如何避免ThreadLocalMap内存泄漏?
- 谁惹的祸?
- 为什么要用弱引用?不用如何?
- 为什么源码要用弱引用?
- 使用弱引用就万事大吉了吗?
- 最佳实践
- 小总结
- Java对象内存布局和对象头
- 对象在堆中的布局
- 对象头
- 对象标记
- 类元信息(又叫类型指针)
- 对象头有多大
- 实例数据
- 对其填充
- 再说说对象头的MarkWord
- Object o = new Object();谈谈你对这句话的理解?一般而言JDK8按照默认情况下,new一个对象占多少内存空?
- JOL证明
- 代码
- GC年龄采用4位bit存储,最大为15,例如MaxTenuringThreshold参数默认值就是15
- 尾巴参数说明
- 换成其他对象试试
- Synchronized与锁升级
- Synchronized的性能变化
- 为什么每个对象都能成为一把锁?
- markOop.cpp
- Monitor(监视器锁)
- Synchronized锁种类及升级步骤
- 多线程访问情况(3种)
- 升级流程
- 无锁
- 偏向锁
- 偏向锁的持有
- 偏向锁JVM命令
- 偏向锁的撤销
- 轻量级锁
- 主要作用
- 轻量级锁的获取
- Code演示
- 步骤流程图示
- 自旋达到一定次数和程度
- 轻量锁和偏向锁的区别和不同
- 重量级锁
- 小总结
- JIT编译器对锁的优化
- 锁消除
- 锁粗化
- AbstractQueuedSynchronizer之AQS
- 先从阿里及其他大厂面试题说起
- 前置知识
- 是什么
- 字面意思
- 技术解释
- AQS为什么是JUC内容中最重要的基石
- 和AQS有关的
- 进一步理解锁和同步器的关系
- 能干嘛
- AQS初步
- AQS内部结构
- AQS自身
- AQS的int变量
- AQS的CLH队列
- 小总结
- 内部类Node(Node类在AQS类内部)
- Node的int变量
- Node的此类讲解
- 内部讲解
- 属性结构
- AQS同步队列的基本结构
- 从我们的ReentrankLock开始解读AQS
- Subtopic
- ReentrantLock、ReentrantReadWriteLock、StampedLock
- 面试题
- 简单聊聊ReentrantReadWriteLock
- 读写锁意义和特点
- 降级
- StampedLock
- 为什么会闪亮出现呢?
- 总结与回顾
前置要求
线程基础知识复习
Futrue和Callable接口
Future接口定义了操作异步任务执行一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。
Callable接口定义了需要有返回的任务需要实现的方法。
比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他事了,过了一会儿去取子任务的执行结果。
public class FutureTaskDemo {
public static void main(String[] args)
throws ExecutionException, InterruptedException
{
FutureTask<Integer> futureTask = new FutureTask<>(()->{
System.out.println(Thread.currentThread().getName()+
"t"+"===come in.");
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
}
return 1024;
});
new Thread(futureTask).start();
System.out.println("======阳哥继续讲课=====");
System.out.println(futureTask.get());
}
}
运行结果:
======阳哥继续讲课=====
Thread-0 ===come in.
1024
推荐 futureTask.get() 放在最后,如果不放在最后的话,我们再来看:
new Thread(futureTask).start();
// 只要出现future.get()方法,不管是否计算完成都阻塞,等待结果出来再运行
System.out.println(futureTask.get());
System.out.println("======阳哥继续讲课=====");
运行结果:
Thread-0 ===come in.
1024
======阳哥继续讲课=====
针对上面说的 futrure.get()方法:
// 只要出现future.get()方法,不管是否计算完成都阻塞,等待结果出来再运行
// 工作中别用这个,别给自己挖坑
System.out.println(futureTask.get());
// 过时不候
System.out.println(futureTask.get(2, TimeUnit.SECONDS));
那么,如何避免阻塞呢?
答:用轮询来代替阻塞。
小总结:不见不散 - 过时不候 - 轮询
从之前的FutureTask说
如果要做一些复杂的任务呢?比如:
应对Future的完成时间,完成了可以告诉我,也就是我们的回调通知。
将两个异步计算合成一个异步计算,这两个异步计算互相对立,同时第二个又依赖第一个的结果。
当Future集合中某个任务最快结束时,返回结果。
等待Future结合中的所有任务都完成。
。。。。。。。。。。
对Future的改进
CompetableFuture和CompletionStage源码分别介绍
类架构说明
接口CompletionStage是什么
CompletionStage代表异步计算过程中的某一阶段,一个阶段完成以后可能会触发另外一个阶段。
一个阶段的计算执行可以是一个Function,Consumer或者Runnable。
一个阶段的执行可能是被单个阶段的完成触发,也可能是有多个阶段一起触发。
代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段,有些类似Linux系统的管道分隔符
类CompletableFuture是什么
在Java8中,CompletableFuture提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合CompletableFuture的方法。
它可以代表一个明确完成的Future,也有可能代表一个完成阶段(CompletionStage),它支持计算完成以后触发一些函数或执行某些动作。
它实现了Future和CompletionStage接口。
核心的四个静态方法,来创建一个异步操作
public static void main(String[] args) throws Exception {
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(1, 20, 1L, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(50), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName() + "t" + "===come in.");
});
// 线程池用在哪些地方?
CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName() + "t" + "===come in.");
}, threadPoolExecutor);
threadPoolExecutor.shutdown();
}
运行结果
ForkJoinPool.commonPool-worker-1 ===come in.
pool-1-thread-1 ===come in.
public static void main(String[] args) throws Exception {
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(1, 20, 1L, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(50), Executors.defaultThreadFactory(),
CompletableFuture<Integer> future3 = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName() + "t" + "===come in.");
return 11;
});
CompletableFuture<Integer> future4 = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName() + "t" + "===come in.");
return 12;
}, threadPoolExecutor);
threadPoolExecutor.shutdown();
}
运行结果:
ForkJoinPool.commonPool-worker-1 ===come in.
pool-1-thread-1 ===come in.
Executor参数说明:
没有指定Executor的方法,直接使用默认的ForkJoinPool.commonPool(),作为它的线程池执行异步代码。
如果指定线程池,则使用我们自定义的或者特别指定的线程池执行异步代码。
函数式接口名称 | 方法名称 | 参数 | 返回值 |
---|---|---|---|
Runnable | run | 无 | 无 |
Function | apply | 7 | 有 |
Consume | appept | 7 | 无 |
Supplier | get | 无 | 有 |
BiConsumer | accept | 2 | 无 |
案例精讲-从电商网站的比价需求说
函数式编程已经成为主流
先说说join和get对比
get()
public static void main(String[] args)
throws ExecutionException, InterruptedException
{
System.out.println(CompletableFuture.supplyAsync(() -> {
try { TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) { }
return 1;
}).whenComplete((v, e) -> {
if (e == null) {
System.out.println("==result==" + v);
}
}).exceptionally(e -> {
e.printStackTrace();
return null;
}).get());
try { TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) { }
}
join()
public static void main(String[] args) {
System.out.println(CompletableFuture.supplyAsync(() -> {
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { }
return 1;
}).whenComplete((v, e) -> {
if (e == null) {
System.out.println("==result==" + v);
}
}).exceptionally(e -> {
e.printStackTrace();
return null;
}).join());
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { }
}
发现了什么
get和join是一样的,只是,join不抛出异常
大厂业务需求说明
案例说明:电商比价需求
- 同一款产品,同时搜索出同款商品在各大电商的售价
- 同一款商品,同时搜索出本产品在某一个电商平台下,各个入驻门店的售价是多少
出来结果是同款产品的在不同地方的价格清单列表,返回一个list
e.g.
《MySQL》in jd price is 88.05
《MySQL》in pdd price is 86.11
《MySQL》in taobao price is 90.43 - 要求深刻理解
3.1 函数式编程
3.2 链式编程
3.3 Stream流式计算
public class FutureTaskDemo {
public static void main(String[] args) {
List<NetMall> list =
Arrays.asList(new NetMall("jd"), new NetMall("ebay"),
new NetMall("pdd"), new NetMall("taobao"),
new NetMall("dangdang"),new NetMall("tmall"),
new NetMall("suning"),new NetMall("amazon"));
long start1 = System.currentTimeMillis();
List<String> list1 = getPriceByStep(list, "mysql");
for (String element : list1) {
System.out.println(element);
}
long end1 = System.currentTimeMillis();
System.out.println("耗时"+(end1-start1)+"毫秒");
long start2 = System.currentTimeMillis();
List<String> list2 = getPriceByStep(list, "mysql");
for (String element : list2) {
System.out.println(element);
}
long end2 = System.currentTimeMillis();
System.out.println("耗时"+(end2-start2)+"毫秒");
}
// 一步步走
public static List<String> getPriceByStep(List<NetMall> list, String mallName) {
return list.stream()
.map(netMall -> String.format("%s in %s price is %.2f",
mallName,
netMall.getMallName(),
netMall.getPrice()))
.collect(Collectors.toList());
}
// 万箭齐发
public static List<String> getPriceByAsync(List<NetMall> list, String mallName) {
return list.stream().
map(netMall ->
CompletableFuture.supplyAsync(() -> String.format("%s in %s price is %.2f",
mallName,
netMall.getMallName(),
netMall.getPrice())))
.collect(Collectors.toList())
.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
}
class NetMall {
private String mallName;
public NetMall(String mallName) {
this.mallName = mallName;
}
public double getPrice() {
return ThreadLocalRandom.current().nextDouble() * 2 + "mysql".charAt(0);
}
public String getMallName() {
return this.mallName;
}
}
CompletableFutrue常用方法
获得结果和触发计算
get()
getNow()
public static void main(String[] args) {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { }
return 1;
});
try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { }
// 立即返回,如果获取不到值,则返回自己定义的默认值
System.out.println(future.getNow(999));
System.out.println("============================================");
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { }
return 1;
});
try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { }
// 立即返回,如果获取不到值,则返回自己定义的默认值
System.out.println(future2.getNow(999));
System.out.println("=============================================");
CompletableFuture<Integer> future3 = CompletableFuture.supplyAsync(() -> {
try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { }
return 1;
});
try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { }
// 立即返回,如果获取不到值,则返回自己定义的默认值
System.out.println(future3.getNow(999));
}
运行结果:
999
============================================
999
=============================================
1
boolean complete(T value)
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { }
return 1;
});
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { }
// 是否打断上面的线程,如果打断,则返回打断后自定义的值
System.out.println(future.complete(-44) + "t" + future.get());
}
运行结果:
-44
对计算结果进行处理
thenApply
由于存在依赖关系(当前步错,不走下一步),当前步骤有异常的话就叫停
先来看看正常的:
public static void main(String[] args) {
Integer num = CompletableFuture.supplyAsync(() -> {
return 1;
}).thenApply(f -> {
return f + 2;
}).thenApply(f -> {
return f + 3;
}).whenComplete((v, e) -> {
if (e == null) {
System.out.println("result is " + v);
}
}).exceptionally(e -> {
e.printStackTrace();
return null;
}).join();
System.out.println("num is " + num);
}
运行结果:
result is 6
num is 6
那么,如果中途发生异常呢?
public static void main(String[] args) {
Integer num = CompletableFuture.supplyAsync(() -> {
return 1;
}).thenApply(f -> {
// 抛出异常
int i = f / 0;
return f + 2;
}).whenComplete((v, e) -> {
if (e == null) {
System.out.println("result is " + v);
}
}).exceptionally(e -> {
e.printStackTrace();
return null;
}).join();
System.out.println("num is " + num);
}
运行结果:
java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
at java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:273)
at java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:280)
at java.util.concurrent.CompletableFuture.uniApply(CompletableFuture.java:604)
at java.util.concurrent.CompletableFuture.uniApplyStage(CompletableFuture.java:614)
at java.util.concurrent.CompletableFuture.thenApply(CompletableFuture.java:1983)
at com.lyw.FutureTaskDemo.main(FutureTaskDemo.java:13)
Caused by: java.lang.ArithmeticException: / by zero
at com.lyw.FutureTaskDemo.lambda$main$1(FutureTaskDemo.java:14)
at java.util.concurrent.CompletableFuture.uniApply(CompletableFuture.java:602)
... 3 more
num is null
handle
有异常也可以往下一步走,根据带的异常参数可以进一步处理
总结
对计算结果进行消费
接受任务的处理结果,并消费处理,无返回结果
thenAccept
thenRun(Runnable runnable)
thenRun(Runnable runnable) 任务A执行完毕,并且B不需要A的结果
thenAccept(Consumer action)
thenAccept(Consumer action) 任务A执行完执行B,B需要A的结果,但是任务B 无返回值
thenApply(Function fn)
thenApply(Function fn) 任务A执行完执行B,B需要A的结果,同时任务B 有返回值
public static void main(String[] args) {
CompletableFuture.supplyAsync(() -> {
return 1;
}).thenApply(f -> {
return f + 2;
}).thenApply(f -> {
return f + 3;
}).thenAccept(r -> System.out.println(r));
System.out.println(CompletableFuture.supplyAsync(()->"resultA")
.thenRun(()->{}).join());
System.out.println(CompletableFuture.supplyAsync(()->"resultA")
.thenAccept(resultA->{}).join());
System.out.println(CompletableFuture.supplyAsync(()->"resultA")
.thenApply(resultA->resultA+"resultB").join());
}
运行结果:
6
null
null
resultAresultB
对计算速度进行选用
对计算结果进行合并
public static void main(String[] args) {
Integer num = CompletableFuture.supplyAsync(() -> {
return 10;
}).thenCombine(CompletableFuture.supplyAsync(() -> {
return 20;
}), (r1, r2) -> {
return r1 + r2;
}).join();
System.out.println(num);
}
运行结果:
30
CompletableFutrue
说说Java"锁"事
大厂面试题复盘
volatile了解吗
volatile 三大特性
volatile 底层
volatile与synchronized有什么区别
说说乐观锁和悲观锁
使用version(乐观锁)解决一下ABA问题,SQL语句写出来
volatile怎么保证可见性
你如果要实现一个aqs如何实现
countdownlatch和CyclicBarrier区别
cas底层实现
synchronized底层,对象内存布局
Object o = new Object();(可以是任意对象),说说它的背后发生的事(其实就是对象的创建过程)
synchronized是不是可重入的?是公平的还是非公平的?锁静态的时候锁的是什么?锁普通方法的时候锁的是什么?
ReentrantLock是公平的还是非公平的?
aqs第1个获取不到锁的那个线程做了哪些事?
aqs的内部结构
aqs中的链表是双向的还是单向的
aqs内部有什么?
cas了解吗,说说原理
从轻松的乐观锁和悲观锁开始
悲观锁
适合写操作多的场景,先加锁可以保证写操作时数据正确
显示的锁定之后再同步资源
乐观锁
适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
乐观锁则直接去操作同步资源,是一种无锁算法,得之我幸不得我命,再抢
乐观锁一般有两种实现方式
乐观锁认为自己在使用数据时 不会有别的线程修改数据
,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作。
乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的
。
通过8种情况演示锁运行案例,看看我们到底锁的是什么
标准访问ab两个线程,请问先打印邮件还是短信
class Phone {
public synchronized void sendEmail() { System.out.println("====sendEmail"); }
public synchronized void sendSMS() {System.out.println("====sendSMS");}
}
public class LockDemo {
public static void main(String[] args) {
Phone phone = new Phone(); // 资源类
new Thread(phone::sendEmail,"a").start();
try { TimeUnit.MICROSECONDS.sleep(10); } catch (InterruptedException e) { }
new Thread(phone::sendSMS,"b").start();
}
}
运行结果:
====sendEmail
====sendSMS
sendEmail方法暂停3秒,请问先打印邮件还是短信
class Phone {
public synchronized void sendEmail() {
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { }
System.out.println("====sendEmail");
}
public synchronized void sendSMS() {System.out.println("====sendSMS");}
}
public class LockDemo {
public static void main(String[] args) {
Phone phone = new Phone(); // 资源类
new Thread(phone::sendEmail,"a").start();
try { TimeUnit.MICROSECONDS.sleep(10); } catch (InterruptedException e) { }
new Thread(phone::sendSMS,"b").start();
}
}
运行结果:
====sendEmail
====sendSMS
新增一个普通的hello方法,请问先打印邮件还是hello
class Phone {
public synchronized void sendEmail() {
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { }
System.out.println("====sendEmail");
}
public synchronized void sendSMS() {System.out.println("====sendSMS");}
public void hello() {System.out.println("===hello");}
}
public class LockDemo {
public static void main(String[] args) {
Phone phone = new Phone(); // 资源类
new Thread(phone::sendEmail,"a").start();
// try { TimeUnit.MICROSECONDS.sleep(10); } catch (InterruptedException e) { }
new Thread(phone::hello,"b").start();
}
}
运行结果:
===hello
====sendEmail
有两部手机,请问先打印邮件还是短信
class Phone {
public synchronized void sendEmail() {
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { }
System.out.println("====sendEmail");
}
public synchronized void sendSMS() {System.out.println("====sendSMS");}
public void hello() {System.out.println("===hello");}
}
public class LockDemo {
public static void main(String[] args) {
Phone phone = new Phone(); // 资源类1
Phone phone2 = new Phone(); // 资源类2
new Thread(phone::sendEmail,"a").start();
// try { TimeUnit.MICROSECONDS.sleep(10); } catch (InterruptedException e) { }
new Thread(phone2::sendSMS,"b").start();
}
}
运行结果:
====sendSMS
====sendEmail
两个静态同步方法,同一部手机,请问先打印邮件还是短信
class Phone {
public static synchronized void sendEmail() {
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { }
System.out.println("====sendEmail");
}
public static synchronized void sendSMS() {System.out.println("====sendSMS");}
}
public class LockDemo {
public static void main(String[] args) {
new Thread(Phone::sendEmail,"a").start();
try { TimeUnit.MICROSECONDS.sleep(10); } catch (InterruptedException e) { }
new Thread(Phone::sendSMS,"b").start();
}
}
运行结果:
====sendEmail
====sendSMS
两个静态同步方法,2部手机,请问先打印邮件还是短信
一个静态同步方法,一个普通同步方法,同一部手机,请问先打印邮件还是短信
class Phone {
public static synchronized void sendEmail() {
System.out.println("====sendEmail");
}
public synchronized void sendSMS() {System.out.println("====sendSMS");}
}
public class LockDemo {
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{phone.sendEmail();},"a");
// try { TimeUnit.MICROSECONDS.sleep(10); } catch (InterruptedException e) { }
new Thread(()->{phone.sendSMS();},"b").start();
}
}
运行结果(在我的机器上是下面这样):
====sendSMS
一个静态方法,一个同步方法,2部手机,请问先打印邮件还是短信
class Phone {
public static synchronized void sendEmail() {
System.out.println("====sendEmail");
}
public synchronized void sendSMS() {System.out.println("====sendSMS");}
}
public class LockDemo {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(()->{phone.sendEmail();},"a");
// try { TimeUnit.MICROSECONDS.sleep(10); } catch (InterruptedException e) { }
new Thread(()->{phone2.sendSMS();},"b").start();
}
}
运行结果(在我的机器上是下面这样):
====sendSMS
总结
高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用RPC方法。
1-2示例,一个对象里如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其他的线程就只能等待,换句话说,某一个时刻内,只能有一个唯一的线程去访问这些synchronize方法。锁的是当前对象this,被锁定后,其他的线程都不能进入到当前对象的其他的synchronize方法。
3-4示例,加个普通方法后发现和同步锁无关,hello
换成两个对象后,不是同一把锁了,情况立刻变化。
5-6示例,都变成静态同步方法之后,情况又变化
三种synchronize锁的内容有一些差别:
对于普通同步方法,锁的是当前示例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁(实例对象本身)
对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
对于同步方法块,锁的是synchronize括号内的对象
7-8示例,当一个线程试图访问同步代码时首先它要得到锁,退出或抛出异常时必须释放锁。
所有的普通同步方法用的都是同一把锁(实例对象本身),就是new出来的具体实例对象本身,本类this,也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
所有的静态同步方法用的也是同一把锁(类对象本身),就是我们说过的唯一模板class。具体实例对象this和唯一模板class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的,但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
sync底层字节码
一定是一个enter两个exit吗,如果自定义异常呢?
反编译synchronized锁的是什么
为什么任何一个对象都可以成为一个锁
在HotSpot虚拟机中,monitor采用ObjectMonitor实现
C++源码解读
ObjectMonitor.java->ObjectMonitor.cpp->objectMonitor.hpp
objectMonitor.hpp
每个对象天生都带着一个对象监视器
公平锁和非公平锁
class Ticket {
private int num = 50;
// 默认用的非公平锁
private Lock lock = new ReentrantLock();
public void sale() {
lock.lock();
try {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "t卖出第"
+ (num--) + "张票,还剩下"+num);
}
} finally {
lock.unlock();
}
}
}
public class LockByteCodeDemo {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{for (int i = 0; i <= 55; i++) ticket.sale();},"a").start();
new Thread(()->{for (int i = 0; i <= 55; i++) ticket.sale();},"b").start();
new Thread(()->{for (int i = 0; i <= 55; i++) ticket.sale();},"c").start();
new Thread(()->{for (int i = 0; i <= 55; i++) ticket.sale();},"d").start();
new Thread(()->{for (int i = 0; i <= 55; i++) ticket.sale();},"e").start();
}
}
以下是运行的结果:
a 卖出第50张票,还剩下49
a 卖出第49张票,还剩下48
a 卖出第48张票,还剩下47
a 卖出第47张票,还剩下46
a 卖出第46张票,还剩下45
c 卖出第45张票,还剩下44
c 卖出第44张票,还剩下43
c 卖出第43张票,还剩下42
c 卖出第42张票,还剩下41
c 卖出第41张票,还剩下40
c 卖出第40张票,还剩下39
c 卖出第39张票,还剩下38
c 卖出第38张票,还剩下37
c 卖出第37张票,还剩下36
c 卖出第36张票,还剩下35
c 卖出第35张票,还剩下34
c 卖出第34张票,还剩下33
c 卖出第33张票,还剩下32
c 卖出第32张票,还剩下31
c 卖出第31张票,还剩下30
c 卖出第30张票,还剩下29
c 卖出第29张票,还剩下28
c 卖出第28张票,还剩下27
c 卖出第27张票,还剩下26
c 卖出第26张票,还剩下25
c 卖出第25张票,还剩下24
c 卖出第24张票,还剩下23
c 卖出第23张票,还剩下22
c 卖出第22张票,还剩下21
c 卖出第21张票,还剩下20
c 卖出第20张票,还剩下19
c 卖出第19张票,还剩下18
c 卖出第18张票,还剩下17
c 卖出第17张票,还剩下16
c 卖出第16张票,还剩下15
c 卖出第15张票,还剩下14
c 卖出第14张票,还剩下13
c 卖出第13张票,还剩下12
c 卖出第12张票,还剩下11
c 卖出第11张票,还剩下10
c 卖出第10张票,还剩下9
c 卖出第9张票,还剩下8
b 卖出第8张票,还剩下7
b 卖出第7张票,还剩下6
b 卖出第6张票,还剩下5
b 卖出第5张票,还剩下4
b 卖出第4张票,还剩下3
b 卖出第3张票,还剩下2
b 卖出第2张票,还剩下1
b 卖出第1张票,还剩下0
查看结果,可怕,基本上全被 线程 c 抢走了。由此可见,非公平锁的一个缺点之一:容易出现锁饥饿的显现。
如果,用公平锁呢?看看下面的代码,情况会改善吗?只需要改成
private Lock lock = new ReentrantLock(true);
运行结果:
a 卖出第50张票,还剩下49
a 卖出第49张票,还剩下48
a 卖出第48张票,还剩下47
a 卖出第47张票,还剩下46
a 卖出第46张票,还剩下45
a 卖出第45张票,还剩下44
a 卖出第44张票,还剩下43
a 卖出第43张票,还剩下42
b 卖出第42张票,还剩下41
a 卖出第41张票,还剩下40
c 卖出第40张票,还剩下39
b 卖出第39张票,还剩下38
a 卖出第38张票,还剩下37
c 卖出第37张票,还剩下36
b 卖出第36张票,还剩下35
a 卖出第35张票,还剩下34
c 卖出第34张票,还剩下33
b 卖出第33张票,还剩下32
d 卖出第32张票,还剩下31
a 卖出第31张票,还剩下30
c 卖出第30张票,还剩下29
b 卖出第29张票,还剩下28
d 卖出第28张票,还剩下27
a 卖出第27张票,还剩下26
c 卖出第26张票,还剩下25
b 卖出第25张票,还剩下24
e 卖出第24张票,还剩下23
d 卖出第23张票,还剩下22
a 卖出第22张票,还剩下21
c 卖出第21张票,还剩下20
b 卖出第20张票,还剩下19
e 卖出第19张票,还剩下18
d 卖出第18张票,还剩下17
a 卖出第17张票,还剩下16
c 卖出第16张票,还剩下15
b 卖出第15张票,还剩下14
e 卖出第14张票,还剩下13
d 卖出第13张票,还剩下12
a 卖出第12张票,还剩下11
c 卖出第11张票,还剩下10
b 卖出第10张票,还剩下9
e 卖出第9张票,还剩下8
d 卖出第8张票,还剩下7
a 卖出第7张票,还剩下6
c 卖出第6张票,还剩下5
b 卖出第5张票,还剩下4
e 卖出第4张票,还剩下3
d 卖出第3张票,还剩下2
a 卖出第2张票,还剩下1
c 卖出第1张票,还剩下0
大致看下源码
按序排队公平锁,就是判断同步队列是否还有先驱节点的存在(我前面还有人吗),如果没有先驱节点才能获得锁。
先占先得非公平锁,是不管这个事的,只要能抢到同步状态就可以。
为什么会有公平锁和非公平锁的设计,为什么默认非公平锁
- 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间。
- 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当1个线程请求获取锁同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放的线程在此刻获取同步状态的概率就变得很大,所以就减少了线程的开销。
使用公平锁会有什么问题
公平锁保证了排队的公平性,非公平锁霸气的忽视这个规则,所以就有可能导致排队的长时间在排队,也没有机会获取到锁。这就是传说中的“饥饿锁”。
什么时候用公平,什么时候用非公平锁
如果为了提高更高的吞吐量,很明显非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了。
否则就用公平锁,大家公平使用。
可重入锁(又名递归锁)
说明
是指在同一个线程在外层方法获取锁的时候,再进入该线程的内含方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前获取过还没释放而阻塞。
如果是一个有synchronized修饰的递归调用方法,程序第二次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
可重入锁四个字拆开来看
可:可以。重:重复。入:进入。锁:同步锁。
进入什么?进入同步域(即 同步代码块 / 方法或显式锁锁定的代码)
隐式锁(即 synchronized关键字使用的锁),默认使用的是可重入锁
指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。简单来说就是,在一个synchronized修改的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
。
与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。
synchronized的重入的实现机制:
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放锁。
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
synchronized的代码演示
public class RnEntryLockDemo {
static Object obj = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (obj) {
System.out.println("外层");
synchronized (obj) {
System.out.println("中层");
synchronized (obj) {
System.out.println("内层");
}
}
}
}, "t1").start();
}
}
运行结果:
外层
中层
内层
ReentrantLock的代码演示
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
new Thread(() -> {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "t" + "外层");
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "t" + "内层");
} finally {
lock.unlock();
}
} finally {
lock.unlock();
}
}, "t1").start();
}
运行结果:
t1 ===外层
t1 ===内层
如果,只有一个lock.unlock呢,程序还会正常结束吗?
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
new Thread(() -> {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "t" + "===外层");
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "t" + "===内层");
} finally {
// lock.unlock();
}
} finally {
lock.unlock();
}
}, "t1").start();
}
运行结果:
t1 ===外层
t1 ===内层
通过结果发现,就算只有一个lock.unlock,也可以正常结束,这是为什么呢?
如果再来一个线程呢?
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
new Thread(() -> {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "t" + "===外层");
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "t" + "===内层");
} finally {
// lock.unlock();
}
} finally {
lock.unlock();
}
}, "t1").start();
new Thread(() -> {
try {
lock.lock();
System.out.println("===2222");
} finally {
lock.unlock();
}
}, "t2").start();
}
运行结果后,坑爹的事情出现了:打印完(t1外层 t2内层)之后,程序还是一直处于运行状态。。。自己倒是没事,倒是别别的线程坑了。所以,一定要注意:加锁几次,就一定以要解锁几次。
死锁及排查
死锁是什么
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
写个死锁看看
public class RnEntryLockDemo {
static Object lockA = new Object();
static Object lockB = new Object();
public static void main(String[] args) {
new Thread(()->{
synchronized (lockA){
System.out.println(Thread.currentThread().getName()+"自己持有A锁,期待获得B锁");
try { TimeUnit.SECONDS.sleep(1); } catch (Exception e){ }
synchronized (lockB) {
System.out.println(Thread.currentThread().getName()+"==期待获得B锁");
}
}
},"A").start();
new Thread(()->{
synchronized (lockB){
System.out.println(Thread.currentThread().getName()+"自己持有B锁,期待获得A锁");
try { TimeUnit.SECONDS.sleep(1); } catch (Exception e){ }
synchronized (lockA) {
System.out.println(Thread.currentThread().getName()+"==期待获得A锁");
}
}
},"B").start();
}
}
运行结果如下,且程序一直处于等待中
A自己持有A锁,期待获得B锁
B自己持有B锁,期待获得A锁
如何证明写的程序是死锁呢?
可以用jps,也可以用jconsole验证
写锁(独占锁)/ 读锁(共享锁)
自旋锁SpinLock
无锁->独占锁->读写锁->邮戳锁
无锁->偏向锁->轻量锁->重量锁
其他细节
LockSupport与线程中断
线程中断机制
从阿里蚂蚁金服面试题讲起
什么是中断
首先,一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。所以,Thread.stop,Thread.suspend,Thread.resume都已经废弃了。
其次,在Java中没有办法立即停止一条线程,然后停止线程却显得尤为重要,如取消一个耗时操作。因此,Java提供了一种用于停止线程的机制,即 中断。
中断只是一种协作机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现
。若要中断一个线程,需要手动调用该线程的 interrupt
方法,该方法也 仅仅是将线程对象的中断标识设成 true
,接着需要自己写代码不断的检测当前线程的标识位,如果为 true,标识别的线程要求这条线程中断,此时究竟该做什么需要程序员自己写代码实现。
每个线程对象都有一个标识,用于表示线程是否被中断;该标识位为true标识中断,为false标识未中断;通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中自己调用。
中断的相关API
// 实例方法,仅仅是设置线程的中断状态为true,不会停止线程
public void interrupt()
// 静态方法,Thread.interrupted();
// 判断线程是否被中断,并清除当前中断状态,这个方法做了两件事情:
// 1. 返回当前线程的中断状态,2. 将当前线程的中断状态设为false。
// 这个方法有点不好理解, 因为连续两次调用结果可能不一样。
public static boolean interrupted()
// 实例方法,判断当前线程是否被中断(通过检查中断标志位)
public boolean isInterrupted()
面试题:如何使用中断标识停止线程?
通过volatile
public class InterruptDemo {
private static volatile boolean isStop = false;
public static void main(String[] args) {
new Thread(() -> {
while (true) {
if (isStop) {
System.out.println("-----isStop = true,程序结束。");
break;
}
System.out.println("------hello isStop");
}
}, "t1").start();
try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { e.printStackTrace(); }
new Thread(() -> {
isStop = true;
}, "t2").start();
}
}
运行结果:
------hello isStop
.......N多个 hello isStop
-----isStop = true,程序结束。
通过AtomicBoolean
private static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
public static void main(String[] args) {
new Thread(() -> {
while (true) {
if (atomicBoolean.get()) {
System.out.println("-----isStop = true,程序结束。");
break;
}
System.out.println("------hello isStop");
}
}, "t1").start();
try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { }
new Thread(() -> {
atomicBoolean.set(true);
},"t2").start();
}
运行结果:
------hello isStop
.......N多个 hello isStop
-----isStop = true,程序结束。
通过Thread自带的中断API方法实现
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("isInterrupted is true, 程序结束");
break;
}
System.out.println("hello Interrupt");
}
}, "t1");
t1.start();
try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { }
new Thread(() -> {
t1.interrupt();
System.out.println("t2已运行");
},"t2").start();
}
运行结果:
hello Interrupt
.....N多个hello Interrupt
t2已运行
isInterrupted is true, 程序结束
如何验证interrupt不是立即停止
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 300; i++) {
System.out.println("=====i" + i);
}
System.out.println("t1.interrupt()调用之后02"
+ Thread.currentThread().isInterrupted());
}, "t1");
t1.start();
System.out.println("t1.interrupt()调用之前,t1线程的中断标志默认值===="
+ t1.isInterrupted());
try { TimeUnit.MILLISECONDS.sleep(3); } catch (Exception e) { }
t1.interrupt();
System.out.println("t1.interrupt()调用之后01" + t1.isInterrupted());
try { TimeUnit.MILLISECONDS.sleep(300); } catch (Exception e) { }
System.out.println("t1.interrupt()调用之后03" + t1.isInterrupted());
}
运行结果:
t1.interrupt()调用之前,t1线程的中断标志默认值====false
=====i0
=====i1
=====i2
t1.interrupt()调用之后01true
......
=====i299
t1.interrupt()调用之后02true
t1.interrupt()调用之后03false
既然不是立即停下来,那如果调用interrupt后加入一个sleep呢
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
int num = 0;
int num2 = 0;
for (int i = 0; i < 30; i++) {
while (true) {
if (Thread.currentThread().isInterrupted()) {
System.out.println("t1线程isInterrupted=true,自己退出了" + num);
num++;
break;
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello isInterrupt"+num2);
num2++;
}
}
}, "t1");
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(()->{
t1.interrupt();
System.out.println("t2线程执行了,t1线程的中断标识状态为="+t1.isInterrupted());
},"t2").start();
}
运行结果:
hello isInterrupt0
t2线程执行了,t1线程的中断标识状态为=true
hello isInterrupt1
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.lyw.InterruptDemo.lambda$main$0(InterruptDemo.java:21)
at java.lang.Thread.run(Thread.java:748)
hello isInterrupt2
hello isInterrupt3
hello isInterrupt4
.....
程序一直运行,停止不下来了
可以发现,既然interrupt()方法不是立即停下来,加了sleep后,程序直接停不下来了。以下是interrupt()方法的源码:
/**
以下是部分注释:
* <p> If this thread is blocked in an invocation of the {@link
* Object#wait() wait()}, {@link Object#wait(long) wait(long)}, or {@link
* Object#wait(long, int) wait(long, int)} methods of the {@link Object}
* class, or of the {@link #join()}, {@link #join(long)}, {@link
* #join(long, int)}, {@link #sleep(long)}, or {@link #sleep(long, int)},
* methods of this class, then its interrupt status will be cleared and it
* will receive an {@link InterruptedException}.
*/
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
根据源码,可以发现,如果此线程在调用Object类的wait() 、 wait(long)或wait(long, int)方法或join() 、 join(long) 、 join(long, int)被阻塞、 sleep(long)或sleep(long, int) ,此类的方法,则其中断状态将被清除并收到InterruptedException 。
解决办法:catch里加Thread.currentThread().interrupt();
静态方法Thead.interrupted()
方法的注释也清晰的表达了“中断状态将会根据传入的ClearInterrupted参数值确定是否重置”。所以,静态方法interrupted将会清除中断状态(传入的参数ClearInterrupted为true),实例方法isInterrupted则不会(传入的参数ClearInterrupted为false)。
总结
具体来说,当对一个线程调用interrupt()时,1. 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,仅此而已
。被设置中断标志的线程将继续正常运行,不受影响
。所以,interrupt()并不能真正的中断线程,需要被调用的线程自己进行配合才行。 2. 如果线程处于被阻塞状态(例如处于sleep、wait、join等状态),在别的线程中调用当前线程对象的interrupt方法,那么线程将立即退出被阻塞状态,并抛出一个InterruptedExceptin异常。
sleep方法抛出InterruptdException后,中断标识也被清空置为false,我们在catch没有通过th.interrupt()方法再次将中断标识置为true,这就导致无限循环了。
LockSupport是什么
LockSupport是用来创建锁和其他同步类的基本线程阻塞源语。LockSupport中的park()和unpard()的作用分别是阻塞线程和接触阻塞线程。
线程等待唤醒机制
3种让线程等待和唤醒的方法
使用Object中的wait()让线程等待,使用Object的notify()唤醒线程
使用JUC包中Condition的await()让线程等待,使用signal()唤醒线程
LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
Object类中的wait和notify方法实现线程等待和唤醒
static Object obj = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "come in");
try { obj.wait(); } catch (InterruptedException e) { }
System.out.println(Thread.currentThread().getName() + "被唤醒");
}
}, "t1");
t1.start();
try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { }
new Thread(() -> {
synchronized (obj) {
obj.notify();
System.out.println(Thread.currentThread().getName() + "发出通知");
}
}, "t2").start();
}
运行结果:
t1come in
t2发出通知
t1被唤醒
如果把synchronized注释掉,立马报错
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
// synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "come in");
try { obj.wait(); } catch (InterruptedException e) { }
System.out.println(Thread.currentThread().getName() + "被唤醒");
// }
}, "t1");
t1.start();
try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { }
new Thread(() -> {
// synchronized (obj) {
obj.notify();
System.out.println(Thread.currentThread().getName() + "发出通知");
// }
}, "t2").start();
}
运行结果:
t1come in
Exception in thread "t1" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.lyw.InterruptDemo.lambda$main$0(InterruptDemo.java:11)
at java.lang.Thread.run(Thread.java:748)
Exception in thread "t2" java.lang.IllegalMonitorStateException
at java.lang.Object.notify(Native Method)
at com.lyw.InterruptDemo.lambda$main$1(InterruptDemo.java:19)
at java.lang.Thread.run(Thread.java:748)
可以先运行nofity,后运行wait吗
static Object obj = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { }
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "come in");
try { obj.wait(); } catch (InterruptedException e) { }
System.out.println(Thread.currentThread().getName() + "被唤醒");
}
}, "t1");
t1.start();
try { TimeUnit.MILLISECONDS.sleep(100); } catch (Exception e) { }
new Thread(() -> {
synchronized (obj) {
obj.notify();
System.out.println(Thread.currentThread().getName() + "发出通知");
}
}, "t2").start();
}
运行结果,可以发现,程序一直处于运行状态
t2发出通知
t1come in
Condition接口中的await和signal方法实现线程的等待和唤醒
private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "come in");
condition.await();
System.out.println(Thread.currentThread().getName() + "被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "t1").start();
new Thread(() -> {
lock.lock();
try {
condition.signal();
System.out.println(Thread.currentThread().getName() + "唤醒线程t1");
} finally {
lock.unlock();
}
}, "t2").start();
}
运行结果:
t1come in
t2唤醒线程t1
t1被唤醒
如果把lock.lock和lock.unlock()去掉呢
private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
public static void main(String[] args) throws Exception {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "come in");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "被唤醒");
}, "t1").start();
new Thread(() -> {
condition.signal();
}, "t2").start();
}
运行结果:
t1come in
Exception in thread "t2" Exception in thread "t1" java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.signal(AbstractQueuedSynchronizer.java:1939)
at com.lyw.InterruptDemo.lambda$main$1(InterruptDemo.java:27)
at java.lang.Thread.run(Thread.java:748)
java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.fullyRelease(AbstractQueuedSynchronizer.java:1723)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2036)
at com.lyw.InterruptDemo.lambda$main$0(InterruptDemo.java:16)
at java.lang.Thread.run(Thread.java:748)
Object和Condition使用的限制条件
线程要先获得锁并持有锁,必须在锁块(synchronized或lock)中。
必须要 先等待,后唤醒,线程才能被唤醒。
LockSupport类中的park等待和unpark唤醒
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程,每个线程都有一个许可(Permit),Permit只有两个值,0和1,默认是0。可以把许可看做是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。
park()
public static void park() {
UNSAFE.park(false, 0L);
}
permit默认是0,所以一开始调用park方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置成1时,park方法会被唤醒,然后会将permit再次设置成0并返回。
unpark()
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,即 之前阻塞中的LockSupport.park()方法会立即返回。
Java内存模型值JMM
先从大厂面试题开始
你知道什么是Java内存模型JMM吗
JMM与volatile它们两个之间的关系
JMM有哪些特性or它的三大特性是什么
为什么要有JMM,它为什么出现?作用和功能上什么
happens-bofore先行发生原则你有了解过吗
计算器硬件存储体系
计算机存储结构,从本地磁盘到主存到CPU,也就是从磁盘到内存,到CPU,一般对应的程序操作就是从数据库查数据到内存然后到CPU计算。
Java内存模型Java Memory Model
JMM(Java内存模型Java Memory Model,简称 JMM)本身是一种抽象的概念,并不真实存在,它仅仅描述的一种约定或规范,通过这组规范定义了程序中(尤其是多线程)各个变量的读写过程访问方式并决定一个线程对共享变量的写入何时以及如何变成对另一个线程可见,关键技术点都是围绕多线程的原子性
,可见性
,有序性
展开。
能干嘛:1.通过JMM来实现线程和主内存之间的抽象关系。2. 屏蔽各个硬件平台和操作系统的内存访问差异及实现让Java程序在各个平台下都能达到一致的内存访问效果。
JMM规范下,三大特性
有序性:对于一个线程的执行代码而言,我们总是习惯认为代码的执行总是从上到下,有序执行。但为了提供性能,编译器和处理器通常会对指令序列进行重新排序。指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致,即可能产生脏读,简单说,两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化。
源代码—>编译器优化的重排—>指令并行的重排—>内存系统的重排—>最终执行的指令
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。处理器在进行重排时必须要考虑指令之间的数据依赖性。多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
JMM规范下,多线程对变量的读写过程
JMM规范下,多线程先行发生原则之happens-before
在JMM中,如果一个操作执行的结果需要对另一个操作可见性,或者代码重排序,那么这两个操作之间必须存在happens-before关系。
happens-bofore之8条:
- 次序规则
一个线程内,按照代码顺序,写在前面的操作先行发生于写在后面的操作。说白一点:前一个操作的结果可以被后续的操作获取。比如前面一个操作把变量x赋值为1,那后面一个操作肯定能知道x已经变成1了。 - 锁定规则
一个unlock操作先行发生于后面(这里的“后面“上指时间上的先后),对同一把锁的lock操作。 - volatile变量规则
对于一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的,这里的“后面”同样是指时间上的先后。 - 传递规则
如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A一定先行发生于操作C。 - 线程启动规则(Thread Start Rule)
Thread对象的start方法先行发生于此线程的每一个动作 - 线程中断规则(Thread Interruption Rule)
对线程interrupt方法的调用先行发生于被中断线程的代码检测中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。 - 线程终止规则(Thread Termination Rule)
线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread::join()方法检测是否结束,Thread::isAlive()的返回值等手段检测线程是否已经终止执行。 - 对象终结规则(Finalizer Rule)
一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。说的直白点,对象没有完成初始化之前,是不能调用finalized()方法的。
volatile与Java内存模型
被volatile修改的变量有2大特点
特点:可见性,有序性。
volatile的内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。当读一个volatile变量时,JMM会把该线程对应的本地变量设置为无效,直接从主内存中读取共享变量。所以volatile的写内存语义是直接刷新到主内存中,读到内存语义是直接从内存中读取。
内存屏障(面试重点必须拿下)
先说生活case
没有管控,顺序难保,比如国庆游玩的长城
设定规则,禁止乱序,比如上海南京步行街武警“人墙”当红灯
是什么
内存屏障,(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得之前所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令
,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性,但volatile无法保证原子性。
内存屏障之前的所有写操作都要会写到主内存,内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。
因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。一句话,对一个volatile域的写,happens-beofre于任意后续对这个volatile域的读,也叫写后读。
volatile凭什么可以保证可见性和有序性
JVM中提供了四类内存屏障指令
C++源码分析
Unsafe.class
/**
* Ensures lack of reordering of loads before the fence
* with loads or stores after the fence.
* @since 1.8
*/
public native void loadFence();
/**
* Ensures lack of reordering of stores before the fence
* with loads or stores after the fence.
* @since 1.8
*/
public native void storeFence();
/**
* Ensures lack of reordering of loads or stores before the fence
* with loads or stores after the fence.
* @since 1.8
*/
public native void fullFence();
Unsafe.cpp -> OrderAccess.hp -> orderAccess_linux_x86.inline.hpp
四大内存屏障分别是什么意思
屏障类型 | 演示说明 | 说明 |
---|---|---|
LoadLoad | Load1;LoadLoad;Load2 | 保证load1的读取操作在load2及后续读取操作之前执行 |
StoreStore | Store1;StoreStore;Store2 | 在store2及其后的写操作执行前,保证store1的写操作已刷新到内存中 |
LoadStore | Load1;LoadStore;Store2 | 在store2及其后的写操作执行前,保证load1的读操作已读取结束 |
StoreLoad | Store1;StoreLoad;Load2 | 保证store1的写操作已刷新主内存之后,load2及其后的读操作才能执行 |
inline void OrderAccess::loadload() { acquire(); }
inline void orderaccess::storestore() { release(); }
inline void OderAccess::loadstore() { qcquire(); }
inline void OrderAccess::storestore() { fence(); }
volatile两个特性
happens-before之volatile变量规则
第一个操作 | 第二个操作:普通读写 | 第二个操作:volatile读 | 第二个操作:volatile写 |
---|---|---|---|
普通读写 | 可以重排 | 可以重排 | 不可以重排 |
volatile读 | 不可以重排 | 不可以重排 | 不可以重排 |
volatile写 | 可以重排 | 不可以重排 | 不可以重排 |
当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会被重新到volatile读之前。 | |||
当第二个操作为volatile写时,无论第一个操作是什么,都不能重排序。这个操作保证了volatile写之后的操作不会被重排到volatile写之前。 | |||
当第一个为volatile写时,第二个操作为volatile时,不能重排。 |
JMM将内存屏障插入策略分为4种
写:
- 在每个volatile写操作的前面插入一个StoreStore屏障
- 在每个volatile写操作的后面插入一个StoreLoad屏障
读:
- 在每个volatile读操作的后面插入一个LoadLoad屏障
- 在每个volatile读操作的后面插入一个LoadStore屏障
static boolean flag = false;
public static void main(String[] args) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()
+"come in.");
while (flag) {
System.out.println("===============");
}
System.out.println("t1 over.");
},"t1").start();
try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { }
new Thread(()->{flag = true;},"t2").start();
}
运行结果:
程序没有停止下来
解决办法,flag加上volatile。
上述代码原理解释:
线程1中为何看不到主线程main修改为flag的值?
问题可能:
- 主线程修改了flag之后没有刷新到主内存,所以t2线程看不到
- 主线程将flag刷新到了主内存,但是t1一直读取的是自己工作内存中flag的值,没有去主要内存中更新获取flag的值。
我们的诉求:
- 线程中修改了工作内存中的副本之后,立即刷新到主内存中。
- 工作内存中每次读取共享变量时,都去主内存中重新获取,然后拷贝到工作内存。
解决:
使用volatile修饰共享变量,就可以达到上面的效果,被volatile修饰的变量有以下特点:
- 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存。
- 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存。
Java内存模型定义的8种工作内存与主内存之间的原子操作:
read > load > use > assign > store > write > lock > unlock
读取 > 加载 > 使用 > 赋值 > 存储 > 写入 > 锁定 > 解锁
read: 作用于主内存,将常量的值从主内存传输到工作内存,主内存到工作内存
load: 作用于工作内存,将read从主内存传输到变量值放入工作内存副本中,即 数据加载
use: 作用于工作内存,将工作内存变量副本到值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作
assign: 作用于工作内存,将从执行引擎收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作
store: 作用于工作内存,将赋值完毕的工作变量的值写回给主内存
write: 作用于主内存,将store传输过来的变量值赋值给主内存中的变量
由于上述只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令
lock: 作用于主内存,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程
unlock: 作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用。
从i++的字节码角度说明,volatile不具有原子性
public class Demo {
public static void main(String[] args) {
MyNumber myNumber = new MyNumber();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myNumber.addPlusPlus();
}
},String.valueOf(i)).start();
}
try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) { }
System.out.println(Thread.currentThread().getName()
+"==="+myNumber.number);
}
}
class MyNumber {
volatile int number = 0;
public void addPlusPlus() { number++;}
}
运行结果:9958。多次运行,发现结果,不是10*1000=10000,几乎是不可能的。注意,是几乎。那么,通过本次示例,说明volatile是不具有原子性的。
通过字节码查看:
public class Demo {
public volatile int number;
public void add() {
number++;
}
}
字节码:
public void add();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field number:I
5: iconst_1
6: iadd
7: putfield #2 // Field number:I
10: return
LineNumberTable:
line 8: 0
line 9: 10
可以发现,number++被拆解成了 3 个指令:执行getfield拿到原始number;执行add进行加1操作;执行putfield写把累加后的值写回。
原子性指的是一个操作是 不可中断
的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
如果第二个线程在第一个线程读取旧值和写回新值期间读取number的域值,那么第二个线程和第一个线程就会看到同一个值,并执行相同值的加 1 操作,这也就造成了线程安全失败,因此对于add方法必须使用synchronized修饰,以便保证线程安全。
多线程环境下,“数据计算”和“数据赋值”操作可能发生多次,即 操作非原子。若数据在加载之后,若主内存count变量发生修改之后,由于线程工作内存中的值在此前已经加载,从而不会对变更操作作出相应变化,即 私有内存和公共内存中变量不同步,进而导致数据不一致。对于volatile变量,JVM只是保证从主内存加载到线程工作内存的值是最新的,也就是数据加载时说最新的。由于可见,volatile解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改共享变量的场景必须使用加锁同步
。
禁止重排序
内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行。
数据依赖性:若两个操作访问同一个变量,切这两个操作有一个为写操作,此时两操作就存在数据依赖性。
案例:
不存在数据依赖关系,可以重排序。
重排前 | 重排后 |
---|---|
int a = 1; // 1 | int b = 2; // 2 |
int b = 2; // 2 | int a = 1; // 1 |
int c = a + b; // 3 | int c = a + b; // 3 |
结论:编译器调整了语句的顺序,但是不影响程序的最终结果 | 重排序OK |
存在数据依赖关系,禁止重排序===>重排序发生,会导致程序运行结果不同。编译器和处理器在重排序时,会遵守数据依赖性,不会改变存在依赖关系的两个操作的执行,但不同处理器和不同线程之间的数据性不会被编译器和处理器考虑,其只会作用于单处理器和单线程环境,下面3种情况,只要重排序两个操作的执行顺序,程序的执行结果就会发生变化。 | |
名称 | 代码示例 |
-------- | ----- |
写后读 | a = 1; b = a; |
写后写 | a = 1; a = 2; |
读后写 | a = b; b = 1; |
public class Demo {
int i = 0;
volatile boolean flag = false;
public void write() {
i = 2;
flag = true;
}
public void read() {
if (flag) {
System.out.println("=====" + i);
}
}
}
i = 2; // 普通写
StoreStore屏障 // 禁止上面的普通写与下面的volatile写重排序
flag = ture; // volatile写
StoreLoad屏障 // 禁止上面的volatile写与下面可能有的volatile读/写重排序
if (flag) // volatile读
LoadLoad屏障 // 禁止处理器把上面的volatile读与下面的普通读重排序
LoadStore屏障 // 禁止处理器把上面的volatile读与下面的普通写重排序
System… // 普通读
如何正确使用volatile
单一赋值即可,但含复合运算符不可以
状态标识,判断业务是否可以结束
开销低低读,写锁策略
DCL双端锁的发布
public class SafeDoubleCheckSingleton {
private static SafeDoubleCheckSingleton singleton;
private SafeDoubleCheckSingleton() { }
public static SafeDoubleCheckSingleton getInstance() {
if (singleton == null) {
// 多线程并发创建对象时,会痛哟加锁保证只有一个线程能创建对象
synchronized (SafeDoubleCheckSingleton.class) {
if (singleton == null) {
// 隐患:多线程环境下,由于重排序,
// 该对象可能还未完成初始化就被其他线程读取
singleton = new SafeDoubleCheckSingleton();
}
}
}
// 对象创建完毕,执行getInstance()将不再需要获取锁,直接返回创建对象
return singleton;
}
}
解决办法:通过volatile声明,实现线程安全的延迟初始化
最后的小总结
内存屏障是什么:是一种屏障指令,它使得CPU 或 编译器 对屏障指令的 前 和 后 所发出的内存操作 执行一个排序的约束。也叫 内存栅栏 或 栅栏指令。
凭什么我们写了一个volatile关键字,系统底层加入内存屏障?两者关系怎么勾搭上的?
字节码角度:flags: ACC_VOLATILE,JVM在把字节码 生成 机器码 的时候,发现操作上volatile的变量的话,就会根据JVM要求,在相应的位置去插入内存屏障指令。
volatile关键字保证可见性,意味着:
- 对一个volatile修饰的变量进行读操作的话,总是能够读到这个变量的最新的值,也就是这个变量最后被修改的值
- 一个线程修改了volatile修饰的变量的值的时候,那么这个变量的新的值,会立即刷新回到主内存中
- 一个线程去读取volatile修饰的变量的值的时候,该变量在工作内存中的数据无效,需要重新到主内存去读取最新的数据
volatile可见性:
对比java.util.concurent.locks.Lock来理解:
CPU执行机器码指令的时候,是使用lock前缀指令来实现volatile的功能的。
Lock指令,相当于内存屏障,功能也类似屏障的功能:
(1)首先对总线/缓存加锁,然后去执行后面的指令,最后,释放锁,同时把高速缓存的数据刷新到主内存
(2)在lock锁住总线/缓存的时候,其他CPU的读写请求就会被阻塞,直到锁释放。Lock过后的写操作,会让其他CPU的高速缓存中相应的数据失效,这样后续这次CPU在读取数据的时候,就会从主内存去加载最新的数据。
加了Lock指令过后的具体表现,就跟JVM添加内容屏障后一样。
CAS
没有CAS之前
是什么
CAS(CompareAndSwap)
CAS有3个操作数,位置内存值V,旧的预期值A,要修改的更新值B。
当且仅当旧的预期值A和内存值V相同时,将内存值V修改为B,否则说明都不做或重来。
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 3004)
+ "===" + atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 4442)
+ "===" + atomicInteger.get());
}
运行结果:
5
true===3004
false===3004
说明,原理
compareAndSet源码:
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates
* that the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this,
valueOffset, expect, update);
}
public final native
boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
硬件级别保证
CAS是JDK提供的非阻塞式
原子性操作,它通过硬件保证
了比较-更新的原子性。它是非阻塞的且自身原子性,也就是说效率更高且通过硬件保证,更可靠。
CAS是一条CPU的原子指令(cmpxchg
)指令,不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxche指令。
执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后执行cas操作,也就是说CAS的原子性实际上说由CPU实现
的,其实在这一点上还是有排他锁的,只是比起用synchronized,这里的排他时间要短的多,所以在多线程环境下性能会比较好。
CASDemo代码
CAS底层原理?如果知道,谈谈你对UnSafe类的理解
UnSafe是CAS的核心类,由于Java方法无法直接访问底层操作系统,需要通过本地(native)方法来访问,UnSafe相当于一个后门,基于该类可以直接操作特定内存的数据。UnSafe类存在于sun.misc包中,其方法内部操作可以像C的指针
一样直接操作内存,因为Java中CAS操作的执行依赖于UnSafe类的方法。
注意UnSafe类中的所有方法都是native修饰的,也就是说UnSafe类中的方法都直接调用操作系统底层资源执行相应任务。
i++线程不安全,那atomicInteger.getAndIncrement():
CAS的全称是Compare- And- Swap,它是一条CPU并发原语。它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。AtomicInteger类主要利用CAS(compare and swap) + volatile + native 方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。
假设线程A和线程B两个线程同时执行getAddInt操作(分别跑在不同的cpu上):
1.AtomicInteger里面的value值原来为3,即 主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
2.线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起
3.线程B也通过getIntVolatile(var, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法,比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
4.这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值3和主内存的值4不一致,说明该值已经被其他线程抢先一步修改过了,那A线程本次修改失败,只能重新读取一边重新来一遍了。
5.线程A重新获取value值,因为变量value被volatile修饰,所以其他线程对它的修改,线程A总是能看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。
原子引用
自旋锁,借鉴CAS思想
自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
public class SpinLockDemo {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void myLock() {
System.out.println(Thread.currentThread().getName()+"come in.");
while (!atomicReference.compareAndSet(null, Thread.currentThread())) {
System.out.println(Thread.currentThread().getName()+"正在自旋,等待抢占资源");
}
System.out.println(Thread.currentThread().getName()+"持有锁成功");
}
public void myUnlock() {
atomicReference.compareAndSet(Thread.currentThread(), null);
System.out.println(Thread.currentThread().getName()+"释放锁成功");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(()->{
spinLockDemo.myLock();
try { TimeUnit.MILLISECONDS.sleep(1); } catch (InterruptedException e) { }
spinLockDemo.myUnlock();
},"t1").start();
new Thread(()->{
spinLockDemo.myLock();
spinLockDemo.myUnlock();
},"t2").start();
}
}
运行结果:
t1come in.
t1持有锁成功
t2come in.
t2正在自旋,等待抢占资源
t2正在自旋,等待抢占资源
...
t2持有锁成功
t1释放锁成功
t2释放锁成功
CAS缺点
ABA问题是怎么产生的?
CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差会导致数据的变化。比如说一个线程one从内存位置取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功。尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。
原子操作类之18罗汉增强
分类
基本类型原子类
AtomicInteger
AtomicBoolean
AtomicLong
class MyNumber {
AtomicInteger atomicInteger = new AtomicInteger();
public void addPlus() {
atomicInteger.incrementAndGet();
}
}
public class SpinLock {
static final int size = 50;
public static void main(String[] args) {
MyNumber num = new MyNumber();
for (int i = 0; i < size; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
num.addPlus();
}
}, String.valueOf(i)).start();
}
System.out.println(Thread.currentThread().getName()
+ ",result=" + num.atomicInteger.get());
}
}
5次运行结果:
main,result=50000
main,result=48000
main,result=48680
main,result=49000
main,result=50000
发现运行结果不是每次相同,我们期望的运行结果是50000。原因是因为多线程共同工作,轮到main线程时,上面的线程还没有运行结束导致的。解决办法:只需要在执行最下面的打印方法代码上一行,将线程暂停几秒钟即可。
加上后,运行5次结果,发现结果都一样,都是5000。但是上面的时间是暂停了2秒钟,对于计算机来说,时间开销太大了,得改造成动态的,只要上面的运算结束了,立即执行下面的打印方法。这时,我们引入CatchDownLatch试试。
public static void main(String[] args) throws InterruptedException {
final int size = 50;
CountDownLatch countDownLatch = new CountDownLatch(size);
MyNumber num = new MyNumber();
for (int i = 0; i < size; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 1000; j++) {
num.addPlus();
}
} catch (Exception e) {
} finally {
countDownLatch.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + ",result=" + num.atomicInteger.get());
}
运行5次结果:
main,result=50000
main,result=50000
main,result=50000
main,result=50000
main,result=50000
数组类型原子类
引用类型原子类
AtomicReference
AtomicStampReference
携带版本号的引用类型原子类,可以解决ABA问题
解决修改过几次
状态戳原子引用
AtomicMarkableReference
原子更新带有标记位的引用类型对象
解决是否修改过。它的定义就是将状态戳简化为true/false。类似于一次性筷子
状态戳true/false原子引用
static AtomicMarkableReference amr = new AtomicMarkableReference(100,false);
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
boolean marked = amr.isMarked();
System.out.println(Thread.currentThread().getName() +"===默认修改标志==="+marked);
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { }
boolean boo = amr.compareAndSet(100, 101, marked, !marked);
},"t1").start();
new Thread(()->{
boolean marked = amr.isMarked();
System.out.println(Thread.currentThread().getName() +"===默认修改标志==="+marked);
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { }
boolean boo = amr.compareAndSet(100,4354,marked,!marked);
System.out.println(Thread.currentThread().getName()+"===操作是否成功==="+boo);
System.out.println(Thread.currentThread().getName()+"==="+amr.getReference());
System.out.println(Thread.currentThread().getName()+"==="+amr.isMarked());
},"t2").start();
}
运行结果:
t1===默认修改标志===false
t2===默认修改标志===false
t2===操作是否成功===false
t2===101
t2===true
对象的属性修改原子类
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
AtomicReferenceFieldUpdater
class BankCount {
private String bankName;
public volatile int price;
AtomicIntegerFieldUpdater<BankCount> aifu =
AtomicIntegerFieldUpdater.newUpdater(BankCount.class, "price");
public void profer(BankCount bankCount) {
aifu.incrementAndGet(bankCount);
}
}
public class SpinLock {
public static void main(String[] args) throws InterruptedException {
BankCount bankCount = new BankCount();
CountDownLatch countDownLatch = new CountDownLatch(1000);
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
bankCount.profer(bankCount);
countDownLatch.countDown();
}, String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()
+ "==price==" + bankCount.price);
}
}
原子操作增强类原理深度解析
为什么LongAdder性能这么快?
class ClickNumber {
int number = 0;
public synchronized void add_Synchronized()
{
number++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void add_AtomicInteger()
{
atomicInteger.incrementAndGet();
}
AtomicLong atomicLong = new AtomicLong();
public void add_AtomicLong()
{
atomicLong.incrementAndGet();
}
LongAdder longAdder = new LongAdder();
public void add_LongAdder() { longAdder.increment(); }
LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x+y,0);
public void add_LongAccumulator()
{
longAccumulator.accumulate(1);
}
}
public class SpinLock {
public static final int SIZE_THREAD = 50;
public static final int _1W = 100000;
public static void main(String[] args) throws InterruptedException {
ClickNumber clickNumber = new ClickNumber();
long startTime;
long endTime;
CountDownLatch countDownLatch1 = new CountDownLatch(SIZE_THREAD);
CountDownLatch countDownLatch2 = new CountDownLatch(SIZE_THREAD);
CountDownLatch countDownLatch3 = new CountDownLatch(SIZE_THREAD);
CountDownLatch countDownLatch4 = new CountDownLatch(SIZE_THREAD);
CountDownLatch countDownLatch5 = new CountDownLatch(SIZE_THREAD);
startTime = System.currentTimeMillis();
for (int i = 1; i <= SIZE_THREAD; i++) {
new Thread(() -> {
try {
for (int j = 1; j <= 100 * _1W; j++) {
clickNumber.add_Synchronized();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
countDownLatch1.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch1.await();
endTime = System.currentTimeMillis();
System.out.println("----costTime: " + (endTime - startTime)
+ " 毫秒" + "t add_Synchronized" + "t" + clickNumber.number);
startTime = System.currentTimeMillis();
for (int i = 1; i <= SIZE_THREAD; i++) {
new Thread(() -> {
try {
for (int j = 1; j <= 100 * _1W; j++) {
clickNumber.add_AtomicInteger();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
countDownLatch2.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch2.await();
endTime = System.currentTimeMillis();
System.out.println("----costTime: " + (endTime - startTime)
+ " 毫秒" + "t add_AtomicInteger" + "t"
+ + clickNumber.atomicInteger.get());
startTime = System.currentTimeMillis();
for (int i = 1; i <= SIZE_THREAD; i++) {
new Thread(() -> {
try {
for (int j = 1; j <= 100 * _1W; j++) {
clickNumber.add_AtomicLong();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
countDownLatch3.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch3.await();
endTime = System.currentTimeMillis();
System.out.println("----costTime: " + (endTime - startTime)
+ " 毫秒" + "t add_AtomicLong" + "t" + clickNumber.atomicLong.get());
startTime = System.currentTimeMillis();
for (int i = 1; i <= SIZE_THREAD; i++) {
new Thread(() -> {
try {
for (int j = 1; j <= 100 * _1W; j++) {
clickNumber.add_LongAdder();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
countDownLatch4.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch4.await();
endTime = System.currentTimeMillis();
System.out.println("----costTime: " + (endTime - startTime)
+ " 毫秒" + "t add_LongAdder" + "t"
+ + clickNumber.longAdder.longValue());
startTime = System.currentTimeMillis();
for (int i = 1; i <= SIZE_THREAD; i++) {
new Thread(() -> {
try {
for (int j = 1; j <= 100 * _1W; j++) {
clickNumber.add_LongAccumulator();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
countDownLatch5.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch5.await();
endTime = System.currentTimeMillis();
System.out.println("----costTime: " + (endTime - startTime)
+ " 毫秒" + "t add_LongAccumulator" + "t" +
+ clickNumber.longAccumulator.longValue());
}
}
运行结果:
----costTime: 12975 毫秒 add_Synchronized 500000000
----costTime: 32001 毫秒 add_AtomicInteger 500000000
----costTime: 30030 毫秒 add_AtomicLong 500000000
----costTime: 646 毫秒 add_LongAdder 500000000
----costTime: 583 毫秒 add_LongAccumulator 500000000
LongAdder的基本思路就是 分散热点
,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的long值,只要将各个槽中的变量值累加返回。
sum()会把所有Cell数组的value值和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降级更新热点。
LongAdder在无竞争的情况,跟AtomicLong一样,当出现竞争关系时则采用化整为零的做法,从空间换时间,用一个数组cells,将一个value拆分进这个数组cells。多个线程需要同时对value进行操作时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程错做完毕,将数组cells的所有值和无竞争值base都加起来作为最终结果。
LongAdder的源码:
/**
as表示cells的引用
b表示获取的base的值
v表示期望值
m表示cells数组的长度
a表示当前线程命中的的cell单元格
*/
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
聊聊ThreadLocal
ThreadLocal简介
class House {
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(()->0);
public void saleHouse() {
int count = threadLocal.get();
count++;
threadLocal.set(count);
}
}
public class ThreadLocalDemo {
public static void main(String[] args) {
House house = new House();
new Thread(()->{
for (int i = 0; i < 3; i++) {
house.saleHouse();
}
System.out.println(Thread.currentThread().getName()+"==卖出"+house.threadLocal.get());
},"t1").start();
new Thread(()->{
for (int i = 0; i < 6; i++) {
house.saleHouse();
}
System.out.println(Thread.currentThread().getName()+"==卖出"+house.threadLocal.get());
},"t2").start();
new Thread(()->{
for (int i = 0; i < 9; i++) {
house.saleHouse();
}
System.out.println(Thread.currentThread().getName()+"==卖出"+house.threadLocal.get());
},"t3").start();
System.out.println(Thread.currentThread().getName()+"==卖出"+house.threadLocal.get());
}
}
运行结果:
t1==卖出3
t2==卖出6
main==卖出0
t3==卖出9
问:上面的程序,有bug吗,应当如何解决?
上面的程序,没有使用remove, 可能会造成内存泄漏问题。解决办法,使用try-fianlly,调用remove进行关闭。
new Thread(() -> {
try {
for (int i = 0; i < 3; i++) {
house.saleHouse();
}
System.out.println(Thread.currentThread().getName() + "==卖出" + house.threadLocal.get());
} catch (Exception ex) {
ex.printStackTrace();
} finally {
house.threadLocal.remove();
}
}, "t1").start();
大厂面试题
ThreadLocal中ThreadLcoalMap的数据结构和关系?
ThreadLocal中的key是弱引用,这是为什么?
ThreadLocal内存泄漏问题,你知道吗?
Thread Local中最后为什么要加remove方法?
从阿里Thread Local规范开始
非线程安全的SimpleDateFormat
public class DateUtils {
private static final SimpleDateFormat formatter =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static synchronized Date parse(String stringDate)
throws ParseException {
return formatter.parse(stringDate);
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
System.out.println(
DateUtils.parseByThreadLocal("2022-01-16 19:21:53"));
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}
运行结果:
发生了异常。原因?对应源码?
解决办法:加锁,或者使用ThreadLocal
public class DateUtils {
private static final ThreadLocal<SimpleDateFormat> sdfThreadLocal =
ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static Date parseByThreadLocal(String stringDate)
throws ParseException {
return sdfThreadLocal.get().parse(stringDate);
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
System.out.println(
DateUtils.parseByThreadLocal("2022-01-16 19:21:53"));
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
}
因为每个Thread内有自己的实例副本且该副本只能由当前线程自己使用。
既然其他Thread不可访问,那就不存在多线程间共享的问题。
统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的。
一句话
,如何才能不争抢:
1.加入synchronized或者Lock控制资源的顺序访问。
2.人手一份,大家各自安好,没必要争夺。
ThreadLocal源码分析
Thread、ThreadLocal、ThreadLocalMap 之间的关系
threadLocalMap实际上就是一个以threadLocal实例为key,任意对象为value的Entry对象。
当我们为threadLocal变量赋值,实际上是就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放。
ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocal为Key),不过是经过了两层包装的Thread Local对象。
JVM内部维护了一个线程版的Map<Thread, T>
(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当作key,放进了ThreadLocalMap中),每个线程要用到这个 T 的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量
,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的的变量。
ThreadLocal内存泄漏问题
强软弱虚4种引用?
class MyObject {
@Override
protected void finalize() throws Throwable {
System.out.println("gc,finalize invoke.");
}
}
public class ReferenceDemo {
public static void main(String[] args) {
MyObject mo = new MyObject();//默认,强引用,死了都不放手
System.out.println("gc before="+mo);
System.gc(); // 手动gc
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { }
System.out.println("gc after="+mo);
}
}
运行结果,发现神奇的效果出现了,居然没被回收掉:
gc before=com.lyw.MyObject@28d93b30
gc after=com.lyw.MyObject@28d93b30
弱引用:它比软引用的生命周期更短。对于弱引用来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
什么是内存泄漏?如何避免ThreadLocalMap内存泄漏?
每个Thread对象维护着一个ThreadLocalMap的引用。
ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储。
调用ThreadLocal的set方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象, 值Value是传进来的对象。
调用ThreadLocal的get方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象。
ThreadLocal本身并不存储值,它只是自己作为一个key来让线程从ThreadLocalMap获取value,正是因为这个原理,所以ThreadThread Local能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响。
谁惹的祸?
为什么要用弱引用?不用如何?
为什么源码要用弱引用?
当function1方法执行完毕后,栈帧销毁 强引用t1 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象。
若这个key是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏。
若这个key是软引用,就大概率
减少内存泄漏的问题(还有一个key为null的雷
)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null
。
使用弱引用就万事大吉了吗?
1.当我们为threadthreadLocal变量赋值,实际上就是当前的Entry(ThreadLocal实例为key,值为value)往这个threadLocalMap中存放。Entry中的引用是弱引用,当threadLocal外部强引用被设置成null(tl=null),那么系统gc的时候,根据可达性分析,这个theadLocal实例就没有任何一条链路能够引用到它,这个threadLocal势必会被回收,这样一来,threadLocalMap就会出现key=null的Entry,就没有办法反访问这些key为null的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref > Thread > ThreadLocalMap > Entry > value 永远无法回收,造成内存泄漏
。
2.当然,如果当前thread运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。
3.但在实际使用中我们是用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()创建线程的时候,为了复用,线程是不会结束的,所以threadLocal内存泄漏就值得我们小心。
结论
从前面的set、get,remove方法看出,在threadLocal的生命周期里,针对threadLocal存在的内存泄漏情况,都会通过exexpungeStaleEntry,cleanSomeSlots,replaceStaleEntry这3个方法清理掉key为null的脏key。
最佳实践
小总结
Java对象内存布局和对象头
对象在堆中的布局
对象内部结构分为:对象头、实例数据、对其填充(保证8字节的倍数)
对象头分为对象标记(markOop)和类元信息(klassOop),类元信息存储的是指向该对象类元数据(klass)的首地址。
对象头
对象标记
默认存储对象的HashCodel、分代年龄和锁标记位等信息。这些信息都是与对象自身定义无关的数据,所以MarkWord被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWord里存储的数据会随着锁标识位的变化而变化。
类元信息(又叫类型指针)
对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
对象头有多大
在64位系统中,MarkWord占了8字节,类型指针占了8字节,一共是16字节。
实例数据
存放类的属性(Field)数据信息,包括父类的属性信息,如果是数组实例部分还包括数组的长度,这部分内存按4字节对齐。
对其填充
虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是位了字节对齐这部分内存按8字节补充对齐。
再说说对象头的MarkWord
32位,了解即可,以64位为准。
64位,对于hotspot源码:oop.cpp > markOop.cpp。
对象布局、GC回收和后面的锁升级就是对象标记MarkWord里面标志位的变化。
Object o = new Object();谈谈你对这句话的理解?一般而言JDK8按照默认情况下,new一个对象占多少内存空?
JOL证明
JOL官网:https://openjdk.java.net/projects/code-tools/jol/
引入jol,因为要用到这个包里的VM类
public static void main(String[] args) {
// VM的细节详细情况
System.out.println(VM.current().details());
// 所有的对象分配的字节都是8的整数倍
System.out.println(VM.current().objectAlignment());
}
运行结果, 可以发现,是按照8的整数倍对齐的:
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
8
代码
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
运行结果;
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 28 0f 00 00 (00101000 00001111 00000000 00000000) (3880)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
看着运行结果,一起来想想,我们上面提到,对象头包括 对象标记markWord 和 类元信息(Class Pointer),其中:条件1 mardWord和类元信息各占8字节,条件2: 虚拟机要求对象的起始地址必须是8字节的整数倍。可是现在,从条件1看,8+8就已经是16了,根本不需要对齐了(对齐字节应该是0),可运行结果为什么显示:new的对象,占了12字节,然后对齐了4个字节呢?那么,markWord+类型指针 到底应该是12字节还是16字节呢?我们接着往下看。
我们给vm设置 -XX:MaxTenuringThreshold=16,再来运行下程序试试。
MaxTenuringThreshold of 16 is invalid; must be between 0 and 15
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
运行后,发现报错了,程序都运行不起来了。说应该是在0-15之间。原来是因为 只给年龄分代设置了 4字节(即 1111,换做十进制是15)。
GC年龄采用4位bit存储,最大为15,例如MaxTenuringThreshold参数默认值就是15
尾巴参数说明
命令:java -XX:+PrintCommandLineFlags -version
运行结果:
-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
哦,原因出来了, 我们可以看到,-XX:+UseCompressedClassPointers 默认是开启的,也就是说,为了节约空间,默认是开启类型指针压缩的,如果够的话,就压缩,如果不够的话,就对齐。
好,那我们来做个试验,如果把它关掉,看看类型指针是占用了多少个字节?下面两次运行结果,分别对应了开启和关闭的情况。
// 默认开始的时候,即 -XX:+UseCompressedClassPointers
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
// 运行结果:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 28 0f 00 00 (00101000 00001111 00000000 00000000) (3880)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
// 手动关闭,即 -XX:-UseCompressedClassPointers
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
// 运行结果:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 00 dc a2 15 (00000000 11011100 10100010 00010101) (362994688)
12 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
通过对比,我们发现,如果开启的话,是12个字节,然后对齐填充了4个字节,变成了16字节;没有开启的话,是16个字节。所以,markWord+类型指针 都是16字节,区别只是在于是否对齐填充了。
换成其他对象试试
Synchronized与锁升级
Synchronized的性能变化
synchonized锁优化的背景:
用锁能够实现数据的安全性,但是能带来性能下降。
无锁能够基于线程并行提升程序性能,但是会带来安全性下降。
求平衡?
synchronized的锁升级过程:无锁 > 偏向锁 > 轻量级锁 > 重量级锁
Java5之前,用户态和内核态之间的切换
Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态和内核态之间切换,这种切换会消耗大量的系统资源,因为用户态和内核态都有各自专用的内存空间,专用的寄存器等,用户切换至内核态需要传递给许多变量、参数给内核,内核态也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
在早期Java版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor) 是依赖于底层的操作系统的Mutex Lock来实现的
,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成。这种状态切换需要消耗处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长,时间成本相对较高,这也是早期的synchronized效率低的原因。
Java6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁
和偏向锁
。
为什么每个对象都能成为一把锁?
markOop.cpp
Monitor可以理解成一种同步工具,也可以理解成一种同步机制,常常被描述成一个Java对象。Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁。
Monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要用户态到内核态的转换,成本非常高。
Monitor(监视器锁)
Monitor锁是在JVM底层实现的,底层代码是C++。本质是依赖于底层操作系统的Mutex Lock实现的,操作系统实现线程之间的切换需要从用户态到内核态的转换,状态转换需要消耗很多的处理器时间,成本非常高。所以Synchronized是Java语言的一个重量级操作。
Monitor与Java对象以及线程是如何关联?
1.如果一个Java对象被某个线程锁住,则该Java对象的MarkWord字段中LockWord指向Monitor的起始地址。
2.Monitor的Owner字段会存放拥有相关联对象锁的线程id。
Mutex Lock的切换需要从用户态到核心态中,因此状态转换需要耗费很多的处理器时间。
Synchronized锁种类及升级步骤
多线程访问情况(3种)
只有一个线程来访问,有且唯一 only one
有2个线程A,B,轮流访问
竞争激烈,多个线程来访问
升级流程
synchronized用的锁是存在Java对象头里的MarkWord中。
锁升级功能主要依赖MarkWord中锁标志位和释放偏向锁标志位
无锁
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
// 运行结果
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
我们看Value的时候,应该是从后往前看。上面的运行结果,可以看到,对象的hashCode是0,这正常吗,不应该是有值的吗,是因为对象的hashCode是懒加载的吗,不调用的话,就没有吗,我们写个demo试试看。
public static void main(String[] args) {
Object obj = new Object();
int hashCode = obj.hashCode();
System.out.println("hashCode="+hashCode);
System.out.println("hashCode hex="+Integer.toHexString(hashCode));
System.out.println("hashCode binary="+Integer.toBinaryString(hashCode));
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
// 运行结果
hashCode=1956725890
hashCode hex=74a14482
hashCode binary=1110100101000010100010010000010
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 82 44 a1 (00000001 10000010 01000100 10100001) (-1589345791)
4 4 (object header) 74 00 00 00 (01110100 00000000 00000000 00000000) (116)
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
果然哦, 我们显式的调用了对象的hashCode后,可以看到,在对象头的信息里,hashCode有值了。nice。
偏向锁
通过CAS方式修改MarkWord中的线程ID
偏向锁的持有
理论落地:
在实际应用运行过程中,“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用他的线程持有,这个线程就是锁的偏向线程
。
那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁(后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁
。而是直接比较对象头里面是否存储了指向当前线程的偏向锁)。
如果相等表示偏向锁上偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。如果自始自终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能很高。
假如不一致意味着发生了竞争,锁已经不是总是偏向于同一个线程了,这h时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。
技术实现:
一个synchronized方法被一个线程抢到了锁时,那这个方法所在的对象会在其所在的MarkWord中将偏向锁修改状态位,同时还会占用前54位来存储线程指针作为标识。若该线程再次访问同一个synchronized方法时,该线程只需去对象头的MarkWord中去判断一个是否持有偏向锁指向本身的ID,无需再进去Monitor去竞争对象了。
偏向锁JVM命令
java -XX:+PrintFlagsInitial | grep BiaseLock*
重要参数说明
实际上偏向锁在jdk6之后是默认开启的,但是启动时间有延迟,所以需要添加参数 -XX:BiaseLockingStartupDelay=0,让其在程序启动时立即启动。
开启偏向锁:-XX:+UseBiaseLocking -XX:BiaseLockingStartupDelay=0
关闭偏向锁:关闭之后程序默认会直接进入轻量级锁状态。-XX:-UseBiaseLocking
偏向锁的撤销
当有另外线程逐步来竞争锁的时候,就不能在使用偏向锁了,要升级为轻量级锁。竞争线程尝试CAS更新对象头失败,会等到全局安全点(此时不再执行任何代码)撤销偏向锁。
偏向锁使用一种等到竞争出现才释放锁的机制
,只有当其他线程竞争锁的时候,持有偏向锁的原来的线程才会被撤销。撤销需要等待全局安全点(该时间点上没有字节码正在执行)
,同时检查持有偏向锁的线程是否还在执行:
1.第一个线程正在执行synchronized方法(处于同步块
),它还没有执行完,其他线程来抢,该偏向锁会被取消掉并出现锁升级
。此时轻量级锁由原来持有偏向锁的线程持有,继续执行其同步代码块,而正在竞争的线程会进入自旋等待获得该轻量级锁。
2.第一个线程执行完成synchronized方法(退出代码块
),则将对象头设置成无锁状态并撤销偏向锁,重新偏向。
轻量级锁
主要作用
有线程来参与锁的竞争,但是获取锁的冲突事件很短,本质就是自旋锁。
轻量级锁的获取
轻量级锁是为了在线程近乎交替执行同步块时提高性能。
主要目的:在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗,说白了先自旋再阻塞。
升级时机: 当关闭偏向锁功能或多个线程竞争偏向锁会导致偏向锁升级成轻量级锁。
假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已经是偏向锁了。而线程B在争抢时发现对象头MarkWord中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS操作希望能够获得锁。此时线程B操作中有2种情况:
如果锁获取成功:直接替换MardWord中的线程ID为B自己的(A -> B),重新偏向于其他线程(即 将偏向锁交给其他线程,相当于当前线程“被”释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位。
如果锁获取失败:则偏向锁升级为轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在进行的线程B会进入自旋等待获得该轻量级锁。
Code演示
关闭偏向锁(关闭之后,程序默认会进入轻量级锁状态):-XX:-UseBiasedLocking
步骤流程图示
自旋达到一定次数和程度
Java6之前,默认启用,默认情况下自旋的次数是10次。或者自旋线程数超过CPU核数一半。-XX:PreBlockSpin=10来修改
Java6之后,自适应。自适应意味着自旋的次数不是固定不变的,而是根据:同一个锁上一次自旋的时间、拥有锁线程的状态来决定。
轻量锁和偏向锁的区别和不同
争夺轻量级锁失败时,自旋尝试抢占锁。
轻量级锁每次退出同步块都需要释放锁,而偏向锁是在竞争发生时才释放锁。
重量级锁
有大量的线程参与锁的竞争,冲突性很高。
小总结
各种锁优缺点,synchronized锁升级和实现原理:
synchronized锁升级过程总结:一句话:就是先自旋,不行再阻塞
。
实际上上把之前的悲观锁(重量级锁)变成在一定条件下使用偏向锁以及使用轻量级锁(自旋锁CAS)的形式。
synchronized在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的MardWord来实现的。
JDK1.6之前synchronized使用的是重量级锁,JDK1.6之后进行了优化,拥有了 无锁 > 偏向锁 > 轻量级锁 > 重量级锁 的升级过,而不是无论什么情况下都使用重量级锁。
偏向锁
:适用于单线程适用的情况,在不存在锁竞争的时候进入同步方法/代码块 则使用偏向锁。
轻量级锁
:适用于竞争较不激烈的情况(这和乐观锁的使用范围类似),存在竞争时升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方法/代码块 执行时间很短的话,采用轻量级锁虽然会占用CPU资源,但是相对比对使用重量级锁还是更高效。
重量级锁
:适用于竞争激烈的情况,如果同步方法/代码块 执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为 重量级锁。
JIT编译器对锁的优化
JIT, Just in time Compiler(一般译为 即时编译器)
锁消除
// 锁消除
// 从JIT角度看相当于无视它,synchronized(obj)不存在了,这个锁并没有被共用扩散到其他线程使用。
// 极端的说就是根本没有加这个锁对象的底层机器码,消除了锁的使用
public class LockClearUpDemo {
Object obj = new Object();
private void m1() {
Object obj = new Object(); // 锁消除
synchronized (obj) {
System.out.println("=====");
}
}
}
锁粗化
// 锁粗化
// 假如方法中首尾相接,前后相邻的都是同一个锁对象,那JIT编译器就会把这几个syn块合并成一个大块
// 加粗加大范围,一次申请锁使用即可,避免此次的申请和示范锁,提升了性能
public class LockBigDemo {
static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) { System.out.println("1"); }
synchronized (obj) { System.out.println("2"); }
synchronized (obj) { System.out.println("3"); }
synchronized (obj) { System.out.println("4"); }
}, "t1").start();
}
}
AbstractQueuedSynchronizer之AQS
先从阿里及其他大厂面试题说起
ReentrankLock实现原理,简单说下AQS
synchronized实现原理,monitor对象什么时候生成的?知道monitor的monitorenter和monitorexit是怎么保证同步的的吗,或者说,这两个操作计算机底层是如何执行的?
刚刚你提到了synchronized的优化过程,详细说一下吧。偏向锁和轻量级锁有什么区别?
前置知识
公平锁和非公平锁
可重入锁
自旋锁
LockSupport
数据机构之链表
设计模式之模版设计模式
是什么
字面意思
AbstractQueuedSynchronizer,简称为AQS
技术解释
用来构建锁或者其他同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量表示持有锁的状态
AQS为什么是JUC内容中最重要的基石
和AQS有关的
ReentrantLock
CountDownLatch
ReentrantReadWriteLock
Semaphore
进一步理解锁和同步器的关系
锁,面向锁的使用者。定义了程序员和锁交互的使用层API,隐藏了实现细节,程序员调用即可。
同步器,面向锁的实现者。比如并发大神Doug Lea提出统一规范并简化了锁的实现,屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等
能干嘛
加锁会导致阻塞。有阻塞就需要排队,实现排队必然需要队列。
解释说明:
抢占资源的线程直接使用处理业务,抢不到资源的必然涉及一种排队等候机制。抢占资源失败的线程继续去等待(类似银行业务办理窗口都满了,暂时没有受理窗口的顾客只能去候客区排队等候),但等候线程仍然保留获取锁的可能 且 获取锁流程仍在继续(候客区的顾客也在等着叫号,轮到了再去受理窗口办理业务)。
既然说到了排队等候机制,那么就一定会有某种队列形成,这样的队列是什么数据结构呢?
如果共享资源被占用,就需要一定的阻塞唤醒机制来保证锁分配。这个机制主要目的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS到抽象表现。它将请求共享资源的线程封装成队列的节点(Node),通过CAS、自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的效果。
AQS初步
AQS内部结构
AQS自身
AQS的int变量
AQS的同步状态state成员变量
/**
* The synchronization state.
*/
private volatile int state;
银行办理业务的受理窗口状态:0就是没人,自由状态可以办理;大于等于1,有人占用窗口,等着。
AQS的CLH队列
小总结
有阻塞必然需要排队,实现排队必然需要队列
state变量+CLH双端队列
内部类Node(Node类在AQS类内部)
Node的int变量
Node的等待状态waitState成员变量
/**
* Status field, taking on only the values:
* SIGNAL: The successor of this node is (or will soon be)
* blocked (via park), so the current node must
* unpark its successor when it releases or
* cancels. To avoid races, acquire methods must
* first indicate they need a signal,
* then retry the atomic acquire, and then,
* on failure, block.
* CANCELLED: This node is cancelled due to timeout or interrupt.
* Nodes never leave this state. In particular,
* a thread with cancelled node never again blocks.
* CONDITION: This node is currently on a condition queue.
* It will not be used as a sync queue node
* until transferred, at which time the status
* will be set to 0. (Use of this value here has
* nothing to do with the other uses of the
* field, but simplifies mechanics.)
* PROPAGATE: A releaseShared should be propagated to other
* nodes. This is set (for head node only) in
* doReleaseShared to ensure propagation
* continues, even if other operations have
* since intervened.
* 0: None of the above
*
* The values are arranged numerically to simplify use.
* Non-negative values mean that a node doesn't need to
* signal. So, most code doesn't need to check for particular
* values, just for sign.
*
* The field is initialized to 0 for normal sync nodes, and
* CONDITION for condition nodes. It is modified using CAS
* (or when possible, unconditional volatile writes).
*/
volatile int waitStatus;
说人话,等候区其他顾客(其他线程)的等待状态。队列中每个排队的个体就是一个Node。
Node的此类讲解
内部讲解
属性结构
AQS同步队列的基本结构
从我们的ReentrankLock开始解读AQS
Lock接口的实现类,基本都是通过一个【聚合】了一个【队列同步器】的子类完成线程访问控制的
ReentrantLock的原理
从最简单的lock方法开始看看公平和非公平
非公平走起,方法lock()
对比公平锁和非公平锁的tryAcquire()方法的代码实现,其实区别就在于非公平锁获取锁时比公平锁中少了一个判断 !hasQueuedPredecessors() 。这个方法中判断了是否需要排队,导致公平锁和非公平锁的差异如下:
公平锁:讲究先来后到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中。
非公平锁:不管是否有等待队列,如果可以获取锁,则立刻占有锁对象。也就是说队列的第一个排队线程在unpark(), 之后还是需要竞争锁(存在线程竞争的情况下)。
整个 ReentrantLock的加锁过程,可以分3个阶段:
1.尝试加锁
2.加锁失败,线程入队列
3.线程入队列后,进入阻塞模式。
unlock()
Subtopic
ReentrantLock、ReentrantReadWriteLock、StampedLock
面试题
你知道Java里有哪些锁?
你说你用读写锁,锁饥饿问题是什么?
有没有比读写锁更快的锁?
StampedLock知道吗?
ReentrantReadWriteLock有锁降级机制你知道吗?
简单聊聊ReentrantReadWriteLock
读写锁说明:一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。
读写锁意义和特点
ReentrantReadWriteLock并不是真正意义上的读写分离,它只允许读读共享,而读写和写写依然是互斥的,大多数实际场景是“读/读”线程间并不存在互斥关系,只有“读/写”线程或“写/写”线程间的操作是需要互斥的。因此引入ReentrantReadWriteLock。一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁。也即一个资源可以多个读操作访问或一个写操作访问,但两者不能同时进行。只有在读多写少的情况下,读写锁才具有较高的性能体现。
降级
锁降级:遵循获取写锁》再获取读锁〉再释放写锁的次序,写锁能够降级成为读锁。如果一个线程持有了资源,在不释放写锁的情况下,它还能占有读锁,即写锁降级成为读锁。
Java8官网说明:重入还允许通过获取写入锁定,然后读取锁然后释放写锁到读取锁,但是,从读锁升级到写锁是不可能的。
写锁和读锁是互斥的:
写锁和读锁是互斥的(这里的互斥是指线程间的互斥,当前线程可以获取到读锁又获取到写锁,但是获取到了读锁不能继续获取写锁),这是因为读写锁要保持写操作的可见性。因为,如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他线程无法感知到当前写线程的操作。
因此,分析读写锁会发现它有潜在的问题:读锁全完,写锁有望;写锁独占,读写全堵。
如果有线程正在读,写线程需要读线程释放锁后才能获取写锁,即读的过程不允许写,只有等待线程释放了锁,当前线程才有可能获取写锁,也就是写入必须等待,这是一种悲观的读锁,人家还在那里等着,你先别去写,省的数据乱。
StampedLock
为什么会闪亮出现呢?
ReentrantReadWriteLock允许多个线程同时读,但是只允许一个线程写,在线程获取到写锁的时候,其他写操作和读操作都处于阻塞状态,读锁和写锁也互斥的,所以在读的时候是不允许写的,读写锁比传统的synchronized速度要快很多,原因就在于ReentrantReadWriteLock支持读并发。
ReentrantReadWriteLock读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。但是,StampedLock采用乐观获取锁后,其他线程尝试写锁时不会被阻塞,这其实是对读锁的优化,所以,在获取乐观读锁后,还需要对结果进行校验。
总结与回顾
最后
以上就是柔弱小霸王为你收集整理的Java之JUC前置要求线程基础知识复习CompletableFutrue说说Java"锁"事LockSupport与线程中断Java内存模型值JMMvolatile与Java内存模型CAS原子操作类之18罗汉增强聊聊ThreadLocalJava对象内存布局和对象头Synchronized与锁升级AbstractQueuedSynchronizer之AQSReentrantLock、ReentrantReadWriteLock、StampedLock总结与回顾的全部内容,希望文章能够帮你解决Java之JUC前置要求线程基础知识复习CompletableFutrue说说Java"锁"事LockSupport与线程中断Java内存模型值JMMvolatile与Java内存模型CAS原子操作类之18罗汉增强聊聊ThreadLocalJava对象内存布局和对象头Synchronized与锁升级AbstractQueuedSynchronizer之AQSReentrantLock、ReentrantReadWriteLock、StampedLock总结与回顾所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复