Java并发理论基础
1. 为什么Java多线程并发很重要
硬件方面:当前摩尔定律(价格不变时,同等大小的集成电路上的元器件个数每隔18-24个月就增加一倍)开始失效,CPU主频不再翻倍,而是采用多核的方式,单核执行效率不再增长,则想要程序处理更快就需要多核并行或并发来编程。
软件方面:互联网企业高并发系统,很多异步、任务等操作需要高效调用,比如说批量调用第三方接口(推送、打标签等),采用多线程并发调用就能大大提升效率。
2. Java底层是什么
Java底层的调用就是在调用C和C++的代码,比如Thread类,底层实际调用的就是thread.c和thread.cpp的代码。当我们在如下的Java代码调用到start的时候,实际会调用到 private native void start0(); 这一行代码,这一行代码在jdk中就会调用thread.c中的JVM_StartThread,实际上是依赖C去调用操作系统底层的代码。C中的JVM_StartThread会调用到hotspot虚拟机中的C++方法JVM_StartThread,使用代码Thread::start(native_thread);又可以调用到os::start_thread(thread);代码,通过os底层创建一个线程。
3. 进程线程管程概念
进程:程序的一次执行,是一个独立单位,没一个进程都有自己的内存空间和系统资源,比如说一个QQ进程、Java程序进程。因为是隔离的,所以进程与进程之间基本上很少存在竞争关系。
线程:在一个进程内可以执行多个任务,每个任务就是一个线程,比如说在QQ中,可以同时去聊天,同时修改个人资料等。因为多个线程可以共用一片内存空间,所以多线程的并发问题就出来了。
管程:Monitor,监视器,实际上是一种同步机制,保证同一时间只有一个线程可以访问被保护的数据和代码。JVM中的synchronized就是通过monitor的进入和退出来实现的,每个对象都有monitor这个对象,每个对象都可以通过它来进行加锁。
4. 多线程带来的问题
- 线程安全问题:多个线程竞争一个资源,比如说同时对i进行++操作,最后得出来的结果会是错误的。
- 上下文切换导致的性能问题:单个处理器同一时间只能处理一个线程执行,但是CPU是通过时间片算法来执行的。CPU会根据线程的状态来动态切换,在切换时保存当前线程的状态,然后去处理别的任务,处理完后再回头接着保存的状态进行处理。
- 死锁、锁饥饿问题:死锁就是两个线程分别加锁了两个对象,而两个线程又分别需要等待对方加锁的对象解锁,此时就陷入了互斥的状态。饥饿就是因为非公平锁存在多个线程排队竞争,有些线程会一直获取不到锁。
5. 用户线程和守护线程
Java线程分为用户线程和守护线程,线程daemon属性为true表示是守护线程,false表示是用户线程。
守护线程:一种特殊线程,在后台完成一些系统性的服务,比如GC垃圾回收线程。当程序中的用户线程,包括main主线程执行完毕后,不管守护线程是否结束,系统都会自动退出。
用户线程:系统工作线程,用户创建的,比如说推送系统中的批量推送线程。main主线程结束了,别的用户线程都还会一直执行。
6. 什么是JUC
JUC是java.util.concurrent包的简称,JDK1.5版本开始有的。Doug Lea主编写的。
CompletableFuture详解
1. FutureTask分析
原始的FutureTask会阻塞,会消耗大量资源,代码如下:
1 | public class FutureTest { |
2. CompletableFuture:對FutureTask的改进版
CompletableFuture可以直接给回调,不阻塞了。也能将多个异步任务组合成一个异步计算,可以等待所有异步任务都完成后返回结果等等。比如说电商系统中,查询商品详情要调用库存服务查询库存,调用营销服务查询活动,调用商品服务查询商详等等,就可以通过CompletableFuture创建多个线程任务来并发查询,查询完成后通过组合结果来返回,避免了单线程的效率低下问题。CompletableFuture实现了CompletionStage接口,就能实现这种阶段性的任务。
举个Linux的小例子:ps -ef | grep java,这个命令的grep java就依赖了ps -ef的结果。
CompletableFuture优点:
1.异步任务结束时,会自动回调某个对象的方法。
2.异步任务出错时,会自动回调某个对象的方法。
3.主线程设置好回调后,不再关心异步任务的执行,异步任务之间可以顺序执行。
1 | public class FutureTest { |
3. CompletableFuture案例
电商商详页在获取详情时,需要获取SPU基本信息,SKU集合,SPU图片列表,属性等,如果线性获取的话极其费时,此时启动一批CompletableFuture任务来并发执行,再通过CompletableFuture.allOf就可以等待所有任务执行完成后获取结果。
其中,我们还能使用thenAcceptAsync方法,让一个任务的结果依赖其他的任务。案例中的SPU规格参数获取就要依赖第一步的SPU基本信息。
1 |
|
Java锁
简单理解悲观锁和乐观锁
悲观锁认为自己在使用数据的时候一定有别的线程也来使用数据,所以悲观锁在获取数据的时候就会先加锁,确保数据不会被其他线程修改到。synchronized和Lock的实现类都是悲观锁。
乐观锁认为自己在使用数据的时候不会有其他线程来修改数据,不会加锁,只会去判断在修改数据的一刻有没有其他线程更新了数据,如果没被其他线程更新,则直接修改数据,如果被更新了,就根据当前锁的实现方式做不同的处理。 一般都是采用无锁方式实现,比如CAS算法,Atomic原子类就是通过CAS实现,一种自旋锁,通过while循环不断去判断是否被其他线程更新了的方式。
git是一个乐观锁很好的例子,当写好了代码进行提交时,因为有其他人在这之前提交了一次,此次代码提交是无法提交上去的,必须先拉下代码来,才能再提交上去。
synchorized 8锁
两个加了synchronized修饰的普通方法,同一个对象调用,分别在两个线程中调用对象方法的时候,这个synchronized会锁住整个对象,执行时会按照调用顺序进行输出。
1
2
3
4
5
6
7
8
9
10class Phone {
//1. 先调用
public synchronized void sendSms() {
log.info("send sms");
}
//2. 后调用
public synchronized void playGame() {
log.info("play game");
}
}两个加了synchronized修饰的普通方法,先调用的加了3秒休眠,同一个对象调用,这个时候会阻塞住三秒,等待sendSms执行完成后再执行playGame。
1
2
3
4
5
6
7
8
9
10
11class Phone {
//1. 先调用
public synchronized void sendSms() {
TimeUnit.SECONDS.sleep(3);
log.info("send sms");
}
//2. 后调用
public synchronized void playGame() {
log.info("play game");
}
}一个加了synchronized的普通方法,一个没加锁的普通方法,同一个对象调用,这个时候,looklookShell直接输出,sendSms等待3秒后输出。
1
2
3
4
5
6
7
8
9
10
11class Phone {
//1. 先调用
public synchronized void sendSms() {
TimeUnit.SECONDS.sleep(3);
log.info("send sms");
}
//2. 后调用
public void looklookShell() {
log.info("looklookShell");
}
}两个加锁普通方法,两个不同的对象分别调用,此时后调用的会先输出,因为不是同一个对象,用的锁也不是同一把。
1
2
3
4
5
6
7
8
9
10
11class Phone {
//1. A对象先调用
public synchronized void sendSms() {
TimeUnit.SECONDS.sleep(3);
log.info("send sms");
}
//2. B对象后调用
public synchronized void playGame() {
log.info("play game");
}
}两个加锁的静态方法,同一个对象调用,这个时候因为是静态方法加锁,此时锁的是这个类本身。所以此时同一个对象调用的话会按照调用顺序输出。
1
2
3
4
5
6
7
8
9
10
11class Phone {
//1. 先调用
public static synchronized void sendSms() {
TimeUnit.SECONDS.sleep(3);
log.info("send sms");
}
//2. 后调用
public static synchronized void playGame() {
log.info("play game");
}
}两个加锁的静态方法,两个对象分别调用,这也是将整个类锁住了,此时两个对象分别调用静态同步方法的话也会按照先后顺序输出。
1 | class Phone { |
一个静态同步方法,一个普通同步方法,同一个对象进行调用,普通同步方法会先输出,静态同步方法会后输出。静态的和非静态的锁是两个不同的对象,两者无竞态条件,所以无休眠的普通方法会先输出。
1
2
3
4
5
6
7
8
9
10
11class Phone {
//1. 先调用
public static synchronized void sendSms() {
TimeUnit.SECONDS.sleep(3);
log.info("send sms");
}
//2. 后调用
public synchronized void playGame() {
log.info("play game");
}
}一个静态同步方法,一个普通同步方法,两个对象分别进行调用,普通同步方法会先输出,静态同步方法会后输出。静态的和非静态的锁是两个不同的对象,两者无竞态条件,所以无休眠的普通方法会先输出。
1
2
3
4
5
6
7
8
9
10
11class Phone {
//1. 先调用
public static synchronized void sendSms() {
TimeUnit.SECONDS.sleep(3);
log.info("send sms");
}
//2. 后调用
public synchronized void playGame() {
log.info("play game");
}
}
8锁总结:
一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法
锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
加个普通方法后发现和同步锁无关
换成两个对象后,不是同一把锁了,情况立刻变化。
都换成静态同步方法后,情况又变化
所有的非静态同步方法用的都是同一把锁——实例对象本身,也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。
所有的静态同步方法用的也是同一把锁——类对象本身,这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!
Synchronized字节码分析
对如下Java代码编译,会得到class字节码文件。再使用javap -c xxx.class命令,就可以得到反编译后的字节码文件。
synchronized底层就是通过monitorexit和monitorenter实现的,如下会出现两次monitorexit,是因为其要保证锁能够在出现异常的情况下也能正常解锁。
1 | //java代码 |
javap -c xxx.class后得到结果:
1 | public class com.whoiszxl.juc.SyncDemo { |
cpp底层源码分析:
每个对象都自带一个monitor,在hotspot虚拟机中,monitor是采用ObjectMonitor实现的,我们可以在ObjectMonitor.java里看到具体的代码实现,其次的实现是在ObjectMonitor.cpp里,我们可以在其中找到一些Java中notify,notifyAll,wait,tryLock等实现。
我们还能够在它的头定义ObjectMonitor.hpp里找到一系列关键属性,比如如下几个:
- _owner: 指向了持有Monitor锁的线程。
- _WaitSet:存放了处于wait状态的线程队列。
- _EntryList:存放了处于等待锁block状态的线程队列。
- _recursions:锁的重入次数。
- _count:记录该线程获取锁的次数。
公平锁和非公平锁
先来后到,先进先出就是公平,能插队的就是非公平。
ReentrantLock简要实现原理
1 | private volatile int state; |
可重入锁
可重入锁又名递归锁,在外层使用锁后,在内层依旧可以使用,并且不死锁。比如说在synchronized方法或者其修饰的代码块里调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的。
每个锁对象都有一个锁计数器和一个指向持有该锁的线程指针,monitorenter执行时,如果计数器为0,则给0加1,并设置指针为指向当前线程;当计数器为1并且指针是指向当前线程,则计数器1再加1;如果计数器不为0且指针没指向该线程,则被锁住。
隐式锁,synchronized关键字使用的锁:
1 | public class LockDemo { |
显示锁,Lock类:
1 | public class LockDemo { |
死锁
死锁就是两个或两个以上的线程在执行过程中互相争夺对方的锁,比如说线程A和B,A持有a锁,B持有b锁,A要获取b锁,B要获取a锁,互不想让导致陷入死锁。还有系统不足、进程运行推进的顺序不合适、资源分配不当的原因也会导致死锁。
如何排查死锁:
- jps 命令获取到进程id,通过jstack pid打印出结果。
- 使用jconsole图形化界面进行远程或本地连接到进程,查询死锁。
死锁Demo
1 | public class DeadLockDemo { |
LockSupport线程中断机制
线程中断机制
线程中断的概念:
一个线程中断不应由其他线程来决定中断或停止,而应由当前线程自己决定。Thread.stop()等方法已经被废弃。中断应该是一种协作机制,通过一个开关标识,其他线程通过控制开关标识,当前线程通过读取开关标识来对应进行中断恢复。
三种实现方式,volatile,AtomicBoolean和Thread自带的API方式
三种方式的本质都是用一个中间标记,其他线程仅仅是修改这个中间标记为中断状态,是否中断还是看当前线程的内部处理,通过监听这个中间标记来做出对应的操作。
1 | public class InterruptThreadDemo { |
实现方式二:Thread自带中断API实现
1 | public class InterruptThreadDemo { |
Thread 中断API还有一个静态方法:Thread.interrupted(); 这个方法是先返回当前线程的中断状态,然后再将中断状态置为false。
LockSupport
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。其中的park()和unpark()分别用来阻塞线程和接触阻塞。
三种方式让线程等待唤醒的方法
第一种:使用Object中的wait()和notify()让线程等待和唤醒,wait()和notify()必须要在同步代码块或方法里成对顺序使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31public class LockSupportDemo {
public static void main(String[] args) {
Object lock = new Object();
new Thread(() -> {
synchronized (lock) {
try {
//1. lock进入等待状态,需要在其他线程中调用notify才能再往下执行。
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("等待结束,开始运行");
}
}).start();
new Thread(() -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock) {
//2. 唤醒操作,调用后 wait()的地方会被唤醒,往下执行逻辑。
lock.notify();
}
}).start();
}
}第二种:使用JUC包中Lock的Condition中的await()和signal()让线程等待和唤醒,Condition中的线程等待和唤醒方法之前一定要先获取锁,并且要保证等待和唤醒的先后顺序,用法和Object的wait和notify类似。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32public class LockSupportDemo {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(() -> {
lock.lock();
try {
System.out.println("开启t1线程");
condition.await();
System.out.println("执行t1线程等待后的逻辑");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}, "t1").start();
new Thread(() -> {
lock.lock();
try {
Thread.sleep(2000);
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}, "t2").start();
}
}第三种:通过LockSupport中的**park()和unpark(thread)**进行实现,其通过Permit(许可)的机制来做到阻塞和唤醒,每个线程只有一个Permit,permit只有0和1两个值,默认是0,类似Semaphore,但是permit的累加上限只是1。
这种方式可以无锁实现,不需要synchronized和lock的加持也能使用,先唤醒后等待也支持。
1 | public class LockSupportDemo { |
Java内存模型 JMM
在计算机的存储结构里,从本地磁盘->主内存->CPU三级缓存->CPU二级缓存->CPU一级缓存->寄存器,运行速度是逐步递增的,CPU的计算速度大大高于主内存,所以CPU在操作数据的时候是先将数据加载到缓存内进行操作的,此时内存里的数据和缓存中的数据就会存在不一致的情况。
此时JVM中就定义了一种Java内存模型(Java Memory Model),就是JMM,用来屏蔽掉各硬件和操作系统的内存访问差异,以实现Java程序在各种平台下都能达到一致的内存访问效果。
JMM本身是一种抽象概念,是一组约定或规范,不是真实存在的。主要关注点是多线程的原子性,可见性,有序性。
原子性,可见性,有序性
可见性就是说:当一个线程修改了某个共享变量的值,其他线程是可以立即知道数据变更的。共享变量都是保存在主内存中的,CPU不能直接去去写主内存的数据,只能将主内存中的数据拷贝一份到自己的工作内存中,线程对数据的读写都要在工作内存中进行。可见性就要保证工作内存的数据能和主内存的数据能一致。
原子性就是指一个操作是不可被中断的,多线程环境下,操作不能被其他线程干扰。
有序性就是说:在编译器和处理器的优化下,一般会对指令进行重排序,有可能会产生脏读,两行代码执行时可能会优化为顺序颠倒,导致会因为数据依赖不对后有执行错误。
多线程下变量的读写过程
- 我们定义的所有共享变量都要存储在物理主内存中
- 每个线程都有自己独立的工作内存,里面保存着使用到的共享变量的副本。
- 线程对共享变量的操作不能直接去主内存里,只能在工作内存里先修改后再写回。
- 一个线程的工作内存无法访问另一个线程的工作内存,必须通过主内存来通信。
happens-before 先行发生
在JMM中,如果一个操作执行的结果需要对另一个操作可见性,或者代码重排序,那么这两个操作之间必须存在happens-before关系。
比如说:A线程执行了x=5, B线程执行了y=x, 如果线程A的操作先行发生于线程B的操作,那么可以确定y=5一定成立。如不存在此规则,则y!=5。happens-before是判断数据是否存在竞争,是否安全的非常有用的手段,依赖这个原则我们可以通过几条简单规则一揽子解决并发环境下两个操作之间是否可能存在冲突的所有问题,而不需要陷入进晦涩的底层编译原理中。
happens-before总原则
如果一个操作先行发生于另一个操作,那么第一个操作的执行结果将对第二个操作可见,且第一个操作的执行顺序在第二个之前。
两个操作之间存在先行发生关系,并不意味着一定要按照先行发生的原则制定的顺序来执行。如果重排序之后的执行结果按照先行发生的原则来执行的结果一直,那么这种重排序并不非法。比如说:1+2+3 重排为了 3+2+1
happens-before 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与JMM
volatile是一个修饰变量的关键字,修饰后可保证该变量具有可见性与有序性。
当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值立即刷新回主内存中。
当读一个volatile变量时,JMM会把该线程对应的工作内存设置为无效,直接从主内存中读取共享变量。
volatile的写内存语义是直接刷到主内存中,读的内存语义是直接从主内存中读取。
内存屏障
保证可见性的手段是防止指令重排,防止指令重排要靠内存屏障。
屏障指令的统称叫做内存屏障,也叫内存栅栏, 就好比将在指令与指令边界驻扎栅栏,一方的指令无法跨越栅栏来到另一方。 内存屏障可以禁止指令重排序,内存屏障之前的写操作时,强制刷入内存;内存屏障之后的读操作可以读取之前的写操作的值,进而实现可见性。
内存屏障的指令分为四类:
LoadLoad: 确保LoadLoad指令之前的Load指令的执行,先于LoadLoad指令之后的Load指令及其后续的load指令。(Load1 -> LoadLoad -> Load2)
StoreStore: 确保StoreStore指令之前的Store指令的执行,先于StoreStore指令之后的Store指令及其后续的Store指令。(Store1 -> StoreStore -> Store2)
LoadStore:确保LoadStore指令之前的Load指令的执行,先于LoadStore指令之后的Store指令及其后续的Store指令。(Load1 -> LoadStore -> Store2)
StoreLoad:确保StoreLoad指令之前的所有内存指令(load,store)执行,先于StoreLoad指令之后的所有内存指令。(Store1 -> StoreLoad -> Load2)
CAS
有序性和可见性可以通过volatile解决,原子性就需要用锁解决了。针对单个变量操作的话,还可以用CAS来保证原子性。CAS是一种乐观锁,更轻量高效,基于比较交换算法(Compare and swap)来解决线程冲突的。
原理就是会将内存位置的值与预期原值进行比较,如果一致,则说明无其他线程修改,处理器会自动将此位置的值更新为新值。反之则不处理。
CAS操作底层使用Unsafe类实现,Unsafe通过调用本地方法,调用到了C++中的Atomic::cmpxchg,这个调用了Atomic中的cmpxchg函数来比较交换,在这个函数里又调用了它的重载函数,这个会在预编译期间决定调用哪个平台的重载函数(目录:src/os_cpu),比如说windows系统就会调用这一段汇编, 最终通过汇编底层调用了处理器提供的CMPXCHG指令实现。
CAS的操作可能失败,失败后会一直自旋做CAS操作,直到成功为止。
CAS能无锁实现原子性,但也有一定缺陷:
- ABA问题:一个值从A到B又到A,CAS判断上虽然没问题,但还是不够安全,这个可以通过邮戳类解决。
- 自旋消耗CPU: 在高并发对同一变量频繁操作时,失败概率增加,重试次数增多导致CPU消耗增加。
- 只能保证单一共享变量的原子性:多个共享变量的原子性还是只能通过锁解决。
Java需要通过JNI来访问底层系统,CAS使用的Unsafe就是个后门,可以像C++一样直接操作内存,其在sun.misc包中。Unsafe的getAndAddInt(this, valueOffset, 1)中的valueOffset就是内存偏移地址,直接通过内存操作数据。CAS是一个系统原语,是系统规定的原子指令,所以执行过程中不允许被中断,所以也不会有数据不一致的问题。
AtomicInteger源码分析:
1 | //1. 创建原子类,调用incrementAndGet方法 |
如何解决ABA的问题:
使用原子邮戳引用类,增加一个版本号机制,比较不再依赖原始数据,类似数据库中的version字段。
1 | public class AtomicDemo { |
原子操作类
基本类型原子类
AtomicInteger代码实现,AtomicBoolean和AtomicLong类似。
1 | public class AtomicDemo { |
数组类型原子类
AtomicIntegerArray代码实现,AtomicLongArray和AtomicReferenceArray类似。
1 | public class AtomicDemo { |
引用类型原子类
- AtomicReference: 可以包装一个自定义的对象,我们可以通过这个对象来做CAS操作。
- AtomicStampedReference: 带有邮戳版本的原子应用类,可以解决ABA的问题。
- AtomicMarkableReference:就是一个将版本号简化为true和false的类,标记这个对象是否修改过。
对象的属性修改原子类
我们在使用引用类型原子类AtomicReference时,锁定的是整个对象,有时候就只想锁定对象里的某一个字段怎么办呢,那么就要用属性修改原子类了。有仨:AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater.
这样就能以一种线程安全的方式操作非线程安全对象内的某些字段了。
使用要求:更新的对象属性必须使用public volatile修饰,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。
一个例子:
1 | public class AtomicUpdaterDemo { |
原子操作增强类LongAdder等
原子操作增强类有LongAdder,LongAccumulator,DoubleAdder,DoubleAccumulator。具体来分析LongAdder。
LongAdder和AtomicLong相比,强就强在计数的性能上。LongAdder专注于计数,初始值默认为0还无法设置值,只能清零。在高并发场景下,LongAdder的计数性能会好很多。
原因是AtomicLong是在自旋CAS的时候会消耗CPU,并发量大的话CPU很容易打满,而LongAdder是分段治之的思想,将value值分为多个cell,当多线程访问这个value的时候通过hash算法匹配到其中一个cell,在求和的时候就将所有的cell累加起来。这样分散了资源竞争,降低了CPU的开销。
//LongAdder源码分析 TODO
ThreadLocal
ThreadLocal提供在线程内存储变量的功能,然后在单个线程中可以共享和设置这些变量。变量在线程之间互相隔离,互不影响。
使用上:我们可以通过threadLocal.set(value)来设置一个值,然后通过threadLocal.get()获取这个值。
原理:就是每个Thread内部都拥有一份ThreadLocalMap,Map中存储了一个Entry数组,这个Entry以threadLocal对象本身为key,以值为value。
1 | //threadLocal.set(value)点进去的源码 |
强软弱虚四大引用
强引用:JVM内存回收时,就算OOM也不会去回收强引用对象,即使该对象永远都不会被用到。这是内存泄露的主要原因之一。
软引用:使用java.lang.ref.SoftReference实现,系统内存足够时不回收,一旦内存不足时就被回收。
弱引用:使用java.lang.ref.WeakReference实现,只要GC一运行就回收,不论内存是否足够。
虚引用:使用java.lang.ref.PhantomReference实现,其不会决定对象的生命周期,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。它不能单独使用也不能通过它访问对象,虚引用必须和引用队列 (ReferenceQueue)联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。 仅仅是提供了一种确保对象被 finalize以后,做某些事情的机制。 PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。
ThreadLocal的内存泄露问题
内存泄漏的概念:不再使用的对象或者变量占用的内存不能被回收,就是内存泄露。
在ThreadLocal中的Entry是弱引用的,大概率减少了内存泄露的风险。当ThreadLocal在栈中使用完了,销毁之后,ThreadLocalMap里的key引用在GC后也会被回收,如果是强引用的话,就会导致key指向的ThreadLocal对象不能被回收。
此时key就为null了,也有问题,不过当我们在调用get,set或remove方法时,就会去尝试删除key为null的entry,可以释放value对象占用的内存。所以,尽量要在不使用某个ThreadLocal对象后,手动调用remove方法去删除它。