java并发程编程笔记


学习并发编程视频时的笔记。
我之前是看JAVA核心技术这本书,不过有点抽象有些没能理解,不过建议看书,书讲的很详细。以下是资源分享:
JAVA核心技术第一、二卷带目录:https://pan.baidu.com/s/1Slgxi4ek7UID8fuq3H3pXw
提取码:yrym

顺便分享一下我找到的一篇文章: Java并发(思维导图),写的很好清晰明了。

第一章 并发编程中线程的基础知识

该小节比较简单,如果已经有基础可以直接跳过。

1.1线程安全

线程安全:一个类被多个线程以任意方式同时调用,且不需要外部额外同步和协同的情况下,仍然保持内部数据正确且表现正确的行为,那么这个类就是线程安全的。
线程安全等级(从上往下等级越低,即前面的线程更安全)

  1. 不可变:不可变的对象一定是线程安全的。
    例如:①final修饰的不可变类,如String,Integer等。②enum枚举类,enum类底层也是去创建final类。
    扩展:final修饰的类和属性一定线程安全吗?①但你创建一个对象时,使用final关键字能够使得另一个线程不会访问到处于“部分创建”的对象,否则是会可能发生的。②final只是用来保证值是不能被直接覆盖的。
  2. 线程安全:线程安全类的任意方法操作都不会使该对象的数据处于不一致或者数据污染的情况。
    例如:java.util.concurrent下的类,例如ConcurrentHashMap、LinkedBlockingQueue等。(concurrent包就是专门来处理并发的,后续会讲)
  3. 有条件的线程安全:对于单独的访问类的方法,是线程安全。但是对于某些复合操作,需要外部类来同步。
    例如:Vector类,从下图中可以看出Vector的add和removeAllElements方法都是有synchronized关键字的,即线程安全的。
    在这里插入图片描述
    但是在如下图的复合操作中,如果方法没有加synchronized关键字就会出现在判断语句中有两个线程去处理add方法的情况,就会导致线程不安全的情况发生,那么我们就可以在下面的方法前面加synchronized来保证线程安全。
    在这里插入图片描述
  4. 线程兼容:线程兼容类即本身不是线程安全的,但是我们可以通过正确使用同步、用锁或synchronized块包含每一个方法调用,使得这些非线程安全的类以线程兼容的方式来保证线程安全。
    例如:使用Collections.synchronizedList来包装一个List,使其变为线程安全。(底层其实也就是用到synchronized处理线程安全问题。)
    在这里插入图片描述
  5. 线程对立:线程对立类是那些不管是否调用外部同步都不能在并发使用时保证其安全的类。

问题扩展:为什么Java设计者要设计非线程安全类呢?个人认为,非线程安全类在单线程下的效率更高,相比使用线程安全类代价大,不能合理利用CPU资源等。

1.2线程的同步异步,阻塞非阻塞

同步和异步
同步:举个例,A为顾客,B为商家。第一步顾客跟商家说要买什么东西,第二步商家去取东西,第三步把东西给顾客。
同步的使用场景:①大多数非异步场景(不用异步,就用同步来调用)如:百度搜索,客户端同步调用服务端搜索接口,等待服务端实时结果。②在编排的流程中,必须等待拿到响应结果才能去做下一步操作,且在实时链路中互相之间有串或关联数据的。如:电商中商品详情页的查询接口的内部实现。
在这里插入图片描述
异步:还是上面的例子,A为顾客,B为商家。第一步,顾客告知商家要买什么商品。第二步,商家回复顾客等会给他送过去,顾客先回去了,同时商家去取商品。第三步,商家拿商品去送给顾客。
在这里插入图片描述
以下是异步的使用场景:其中第一个是涉及Callable 和 Future 创建线程。
在这里插入图片描述

同步异步比较同步异步
优势1、可以拿到实时结果进行处理,上下文信息始终在一个代码块,代码处理上更加方便直观。2、对错误和异常处理可以做到实时1、不影响主流程的执行,降低响应时间,提高应用的性能和效率 。2、及时释放系统资源,如线程占用,让系统去做更有价值的事。
劣势1、耗时的接口响应会影响整个流程的性能。1、为了保障数据最终一致性,需要对账系统去做好监控和保障。2、需要更多异步任务去补偿系统间的数据一致性。

阻塞和非阻塞
在这里插入图片描述
总结
同步异步关注点是得到结果的方式,同步是实时返回结果,异步是通过共享变量、通知消息或回调来得到结果。
阻塞非阻塞关注点在程序在等待调用结果返回时的状态。

1.3并发和并行

并发:逻辑上的同时处理。能处理多个事件,但是并不一定是同时进行的。这里可能只有一个处理器的多线程,由系统的调度,让不同的线程在不同的小的时间段内执行各自的线程任务逻辑。
并行:物理或是实际的同时处理。能在同一时刻处理多个事件。并行具有并发的含义,并发不一定是并行的,因为不是同时进行。

1.4线程状态及java中线程常见的方法

了解线程的状态

  1. NEW(新创建):线程创建但是还没有调用start()方法
  2. RUNNABLE (可运行):可运行线程的状态,线程已经在JVM虚拟机中执行,但是可能需要等待操作系统资源,如处理器。RUNNABLE包括RUNNING和READY
  3. BLOCKED (被阻塞):阻塞的线程意味着正在等待监视器锁,来进入或重入synchronized代码块或者方法
  4. WAITING(等待) :等待状态,需要其他线程中断或通知来唤醒
  5. TIMED_WAITING (计时等待):定时等待状态,在指定等待时间后返回,或提前被其他线程中断或通知返回
  6. TERMINATED (被终止):终止线程的线程状态,线程已执行完成
    线程状态之间的切换
    在这里插入图片描述
    线程方法
    Thread.yield():线程让步,使用了这个方法,当前线程就会退出CPU时间片,让其他线程或当前线程使用CPU时间篇执行。
    在这里插入图片描述
    Thread.sleep():线程休眠,主动让出当前CPU时间,在指定时间过后,CPU会返回继续执行该线程。sleep方法不会释放当前所持有的锁。
    在这里插入图片描述
    在这里插入图片描述
    Thread.join():等待该线程死亡/终止,当前线程会等待调用该方法的线程执行完毕后才能继续执行。(这里我推荐看一下JAVA多线程中join()方法的详细分析 这篇文章,特别是下面留言部分,就能明白了。)
    在这里插入图片描述
    在这里插入图片描述
    Object.wait():Object类的方法,调用前必须拥有对象锁,例如在synchronized代码块内,调用wait方法后,对象锁会释放,线程进入WAITING等待状态。
    在这里插入图片描述
    在这里插入图片描述

1.5死锁以及如何避免

死锁:指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
如何避免死锁

  1. 不使用锁,不使用2把及以上的锁。
  2. 必须使用2把及以上锁的时候,确保在整个应用程序中对获取锁的顺序是一致的。
  3. 尝试获取具有超时释放的锁,例如Lock中的tryLock来获取锁。(tryLock 是防止自锁的一个重要方式。tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。)
  4. 当发生了Java-level的死锁时,重启程序来干掉进程/线程。
    定位死锁问题(特别是程序很复杂时要如何找出死锁的具体位置)
    jps:列举正在运行的虚拟机进程并显示虚拟机执行的主类以及这些进程的唯一ID(PID)
    jstack:用于JVM当前时刻的线程快照,得到JVM当前每一条线程正在执行的堆栈信息,定位线程长时间卡顿问题,如死锁、死循环等问题。
    在这里插入图片描述
    在这里插入图片描述

第二章 Java内存模型及线程实现案例分析

该内容前面部分主要操作系统知识,可以自己去深入的学习。

2.1Java内存模型

为什么要了解内存模型?①了解更深层次内存的使用和读取实现,方便日后分析多线程内存相关问题。②工作中遇到的并发上的问题并不好重现,需要对理论知识掌握足够深刻,才能更好分析。

操作系统的缓存结构
如下图所视,为了更快的读取内存。设计如下的流程,其中L1和L2是那个CPU自己的高速缓存,L3是多CPU之间共享的缓存。L1相比与L2存储的内容更少,L2相比L3更少。L1和L2的缓存命中率均为80%,达到L3缓存的数据占比4%左右

或许有人好奇为什么要这样设计:L1更小就可以更快的查找到存在它里面的内容,相比L2比L1大,查询速度也比L1慢。打个比方,把下图的情形比作企业内员工的文件存储,L1就好比员工工位上的桌面,现阶段要紧急处理的文件就放在上面桌面上;L2就好比桌子的抽屉,一些后面要处理但不是很紧急的文件就放在里面;L3就好比企业的文件库,一些还无需处理的文件就放在里面。(建议大家好好去学一下操作系统,这里就不深入讲了,而且我太久没看操作系统也不太记得…)
在这里插入图片描述
java的内存模型
在这里插入图片描述
举个例子:看一下下面的图和代码,下面的代码运行后会有两种不同的情况:这是因为线程A在自己的工作内存把a修改为1后,如果A线程及时修改了主内存中a的值则B线程在还未修改a之前就在主内存中取得a=1的结果,如果A线程没有及时修改主内存中a的值则B线程在还未修改a之前取到值还是a=0的结果。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
重排序:重排序即java内存模型会重新把语句的执行顺序改变,会导致运行的结果和预想的不一样。详细自己查找文章看一下。
在这里插入图片描述
在这里插入图片描述
Happens-before规则

  1. 程序次序规则:在程序中若操作A先于操作B发生,那么线程中操作A也先于操作B发生。
  2. 对象终结规则:一个对象的构造函数的完成先行发生与其finalize()方法。
  3. 锁规则:对同一个锁,加锁操作先行发生与解锁操作。
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则操作A先行发生于操作C。
  5. volatile变量规则:对于一个volatile变量的写操作先行发生于对这个变量的读操作。
  6. 线程启动规则:Thread对象的start()方法先行发生于此线程中的每个指令操作。
  7. 线程中断规则:一个线程对另一个线程调用interrupt()方法,先行发生于被中断线程检测到中断事件。
  8. 线程结束规则:线程中所有的操作都先行发生于线程的终止时,如线程结束、Thread.join()返回。

2.2synchronized和volatile关键字

synchronized
synchronized是java语言关键字,用来给方法或代码块加锁,控制方法或代码块同一时间只能有一个线程执行,用来解决多个线程同时访问时出现的并发问题。

synchronized使用分类
synchronized方法:

  • 方法使用ACC_SYNCHRONIZED标识。(反编译后可以看到)
  • 如果是static方法,锁是作用在上,即同个类中的所有对象使用同个synchronized方法会相互影响。
  • 如果是非static方法,锁是作用在具体的类对象上,即同个类的不同对象使用类中的的同个synchronized方法互不影响。

synchronized代码块:

  • 使用monitorenter和monitorexit指令控制线程进出。(反编译可以看到)

synchronized与ReentrantLock
在开发是还可以用ReentrantLock锁来解决并发问题。
synchronized和ReentrantLock相同点:①都是用于多线程中对资源加锁,控制代码同一时间只能有单个线程在执行。②当一个线程获取到锁,其他线程均需要阻塞等待。③均为可重入锁。(可重入,就是可以重复获取相同的锁,synchronized和ReentrantLock都是可重入的,可重入降低了编程复杂性)
synchronized和ReentrantLock不同点:①synchronized是java语言的关键字,由虚拟机字节码指令实现;ReentrantLokc是java sdk提供的API级别锁实现。②synchronized可以在方法级别加锁,ReentrantLock则不行。③ReentrantLock可以通过方法tryLock等待指定时间的锁,超时返回,synchronized则不行。 ④ReentrantLock提供了公平锁和非公平锁实现,synchronized只有非公平锁(线程相互竞争)。

volatile
volatile是java语言关键字,也是一个指令的关键字

  1. 用来保证多线程间对变量的内存可见性,将最新变量值及时通知给其他线程。
    在这里插入图片描述
  2. 禁止volatile前后的程序指令进行重排序。
  3. 不保证线程安全,不可用于数字的线程安全递增。

volatile使用场景
在这里插入图片描述
在这里插入图片描述
synchronized和volatile区别
①synchronized是用于同步锁控制,具有原子性,控制同一时间只用一个线程执行一个方法或代码块
②volatile只保证线程间的内存可见性,不具备锁的特性,无法保证修饰对象的原子性。

原子性和可见性可以看:volatile为什么不能保证原子性

2.3创建线程的几种方式

了解创建线程的几种方式
方式一:通过Runnable接口创建线程。①重写Runnable的run方法。②使用runnable对象构造Thread对象。③启动线程。
在这里插入图片描述
方式二:继承Thread类创建线程。(不推荐,因为类是单一继承的,如果继承了Thread类,就没办法再继承别的类)①继承Thread类,重写run方法。②构造这个Thread子类。③调用start()方法启动线程。
在这里插入图片描述
方式三:使用Callable和FutureTask创建线程,该方式可以拿到返回的线程结果。(FutureTask是Future的子类,同时也是Runnable的子类)①实现Callable接口,重写call方法。②传入Callable对象,构造FutureTask(Runnable的子类)对象。③传入FutureTask对象构造Thread对象,启动线程。
在这里插入图片描述
方式四:将Runnable或Callable放到线程池ExecutorService中执行。①实现Callable/Runnable接口,重写call/run方法。②构建ExecutorService线程池对象,调用线程池execute或者submit方法执线程。③对于submit方式提交,使用Future来获取线程执行结果。
在这里插入图片描述

线程执行过程
大概的过程如下,具体可以自己看一下源码。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

2.4ThreadLocal

ThreadLocal什么是ThreadLocal?
ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序,ThreadLocal并不是一个Thread,而是Thread的局部变量。
ThreadLocal可以看这篇文章:彻底理解ThreadLocal

没有使用ThreadLocal时,需要在整个上下文调用的方法中将关键参数透传。
存在的问题:①从代码整洁度上看,每个方法要加这个参数,如果内部方法调用链路较长,那么方法入参看起来会很臃肿。②如果某处透传时将参数值改掉或者设置为null,后续调用方法中用到这个参数的代码会受到影响。

java四种引用关系
可以看一下这一篇了解:jvm内存分配,GC,OOM,
在这里插入图片描述

  1. 强引用:正常使用过程中创建的对象一般都为强引用,强引用不会被JVM回收,即使触发OOM,也不会回收强引用的对象;当显式的将强引用的对象赋值为null的时候,JVM会在某个时间回收该对象;
  2. 软引用(SoftReference):在Java中用java.lang.ref.SoftReference类来表示,JVM会在内存快要溢出时回收;
  3. 弱引用(WeakReference):在Java中用java.lang.ref.WeakReference类来表示,无论JVM内存是否充足,弱引用对象都有可能被回收;
  4. 虚引用(PhantomReference):在Java中用java.lang.ref.PhantomReference类来表示,有的时候也称为幻象引用,不影响对象的生命周期,必须和引用队列关联使用,在任何时候都有可能被回收;

下面两个图,如果弱引用和别的对象有关联关系的话,尽管使用GC还是可以取到对象值。
在这里插入图片描述
在这里插入图片描述

ThreadLocal数据结构
在这里插入图片描述
ThreadLocal实现原理
在这里插入图片描述
ThreadLocal OOM问题
在这里插入图片描述
OOM问题避免:①在使用ThreadLocal时,都要在线程全部执行完之后再finally代码块中调用remove()方法,清除内存(线程池中使用要尤为注意)。②保存在ThreadLocal的数据不要太大。

第三章 线程池的实现和应用

3.1线程池的创建和常用参数分析

创建线程池的参数
在这里插入图片描述
线程池底层重要源码如下:
在这里插入图片描述

  • corePoolSize:核心线程数,保持在线程池中线程的数量
  • maximumPoolSize:线程池允许的最大线程数
  • keepAliveTime/timeUnit:线程池中线程空闲不被释放的最大时间,配合timeUnit使用,为0表示永远不被释放。
  • workQueue:BlockingQueue,工作线程任务的阻塞队列,用来存放等待执行的任务,默认实现:LinkedBlockingQueue
  • threadFactory:线程池创建工厂,子类通过自定义实现接口“Thread newThread(Runnable
    r)”通过工厂创建线程池具体的Thread线程。默认实现:DefaultThreadFactory
    在这里插入图片描述
    线程池参数–拒绝策略
    handler(RejectedExecutionHandler):当workQueue无法存放新加任务,或添加新任务后线程池停止工作,使用设置的拒绝策略拒绝新加任务的执行,可以用rejectedExecution来实现自己的拒绝策略。默认拒绝策略:AbortPolicy,直接抛出异常

常见拒绝策略:

  • CallerRunsPolicy:调用方执行策略,当前调用线程或添加任务的线程执行,这种方式当线程池无法执行时,使用调用方资源来执行任务。
  • AbortPolicy:异常策略,直接抛出RejectedExecutionException异常
  • DiscardPolicy:直接抛弃策略,对任务不做任何事情,忽略该任务,不执行不报错
  • DiscardOldestPolicy:抛弃最早任务策略,将workQueue的一个任务取出抛弃,将当前任务放入workQueue中执行

线程池状态

  • RUNNING:初始状态,运行中
  • SHUTDOWN:关闭状态, shutdown()方法后变为该状态,不再接受新任务,仍处理已添加任务
  • STOP:停止状态,调用shutdownNow()方法后会从RUNNING状态进入到这个状态,此时不接受新任务,并且会将执行中的线程中断
  • TIDYING:整理状态,此时队列中任务数量已经是0
  • TERMINATED:终结状态,由TIDYING状态后调用terminated()后进入该状态
    在这里插入图片描述

3.2常用线程池

了解Java SDK的常用线程池

  • 可缓存的线程池:
    ①通过Executors.newCachedThreadPool来创建
    ②核心线程数为0,最大线程数为无限大(实际是int最大值),空闲线程可以缓存60s,空闲超过60s的线程会被回收
    ③使用了SynchronousQueue同步队列,添加任务的同时,需有工作线程来取任务才可完成任务的添加和执行
    在这里插入图片描述
  • 固定线程数量的线程池 :
    ①通过Executors.newFixedThreadPool来创建
    ②核心线程数和最大线程数一样
    ③达到核心线程数后,空闲线程不会超时被终止或释放
    ④每添加一个任务后,会将任务添加到工作任务队列,线程池创建一个线程,线程数等于核心线程数时,就不会再创建线程
    在这里插入图片描述
  • 单线程的线程池 :
    ①通过Executors.newSingleThreadExecutor来创建
    ②核心线程数和最大线程数一样,且均为1,也就是只有一个线程在执行队列的工作任务
    ③使用了代理模式来创建线程池在这里插入图片描述
  • 定时执行的线程池:
    ①通过Executors.newScheduledThreadPool来创建
    ②可指定核心线程数,最大线程数为无限大(实际是int最大值),核心线程数空闲不会超过被回收
    ③使用了DelayedWorkQueue延时队列,通过延时队列来控制时间来执行
    在这里插入图片描述
    在这里插入图片描述
    线程池执行过程
    底层源码如下:
    在这里插入图片描述execute底层执行步骤:
  1. 首先来检查线程池的运行状态和工作线程数量,如果工作线程的线程数少于核心线程数,则会创建一个新的线程来执行给定的任务,通过调用addWorker来执行任务。
int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
  1. 如果超过了核心线程数量,把任务放到工作任务队列中,若工作队列没有满,则添加任务后,仍需要重新检查一下线程池的状态,如果没有继续运行(不是RUNNING状态)就把任务移除,使用拒绝策略来处理当前任务;否则将创建或唤醒工作线程来执行任务
 if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
  1. 线程池非RUNNING状态或添加任务失败后,使用拒绝策略来处理当前任务
else if (!addWorker(command, false))
reject(command);

线程池addWorker执行过程
boolean addWorker(Runnable firstTask, boolean core) 方法,返回true表示接收了任务,否则表示拒绝任务
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 实际执实际执行方法ThreadPoolExecutor#runWorker,最终执行时是执行我们最开始创建的Runnable对象的run方法

  2. 执行任务,Java充分对各种阶段留好钩子,方便业务自己实现钩子定制,如beforeExecute()、afterExecute(), 可在自定义线程池时实现这些方法

3.3线程池常用队列之LinkeBlockingQueue

在这里插入图片描述
LinkdeBlockingQueue类属性说明:
在这里插入图片描述
在这里插入图片描述
队列中添加元素的过程:

  • 队列未满,notFull.signal()唤醒阻塞在添加元素等待的等待线程
  • 队列有元素,notEmpty.signal()唤醒阻塞在取出元素等待线程(需先获取takeLock锁)

在这里插入图片描述
队列中添加元素的过程:

  • 队列未满,notEmpty.signal()唤醒阻塞在添加元素等待的等待线程
  • 队列有元素,notFull.signal()唤醒阻塞在取出元素等待线程(需先获取putLock锁)

在这里插入图片描述
添加-取出元素方法区别与对比:
在这里插入图片描述
添加-取出元素方法区别与对比:
在这里插入图片描述

3.4可定时执行的线程池原理分析

。。。那个视频课线程池部分讲的我好懵,后期有时间清一下思路再补上

3.5 线程池的同步异步调用Callable、Feture

第四章 java锁底层实现和应用

4.1乐观锁CAS实现及应用

乐观锁悲观锁的区别
为什么要加锁?因为保证多个线程更新一个资源时,防止数据冲突和脏乱,做到线程安全。
在这里插入图片描述
CAS乐观锁
具体看这一篇:CAS算法

CAS解释:全名compare and swap,先比较然后设置,CAS是一个原子操作
适用场景:更新一个值,不依赖于加锁实现,可以接受CAS失败(为什么会失败?因为A线程先比较了某个共享值发现没有被别的线程改变过,所以就设置不加锁,但是就在A线程比较后但是又在设置前有个B线程使用了,这就可能导致失败)
局限:只可以更新一个值,如AtomicReference、AtomicInteger需要同时更新时,无法做到原子性

CAS原理:AtomicInteger类中的compareAndSwapInt方法是native方法是调用JNI的方法,底层是通过一个CPU指令完成。
在这里插入图片描述
在这里插入图片描述
CAS的自旋问题:
在这里插入图片描述

CAS的ABA问题:CAS的ABA问题详解
在这里插入图片描述

4.2数据库悲观锁乐观锁实现

悲观锁

SELECT … LOCK IN SHARE MODE

  • 共享锁,在事务内生效
  • 给符合条件的行添加的是共享锁,其他事务会话同样可以继续给这些行添加共享锁,在锁释放前,其他事务无法对这些行进行删除和修改;
  • 两个事务同时对一行加共享锁后,无法更新,直到只有一个事务对该行加共享锁;
  • 如果两个加了共享锁的事务同时更新一行,会发生Deadlock死锁问题;
  • 某行已有排它锁,无法继续添加共享锁
  • 不会阻塞正常读
  • 适用于写两张存在关联关系的表数据,如parent和child表,写入child表时需要确保parentId在parent表中已写入数据且不会被删除

SELECT … FOR UPDATE

  • 排它锁,在事务内生效
  • 给符合条件的行添加的是排它锁,其他事务无法再加排它锁,在释放前,其他事务无法对这些行进行删除和修改
  • 某行已有共享锁,无法继续添加排它锁
  • 第一个事务对某行加了排它锁,第二个事务继续加排它锁,第二个事务需要等待
  • 加锁有超时时间
  • 不会阻塞正常读
  • 适用于并发更新会出现问题的场景,如金融账户转账,电商下单时的库存扣减,避免最终数字不准确

乐观锁

UPDATE set … version = version + 1 where version = v e r s i o n version version

  • CAS思路,使用version版本控制,保证同一时间只有一个事务可以更新成功
  • 根据影响的行数来判断是否更新成功,更新失败的继续重新获取version值更新,可以设置最大重试次数

例子:电商下单的库存更新
使用更新语句:update product_stock set number= number – 1 where product_id = productId and number – 1 >= 0
判断更新影响的行数来成功还是失败

4.3AQS的数据结构

AQS

  • 即AbstractQueuedSynchronizer, 队列同步器;
  • 提供一个框架来实现阻塞锁和相关的依赖于先进先出(FIFO)等待队列;
  • 各种同步组件的核心抽象实现类;
  • 管理等待队列,锁的占用和释放,中断、超时和通知等

AQS的作用:
在这里插入图片描述
AQS:
在这里插入图片描述

  • head
    等待队列的头节点,初始值为null,延迟初始化;
    除了初始化,后续只能通过setHead()方法来设置;
    如果head节点存在,它的waitStatus不能为CANCELLED;

  • tair
    等待队列的尾节点,初始值为null,延迟初始化;
    只能通过添加新节点的enq()方法来更新tair节点;

  • state
    int 值,0表示当前AQS没有线程占用锁;
    等于1表示有线程占用锁,后续线程需要排队等待获取锁;
    大于1表示已占用锁的线程重入了锁,state可表示重入锁的次数;

  • exclusiveOwnerThread
    获得独占锁的线程;

AQS.Node:
在这里插入图片描述

  • waitStatus
    0:初始状态
    CANCELLED:节点是取消状态,等待超时或者被中断
    SIGNAL:后续节点处于等待状态,需要被唤醒
    CONDITION:节点处于等待中
    PROPAGATE:下一个需要被无条件传播

  • prev
    等待队列的前一个节点,用于检查前节点的状态,若前节点为空,则会在等待队列中取消前节点直到找到没取消的位置( head节点是不会被取消的)

  • next
    等待队列的后一个节点

  • thread
    当前节点加入等待队列的线程

  • nextWaiter
    在Condition队列上等待的下一个节点

4.4ReentrantLock的加锁解锁过程

ReentrantLock简单加锁解锁过程
在这里插入图片描述
ReentrantLock怎么实现可重入
在这里插入图片描述
AQS加锁排队等待的实现
在这里插入图片描述
在这里插入图片描述
公平锁和非公平锁的实现区别
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

第五章 并发容器

5.1KV集合HashMap的实现原理

HashMap时存储K-V键值对的集合

HashMap数据结构
在这里插入图片描述

  • table:Node节点数组
  • entrySet:HashMap.Node的Set集合
  • size:HashMap集合中元素的个数
  • modCount:标记HashMap修改的次数,每次调用put和clear方法,modCount会增加
  • threshold:当size大于threshold,就需要扩容,threshold默认为数组长度*loadFactor
  • loadFactor:加载因子,默认是0.75

HashMap.Node数据结构
在这里插入图片描述

  • hash:key.hashCode()经过移位计算后得到的值
  • key:HashMap要存储的键,通过key来获取值
  • Value:HashMap要存储的值
  • next:指向下一个Node节点

在这里插入图片描述

5.2HashMap在高并发场景下死循环分析

HashMap put过程
在这里插入图片描述

JDK7中HashMap扩容代码:

 /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

HashMap在无并发场景下扩容:
在这里插入图片描述

HashMap在并发场景下扩容:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

如上图所视,这时候如果查找keyHash=9的值时就会发现查找会变成table[1].Node–>keyHash=5–>keyHash=1–>keyHash=5–>keyHash=1–>keyHash=5–>。。。的死循环中(具体看底层取值代码源码,如下图)
在这里插入图片描述

5.3ConcurrentHashMap (1.7JDK)

谈谈ConcurrentHashMap1.7和1.8的不同实现
ConcurrentHashMap1.8 - 扩容详解

ConcurrentHashMap 数据结构
在这里插入图片描述
在这里插入图片描述

  • Segment集成ReentrantLock,具有加锁解锁的功能,segments有多少个元素,说明就有多少把锁,扮演了分段锁,降低并发竞争度
  • 只有写才会对对应的Segment加锁,读不加锁

5.4CopyOnWriteArrayList如何实现线程安全

为什么要使用CopyOnWriteArrayList?
先来看一个ArrayList例子:这里模仿了两个线程,一个进行元素的遍历,一个进行元素的添加。
在这里插入图片描述
运行后输出报错信息:
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)

阅读源码发现:

  • ArrayList当读写线程同时运行时,使用fast-fail机制,抛出ConcurrentModificationException异常
  • ArrayList在读写线程同时运行时,无法满足开发者需求

改用CopyOnWriteArrayList后:

  • 读写同时进行时,不会抛出ConcurrentModificationException异常
    在这里插入图片描述

CopyOnWriteArrayList实现原理
底层源码如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
CopyOnWriteArrayList优缺点:
优点:线程安全,可以兼容读写并发
缺点:耗内存(写时复制);数据实时性不高,可能获取到旧数据

适用场景:
读多写少,如白名单黑名单等
集合数量不大
数据要求不是强实时

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值