目录
1.Unsafe类
2.Exchanger类
3.ForkJoin 框架
4.LockSupport
5.自定义线程池
6.AQS架构
7.ReentrantLock使用场景
8.线程中断
9.ABA问题
10.Lock锁和Condition条件
1.Unsafe类
通常我们最好也不要使用Unsafe类,除非有明确的目的,并且也要对它有深入的了解才行。要想使用Unsafe类需要用一些比较tricky的办法。Unsafe类使用了单例模式,需要通过一个静态方法getUnsafe()来获取。但Unsafe类做了限制,如果是普通的调用的话,它会抛出一个SecurityException异常;只有由主类加载器加载的类才能调用这个方法。其源码如下:
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
获取到Unsafe实例之后,我们就可以为所欲为了。Unsafe类提供了以下这些功能:
1.1、内存管理。包括分配内存、释放内存等。
该部分包括了allocateMemory(分配内存)、reallocateMemory(重新分配内存)、copyMemory(拷贝内存)、freeMemory(释放内存 )、getAddress(获取内存地址)、addressSize、pageSize、getInt(获取内存地址指向的整数)、getIntVolatile(获取内存地址指向的整数,并支持volatile语义)、putInt(将整数写入指定内存地址)、putIntVolatile(将整数写入指定内存地址,并支持volatile语义)、putOrderedInt(将整数写入指定内存地址、有序或者有延迟的方法)等方法。getXXX和putXXX包含了各种基本类型的操作。
利用copyMemory方法,我们可以实现一个通用的对象拷贝方法,无需再对每一个对象都实现clone方法,当然这通用的方法只能做到对象浅拷贝。
1.2、数组操作。
这部分包括了arrayBaseOffset(获取数组第一个元素的偏移地址)、arrayIndexScale(获取数组中元素的增量地址)等方法。arrayBaseOffset与arrayIndexScale配合起来使用,就可以定位数组中每个元素在内存中的位置。
由于Java的数组最大值为Integer.MAX_VALUE,使用Unsafe类的内存分配方法可以实现超大数组。实际上这样的数据就可以认为是C数组,因此需要注意在合适的时间释放内存。
1.3、多线程同步。包括锁机制、CAS操作等。
这部分包括了monitorEnter、tryMonitorEnter、monitorExit、compareAndSwapInt、compareAndSwap等方法。
其中monitorEnter、tryMonitorEnter、monitorExit已经被标记为deprecated,不建议使用。
Unsafe类的CAS操作可能是用的最多的,它为Java的锁机制提供了一种新的解决办法,比如AtomicInteger等类都是通过该方法来实现的。compareAndSwap方法是原子的,可以避免繁重的锁机制,提高代码效率。这是一种乐观锁,通常认为在大部分情况下不出现竞态条件,如果操作失败,会不断重试直到成功。
1.4、挂起与恢复。
这部分包括了park、unpark等方法。
将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。整个并发框架中对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本pack方法,但最终都调用了Unsafe.park()方法。
1.5、内存屏障。
这部分包括了loadFence、storeFence、fullFence等方法。这是在Java 8新引入的,用于定义内存屏障,避免代码重排序。
loadFence() 表示该方法之前的所有load操作在内存屏障之前完成。同理storeFence()表示该方法之前的所有store操作在内存屏障之前完成。fullFence()表示该方法之前的所有load、store操作在内存屏障之前完成。
2.Exchanger类
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 如果第一个线程先执行exchange方法,它会一直等待第二个线程也执行exchange,当两个线程都到达同步点时,
* 这两个线程就可以交换数据,将本线程生产出来的数据传递给对方
* @author lzhcode
*
*/
public class TestExchanger {
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
final Exchanger exchanger = new Exchanger();
service.execute(new Runnable(){
public void run() {
try {
String data1 = "zxx";
System.out.println("线程" + Thread.currentThread().getName() +
"正在把数据" + data1 +"换出去");
Thread.sleep((long)(Math.random()*10000));
String data2 = (String)exchanger.exchange(data1);
System.out.println("线程" + Thread.currentThread().getName() +
"换回的数据为" + data2);
}catch(Exception e){
}
}
});
service.execute(new Runnable(){
public void run() {
try {
String data1 = "lhm";
System.out.println("线程" + Thread.currentThread().getName() +
"正在把数据" + data1 +"换出去");
Thread.sleep((long)(Math.random()*10000));
String data2 = (String)exchanger.exchange(data1);
System.out.println("线程" + Thread.currentThread().getName() +
"换回的数据为" + data2);
}catch(Exception e){
}
}
});
}
}
3.ForkJoin 框架
在日常的业务需求中,经常出现的批量查询,批量写入等接口的提供,一般来说,最简单最low的方式就是写一个for循环来一次执行,但是当业务方对接口的性能要求较高时,就比较尴尬了
通常可以想到的方式是采用并发操作,首先想到可以实现的方式就是利用线程池来做
通常实现方式如下
// 1. 创建线程池
ExecutorService executorService = new ThreadPoolExecutor(3, 5, 60,
TimeUnit.SECONDS,
new LinkedBlockingDeque<Runnable>(10), new DefaultThreadFactory("biz-exec"),
new ThreadPoolExecutor.CallerRunsPolicy());
// 2. 创建执行任务
List<Future<Object>> futureList = new ArrayList<>();
for(Object arg : list) {
futureList.add(executorService.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
// xxx
}
}));
}
// 3. 结果获取
for(Future f: futureList) {
Object obj = f.get();
}
用上面的这种方式并没有什么问题,我们接下来考虑的是如何使用ForkJoin框架来实现类似的功能
任务分割
ForkJoinTask
: 基本任务,使用forkjoin框架必须创建的对象,提供fork,join操作,常用的两个子类
-
RecursiveAction
: 无结果返回的任务 -
RecursiveTask
: 有返回结果的任务
说明:
-
fork
: 让task异步执行 -
join
: 让task同步执行,可以获取返回值 - ForkJoinTask 在不显示使用ForkJoinPool.execute/invoke/submit()方法进行执行的情况下,也可以使用自己的fork/invoke方法进行执行
结果合并
ForkJoinPool
执行 ForkJoinTask
,
- 任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。
- 当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务
三中提交方式:
-
execute
异步,无返回结果 -
submit
异步,有返回结果 (返回Future<T>
) -
invoke
同步,有返回结果 (会阻塞)
public class CountTask extends RecursiveTask<Integer> {
private int start;
private int end;
private static final int THRED_HOLD = 30;
public CountTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
boolean canCompute = (end - start) <= THRED_HOLD;
if (canCompute) { // 不需要拆分
for (int i = start; i <= end; i++) {
sum += i;
}
System.out.println("thread: " + Thread.currentThread() + " start: " + start + " end: " + end);
} else {
int mid = (end + start) / 2;
CountTask left = new CountTask(start, mid);
CountTask right = new CountTask(mid + 1, end);
left.fork();
right.fork();
sum = left.join() + right.join();
}
return sum;
}
}
@Test
public void testFork() throws ExecutionException, InterruptedException {
int start = 0;
int end = 200;
CountTask task = new CountTask(start, end);
ForkJoinPool pool = ForkJoinPool.commonPool();
Future<Integer> ans = pool.submit(task);
int sum = ans.get();
System.out.println(sum);
}
4.LockSupport
特点:
1、基于UnSafe原语
实现
LockSupport类在jdk源码中基本定义就是创建锁和其他同步类的 基本线程阻塞,直接与UnSafe原语
类打交道
2、重入性
LockSupport是非重入锁,如果一个线程连续2次调用 LockSupport .park(),那么该线程一定会一直阻塞下去。
3、面向线程锁
面向线程锁是LockSupport很重要的一个特征,这样也就没有公平锁和非公平的区别了的,同时面向线程锁的特征在一定程度上降低代码的耦合度。
LockSupport比Object的wait/notify有两大优势:
①LockSupport不需要在同步代码块里 。所以线程间也不需要维护一个共享的同步对象了,实现了线程间的解耦。
②unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序。
LockSupport在Java的工具类用应用很广泛,咱们这里找几个例子感受感受。以Java里最常用的类ThreadPoolExecutor为例。先看如下代
public class TestObjWait {
public static void main(String[] args)throws Exception {
ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<Runnable>(1000);
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5,5,1000, TimeUnit.SECONDS,queue);
Future<String> future = poolExecutor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
TimeUnit.SECONDS.sleep(5);
return "hello";
}
});
String result = future.get();
System.out.println(result);
}
}
代码中我们向线程池中扔了一个任务,然后调用Future的get方法,同步阻塞等待线程池的执行结果。
这里就要问了:get方法是如何组塞住当前线程?线程池执行完任务后又是如何唤醒线程的呢?
咱们跟着源码一步步分析,先看线程池的submit方法的实现:
在submit方法里,线程池将我们提交的基于Callable实现的任务,封装为基于RunnableFuture实现的任务,然后将任务提交到线程池执行,并向当前线程返回RunnableFutrue。
进入newTaskFor方法,就一句话:return new FutureTask<T>(callable);
所以,咱们主线程调用future的get方法就是FutureTask的get方法,线程池执行的任务对象也是FutureTask的实例。
接下来看看FutureTask的get方法的实现:
比较简单,就是判断下当前任务是否执行完毕,如果执行完毕直接返回任务结果,否则进入awaitDone方法阻塞等待。
FutureTask->get->awaitDone
awaitDone方法里,首先会用到上节讲到的cas操作,将线程封装为WaitNode,保持下来,以供后续唤醒线程时用。再就是调用了LockSupport的park/parkNanos组塞住当前线程。
上边已经说完了阻塞等待任务结果的逻辑,接下来再看看线程池执行完任务,唤醒等待线程的逻辑实现。
前边说了,咱们提交的基于Callable实现的任务,已经被封装为FutureTask任务提交给了线程池执行,任务的执行就是FutureTask的run方法执行。如下是FutureTask的run方法:
FutureTask->run
c.call()就是执行我们提交的任务,任务执行完后调用了set方法,进入set方法发现set方法调用了finishCompletion方法,想必唤醒线程的工作就在这里边了,看看代码实现吧:
FutureTask->finishCompletion
总结:
Runnable
通常用于那些不需要返回任何结果的任务。Callable
通常用于那些需要返回计算结果或者需要抛出异常的任务- Executor就是Runnable和Callable的调度容器,Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果、设置结果操作。get方法会阻塞,直到任务返回结果
- FutureTask是Future也是Runnable,又是包装了的Callable( 如果是Runnable最终也会被转换为Callable )
- 线程池里的submit返回FutureTask,而FutureTask的get方法里调用了LockSupport的park和unpark
Java线程——Callable与Runnable的区别_sinat_39634657的博客-博客_callable和runnable的区别
5.自定义线程池
@Bean(destroyMethod="shutdown")
public ThreadPoolTaskExecutor defaultThreadPool() {
// CPU可用核心数
int cpuNum = Runtime.getRuntime().availableProcessors();
//1.IO密集型 估算线程池大小
//线程数 = CPU可用核心数/(1-阻塞系数)
//计算密集型任务的阻塞系数为0,而IO密集型任务的阻塞系数则接近于1。一个完全阻塞的任务是注
//定要挂掉的,所以我们无须担心阻塞系数会达到1。
//阻塞系数可以采用一些性能分析工具或java.lang.managenment API来确定线程话在系统I/O操作
//上的时间与CPU密集任务所消耗的时间比值。
//2.计算密集型估算线程池大小threadNum = cpuNum +1 或则 计算密集型估算线程池大小threadNum = cpuNum * 2;
int threadNum = cpuNum * 2;
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心线程数目
executor.setCorePoolSize(threadNum-1);
//指定最大线程数
executor.setMaxPoolSize(threadNum);
//队列中最大的数目
executor.setQueueCapacity(300);
executor.setThreadFactory(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
//如果为守护线程,恢复为常规
if(t.isDaemon()) {
t.setDaemon(false);
}
//如果有设置线程优先级,恢复为常规
if(Thread.NORM_PRIORITY != t.getPriority()) {
t.setPriority(Thread.NORM_PRIORITY);
}
return t;
}
});
//线程名称前缀
executor.setThreadNamePrefix("defaultThreadPool_");
//rejection-policy:当pool已经达到max size的时候,如何处理新任务
//CALLER_RUNS:不在新线程中执行任务,而是由调用者所在的线程来执行
//对拒绝task的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//线程空闲后的最大存活时间
executor.setKeepAliveSeconds(60);
//加载
executor.initialize();
return executor;
}
6.AQS架构
6.1它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)
在 AbstractQueuedSynchronizer
(AQS) 框架下,当当前线程释放锁之后,通常只有队列头部的线程会被唤醒并获得机会去争夺锁。这是AQS设计的一个关键特性,以确保有效和有序的线程调度。具体的行为取决于锁的公平性设置:
- 公平锁:在公平锁(比如
ReentrantLock
在公平模式下)中,当锁被释放时,它会按顺序唤醒等待队列中的头部线程(即等待时间最长的线程)。这保证了等待时间最长的线程首先获得锁,从而实现公平的锁分配。- 非公平锁:对于非公平锁(比如
ReentrantLock
在非公平模式下),当锁被释放时,新尝试获取锁的线程可能会插队并抢先获得锁,而不是唤醒队列中的头部线程。这可能导致等待队列中的线程被“饿死”,但通常能提高性能,因为它减少了线程之间的上下文切换。
6.2AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)任何时候,只有一个线程能够改变锁的状态。这就是独占性。其他线程必须等待直到锁被释放。
6.3不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种模板方法:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int): 独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
6.4以ReentrantLock的源码为例来深入理解AQS
ReentrantLock
- 仅允许一个线程获取锁,其他线程必须等待。
- 使用AQS的独占模式实现,通过CAS操作修改状态。
- 主要方法是
lock()
和unlock()
。
ReentrantLock类结构
下面看下Sync类源代码:
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
abstract void lock();
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
//releasesy一般传1
int c = getState() - releases;
//当前线程不是独占的不满足条件抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果state - releasesd等于0把空闲状态free置为true,同时把独占线程置为null
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//设置新的state
setState(c);
return free;
}
protected final boolean isHeldExclusively() {
//判断Thread是否和当前线程相等从而判断是否为独占的
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
final Thread getOwner() {
//等于0表示没有任何线程占用这个资源,有的话返回占用的线程
return getState() == 0 ? null : getExclusiveOwnerThread();
}
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
final boolean isLocked() {
return getState() != 0;
}
/**
* Reconstitutes the instance from a stream (that is, deserializes it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
}
FairSync 类里的tryAcquire方法:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//队列里找不到等待的其他线程,就设置为当前线程独占,非公平锁(具体实现是Sync类的
//nonfairTryAcquire方法,见上面代码)下的tryAcquire没有!hasQueuedPredecessors
//判断其他代码都一样
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//可重入锁主要体现在这段逻辑,占用的线程就是当前线程把state加1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
AbstractQueuedSynchronizer的 acquire方法
public final void acquire(int arg) {
//1.addWaiter(Node.EXCLUSIVE)把最后一个线程放在队列尾部
//2.acquireQueued循环判断新入的节点是否为前驱节点,是前驱节点时
//3.整个if的判断逻辑是没有获得锁成功的话加入等待队列的的再打断自己
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquire方法调用流程图
AbstractQueuedSynchronizer的 acquireQueued方法
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//不断自旋
for (;;) {
final Node p = node.predecessor();
//是前驱节点时,尝试获取许可tryAcquire
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
6.5以CountDownLatch的原理为例来深入理解AQS
CountDownLatch内部类继承了AQS,所以它内部也是FIFO队列,同时也一样是前驱节点唤醒后继节点,不能像CyclicBarrier那样使用完毕后还可以复用;
- 允许多个线程等待某个事件完成。
- 使用AQS的共享模式实现,通过计数器和FIFO队列协调线程。
- 主要方法是
countDown()
和await()
。
6.5.1任务分为N个任子线程去执行,state也初始化为N(注意N要要与线程个数一致)
6.5.2这N个线程也是并行执行的,每个子线程执行完后后countDown()一次,state会CAS减1
6.5.3等到所有子线程都执行完成之后即(state==0),会调用LockSupport.unpark(s.thread)
6.5.4然后主调用会从await()函数返回(之前是通过LockSupport.park(this);阻塞),继续后余动作
源码分析: 【JDK源码分析】并发包同步工具CountDownLatch - 还是搬砖踏实
7.ReentrantLock使用场景
场景1:如果发现该操作已经在执行中则不再执行(有状态执行)
ReentrantLock lock = new ReentrantLock();
if(lock.tryLock()){
try{
}finally{
lock.unlock();
}
}
场景2:如果发现该操作已经在执行,等待一个一个执行(同步执行,类似synchronized)
场景3:如果发现该操作已经在执行,则尝试等待一段时间,等待超时则不执行(尝试等待执行)
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
public class AttemptLocking {
private ReentrantLock lock = new ReentrantLock();
public void untimed() {
boolean captured = lock.tryLock();
try {
System.out.println("tryLock(): " + captured);
} finally {
if (captured)
lock.unlock();
}
}
public void timed() {
boolean captured = false;
try {
captured = lock.tryLock(2, TimeUnit.SECONDS);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
System.out.println("tryLock(2, TimeUnit.SECONDS): " + captured);
} finally {
if (captured)
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
final AttemptLocking al = new AttemptLocking();
al.untimed(); // True -- 可以成功获得锁
al.timed(); // True --可以成功获得锁
//新创建一个线程获得锁并且不释放
new Thread() {
{
setDaemon(true);
}
public void run() {
al.lock.lock();
System.out.println("acquired");
}
}.start();
Thread.sleep(100);// 保证新线程能够先执行
al.untimed(); // False -- 马上中断放弃
al.timed(); // False -- 等两秒超时后中断放弃
}
}
场景4:如果发现该操作已经在执行,等待执行。这时可中断正在进行的操作立刻释放锁继续下一操作(类似于wait())
ReentrantLock lock = new ReentrantLock();
try{
lock.lockInterruptibly();
}finally{
lock.unlock();
}
另外在ReentrantLock类中定义了很多方法,比如:
isFair() //判断锁是否是公平锁
isLocked() //判断锁是否被任何线程获取了
isHeldByCurrentThread() //判断锁是否被当前线程获取了
hasQueuedThreads() //判断是否有线程在等待该锁
8.线程中断
Thread.currentThread().interrupt()
并不能立即停止一个处于运行状态(Running)的线程。这个方法实际上是用来设置线程的中断状态的,它会告诉线程应该在下一个合适的时机中断它的执行。
- 如果线程当前正在执行非阻塞的操作,它将继续保持在运行状态(Running),直到它到达一个检查中断状态的点。在检查中断状态之后,它可能会决定安全地终止,这样做通常涉及清理资源并退出执行路径,但它仍然是在运行状态,直到它决定如何响应中断。
- 如果线程正在进行可中断的阻塞操作(如
sleep()
,wait()
,join()
),那么调用interrupt()
会导致抛出InterruptedException
,并且如果它在等待状态(Waiting)或阻塞状态(Blocked),它会转到可运行状态(Runnable)
设计这个 API 的目的是线程在接收到中断请求后,可以安全地清理资源并退出,而不是被强行停止。这比使用
Thread.stop()
等强制终止线程的方式更安全、更优雅。
9.ABA问题
ABA问题是指在CAS操作中带来的潜在问题
CAS意思是 compare and swap 或者 compare and set , CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。
如果CAS操作是基于CPU内核的原子操作,那基本是不会出现ABA问题的,但是如果CAS本身操作不满足原子性,则会带来ABA问题,
比如两个线程
- 线程1 查询A的值为a,与旧值a比较,
- 线程2 查询A的值为a,与旧值a比较,相等,更新为b值
- 线程2 查询A的值为b,与旧值b比较,相等,更新为a值
- 线程1 相等,更新B的值为c
可以看到这样的情况下,线程1 可以正常 进行CAS操作,将值从a变为c 但是在这之间,实际A值已经发了a->b b->a的转换仔细思考,这样可能带来的问题是,如果需要关注A值变化过程,是会漏掉一段时间窗口的监控
10.Lock锁和Condition条件
Lock的特性:
1).Lock不是Java语言内置的;
2).synchronized是在JVM层面上实现的,如果代码执行出现异常,JVM会自动释放锁,但是Lock不行,要保证锁一定会被释放,就必须将unLock放到finally{}中(手动释放);
(新版本的jdk新能相差不大);
4).ReentrantLock增加了锁:
a. void lock(); // 无条件的锁;
b. void lockInterruptibly throws InterruptedException;//可中断的锁;
解释: 使用ReentrantLock如果获取了锁立即返回,如果没有获取锁,当前线程处于休眠状态,直到获得锁或者当前线程可以被别的线程中断去做其他的事情;但是如果是synchronized的话,如果没有获取到锁,则会一直等待下去;
c. boolean tryLock();//如果获取了锁立即返回true,如果别的线程正持有,立即返回false,不会等待;
d. boolean tryLock(long timeout,TimeUnit unit);//如果获取了锁立即返回true,如果别的线程正持有锁,会等待参数给的时间,在等待的过程中,如果获取锁,则返回true,如果等待超时,返回false;
Condition的特性:
1.Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。不同的是,Object中的这些方法是和同步锁捆绑使用的;而Condition是需要与互斥锁/共享锁捆绑使用的。
能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition。
例如,假如多线程读/写同一个缓冲区:当向缓冲区中写入数据之后,唤醒"读线程";当从缓冲区读出数据之后,唤醒"写线程";并且当缓冲区满的时候,"写线程"需要等待;当缓冲区为空时,"读线程"需要等待。如果采用Object类中的wait(), notify(), notifyAll()实现该缓冲区,当向缓冲区写入数据之后需要唤醒"读线程"时,不可能通过notify()或notifyAll()明确的指定唤醒"读线程",而只能通过notifyAll唤醒所有线程(但是notifyAll无法区分唤醒的线程是读线程,还是写线程)。 但是,通过Condition,就能明确的指定唤醒读线程。