Java 并发编程之美
2021-07-09 16:26:18 1 举报
AI智能生成
并发编程
作者其他创作
大纲/内容
基础
进程
系统进行资源分配和调度的基本单位
CPU资源例外,线程是CPU分配的基本单位
进程中的多个线程共享进程资源
在java中,main函数就是启动了JVM的一个进程,main函数所在的线程是这个进程中的一个线程,为主线程
线程
sleep wait区别
sleep()方法正在执行的线程主动让出CPU
并不会释放同步资源锁
wait()方法则是指当前线程让自己暂时退让出同步资源锁
只有调用了notify()方法,之前调用wait()的线程才会解除wait状态,可以去参与竞争同步资源锁,进而得到执行
父子线程
当父线程结束后,子线程还是可以继续存在的,也就是子线程的生命周期并不受父线程的影响
守护线程
如果你希望在主线程结束后JVM进程马上结束,那么在创建线程时可以将其设置为守护线程,如果你希望在主线程结束后子线程继续工作,等子线程结束后再让JVM进程结束,那么就将子线程设置为用户线程
JVM发现当前已经没有用户线程了,就会终止JVM进程
杀死守护线程
当tomcat收到shutdown命令后并且没有其他用户线程存在的情况下tomcat进程会马上消亡,而不会等待处理线程处理完当前的请求
因为tomcat线程都是守护线程
threadLocal
创建一个ThreadLocal变量后,每个线程都会复制一个变量到自己的本地内存
Thread类中有一个threadLocals和一个inheritableThreadLocals,它们都是ThreadLocalMap类型的变量
其实每个线程的本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量里面
ThreadLocal不支持继承性
InheritableThreadLocal
继承自ThreadLocal,其提供了一个特性,就是让子线程可以访问在父线程中设置的本地变量
场景
子线程需要使用存放在threadlocal变量中的用户登录信息
统一的id追踪的整个调用链路记录下来
并发和并行
并发
一个时间段内多个任务同时执行
并行
单位时间片内多个任务同时执行
线程安全
JMM
synchronized
代码块正常退出或者waite方法可以退出锁
由于Java中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,synchronized关键字会引起线程上下文切换并带来线程调度开销
volatile
当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值
不保证原子性
避免指令重排
CAS(compareAndSwap)
CAS有3个操作数,分别为:内存位置(V)、预期原值(A)和新值(B)
ABA问题
JDK中的AtomicStampedReference类给每个变量的状态值都配备了一个时间戳,从而避免了ABA问题的产生
锁
悲观锁
悲观锁指对数据被外界修改持保守态度,认为数据很容易就会被其他线程修改,所以在数据被处理前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态
乐观锁
乐观锁并不会使用数据库提供的锁机制,一般在表中添加version字段或者使用业务状态来实现。乐观锁直到提交时才锁定,所以不会产生任何死锁
公平锁
new ReentrantLock(true)
非公平锁
new ReentrantLock(false
独占锁
独占锁保证任何时候都只有一个线程能得到锁,ReentrantLock就是以独占方式实现的
共享锁
ReadWriteLock读写锁,它允许一个资源可以被多线程同时进行读操作
可重入锁
synchronized内部锁是可重入锁。可重入锁的原理是在锁内部维护一个线程标示,用来标示该锁目前被哪个线程占用,然后关联一个计数器。一开始计数器值为0,说明该锁没有被任何线程占用。当一个线程获取了该锁时,计数器的值会变成1,这时其他线程再来获取该锁时会发现锁的所有者不是自己而被阻塞挂起
但是当获取了该锁的线程再次获取锁时发现锁拥有者是自己,就会把计数器值加+1,当释放锁后计数器值-1。当计数器值为0时,锁里面的线程标示被重置为null,这时候被阻塞的线程会被唤醒来竞争获取该锁
自旋锁
由于Java中的线程是与操作系统中的线程一一对应的
当一个线程在获取锁(比如独占锁)失败后,会被切换到内核状态而被挂起
当该线程获取到锁时又需要将其切换到内核状态而唤醒该线程
所以自旋锁当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不放弃CPU使用权的情况下,多次尝试获取(默认次数是10,可以使用-XX:PreBlockSpinsh参数设置该值
LongAdder
背景
使用AtomicLong时,在高并发下大量线程会同时去竞争更新同一个原子变量,但是由于同时只有一个线程的CAS操作会成功,这就造成了大量线程竞争失败后,会通过无限循环不断进行自旋尝试CAS的操作,而这会白白浪费CPU资源
为了解决高并发下多线程对一个变量CAS争夺失败后进行自旋而造成的降低并发性能问题,LongAdder在内部维护多个Cell元素(一个动态的Cell数组)来分担对单个变量进行争夺的开销
AtomicLong
过程
LongAdder的真实值其实是base的值与Cell数组里面所有Cell元素中的value值的累加,base是个基础值,默认为0
通过内部cells数组分担了高并发下多线程同时对一个原子变量进行更新时的竞争量,让多个线程可以同时对cells数组里面的元素进行并行的更新操作
数组元素Cell使用@sun.misc.Contended注解进行修饰,这避免了cells数组内多个原子变量被放入同一个缓存行,也就是避免了伪共享
CopyOnWriteArrayList
使用写时复制的策略来保证list的一致性,而获取—修改—写入三步操作并不是原子性的,所以在增删改的过程中都使用了独占锁,来保证在某个时间只有一个线程能对list数组进行修改
提供了弱一致性的迭代器,从而保证在获取迭代器后,其他线程对list的修改是不可见的,迭代器遍历的数组是一个快照
LockSupport
主要作用是挂起和唤醒线程,该工具类是创建锁和其他同步类的基础
如果thread之前没有调用park,则调用unpark方法后,再调用park方法,其会立刻返回
因调用park()方法而被阻塞的线程被其他线程中断而返回时并不会抛出InterruptedException异常
锁
AQS(AbstractQueuedSynchronizer)
AQS是一个FIFO的双向队列,其内部通过节点head和tail记录队首和队尾元素,队列元素的类型为Node
组成
Node
thread变量用来存放进入AQS队列里面的线程
SHARED用来标记该线程是获取共享资源时被阻塞挂起后放入AQS队列的
EXCLUSIVE用来标记线程是获取独占资源时被挂起后放入AQS队列的
waitStatus记录当前线程等待状态,可以为CANCELLED(线程被取消了)、SIGNAL(线程需要被唤醒)、CONDITION(线程在条件队列里面等待)、PROPAGATE(释放共享资源时需要通知其他节点)
prev记录当前节点的前驱节点
next记录当前节点的后继节点
state
以通过getState、setState、compareAndSetState函数修改其值
对于ReentrantLock的实现来说,state可以用来表示当前线程获取锁的可重入次数
对于读写锁ReentrantReadWriteLock来说,state的高16位表示读状态,也就是获取该读锁的次数,低16位表示获取到写锁的线程的可重入次数
对于semaphore来说,state用来表示当前可用信号的个数
对于CountDownlatch来说,state用来表示计数器当前的值
内部类ConditionObject
可以直接访问AQS对象内部的变量
是个条件变量,每个条件变量对应一个条件队列(单向链表队列),其用来存放调用条件变量的await方法后被阻塞的线程
在每个条件变量内部都维护了一个条件队列,用来存放调用条件变量的await()方法时被阻塞的线程。注意这个条件队列和AQS队列不是一回事
另外一个线程调用条件变量的signal()或者signalAll()方法时,会把条件队列里面的一个或者全部Node节点移动到AQS的阻塞队列里面,等待时机获取锁
synchronized同时只能与一个共享变量的notify或wait方法实现同步,而AQS的一个锁可以对应多个条件变量
ReentrantLock
使用AQS来实现的,并且根据参数来决定其内部是一个公平还是非公平锁,默认是非公平锁
AQS的state状态值表示线程获取该锁的可重入次数,在默认情况下,state的值为0表示当前锁没有被任何线程持有
第一个调用Lock的线程会通过CAS设置状态值为1,CAS成功则表示当前线程获取到了锁,然后setExclusiveOwnerThread设置该锁持有者是当前线程
ReentrantReadWriteLock
ReentrantReadWriteLock采用读写分离的策略,允许多个线程可以同时获取读锁
内部维护了一个ReadLock和一个WriteLock,它们依赖Sync实现具体功能。而Sync继承自AQS,并且也提供了公平和非公平的实现
使用state的高16位表示读状态
使用低16位表示获取到写锁的线程的可重入次数
获取读锁,如果当前没有其他线程持有写锁,则当前线程可以获取读锁,AQS的状态值state的高16位的值会增加1,然后方法返回。否则如果其他一个线程持有写锁,则当前线程会被阻塞
StampedLock(java8新增)
不可重入锁
三种模式
写锁writeLock
悲观读锁readLock
乐观读锁tryOptimisticRead
获取读锁只是使用位操作进行检验,不涉及CAS操作,所以效率会高很多
并发队列
阻塞队列(锁实现)
LinkedBlockingQueue
内部是通过单向链表实现的,使用头、尾节点来进行入队和出队操作,也就是入队操作都是对尾节点进行操作,出队操作都是对头节点进行操作
对头、尾节点的操作分别使用了单独的独占锁从而保证了原子性,所以出队和入队操作是可以同时进行的。另外对头、尾节点的独占锁都配备了一个条件队列,用来存放被阻塞的线程,并结合入队、出队操作实现了一个生产消费模型
ArrayBlockingQueue
ArrayBlockingQueue通过使用全局独占锁实现了同时只能有一个线程进行入队或者出队操作,这个锁的粒度比较大,有点类似于在方法上添加synchronized的意思
PriorityBlockingQueue
PriorityBlockingQueue是带优先级的无界阻塞队列,每次出队都返回优先级最高或者最低的元素。其内部是使用平衡二叉树堆实现的,所以直接遍历队列元素不保证有序
PriorityBlockingQueue内部有一个数组queue,用来存放队列元素,size用来存放队列元素个数。allocationSpinLock是个自旋锁,其使用CAS操作来保证同时只有一个线程可以扩容队列,状态为0或者1,其中0表示当前没有进行扩容,1表示当前正在扩容
lock独占锁对象用来控制同时只能有一个线程可以进行入队、出队操作
DelayQueue
DelayQueue并发队列是一个无界阻塞延迟队列,队列中的每个元素都有个过期时间,当从队列获取元素时,只有过期元素才会出队列。队列头元素是最快要过期的元素
非阻塞队列(CAS实现)
ConcurrentLinkedQueue
线程安全的无界非阻塞队列,其底层数据结构使用单向链表实现,对于入队和出队操作使用CAS来实现线程安全
底层使用单向链表数据结构来保存队列元素,每个元素被包装成一个Node节点。队列是靠头、尾节点来维护的,创建队列时头、尾节点指向一个item为null的哨兵节点。第一次执行peek或者first操作时会把head指向第一个真正的队列元素。由于使用非阻塞CAS算法,没有加锁,所以在计算size时有可能进行了offer、poll或者remove操作,导致计算的元素个数不精确,所以在并发情况下size函数不是很有用
入队、出队都是操作使用volatile修饰的tail、head节点,要保证在多线程下出入队线程安全,只需要保证这两个Node操作的可见性和原子性即可
线程池
0 条评论
下一页