一文带你看懂Java多线程并发,深度剖析AQS源码
文章目录
- 1.线程基础知识
- 2.线程安全举措
1.摘要
本问旨在通过例子来带领大家分析Java并发编程中线程不安全行为,并相应给出对应的解决方案。
1.线程基础知识
(1) 概念:cpu执行最基本单元
。
这里有必要额外解释下进程,进程是系统进行资源分配和调度的基本单元
。
通常来说一个应用程序占用一个进程,当然一个服务也可以开启多个进程。
一个进程至少包含一个线程,进程内所有线程共享进程资源
。
(2) 状态:
线程五种状态:分别是新建、就绪、运行、阻塞、死亡。
1.创建线程让线程来到新建状态。
2.调用start()方法,此时线程来到就绪状态。
3.CPU时间片轮训调度执行到该线程,此时线程处于运行状态。
4.线程执行sleep/ wait /join 方法,来到阻塞状态。
5.线程内run方法执行结束,线程凋亡。
(3) 分类:
线程分为用户线程和守护线程
。用户线程则是用户创建普通线程,JVM启动从Main函数开始执行,这个主线程也是一个就是一个用户线程,其中JVM运行中还同时开启很多守护线程,比如低优先级的GC线程。
两者的区别:
守护线程会随着最后一个用户线程结束后一起结束
。JVM在执行过程中,并不会因为守护线程结束而结束运行,相反,当用户线程/主线程执行结束,JVM则会直接退出,此时如果还有后台守护线程仍然在运行中,则直接一并结束运行。
(4) 线程创建几种方式:
1) 类继承Thread 类,重写run方法
2) 类实现runnable 接口,重写run方法
3) 类实现 callabled 接口,重写call方法,并结合FutureTask 获取返回值。【这个方法可以带返回值
】
4) 使用Executors 构建线程池。前面三种都是创建单个线程的方式。
(5) 多线程并发编程的意义:
随着互联网数据和访问请求流量的日益增加,单核CPU /单线程处理任务的效率已经无法满足人们都系统响应的要求,且伴随着多核CPU时代的到来,多线程并发处理任务,能同时利用多个CPU同时并发处理海量的系统请求,极大的提升系统性能。
2.线程安全举措
(1) 原生不可变类
【使用final修饰的类】
(2) 使用ThreadLocal
来存储对象,ThreadLocal 对象属于线程私有,每个对象只会存储对象的一个副本,线程在操作该对象时,只会操作当前线程内部的对象,从而做到数据隔离,线程安全。
(3) volatile/synchronized 关键字
volatile: 只能解决有序性和可见性。不能保证原子性。通常用来解决多线程情况下,每次需要获取对象最新数据。
有序性:
Java内存模型允许编译器和处理器对指令进行重排需从而获取更优越的性能。但是这个只是会对不存在依赖性的指令发生重排序。单线程下不会出现问题,多线程情况下有可能就是因为执行重排而导致执行异常。因此对于多线程情况下,某些变量写操作不依赖当前变量的值,且需要获取最新数据时,可以使用volatile 关键字进行声明,避免指令重排,且维持可见性。
可见性:线程在获取当前对象的值总是从主内存获取最新的数据,而不是
读取CPU和主内存之间的缓存
,可以有效解决数据未实时同步更新的情况
缓存扩展补充:
为了解决主内存与CPU 之间运行速度之间差,或者说为了进一步提升CPU处理数据的速度,通常会在主内存和CPU之间使用多级缓存用以存储数据,CPU执行时,直接从缓存中取存数据,然后缓存在将数据同步更新至主内存中去。
L1、 L2、 L3 每一级缓存存储数据空间更大,同时缓存速度依次下降。由于CPU执行不是以变量来加载执行的,而是以Cache行为单位与主内存进行数据交换的,一般是2的幂次数字节,因此当多个变量存在一个Cache行中,多线程同时修改一个Cache行里面的多个变量时,由于同一时刻只能存在一个线程操作缓存行
,根据缓存一致性协议
,相比将多个变量存在不同Cache行进行操作,性能会有所下降,这就是所谓的伪共享
。
如何避免伪共享?JDK8之前都是通过字节填充
的方式来避免,就是说创建一个变量时使用填充字段来填充该变量所在的缓存行,避免将多个变量存放在同一个缓存行中
。
JDK8提供了一个注解来解决伪共享问题。@sun.misc.Contended
. 可作用于类和对象上,默认只能用于Java核心类,用户类路径下需要手动添加JVM参数开启。
-XX:-RestrictContended
【开启注解】
-XX:ContendedPaddingWidth
【填充宽度默认128可设置】
(4) 原子类/CAS
JDK内置AtomicLong、AtomicInteger、LongAdder、LongAccumulator 等
CAS 操作通过CPU原语实现,是一种硬件实现线程安全方式,其中可以使用Unsafe类来给对象定制化CAS操作,谨慎使用Unsafe类,这个类可以直接操作内存。【Unsafe类无法直接实例化(JDK大佬对其做了限制,不允许直接操作,底层就是做了一层判断,判断当前类加载器是不是启动类加载器,不是的话,直接抛异常。开发通常使用的加载器就是应用程序加载器
),但是可以借助反射技术来实例化】
(5) 锁
- synchronized关键字实现同步
JVM 内置锁,底层依赖于互斥锁MuteLock,线程获取锁,会相应进入MonitorEnter,退出则进入MoniterExit ,内置使用state维护锁状态。 - 各种显示锁如 ReentrantLock、ReentrantReadWriteLock、JDK8新增StampedLock等。底层都是基于AQS实现。
下面通过ReentrantLock来认识AQS。AQS是实现同步器的抽象组件JUC包所有锁的底层就是用的AQS。
ReentrantLock有个Sync类直接继承AQS。内部四个关键对象分别是
head 头指针,tail尾指针,exclusiveOwnerThread= threadName 当前独占锁所属线程。state 表示获取锁次数。其中state从0变成1 时候,表示当前有线程获取锁,如果此时当前线程继续获取锁,则state则会执行state=state+1 操作。【该锁是可重入独占锁,允许获取锁线程重复获取锁】
整个流程图如下:
这个是默认非公平锁实现。
下面着重分析入队列方法enq(final Node node)
和 公平锁tryAcquire()
方法
private Node enq(final Node node) {for (;;) {Node t = tail;if (t == null) { // Must initializeif (compareAndSetHead(new Node())) tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}
第一次压入队列执行逻辑图: 后续Node 对象入队列,只会在队列尾部添加,以双向链表形式构建
,并且设置tail指针指向当前Node对象。
公平锁和非公平锁最大的区别就在tryAcquire()方法的实现。JDK21
为例子进行说明
/*** Acquires only if thread is first waiter or empty*/protected final boolean tryAcquire(int acquires) {if (getState() == 0 && !hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(Thread.currentThread());return true;}return false;}// 公平锁补充判断逻辑 !hasQueuedPredecessors/* 前面注释省略* @return {@code true} if there is a queued thread preceding the* current thread, and {@code false} if the current thread* is at the head of the queue or the queue is empty* @since 1.7*/// 如果队列存在当前线程且当前线程是队列头部或者队列为空 则允许放行获取竞争锁public final boolean hasQueuedPredecessors() {Thread first = null; Node h, s;// 如果头节点不为空且 //头指针下一个节点为空【为真 则说明正在安排插入第一个线程入队列】 或者// 第一个节点waiter 线程为null 【为真 则正在安排插入第一个线程入队列 】或者// 第一个node节点的pre ==null 【为真 则正在安排插入第一个线程入队列】。// 如果以上都不满足,那么第一个node节点早已经完成入队操作。 if ((h = head) != null && ((s = h.next) == null ||(first = s.waiter) == null ||s.prev == null))first = getFirstQueuedThread(); // retry via getFirstQueuedThread// 在以上情况都不满足的情况下, 如果first 和当前线程不一致,那么就是返回true。// 那么将不参与获取锁竞争。tryAcquire() 方法判断逻辑是 !hasQueuedPredecessors()return first != null && first != Thread.currentThread();}// 获取队列节点的第一个线程public final Thread getFirstQueuedThread() {Thread first = null, w; Node h, s;if ((h = head) != null && ((s = h.next) == null ||(first = s.waiter) == null ||s.prev == null)) {// traverse from tail on stale reads// 从尾部tail 节点开始读,反向读,并且每次读都将上一个非null节点赋值给当前节点// 直到p的上一个节点null结束也就是指定到第一个node后,// 下一次循环就不符合条件,不在执行了。p.waiter就是第一个node节点的线程for (Node p = tail, q; p != null && (q = p.prev) != null; p = q)if ((w = p.waiter) != null)first = w;}return first;}
这个第一个节点Node属性waiter设置为null。
条件队列和同步队列:
AQS 内部采用FIFO(先进先出)基于双向链表实现的同步队列
,用于阻塞排队获取锁,同时也提供了基于单向链表实现的条件队列
,用以条件控制等待获取锁。下面结合图进行介绍。
其中条件队列可以有多个,当锁Contion对象调用await 方法,会将当前线程释放锁,追加至条件队列。当锁Condition对象调用singnal或者singalAll(唤醒所有线程)方法唤醒线程,将条件队列线程追加至同步队列尾部。