导图社区 JUC
这是一篇关于JUC的思维导图,包含线程间通信、 并发容器类、Callable接口、阻塞队列、ThreadPool线程池等知识总结。
编辑于2023-12-20 18:03:44JUC
synchronized
锁对象
普通同步方法
锁对象:方法调用对象
静态同步方法
锁对象:当前类的字节码对
同步方法块
锁对象:字节码对象
锁对象:实现类对象
由JVM控制
synchronized也是可重入锁
在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁
Lock
ReentrantLock可重入锁
new ReentrantLock() 默认为非公平锁
new ReentrantLock ( true )传入true为公平锁

公平锁与非公平锁
公平锁: 线程等待队列最前面的线程最先拿到锁
非公平锁 : 随机线程拿到锁 产生线程饿死问题
由于非公平锁,导致某个线程一致得不到执行,每次都让其他线程抢到了锁
限时等待
public boolean tryLock ( long timeout, TimeUnit unit)
等待一段时间后尝试获取锁,如果没有获取到返回false,获取到了返回true ( 等待不停抢锁会线程阻塞 ,直到抢到锁获取已到等待时间)
ReentrantLock和synchronized区别
同
synchronized与ReentrantLock都是独占锁
synchronized与ReentrantLock都是可重入锁
synchronized与ReentrantLock都是悲观锁
异
synchronized加锁和解锁由jvm完成使用者无法控制 而ReentrantLock加锁和解锁都由使用者控制
ReentrantLock可重入锁 加锁几次需要手动释放几次锁,而sync是自动完成的
synchronized不可响应中断,拿不到锁就一直等着,而ReentrantLock可以使用tryLock 响应中断,一段时间拿不到锁就不拿了,不用一直阻塞等待
ReentrantReadWriteLock 读写锁
reentrantReadWriteLock.writeLock() 获取读锁
reentrantReadWriteLock.readLock() 获取写锁
读写锁允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞
写写不可并发
读写不可并发
读读可以并发
锁降级
锁降级就是从写锁降级成为读锁。在当前线程拥有写锁的情况下,再次获取到读锁,随后释放写锁的过程就是锁降级
线程间通信
synchronized
等待
锁对象 . wait ()
释放锁,出让CPU执行权,等待被唤醒重新获取锁,然后在wait处往下执行代码
唤醒
锁对象 . notify ( )
随机唤醒一条等待的线程,被唤醒的线程也需要去竞争锁
锁对象 . notifyAll ( )
唤醒全部等待的线程
ReentrantLock
等待
reentrantLock . newCondition ( ) . await ( )
可以创建多个锁条件
Condition aCondition =reentrantLock.newCondition();
Condition bCondition =reentrantLock.newCondition();
Condition cCondition =reentrantLock.newCondition();
唤醒
condition. signal ( )
唤醒指定的锁条件对象的一条等待唤醒线程
condition. signalAll ( )
唤醒指定的锁条件对象的全部等待唤醒线程
相比 synchronized 更细 ,可以指定某条等待线程唤醒
虚假唤醒
被唤醒的线程 if条件已经不满足,但是由于被唤醒将在抢到锁资源后又在wait处被唤醒,导致线程执行顺序混乱
解决办法将if条件换成where 被唤醒抢到锁后依然要进行条件判断
将产生线程全部都在等待的假死状态
处理办法 : 使用 notifyAll 或者 signalAll 唤醒全部的线程,不满足条件的线程依然会进入等待状态
并发容器类
Collections工具类
synchronizedList
synchronizedMap
synchronizedCollection
synchronizedSet
synchronizedSortedMap
synchronizedSortedSet
将线程不安全的容器转换为线程安全的容器 包含list,set,map
CopyOnWrite
写时复制容器
当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器
CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器
对CopyOnWrite容器进行并发的读,而不需要加锁,写时加锁
辅助类
CountDownLatch ( 倒计数器 )
new CountDownLatch(int count) 实例化一个计数器,初始值为 count
countDown() 每调用一次,计数器减一
await() 等待 当计数器减为0执行
CyclicBarrier(循环栅栏)
CyclicBarrier(int parties, Runnable barrierAction) 创建一个CyclicBarrier实例,parties指定参与相互等待的线程数,barrierAction一个可选的Runnable命令,该命令只在每个屏障点运行一次,可以在执行后续业务之前共享状态。该操作由最后一个进入屏障点的线程执行
CyclicBarrier(int parties) 创建一个CyclicBarrier实例,parties指定参与相互等待的线程数。
await() 该方法被调用时表示当前线程已经到达屏障点,当前线程阻塞进入休眠状态,直到所有线程都到达屏障点,当前线程才会被唤醒。
Semaphore(信号量)
Semaphore可以控制同时访问的线程个数,假设资源数目为N,每一个线程均可获取一个资源,但是当资源分配完毕时,后来线程需要阻塞等待,直到前面已持有资源的线程释放资源之后才能继续。
public Semaphore(int permits) // 构造方法,permits指资源数目(信号量)
public void acquire() throws InterruptedException // 占用资源,当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量,或超时
public void release() // (释放)实际上会将信号量的值加1,然后唤醒等待的线程
Callable接口
使用
创建类现实callable接口 重写call方法
创建 FutureTask, 调用 带参构造,传入 callable实现类对象
new FutureTask<String>(callable)
创建线程类对象 new Thread(futureTask).start() ,调用 start 方法
FutureTask.get() 获取线程返回信息
使用注意事项
FutureTask.get() 会阻塞当前线程 建议放到最后
只计算一次,FutureTask会复用之前计算过得结果
callable接口与runnable接口的区别
具体的方法不同,一个是call方法一个是run方法
Runnable没有返回值,而callable可以拿到返回值对象
run方法不能抛出异常只能在run方法内消耗异常 而call方法可以抛出异常
获取多线程的四种方式
继承Thread,重写run方法,调用start执行线程
实现Runnable接口,重写run方法,调用start执行线程
实现Callable接口,重写call方法,创建FutureTask对象,执行Thread的start方法执行线程
创建线程池,执行submit方法
阻塞队列
BlockingQueue即阻塞队列
实现类
ArrayBlockingQueue:由数组结构组成的有界阻塞队列
LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列
PriorityBlockingQueue:支持优先级排序的无界阻塞队列
DelayQueue:使用优先级队列实现的延迟无界阻塞队列
SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列
LinkedTransferQueue:由链表组成的无界阻塞队列
LinkedBlockingDeque:由链表组成的双向阻塞队列
四组方法
抛出异常
插入
add(e)
当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException:Queue full
移除
remove()
当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException
检查
element()
当阻塞队列空时,再调用element检查元素会抛出NoSuchElementException
特殊值
插入
offer(e)
插入方法,成功ture失败false
移除
poll()
移除方法,成功返回出队列的头元素,队列里没有就返回null 同时删除该元素
检查
peek()
检查方法,成功返回队列中的头元素,没有返回null 不会删除该元素
阻塞
插入
put(e)
当阻塞队列满时,再往队列里put元素,队列会一直阻塞生产者线程直到put数据or响应中断退出
移除
take()
当阻塞队列空时,再从队列里take元素,队列会一直阻塞消费者线程直到队列可用
检查
不可用
超时
插入
offer(e, time, unit)
如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。 返回一个特定值以告知该操作是否成功(典型的是 true / false)。
移除
poll(time, unit)
检查
不可用
ThreadPool线程池
线程池工具类
Executors
Executors.newFixedThreadPool()
Executors.newSingleThreadExecutor()
Executors.newCachedThreadPool();
Executors.newScheduledThreadPool();
Executors.newSingleThreadScheduledExecutor();
线程工具类创建的线程将导致大量线程堆积,导致OOM
自定义线程池
//创建线程池 ThreadPoolExecutor executor = new ThreadPoolExecutor( 6, 12, 3, TimeUnit.SECONDS, new LinkedBlockingQueue<>(8) , new ThreadFactory() { int i; @Override public Thread newThread(Runnable r) { ++i; return new Thread(r, "线程:" + i); } }, new ThreadPoolExecutor.CallerRunsPolicy() );
new ThreadPoolExecutor
corePoolSize 核心线程数
maximumPoolSize 最大线程数
keepAliveTime 非核心线程存活时长
TimeUnit 存活时长单位
BlockingQueue 线程等待队列
前五个参数也可以自定义一个线程池
ThreadFactory 线程工厂
RejectedExecutionHandler 拒绝策略
队列满了,且非核心线程也满了 将采用拒绝策略 四种拒绝策略
AbortPolicy
默认拒绝策略,直接抛出异常
callerRunsPolicy
交由当前线程池所处线程执行
DiscardPolicy
直接丢弃,不处理,也不抛出异常
DiscardOldestPolicy
丢弃队列中最旧的也就是队列中最前面的线程,然后将新线程添加进去
线程池执行线程任务
execute ( )
只能传入 实现Runnable接口的线程任务
submit ( )
可以传入实现 Callable接口的线程任务
新创建的线程池线程数量为0 核心线程一旦创建会一直保留
多线程高并发底层原理
JVM内存模型
内存划分
主内存
保存了所有的变量
工作内存
每个线程都有自己的工作内存,线程独享,保存了线程用到的变量副本(主内存共享变量的一份拷贝)。工作内存负责与线程交互,也负责与主内存交互
共享变量
如果一个变量被多个线程使用,那么这个变量会在每个线程的工作内存中保有一个副本,这种变量就是共享变量
内存模型的三大特性
原子性
即不可分割 列如 a = 1 + 1 ( 原子性 ) a + + ( 非原子性 )
synchronized
java.util.concurrent包下的Atomic原子类
可见性
每个线程都有自己的工作内存,所以当某个线程修改完某个变量之后,在其他的线程中,未必能观察到该变量已经被修改
final
volatile
synchronized
有序性
java会对一些指令进行重新排序
volatile
synchronized
volatile 关键字
多线程环境下的作用
可见性
public class VolatileDemo { private static Integer flag = 1; public static void main(String[] args) throws InterruptedException { new Thread(new Runnable() { @Override public void run() { System.out.println("我是子线程工作内存flag的值:" + flag); while(flag == 1){ } System.out.println("子线程操作结束..." + flag); } }).start(); Thread.sleep(500); flag = 2; System.out.println("我是主线程工作内存flag的值:" + flag); } }
保证此变量对所有的线程的可见性。
有序性
public class VolatileOrderDemo { static int a,b; static int x,y; public static void main(String[] args) throws InterruptedException { int i = 0; while (true){ i++; a = b = x = y = 0; Thread thread1 = new Thread(() -> { a = 1; x = b; }, ""); Thread thread2 = new Thread(() -> { b = 1; y = a; }, ""); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("第" + i + "次打印:x=" + x + ", y=" + y); if (x == 0 && y == 0){ break; } } } }
禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个 内存屏障
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。 并不能解决原子性问题
volatile 原理
可见性
当一个共享变量被volatile修饰后,会保证每个线程将变量修改后的值立即同步回主内存中,当其他线程有需要读取变量时会读取到最新的变量值。
被volatile修饰的共享变量的工作机制
当线程对变量操作时会从主内存读到自己的工作内存中,当线程对变量进行修改后,那么其他已经读取了此变量的线程中的变量副本就会失效,这样其他线程在使用变量时,发现已经失效,就回去主内存中重新获取该变量的值,这样就获取到了最新值
MESI缓存一致性协议
缓存行
CPU高速缓存的中可以分配的最小存储单位,高速缓存中的变量都是存在缓存行中的
MESI的核心思想就是,当CPU对变量进行写操作时发现,变量是共享变量,那么就会通知其他CPU中将该变量的缓存行设置为无效状态。当其他CPU在操作变量时发现此变量的缓存行已经无效,那么就会去主内存中重新读取最新的变量。
有序性
通过设置内存屏障,禁止指令重排
写内存屏障
读内存屏障
通用内存屏障
CAS
解释
Compare and Swap。比较并交换的意思 CAS是解决多线程并发安全问题的一种乐观锁算法
基本参数
内存地址A
旧值 B
新值 C
它的作用是将指定内存地址A的内容与所给的旧值B相比,如果相等,则将其内容替换为指令中提供的新值C;如果不等,则更新失败
AQS
AbstractQueuedSynchronizer抽象队列同步器简称AQS,它是实现同步器的基础组件(框架)
Lock
Semaphore
CountDownLatch
CyclicBarrier
通过AQS来实现的。具体用法是通过继承AQS实现其模板方法,然后将子类作为同步组件的内部类
主要组成
共享资源变量 state 为0锁可用 为1表示锁被占用
FIFO(first-in-first-out) 线程等待队列
基于AQS实现锁的思路
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false
sHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
AQS的加锁解锁和等待实现原理
加锁,解锁其实就是 AQS 利用CAS修改自己的state属性
唤醒队列是调用的 Unsafe 的park() 和unpark()
NonfairSync 非公平锁
/** * Sync object for non-fair locks */ static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) // CAS把stat设置为1 setExclusiveOwnerThread(Thread.currentThread()); // 获取到锁 else acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); // 使用了Sync抽象类的nonfairTryAcquire方法 } }
1、上来 compareAndSetState ,抢一下试一下
2、抢不成功 acquire(1);
1、tryAcquire:先抢 if (compareAndSetState(0, acquires)) 直接抢
2、acquireQueued:抢失败入队
FairSync 公平锁
/** * Sync object for fair locks */ static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && // 从线程有序等待队列中获取等待 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; } }
acquire(1); 因为公平锁上来不乱抢,利用acquire(1)进行排队
1、tryAcquire; if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) 先排队
2、acquireQueued:
锁的升级与降级
降级
锁降级就是从写锁降级成为读锁。在当前线程拥有写锁的情况下,再次获取到读锁,随后释放写锁的过程就是锁降级
升级 ( 只针对synchronized CPU效率随着锁的升级变慢 )
1.没有线程竞争,给对象头的锁标记位标记为偏向锁
偏向于第一个获得它的线程的锁,执行完同步任务后并不会释放锁,用于第二次如果还是当前线程则不需要在持有锁 .如果对象头的Mark word 状态为01,且是偏向锁状态,判断记录的Thread id 是否为当前线程ID 如果是 则以后此线程进入同步块时,不需要CAS进行加锁
2.有线程竞争,将 偏向锁 升级为 轻量级锁
轻量级锁:JVM利用while(true)【自旋】的方式获取锁,目的就是CPU快速抢到一个锁
线程自旋抢锁
3.如果某个线程自旋十次都没有抢到锁,将升级为重量级锁
重量级锁:JVM利用线程等待唤醒机制抢锁。JVM不到万不得已不会用wait和notify机制.线程暂停切换恢复现场很慢
线程直接挂起等待被唤醒
如何规划线程数
核心线程 = CPU数 * 2 , 非核心线程 = CPU数 * 4
总线风暴
volatile写的太多。线程太多,通知CPU修改自己缓存数据
等待处理
sychronized的自旋锁、偏向锁、轻量级锁、重量级锁,分别介绍和联系