JUC2
Future
- Future接口(FutureTask实现类)定义了操作异步任务执行一些方法,如获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕等。
- 如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他事情了,忙其他事情或者先执行完,过了一会儿才去获取子任务的执行结果或变更的任务状态
- Future是java5的一个接口,它提供了一种异步并行计算的功能
- 如果主线程需要执行一个很耗时的计算任务,我们就可以通过future把这个任务放到异步线程中执行
- 主线程继续处理其他任务或先行结束,再通过Future获取计算结果
- 三个特点(多线程、有返回、异步任务)
- Future优点:future+线程池异步多线程任务配合,能显著提高程序的执行效率
public class FutureThreadPoolDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
FutureThreadPoolDemo futureThreadPoolDemo = new FutureThreadPoolDemo();
futureThreadPoolDemo.m1();//1114毫秒
futureThreadPoolDemo.m2();//540毫秒
}
public void m1() throws InterruptedException {
//3个任务,目前只有一个线程来处理、
long startTime = System.currentTimeMillis();
Thread.sleep(500);
Thread.sleep(300);
Thread.sleep(300);
long endTime = System.currentTimeMillis();
System.out.println(endTime - startTime);
}
public void m2() throws ExecutionException, InterruptedException {
//3个任务,目前开启多个异步任务线程
long startTime = System.currentTimeMillis();
ExecutorService threadPool = Executors.newFixedThreadPool(3);
FutureTask<String> futureTask1 = new FutureTask<>(()->{
Thread.sleep(500);
return "";
});
FutureTask<String> futureTask2 = new FutureTask<>(()->{
Thread.sleep(300);
return "";
});
FutureTask<String> futureTask3 = new FutureTask<>(()->{
Thread.sleep(300);
return "";
});
threadPool.submit(futureTask1);
threadPool.submit(futureTask2);
threadPool.submit(futureTask3);
//获取返回结果,
System.out.println(futureTask1.get());
System.out.println(futureTask2.get());
System.out.println(futureTask3.get());
threadPool.shutdown();
long endTime = System.currentTimeMillis();
System.out.println(endTime - startTime);
}
}
- Future缺点:get方法容易引起阻塞
public class FutureAPIDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask<>(()->{
System.out.println("begin");
Thread.sleep(5000);
return "end";
});
Thread t1 = new Thread(futureTask, "t1");
t1.start();
//获取线程返回值,如果futureTask.get()方法没有放在最后,则会导致阻塞,当5秒后线程执行结束由返回值时才会继续执行
System.out.println(futureTask.get());
System.out.println("主线程执行其他任务");
}
}
- 使用过时抛出异常(多个线程可能会抛出多个异常)
System.out.println(futureTask.get(3, TimeUnit.SECONDS));
- 使用循环判断是否执行完成(需要不停的轮询,会耗费cpu资源,而且也不见得及时得得到计算结果)
while (true){
//每隔0.5秒判断是否完成
if (futureTask.isDone()){
System.out.println(futureTask.get());
break;
}else {
Thread.sleep(500);
}
}
- 结论:Future对于结果的获取不是很友好,只能通过阻塞或轮询的方式得到任务的结果
CompletableFuture
- get()方法在Future计算完成之前会一直处在阻塞状态下
- isDone()方法容易消耗CPU资源
- 对于真正的异步处理我们希望是可以通过传入回调函数,在Future结束时自动调用该回调函数,这样,我们就不用等待结果
- 阻塞的方式和异步编程的设计理念相违背,而轮询的方式会消耗无谓的CPU资源,因此,JDK8设计出CompletableFuture
- CompletableFuture提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方
- CompletionStage
- 代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另一个阶段
- 一个阶段的执行可能是被单个阶段的完成触发,也可能是由多个阶段一起触发
- CompletableFuture提供了非常强大的Future扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数是编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合CompletableFuture的方法
- 它可能代表一个明确完成的Future,也有可能代表一个完成阶段(ComplationStage),它支持在计算完成之后触发一些函数或执行某些动作。
- 实现了Future和CompletionStage接口
- CompletableFutur的四个创建异步任务的核心方法(构造方法创建的是一个不完备的对象)
public class CompletableFutureTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
//runAsync()没有返回值
CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
},threadPool);
//null
System.out.println(completableFuture.get());
//supplyAsync()有返回值
CompletableFuture<String> completableFuture1 = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "hello supplyAsync";
},threadPool);
//hello supplyAsync
System.out.println(completableFuture1.get());
}
}
- CompletableFuture,它是Future的功能增强版,减少阻塞和轮询,可以传入回调对象,当异步任务完成或发生异常时,自动调用回调对象的回调方法
public class CompletableFutureTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(()->{
return "hello completableFuture";
},threadPool).whenComplete((v,e)->{ //v return上面return返回的值 e异常信息
if (e == null){
//hello completableFuture
System.out.println(v);
}
}).exceptionally(e ->{ //e 异常信息,如果有异常则不会执行进whenComplete
e.printStackTrace();
return null;
});
//hello completableFuture
System.out.println(completableFuture.get());
//关闭线程池
threadPool.shutdown();
}
}
- 异步任务结束时,会自动回调某个对象的方法
- 主线程设置号回调后,不再关系异步任务的执行,异步任务之间可以顺序执行
- 异步任务出错时,会自动回调某个对象的方法
- 电商网站比价需求分析
- 同一款产品,同时搜索出同款产品在各大电商平台的售价
- 同一款产品,同时搜索出本产品在同一个电商平台下,各个入驻卖家售价是多少
public class CompletableFutureMallDemo {
//初始化电商网站
static List<NetMall> malls = Arrays.asList(
new NetMall("京东"),
new NetMall("淘宝"),
new NetMall("当当")
);
//使用单线程一家家遍历
public static List<String> getPrice(List<NetMall> malls,String productName){
return malls.stream().map(mall->String.format(
productName+" in %s price is %.2f",mall.getMallName(),mall.calcPrice(productName))
).collect(Collectors.toList());
}
//使用CompletableFuture
public static List<String> getPriceByCompletableFuture(List<NetMall> malls,String productName){
//线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
return malls.stream().map(mall-> CompletableFuture.supplyAsync(()->
String.format(productName+" in %s price is %.2f",mall.getMallName(),mall.calcPrice(productName)))
).collect(Collectors.toList()).stream().map(s -> s.join()).collect(Collectors.toList());
}
public static void main(String[] args) {
//******************3054*******************
long startTime = System.currentTimeMillis();
List<String> prices = getPrice(malls, "java");
for (String price : prices){
System.out.println(price);
}
long endTime = System.currentTimeMillis();
System.out.println("花费的时间:" + (endTime - startTime));
//*******************1014*******************
long startTime1 = System.currentTimeMillis();
List<String> prices1 = getPriceByCompletableFuture(malls, "java");
for (String price : prices1){
System.out.println(price);
}
long endTime1 = System.currentTimeMillis();
System.out.println("花费的时间:" + (endTime1 - startTime1));
}
}
class NetMall{
//电商平台名
private String mallName;
public NetMall(String mallName){
this.mallName = mallName;
}
//模拟根据产品获取价格
public double calcPrice(String productName) {
try {
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
return ThreadLocalRandom.current().nextDouble() * 2 + productName.charAt(0);
}
public String getMallName() {
return mallName;
}
}
- CompletableFuture常用方法
- 获得结果和触发计算
- get() 获取结果
- get(long timeout,TimeUnit unit) 获取结果,超时则抛出异常
- join() 获取结果,无需抛出异常
- getNow(T valueIfAbsent) 在获取时如果还没有执行完则返回一个执行的值valueIfAbsent
- complete(T value) 是否打断get方法立即返回value值
- 对计算结果进行处理
- thenApply 计算结果存在依赖关系,这两个线程串行化,上一步计算结果传给下一步
- handle 计算结果存在依赖关系,这两个线程串行化,上一步计算结果传给下一步(多一个异常参数)
- 对计算结果进行消费
- thenAccept 接收处理结果,并消费处理,无返回结果
- threnRun 任务A执行完执行任务B,并且B不需要A的结果
- 对计算速度进行选用
- applyToEither
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture playA = CompletableFuture.supplyAsync(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "PlayA";
},threadPool);
CompletableFuture playB = CompletableFuture.supplyAsync(()->{
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "PlayB";
},threadPool);
CompletableFuture result = playA.applyToEither(playB, f -> {
return f + " is winner";
});
System.out.println(result.join()); //PlayA is winner
- 结果合并
- 两个CompletionStage任务都完成后,最终能把两个任务的结果一起交给thenCombine来处理
- 先完成的先等着,等待其他分支任务
public class CompletableFutureCombineDemo {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture<Integer> completableFuture1 = CompletableFuture.supplyAsync(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1;
});
CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1;
});
CompletableFuture<Integer> completableFuture = completableFuture1.thenCombine(completableFuture2, (x, y) -> {
return x + y;
});
System.out.println(completableFuture.join()); //2
}
}
public class CompletableFutureCombineDemo {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(3);
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace(); }
return 1;
}).thenCombine(CompletableFuture.supplyAsync(()->{
try {Thread.sleep(1000);} catch (InterruptedException e) { e.printStackTrace(); }
return 1;
}),(x,y)->{
return x+y;
});
System.out.println(completableFuture.join()); // 2
}
}
多线程锁
- 悲观锁
- 认为自己在使用数据的时候一定有别的线程类修改数据,因此在获取数据的时候会先加上锁,确保数据不会被别的线程修改
- 适合写操作多的场景,先加锁可以保证写操作时数据正确
- 显式的锁定之后再操作同步资源
- 乐观锁
- 认为自己在使用数据时不会有别的线程修改数据或资源,所有不会添加锁
- 在Java中是通过使用无锁编程来实现的,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据
- 适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升
-
synchronized字节码分析
-
同步代码块
public void m1() {
synchronized(this.object) {
System.out.println("hello synchronized code block");
}
}
- monitorenter 锁,打开监视器
- monitorexit 关闭监视器
- 同步方法
public synchronized void m2(){
System.out.println("hello synchronized");
}
- 调用指令会检查方法的ACC_SYSCHRONIZED访问标志是否被设置。如果设置了,执行线程会将先持有monitor锁,然后在执行方法,最后在方法完成时释放monitor
- 静态同步方法
public static synchronized void m3() {
System.out.println("hello synchronized");
}
- ACC_STATIC,ACC_SYNCHRONIZED访问标志区别该方法是否静态同步方法
- 公平锁
- 指多个线程按照申请锁的顺序来获取锁,
- Lock lock = new ReentrantLock(true); //参数ture为公平锁
- 非公平锁
- 是指多个线程获取锁的顺序并不按照申请锁的顺序,在高并发环境下,有可能造成优先级翻转或者饥饿的状态(某个线程一直得不到锁)
- Lock lock = new ReentrantLock(fale);
- Lock lock = new ReentrantLock(); //默认非公平锁
- 为什么会有公平锁/非公平锁的设计
- 恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员角度来看这个时间微乎其微,但是从CPU的角度看,这个时间差存在的还是很明显的。所以非公平锁更能充分的利用CPU的时间片,尽量减少CPU空闲状态时间
- 使用多线程很重要的考量点是线程切换的开销,当采用非公平锁时,当一个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销
- 如果为了更大的吞吐量,很显然非公平锁时比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了。否则那就用公平锁
- 可重入锁(递归锁)
- 指在同一线程在外层方法获取锁的时候,再进入该线程的内存方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没有释放而阻塞
- ReentrantLock和synchronized都是可重入锁,可一定程度避免死锁
- 可重入锁分种类
- 隐式锁(即synchronized关键字使用的锁)默认是可重入锁
- 显式锁(即Lock)也有ReentrantLock这种的可重入锁
- synchronized的可重入锁实现机制
- 每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针
- 当执行monitorenter时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1
- 在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器设置为1,否则需要等待,直至持有线程释放该锁
- 当执行monitorexit是,Java虚拟机则需将锁对象的计数器减1.计数器为零表示锁已释放
中断机制
- 首先,一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止,自己来决定自己的命运
- 其次,在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此Java提供一种用于停止线程的协商机制————中断,也即中断标识符机制
- 中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完成需要程序员自己实现。
- 若要中断一个线程,你需要手动调用该线程的interrupt方法,该方法仅仅是将线程对象的终端标识设为true
- 接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程请求这条线程中断
- 此时究竟该做什么需要自己写代码实现
- 每个线程对象都有一个中断标识位,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;
- 通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用
- 中断的现骨干API方法
方法 | 说明 |
---|---|
public void interrupt | 实例方法,仅仅是设置中断状态为true,发起协商而不会立刻停止线程 |
public static boolean interrupted | 静态方法,判断线程是否被中断并清除当前中断状态 |
public boolean isInterupt | 实例方法,判断当前线程是否被中断 |
- 如何停止中断运行中的线程
- 通过一个volatile变量实现
public class InterruptDemo {
static volatile boolean isStop = false;
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (true){
//判断isStop为Ture则结束线程
if (isStop){
System.out.println(Thread.currentThread().getName()+" isStop修改为true,程序停止");
break;
}
System.out.println("hello volatile");
}
},"t1").start();
Thread.sleep(100);
new Thread(()->{
isStop = true;
},"t2").start();
}
}
- 通过AtomicBoolean
public class InterruptDemo {
static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
while (true){
//判断atomicBoolean为Ture则结束线程
if (atomicBoolean.get()){
System.out.println(Thread.currentThread().getName()+" atomicBoolean修改为true,程序停止");
break;
}
System.out.println("hello atomicBoolean");
}
},"t1").start();
Thread.sleep(100);
new Thread(()->{
atomicBoolean.set(true);
},"t2").start();
}
}
- 通过Thread类自带的终端api实例方法实现
public class InterruptDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while (true){
//判断线程是否为中断状态
if (Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread().getName()+" interrupted修改为true,程序停止");
break;
}
System.out.println("hello interrupted");
}
},"t1");
t1.start();
Thread.sleep(100);
new Thread(()->{
//设置t1线程为中断状态
t1.interrupt();
},"t2").start();
}
}
- 具体来说,当对一个线程调用interrupt()时
- 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。所以,interrupt()并不能真正的中断线程,需要被调用的线程自己进行配合才行
- 如果线程处于被阻塞状态(例如处于sleep,wait,join等状态),在别的线程中调用当前线程对象的interrupt方法那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常
public class InterruptDemo {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while (true){
//判断线程是否为中断状态
if (Thread.currentThread().isInterrupted()){
System.out.println(Thread.currentThread().getName()+" interrupted修改为true,程序停止");
break;
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
/**
* 需要再捕获异常的时候重新调用一次
* 处理阻塞时t2线程调用t1线程的中断标识会抛出异常并且中断状态清除
* 如果不重新调用一次中断状态则会死循环
*/
Thread.currentThread().interrupt();
e.printStackTrace();
}
System.out.println("hello interrupted");
}
},"t1");
t1.start();
Thread.sleep(100);
new Thread(()->{
//设置t1线程为中断状态
t1.interrupt();
},"t2").start();
}
}
- LockSupport
- 用来创建锁和其他同步类的基本线程阻塞原语
- 线程等待唤醒机制
- 三种让线程等待和唤醒的方法
- 方式1:使用Object中的wait()方法让线程等待,使用Object中的notify()唤醒线程
- wait和notify方法必须要在同步块或同步方法里面,先wait后notify才行
- 方式2:使用JUC包中的Condition的await()方法让线程等待,使用signal()方法唤醒线程
- Condition中的线程等待和唤醒方法,需要先获取锁,先await,再signal
- 方式3:LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
- 方式1:使用Object中的wait()方法让线程等待,使用Object中的notify()唤醒线程
- 三种让线程等待和唤醒的方法
- LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit)
- permit许可证默认没有不能放行,所以一开始调park()方法当前线程就会阻塞,直到别的线程给当前线程发放permit,park方法才会被唤醒
- 调用unpark(thread)方法后,就会将thread线程的许可证permit发放,会自动唤醒park线程,即之前阻塞中的LockSupport.park()方法会立即返回
public class LockSupportDemo {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t come in");
LockSupport.park(); //阻塞,验证是否有许可
System.out.println(Thread.currentThread().getName()+"\t 被唤醒");
},"t1");
t1.start();
new Thread(()->{
LockSupport.unpark(t1); //唤醒,给t1许可
System.out.println(Thread.currentThread().getName()+"\t 发出通知");
},"t2").start();
}
}
- 使用LockSupport无锁块要求,之前错误的先唤醒后等待,LockSupport照样支持
- LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程
- 线程阻塞需要消耗凭证(permit),这个凭证最多只有一个
- 当调用park方法时
- 如果有凭证,则会直接消耗掉这个凭证然后正常退出
- 如果无凭证,就必须阻塞等待凭证可用
- 而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效
JMM
- cpu和物理内存的速度不一致,中间会有多级缓存
- cpu运行并不是直接操作内存而是先把内存里边的数据读到缓存,而内存的读和写操作的时候就会造成不一致的问题
- JVM规范中试图定义一种Java内存模型(java Memory Model,简称JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
- JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在它仅仅描述的是一组约定或规范,通过这组规范定义了程序中(尤其多线程)各个变量的读写访问方法并决定一个线程对共享变量的写入何时以及如何变成对另一线程可加,关键技术点都是围绕多线程的原子性、可见性、和有序性展开的。
- 能干嘛
- 通过JMM来实现线程和主内存之间的抽象关系
- 屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序在各个平台都能达到一致的内存访问效果
- JMM规范下,三大特性
- 可见性
- 是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即直到该变更,JMM规定了所有的变量都存储在主内存中
- 系统主内存共享变量数据修改被写入的时机是不确定的,多线程并发下很可能出现脏读,所以每个线程都有自己的工作内存,线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等)都必须在线程自己的工作内存中进行,而不能够直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成
- 存在脏读
- 主内存中有变量X,初始值为0
- 线程A要将X+1,现将x=0拷贝到自己的私有内存中,然后更新x的值x=1
- 线程A将更新后的x回刷到主内存的时间是不固定的
- 刚好在线程A没有回刷x到主内存时,线程B同样从主内存中读取x=0,和线程A一样的操作
- 最后期望的是x被加两次,x=2,实际上x只被加了一次x=1
- 原子性
- 指一个操作是不可打断的,即多线程环境下,操作不能被其他线程干扰
- 有序性
- 对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。但为了提升性能,编译器和处理器通常会对指令序列进行重新排序。Java规范规定了JVM线程内部维持顺序化语义,即只要程序的最终结果和它顺序化执行的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序
- 优缺点
- JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能
- 但是,质量重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致(即可能产生“脏读”),简单说,两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得从上到下顺序执行,执行顺序会被优化
- 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致
- 处理器在进行重排序时必须要考虑指令之间的数据依赖性
- 多线程环境下中线程交替执行,由于编译器优化排序重排的存在,两个线程中使用的变量能够保证一致性是无法确定的,结果无法预测
- JMM规范下,多线程对变量的读写过程
- (读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到线程自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储这主内存的变量副本拷贝,因此不同的线程间无法访问对方的工作内容,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图
- JMM规范下,多线程先行发生原则之happens-before
- 在JMM中,如果一个操作执行的结果需要对另一个操作可见性或者代码重排序,那么这两个操作之间必须存在happens-before(先行发生)原则
- 总原则
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将会对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前
- 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则执行的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法
- happens-before之8条
- 次序规则
- 一个线程内,按照代码执行顺序,写在前面的操作先行发生于写在后面的操作
- 锁定规则
- 一个unLock操作线程发生于后面对一个锁的lock操作线程
- 对于同一把锁object,ThreadA一定要先unlock同一把锁后ThreadB才能获得该锁
- volatile变量规则
- 对一个volatile变量的写操作先行发生于后面对这个变量的读操作,前面的写对后面的读是可见的。
- 传递规则
- 如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
- 线程启动规则
- Thread对象的start()方法线程发生于此线程的每一个动作
- 线程中断规则
- 对线程的interrupt()方法的调用线程发生于被中断线程的代码检测到中断事件的发生
- 线程终止规则
- 线程中的所有操作都先行发生于对此线程的终止检测,可以通过isAlive()等手段检测线程是否已经终止执行
- 对象终结规则
- 一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始
- 次序规则
public class Demo {
private int value = 0;
public int getValue(){
return value;
}
public void setValue(){
++value;
}
}
- 假如存在线程A和B,线程A先调用setValue(),然后线程B调用同一个对象的getValue(),那么线程B收到的返回值是什么?
- 由于两个方法是由不同的线程调用,不再同一线程中,所以肯定不满足程序次序规则
- 两个方法都没有使用锁,所以不满足锁定规则
- 变量不是用volatile修饰的,所以volatile变量规则不满足
- 传递规则肯定不满足
- 所以我们无法通过happens-before原则推到出线程A happens-before线程B,虽然可以确认在时间上线程A优先于线程B指定,但就是无法确认线程B获取的结果是什么,所以这段代码不是线程安全的。
- 修复上面的方法
- 把getter/setter方法都定义为synchronized方法
- 把value定义为volatile变量,利用volatile保证读取操作的可见性,利用synchronized保证复合操作的原子性
public class Demo {
private volatile int value = 0;
public int getValue(){
return value; //利用volatile保证读取操作的可见性
}
public synchronized void setValue(){
++value; //利用synchronized保证复合操作的原子性
}
}
Volatile
- 被volatile修饰的变量有2大特点
- 可见性
- 有序性
- volatile的内存语义
- 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新会主内存中
- 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,重新回到主内存读取最新共享变量
- 所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取
- volatile凭什么可以保证可见性和有序性
- 内存屏障Memory
- 内存屏障(也称内存栅栏,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入指定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性(禁重排),但volatile无法保证原子性
- 内存屏障之前的所有写操作都要回写到主内存
- 内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)
- 写屏障(Store Memory Barrier):告诉处理器在写屏障之前将所有存储在缓存(store bufferes)中的数据同步到主内存中。也就是说当看到Store屏障指令,就必须把该指令之前所有写入指令执行完毕才能继续往下执行
- 读屏障(Load Memory Barrier):处理器在读屏障之后的读操作,都是在读屏障之后执行。也就是说在Load屏障指令之后就能保证后面的读取数据指令一定能够读取到最新的数据
- 因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。对于volatile变量的写,先行发生于任何后序对这个volatile变量的读,也叫写后读
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad | load1;LoadLoad;Load2 | 保证Load1的读取操作在Load2及后序读取操作之前执行 |
StoreStore | Store1;StoreStore;Store2 | 在store2及其后的写操作执行前,保证Store1的写操作已刷新到主内存 |
LoadStore | Load1;LoadStore;Store2 | 在store2及其后的写操作执行前,保证load1的读操作已读取结束 |
StoreLoad | Store1;StoreLoad;Load2 | 保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行 |
- 读屏障
- 在每个volatile读操作的后面插入一个LoadLoad屏障
- 在每个volatile读操作的后面插入一个LoadStore屏障
- 写屏障
- 在每个volatile写操作的前面插入一个StoreStore屏障
- 在每个volatile写操作的后面插入一个StoreLoad屏障
CAS(原子类)
- java.util.concurrent.atomic包下面的类
- 没有CAS之前,多线程环境下不使用原子类保证线程安全i++(基本数据类型)
public class Test {
volatile int number = 0;
//读取
public int getNumber(){
return number;
}
//写入,加锁保证原子性
public synchronized void setNumber(){
number++;
}
}
- 使用CAS之后,多线程环境下,使用原子类保证线程安全i++(基本数据类型)
public class Test {
AtomicInteger atomicInteger = new AtomicInteger();
public int getAtomicInteger(){
return atomicInteger.get();
}
public void setAtomicInteger(){
atomicInteger.getAndIncrement();
}
}
-
CAS
- compare and swap的缩写,中文翻译成比较并交换,实现并发算法时常用到的一种技术。
- 它包含三个操作数——内存位置、预期原值及更新值
- 执行CAS操作的时候,将内存位置的值与预期原值比较
- 如果相匹配,那么处理器会自动将位置值更新为新值
- 如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功
-
CAS有三个操作数,位置内存值V,旧得预期值A,要修改的更新值B
-
当且仅当就的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做或重来(自旋)。
- CASDemo代码
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
//将atomicInteger从5变到2022,判断预期值5和内存中的值5是否相等,相等则更新为2022(乐观锁)
System.out.println(atomicInteger.compareAndSet(5, 2022)); //true
System.out.println(atomicInteger.get()); //2022
System.out.println(atomicInteger.compareAndSet(5, 2023)); //false预期值5!=2022 修改失败
System.out.println(atomicInteger.get()); //2022
}
}
-
硬件级别保证
- CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性
- CAS是一条CPUd的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg
- 执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起synchronized重量级锁,这里的排他时间要短的多,所以在多线程情况下性能会比较好
-
compareAndSet底层调的是unsafe.compareAndSwapInt()
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
- compareAndSet()方法的源代码
- public final native boolean compareAndSwapObject(Object var1,long var2,Object var4,Object var5)
- public final native boolean compareAndSwapInt(Object var1,long var2,int var4,int var5)
- public final native boolean compareAndSwapLong(Object var1,long var2,long var4,long var6)
- 参数说明
- var1:表示要操作的对象
- var2:表示要操作对象中属性地址的偏移量
- var4:表示需要修改数据的期望的值
- var5/6表示需要修改为的新值
- unsale类
- Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以向C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法
- Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
- 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的
- 变量value用volatile修饰,保证了多线程之前的内存可见性
- getAndIncrement
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
- CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用语完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所有的数据不一致问题
- 原子引用AtomicReference
@Data
@AllArgsConstructor
@NoArgsConstructor
class User{
private String userName;
private int age;
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
//原子引用类
AtomicReference<User> atomicReference = new AtomicReference<User>();
User zhangsan = new User("张三",20);
User lisi = new User("李四",20);
atomicReference.set(zhangsan);
//修改为李四 true(修改成功)
System.out.println(atomicReference.compareAndSet(zhangsan, lisi));
//User(userName=李四, age=20)
System.out.println(atomicReference.get().toString());
//再次修改为李四 false(修改失败,因为已经修改成了李四再次修改 zhangsan != list)
System.out.println(atomicReference.compareAndSet(zhangsan, lisi));
//User(userName=李四, age=20)
System.out.println(atomicReference.get().toString());
}
}
- CAS与自旋锁
- CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,以达到锁的效果,自旋锁,看字面意思,自己旋转。是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
- 通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来发现当前有线程持有锁,所以只能通过自旋等待,直到A释放锁后B随后抢到
public class SpinLockDemo {
AtomicReference<Thread> atomicReference = new AtomicReference<Thread>();
public void lock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"\t"+"come in");
//拿到锁则跳出循环,没拿到一直循环
while (!atomicReference.compareAndSet(null,thread)){
}
}
public void unlock(){
Thread thread = Thread.currentThread();
atomicReference.compareAndSet(thread,null);
System.out.println(Thread.currentThread().getName()+"\t"+"task over unlock");
}
public static void main(String[] args) {
SpinLockDemo spinLockDemo = new SpinLockDemo();
new Thread(()->{
spinLockDemo.lock();
try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); }
spinLockDemo.unlock();
},"A").start();
try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
new Thread(()->{
spinLockDemo.lock();
try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); }
spinLockDemo.unlock();
},"B").start();
}
}
- CAS缺点
- 循环时间长开销很大
- 引出ABA问题
- CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么再这个时间差类会导致数据的变化。
- 比如说一个线程1从内存位置V中取出A,这时另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B,然后线程2又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,预期OK,然后线程1操作成功
- 尽管线程1的CAS操作成功,但是不代表这个过程就是没有问题的。
- 版本号时间戳原子引用
- AtomicStampedReference
@Data
@NoArgsConstructor
@AllArgsConstructor
class Book{
private int id;
private String bookName;
}
public class AtomicStampedDemo {
public static void main(String[] args) {
Book book_java = new Book(001, "JAVA");
AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<Book>(book_java,1);
//Book(id=1, bookName=JAVA)
System.out.println(stampedReference.getReference());
Book book_mysql = new Book(002, "MYSQL");
stampedReference.compareAndSet(book_java,book_mysql,stampedReference.getStamp(),stampedReference.getStamp()+1);
//Book(id=2, bookName=MYSQL)
System.out.println(stampedReference.getReference());
stampedReference.compareAndSet(book_mysql,book_java,stampedReference.getStamp(),stampedReference.getStamp()+1);
//Book(id=1, bookName=JAVA)
System.out.println(stampedReference.getReference());
}
}
- 多线程情况下使用不带时间戳的原子引用存在的ABA问题
public class ABADemo {
static AtomicInteger atomicInteger = new AtomicInteger(100);
public static void main(String[] args) {
new Thread(()->{
//修改为101
atomicInteger.compareAndSet(100,101);
//修改回100
atomicInteger.compareAndSet(101,100);
},"t1").start();
new Thread(()->{
try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace();}
//true 修改成功
System.out.println(atomicInteger.compareAndSet(100, 2022));
//2022
System.out.println(atomicInteger.get());
},"t2").start();
}
}
- 使用AtomicStampedReference
public class ABADemo {
static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
new Thread(()->{
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t首次版本号:" + stamp);
//暂停500ms,保证后面t2线程初始化拿到的版本号和t1一样
try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace();}
//修改为101
stampedReference.compareAndSet(100,101,stampedReference.getStamp(), stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t第二次版本号:"+stampedReference.getStamp());
//修改回100
stampedReference.compareAndSet(101,100,stampedReference.getStamp(), stampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t第三次版本号:"+stampedReference.getStamp());
},"t1").start();
new Thread(()->{
int stamp = stampedReference.getStamp();
System.out.println(Thread.currentThread().getName()+"\t首次版本号:" + stamp);
//暂停1秒钟,等待t1线程发生ABA问题
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace();}
//false 修改失败
System.out.println(stampedReference.compareAndSet(100, 2022,stamp, stamp+1));
//100
System.out.println(stampedReference.getReference());
},"t2").start();
}
}
-
原子操作类
-
基本类型原子类
- AtomicInteger
- AtomicBoolean
- AtomicLong
-
AtomicIntegerDemo
public class AtomicIntegerDemo {
public static void main(String[] args) throws InterruptedException {
int size = 50;
AtomicInteger atomicInteger = new AtomicInteger();
//计数
CountDownLatch countDownLatch = new CountDownLatch(size);
for (int i = 0; i < size; i++) {
new Thread(()->{
try {
atomicInteger.getAndIncrement();
}finally {
countDownLatch.countDown();
}
},"t"+i).start();
}
//等待50个线程全部执行完再往下执行,否则等待
countDownLatch.await();
//50
System.out.println(atomicInteger.get());
}
}
-
数组类型原子类
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
-
引用类型原子类
- AtomicReference
- AtomicStampeReference
- AtomicMarkableReference
- 原子更新带有标记为的引用类型对象
- 解决是否修改过,它的定义就是将状态戳简化为true|false
-
AtomicMarkableReferenceDemo.java
public class AtomicMarkableReferenceDemo {
static AtomicMarkableReference markableReference = new AtomicMarkableReference(100,false);
public static void main(String[] args) {
new Thread(()->{
boolean marked = markableReference.isMarked();
//暂停1秒钟,等待后面的t2线程拿到一样的flag标识,都是false
System.out.println(Thread.currentThread().getName()+"\t默认标识:"+marked);
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace();}
markableReference.compareAndSet(100,1000,marked,!marked);
},"t1").start();
new Thread(()->{
boolean marked = markableReference.isMarked();
//false
System.out.println(Thread.currentThread().getName()+"\t默认标识:"+marked);
try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace();}
boolean b = markableReference.compareAndSet(100,2000,marked,!marked);
//false 修改失败,状态已经被修改过了,t1将false修改为true,所以t2再修改时修改失败
System.out.println(Thread.currentThread().getName()+"\tt2线程casResut:"+b);
//true
System.out.println(Thread.currentThread().getName()+"\t"+markableReference.isMarked());
//1000
System.out.println(Thread.currentThread().getName()+"\t"+markableReference.getReference());
},"t2").start();
}
}
- 对象的属性修改原子类
- AtomicIntegerFieldUpdater(基于反射应用程序,可对指定类的指定volatile int字段进行原子更新)
- AtomicLongFieldUpdater
- AtomicReferenceFieldUpdater
- 使用目的
- 以一种线程安全的方式操作非线程安全对象内的某些字段
- 使用要求
- 更新的对象属性必须使用public valatile修饰
- 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdate()创建一个更新器,并且需要设置想要更新的类和属性
- 需求
- 10个线程,每个线程转账1,不使用synchronized,尝试使用AtomicIntegerFieldUpdater来实现
- 使用synchronized
class BankAccount{
String bankName = "CCB";
public int money = 0;
public synchronized void add(){
money++;
}
}
public class AtomicIntegerFieldUpdateDemo {
public static void main(String[] args) throws InterruptedException {
BankAccount bankAccount = new BankAccount();
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
bankAccount.add();
}finally {
countDownLatch.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch.await();
//10
System.out.println(bankAccount.money);
}
}
- 使用AtomicIntegerFieldUpdater
class BankAccount{
String bankName = "CCB";
public volatile int money = 0;
AtomicIntegerFieldUpdater<BankAccount> fieldUpdater =
AtomicIntegerFieldUpdater.newUpdater(BankAccount.class,"money");
public void transMoney(BankAccount bankAccount){
fieldUpdater.getAndIncrement(bankAccount);
}
}
public class AtomicIntegerFieldUpdateDemo {
public static void main(String[] args) throws InterruptedException {
BankAccount bankAccount = new BankAccount();
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
bankAccount.transMoney(bankAccount);
}finally {
countDownLatch.countDown();
}
},String.valueOf(i)).start();
}
countDownLatch.await();
//10
System.out.println(bankAccount.money);
}
}
- 需求
- 多线程并发调用一个类的初始化方法,如果为被初始化,将执行初始化工作
- 要求只能被初始化一次,只有一个线程操作成功
class MyVar{
public volatile Boolean isInit = Boolean.FALSE;
AtomicReferenceFieldUpdater<MyVar,Boolean> referenceFieldUpdater =
AtomicReferenceFieldUpdater.newUpdater(MyVar.class,Boolean.class,"isInit");
public void init(MyVar myVar){
if(referenceFieldUpdater.compareAndSet(myVar,Boolean.FALSE,Boolean.TRUE)){
System.out.println(Thread.currentThread().getName()+"\t 初始化");
}else{
System.out.println("已经有线程在初始化");
}
}
}
public class AtomicReferenceFieldUpdateDemo {
public static void main(String[] args) {
MyVar myVar = new MyVar();
for (int i = 0; i <5; i++) {
new Thread(()->{
myVar.init(myVar);
},"t"+i).start();
}
}
}
- 控制台输出
t0 初始化
已经有线程在初始化
已经有线程在初始化
已经有线程在初始化
已经有线程在初始化
- 原子操作增强类
- DoubleAccumulator(一个或多个变量共同维护使用提供的函数更新的运行double值)
- DoubleAdder(一个或多个变量共同维护最初的零和double总和)
- LongAccumulator(一个或多个变量共同维护使用提供的函数更新运行long值)
- LongAdder
- 一个或多个变量共同维护最初为零的总和为long,当多个线程更新用于收集统计信息单不用于细粒度同步控制目的公共和时,此类通常优于AtomicLong。在低更新争用下,这两个类具有相似的特征。但在高争用的情况下,这一类预期吞吐量明显更高,但代价是空间消耗更高
- 如果是JDK8,推荐使用LongAddr对象,比AtomicLong性能更好(减少乐观锁的重试次数)
- 案例:热点商品点赞计数器,点赞数加加统计,不要求实时精确
- LongAdder常用API
- void add(long x):将当前的加x
- void increment():将当前的value加1
- void decrement():将当前的value减1
- long sum():返回当前值。特别注意,在没有并发更新value的情况下,sum会返回一个精确值,在存在并发的情况下,sum不保证返回精确值。
- void reset():将value重置为0,可用于代替重新new一个LongAddrer,但此方法值可以在没有并发更新的情况下使用
- long sumThenReset():获取当前value,并将value重置为0
public class LongAdderApiDemo {
public static void main(String[] args) {
//LongAdder只能用来计算加法,且从零开始计算
LongAdder longAdder = new LongAdder();
longAdder.increment();
longAdder.increment();
longAdder.increment();
//求和
System.out.println(longAdder.sum());
//LongAccumulator提供了自定义的函数操作
//参数1 自定义函数操作 参数2 初始值
LongAccumulator longAccumulator = new LongAccumulator((x,y)->x+y,0);
//0+1
longAccumulator.accumulate(1);
//1+3
longAccumulator.accumulate(3);
//4
System.out.println(longAccumulator.get());
}
}
- 案例:点赞计数,50个线程,每个线程100w次,总点赞数
- synchronized:1341ms
- AtomicLong:900ms
- LongAdder:93ms
- LongAccumulator:81ms
class ClickNumber{
int number = 0;
//使用synchronized
public synchronized void clickBySynchronized(){
number++;
}
//使用AtomicLong
AtomicLong atomicLong = new AtomicLong(0);
public void clickByAtomicLong(){
atomicLong.getAndIncrement();
}
//使用LongAdder
LongAdder longAdder = new LongAdder();
public void clickByLongAdder(){
longAdder.increment();
}
//使用LongAccumulator
LongAccumulator longAccumulator = new LongAccumulator((x,y)->x+y,0);
public void clickByLongAccumulator(){
longAccumulator.accumulate(1);
}
}
public class AccumulatorCompareDemo {
public static void main(String[] args) throws Exception {
ClickNumber clickNumber = new ClickNumber();
long startTime;
long endTime;
CountDownLatch countDownLatch1 = new CountDownLatch(50);
CountDownLatch countDownLatch2 = new CountDownLatch(50);
CountDownLatch countDownLatch3 = new CountDownLatch(50);
CountDownLatch countDownLatch4 = new CountDownLatch(50);
startTime = System.currentTimeMillis();
for (int i = 0; i < 50; i++) {
new Thread(()->{
try {
for (int j = 0;j<1000000;j++){
clickNumber.clickBySynchronized();
}
}finally {
countDownLatch1.countDown();
}
},"synchronized").start();
}
countDownLatch1.await();
endTime = System.currentTimeMillis();
//时间:1341 总数:50000000
System.out.println("时间:" + (endTime - startTime)+"毫秒\t总数:"+clickNumber.number);
startTime = System.currentTimeMillis();
for (int i = 0; i < 50; i++) {
new Thread(()->{
try {
for (int j = 0;j<1000000;j++){
clickNumber.clickByAtomicLong();
}
}finally {
countDownLatch2.countDown();
}
},"AtomicLong").start();
}
countDownLatch2.await();
endTime = System.currentTimeMillis();
//时间:900毫秒 总数:50000000
System.out.println("时间:" + (endTime - startTime)+"毫秒\t总数:"+clickNumber.atomicLong.get());
startTime = System.currentTimeMillis();
for (int i = 0; i < 50; i++) {
new Thread(()->{
try {
for (int j = 0;j<1000000;j++){
clickNumber.clickByLongAdder();
}
}finally {
countDownLatch3.countDown();
}
},"LongAdder").start();
}
countDownLatch3.await();
endTime = System.currentTimeMillis();
//时间:93毫秒 总数:50000000
System.out.println("时间:" + (endTime - startTime)+"毫秒\t总数:"+clickNumber.longAdder.sum());
startTime = System.currentTimeMillis();
for (int i = 0; i < 50; i++) {
new Thread(()->{
try {
for (int j = 0;j<1000000;j++){
clickNumber.clickByLongAccumulator();
}
}finally {
countDownLatch4.countDown();
}
},"LongAccumulator").start();
}
countDownLatch4.await();
endTime = System.currentTimeMillis();
//时间:81毫秒 总数:50000000
System.out.println("时间:" + (endTime - startTime)+"毫秒\t总数:"+clickNumber.longAccumulator.get());
}
}
-
LongAderr相对于AtomicLong额外继承了Striped64类
-
Striped64几个重要的成员函数
/** CPU数量,即cells数组最大长度 */
static final int NCPU = Runtime.getRuntime().availableProcessors();
/**
* cells数组,为2的幂,2 4 8 16 ... 方便以后位运算
*/
transient volatile Cell[] cells;
/**
* 基础value值,当并发较低时,只累加该值主要用于没有竞争的情况,通过CAS更新
*/
transient volatile long base;
/**
* 创建或扩容cells数组时使用的自旋锁变量调整单元格大小(扩容),创建单元格时使用的锁
*/
transient volatile int cellsBusy;
- Cell是Striped64类中的一个内部类
-
LongAdder的基本思想就是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽位,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。如果要获取真正的Long值,只要将各个槽中的变量值累加返回
-
sum()会将所有Cell数组中的value和base累加作为返回值,核心的思想就是将之前AtomicLong一个value的更新压力分散到多个value中去,从而降低更新热点
-
LongAdder在无竞争的情况下,跟AtomicLong一样,对同一个base进行操作,当出现竞争关系的时则是采用化整为零分散热点的做法,用空间换时间,用一个数组cells,将一个value拆分进这个数组cells。多个线程需要同时对value进行操作的时候,可以对线程id进行hash的到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells的所有值和base都加起来作为最终结果
ThreadLocal
- 实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不用麻烦别人,不和其他人共享),主要解决了让每个线程绑定自己的值,通过get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题
- 案例1:5个销售卖房子,集团高层只关心销售总量的准确统计数,按照总销售额统计,方便集团公司给部门发送奖金
//资源类
class House{
int saleCount = 0;
public synchronized void sale(){
saleCount++;
}
}
public class ThreadLocalDemo {
public static void main(String[] args) throws Exception{
House house = new House();
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(()->{
try {
for (int j = 0; j < 5; j++) {
house.sale();
}
}finally {
countDownLatch.countDown();
}
},"t"+i).start();
}
countDownLatch.await();
//25
System.out.println(house.saleCount);
}
}
- 案例2:5个销售卖完随机数房子,各自独立销售额度,自己业务按提成走
//资源类
class House{
int saleCount = 0;
public synchronized void sale(){
saleCount++;
}
ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(()->0);
public void saleVolumeByThreadLocal(){
saleVolume.set(1+saleVolume.get());
}
}
public class ThreadLocalDemo {
public static void main(String[] args) throws Exception{
House house = new House();
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
new Thread(()->{
try {
int size = new Random().nextInt(5)+1;
for (int j = 0; j < size; j++) {
house.sale();
house.saleVolumeByThreadLocal();
}
System.out.println(Thread.currentThread().getName()+"\t销售卖出:"+house.saleVolume.get());
}finally {
countDownLatch.countDown();
/**
* 释放线程中的ThreadLocal变量,防止内存泄漏
* 尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能会影响后序业务和造成内存泄漏等问题
*/
house.saleVolume.remove();
}
},"t"+i).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"\t共计卖出:"+house.saleCount);
}
}
t2 销售卖出:1
t4 销售卖出:5
t3 销售卖出:4
t1 销售卖出:1
main 共计卖出:15
- ThreadLoaclMap实际上就是一个以ThreadLoacl实例为key,任意对象为value的Entry对象
- 当我们为threadLocal变量赋值,实际上就是以当前threadLoacl实例为key,值为value的Entry往这个threadLocalMap中存放
- 引用
- Java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作
- 强引用
- 当内存不足时,JVM开始垃圾回收,对于强引用的对象,就算出现了OOM也不会对该对象进行回收。
class MyObject{
@Override
protected void finalize() throws Throwable {
//finalize的通常目的是在对象被不可撤销地丢弃之前执行清理操作
System.out.println("-------invoke finalize method-------");
}
}
public class ReferenceDemo {
public static void main(String[] args) {
//强引用
MyObject myObject = new MyObject();
//实例对象没有引用指向,gc该对象会被回收
myObject = null;
System.gc();//开启垃圾回收
}
}
-------invoke finalize method-------
-
软引用
- 软引用时一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集
- 对于只有软引用的对象来说
- 当系统内存充足时不会被回收
- 当系统内存不足时会被回收
- 软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收
-
配置jvm参数,设置内存大小为10m -Xms10m -Xmx10m
-
创建一个弱引用对象,然后创建一个20m的byte数组,内存不足,则弱引用对象会被回收
class MyObject{
@Override
protected void finalize() throws Throwable {
//finalize的通常目的是在对象被不可撤销地丢弃之前执行清理操作
System.out.println("-------invoke finalize method-------");
}
}
public class ReferenceDemo {
public static void main(String[] args) throws Exception {
//软引用
SoftReference<MyObject> myObjectSoftReference = new SoftReference<>(new MyObject());
//开启垃圾回收
System.gc();
Thread.sleep(1000);
//20MB
byte[] bytes = new byte[20 * 1024 * 1024];
}
}
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at ReferenceDemo.main(ReferenceDemo.java:18)
-------invoke finalize method-------
- 弱引用
- 弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短
- 对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存
class MyObject{
@Override
protected void finalize() throws Throwable {
//finalize的通常目的是在对象被不可撤销地丢弃之前执行清理操作
System.out.println("-------invoke finalize method-------");
}
}
public class ReferenceDemo {
public static void main(String[] args) throws Exception {
WeakReference<MyObject> myObjectWeakReference = new WeakReference<>(new MyObject());
System.gc();
Thread.sleep(1000);
}
}
-------invoke finalize method-------
-
软引用和弱引用的使用场景
-
假如有一个引用需要读取大量的本地图片
- 如果每次读取图片都从硬盘读取则会严重影响性能
- 如果一次性全部加载到内存中有可能造成内存溢出
-
此时使用软引用可以解决这个问题
- 设计思路:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所在得空间,从而有效地避免OOM的问题
- Map<String,SoftReference
> imageCache = new HashMap<String,SoftReference >();
-
虚引用
-虚引用必须和引用队列(ReferenceQueue)联合使用- 虚引用需要java.lang.ref.PhantomReference类来实现,顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用
- PhantomReference的get方法总是返回null
- 虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅就提供一个确保对象被finalize以后,做某些事情的通知机制。PhantomReference的get方法总是返回null,因此无法访问对应的引用对象
- 处理监控通知使用
- 设置虚引用关联对象的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知挥着后续添加进一步的处理,用来实现比finalize机制更加灵活的回收操作
-
设置jvm参数 -Xms10m -Xmx10m
class MyObject{
@Override
protected void finalize() throws Throwable {
//finalize的通常目的是在对象被不可撤销地丢弃之前执行清理操作
System.out.println("-------invoke finalize method-------");
}
}
public class ReferenceDemo {
public static void main(String[] args) throws Exception {
MyObject myObject = new MyObject();
//引用队列
ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue<>();
//虚引用
PhantomReference<MyObject> myObjectPhantomReference = new PhantomReference<>(myObject, referenceQueue);
List<byte[]> list = new ArrayList<>();
new Thread(()->{
while (true){
list.add(new byte[1 * 1024 * 1024]);
try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace();}
System.out.println(myObjectPhantomReference.get());
}
},"t1").start();
new Thread(()->{
while (true){
Reference<? extends MyObject> reference = referenceQueue.poll();
if (reference != null){
System.out.println("-----有虚对象回收加入了队列-----");
break;
}
}
},"t2").start();
}
}
- ThreadLocal是一个壳子,真正的存储结构是ThreadLocal里有ThreadLocalMap(static)这个内部类,每个Thread对象维护着一个ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
- 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值Value是传递进来的对象
- 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
- ThreadLocal本身并不存储值(ThreadLocal是一个壳子),它只是自己作为key来让线程从ThreadLocalMap获取value
- 正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响
- ThreadLoaclMap中entry使用的是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
- 案例
public void func1(){
//新建了一个ThreadLocal对象,threadLocal是强引用指向这个对象
ThreadLocal<String> threadLocal = new ThreadLocal<>();
//调用set()方法后新建一个Entry,通过源码可知Entry对象里面的k是弱引用指向这个对象
threadLocal.set("cml");
threadLocal.get();
}
- 为什么需要弱引用
- 当func1方法执行完毕后,栈帧销毁强引用threadLocal。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象
- 若这个key引用时强引用,就会导致key指向的ThreadLocal对象以及V指向的对象不能被gc回收,造成内存泄漏
- 若这个key引用时弱引用就大概率会减少内存泄漏问题。使用若引用,就可以是ThreadLocal对象在方法执行完毕后顺利回收Entry的key引用指向为null
Java对象内存布局和对象头
-
在HotSpot虚拟机里,对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充
-
对象头
- 对象标记Mark Word
- 类元信息(类型指针)
- 对象内存结构分为:对象头、实例数据、对齐填充(保证8个字节的倍数)
- 对象头分为对象标记(markOop)和类元信息(klassOop),类元信息存储的是指向该对象类元数据(klass)的首地址
- 对象标记
- 哈希吗
- GC标记
- GC次数
- 同步锁标记
- 偏向锁标记
- HotSpot虚拟机对象标记Mark Word
- 默认存储对象的HashCode、分代年龄和锁标志位等信息
- 这些信息都是与对象自身定义无关的数据,所以MarkWord被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。
- 它会根据对象的状态复用自己的存储空间,也就是说在运行期间MarkWord里存储的数据会随着锁标志位的变化而变化
存储内筒 | 标志位 | 状态 |
---|---|---|
对象哈希码、对象分代年龄 | 01 | 未锁定 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
空,不需要记录信息 | 11 | GC标记 |
偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
- 在64位系统中,Mark Word占了8个字节,类型指针占了8个字节,一共是16个字节
- 在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下所示
锁状态 | 25bit | 31bit | 1bit(cms_free) | 4bit(分代年龄) | 1bit(偏向锁) | 2bit(锁标志位) |
---|---|---|---|---|---|---|
无锁 | unused | hashCode | 0 | 01 | ||
偏向锁 | ThreadId | ThreadId | 1 | 01 |
- 类元信息(类型指针)
- 对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
- 实例数据
- 存储类的属性(Field)数据信息,包括父类的属性信息
- 对齐填充
- 虚拟机要求对象的起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这部分内存按8字节补充对齐