Synchronized保证三大特性
原子性
对num++增加同步代码块后,保证同一时间只有一个线程操作num++。就保证了不会出现问题。
可见性
synchronied对应lock原子操作会刷新工作内存中共享变量的值
有序性
加入synchronied依然会发生指令重排,只不过我们有同步代码块,可以保证只有一个线程来执行同步代码块中的代码。保证有序性。
Synchronzied的特性
可重入性
一个线程可以多次执行synchronzied,重复获得同一把锁(递归锁)
原理:
synchronzied的锁对象中有一个计数器(recursion变量)会记录线程获得几次锁。在执行完同步代码块的时候,计数器的数量会-1,直到计数器的数量为0,就释放这个锁,
1.可以避免死锁
2.可以让我们更好的封装代码
不可冲断特性
一个线程获得锁后,其他线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,其他线程会一直阻塞或者等待,不可被中断。
synchronzied简单原理
如果是同步代码块,则使用的是监视器进入(monitorenter)、监视器(monitorexit)退出指令来实现的锁
如果是同步方法,则是在方法的头上增加ACC_SYNCHRONIZED 标记,表明当前方法是同步方法
不过两者的本质都是对对象监视器 monitor 的获取。
并发包下的一些类
AtomicXXXXXFieldUpdater
使用AtomicXXXFieldUpdater修饰的类对应的字段,在进行更新时同样可以保证原子性。
使用AtomicStampedReference修饰对象,也可以保证原子性。
AtomicStampedReference比AtomicXXXFieldUpdater会多出16字节的空间
CountDownLatch
就是一个计数器,给计数器一个初始的值。每个任务都将计数器值减一,当计数器的值为0.就执行后面的代码(此次计数器不为0的时候,是被阻塞的)。
简介
countDownLatch使一个线程等待其他线程各自执行完比后再执行
是通过一个state(相当于计数器)的东西来实现的,计数器的初始值就是线程的数量或者任务的数量
每当一个线程执行完毕后,计数器就减一,当计数器的值为0时,表示所有的线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了。
coutDownLatch的方便之处在于,你可以在一个线程中使用,也可以在多个线程上使用,一切只依据状态值,这样便不会受限于任何的场景。
countDownLatch.countDown(); -1
countDownLatch.await(); 阻塞,为0的时候才继续执行
CyclicBarrier(栅栏)
可循环(Cyclic)使用的屏障(Barrier)。它要做到事情是,让一组线程到达一个屏障时被阻塞,直到最后一个线程达到屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
构造方法
public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)
parties 是参与线程的个数
第二个构造方法有一个 Runnable 参数,这个参数的意思是 *最后一个到达线程要做的任务
重要方法
public int await() throws InterruptedException, BrokenBarrierException
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException
线程调用 await() 表示自己已经到达栅栏
BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时
public void reset()
将屏障重置为初始状态。 如果任何一方正在等待屏障,他们将返回 BrokenBarrierException 。
这样就可以重复利用这个屏障
CyclicBarrier 与 CountDownLatch 区别
CountDownLatch
是一次性的。CyclicBarrier
是可循环利用的CountDownLatch
参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。CyclicBarrier
参与的线程职责是一样的
Exchanger
用于两个工作线程之间交互数据的封装工具类
简单来说就是一个线程在完成一定的事务后想与另外一个线程交互数据,则第一个先拿出数据的线程会一直等待第二个线程,直到第二个线程拿着数据来到时,才能彼此交互对应的数据
Exchanger
泛型类型,其中 V 表示可交换的数据类型
public V exchange(V x) throws InterruptedException
等待另一个线程到达此交互点,然后将给定的对象传输它,接收其对象作为回报
可以被打断
如果已经有个线程正在等待了,则直接交互数据
ReentrantLock
当多个线程需要访问某个公共资源的时候,我们需要通过加锁来保证资源的访问不会出现问题。java中提供了两种方法来加锁
-
一种是通过synchronized关键字,另外一种是通过juc包下的API实现
-
synchronized是 JVM底层支持的,而juc包则是
jdk
实现。
公平锁和非公平锁
- 当有线程竞争锁的时候,当前线程会首先尝试获得锁而不是在队列里面进行排队等候。这对于那些已经在队列种排队的线程来说是不公平的。
- 默认情况下是非公平锁。通过构造函数传入true,可为公平锁
- 锁的存储结构就两个东西:“双向链表”+“int 类型状态”。ReentrantLock的实现是一个自旋锁。通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键
获得锁
public void lock()
public void lockInterruptibly() throws InterruptedException
public boolean tryLock()
public boolean tryLock(long timeout,TimeUnit unit) throws InteruptedExeption
lock
正常获取锁,如果没有获取到锁,就会被阻塞
lockInterruptibly
获取锁,如果没有获取到锁,会被阻塞,但是可以被中断
tryLock
尝试获取锁,如果获取到了锁,则返回true,否则返回false
timeOut表示等待的时间
trylock在获取锁的时候,不会考虑此时是否有其他线程在等待,会破坏公平
如果你希望遵守公平设置此锁,然后使用trylock(0,TimeUnit.SECONDS)这几乎是等效的(它也检测中断)
释放锁
public void unlock()
尝试释放锁,必须是锁的持有者才能释放锁
锁的调试
protected Thread getOwner()
public final boolean hasQueuedThreads()
public final int getQueueLength()
protected Collection getQueuedThreads()
getOwner
返回持有锁的线程
hasQueuedThreads
是否有线程在等待获取锁
getQueueLength
获取等待锁的线程数目
getQueuedThreads
返回正在等待的线程集合
Lock和synchronized的区别
底层实现:
-
Lock基于AQS实现,通过state和一个CLH队列来维护锁的获取和释放
-
synchronied需要通过monitor,经历一个从用户态到内核态的转变过程,更加耗时。
其他区别
synchronized | Lock |
---|---|
是java内置关键字,属于jvm层 | java API,是一个类 |
无法判断是否获取锁的状态 | 可以判断是否获取到了锁 |
会自动加锁与是否锁 | 需要在finally种手动释放锁,否则会造成线程死锁 |
没有获取到锁,线程会一直等待 | 如果尝试获取不到锁,线程可以不用一直等待就结束 |
synchronized的锁是可重入、不可中断、非公平
Lock锁是可重入、可中断、可以公平也可以非公平
信号量(Semaphore)
主要用于限流。也可以用做锁。限制同一时间可以访问资源的线程数目
常用API
构造方法
public Semaphore(int permits)
public Semaphore(int permits , boolean fair)
permits
:同一时间可以访问资源的线程数目fair
:尽可能的保证公平
重要方法
public void acquire() throws InterruptedException
public void release()
acquire()
:获取一个许可证,如果许可证用完了,则陷入阻塞。可以被打断。release()
:释放一个许可证
public boolean tryAcquire(int permits)
public boolean tryAcquire(int permits,
long timeout,
TimeUnit unit)
throws InterruptedException
尝试获取许可,拿到返回true,否则false
Condition
用于线程间通信,也就是和Object类的wait和notify具有同样的功能,不过Condition的功能更将多样化,Conditon可以绑定锁,实现选择性唤醒。
Condition和Lock搭配使用;condition必须使用lock.newCondition();进行创建,必须存在于lock种,否则抛出异常
condition.await(); 等待
condition.signalAll(); 通知
ReentrantReadWriteLock读写锁
读写锁的目的是为了让读与读直接不加锁
冲突 | 策略 |
---|---|
读 — 读 | 并行化 |
读 — 写 | 串行化 |
写 — 读 | 串行化 |
写 — 写 | 串行化 |
StampedLock
ReadWirteLock出现的问题
- 如果有线程正在读,写线程需要等待读线程释放锁后,才可以获取到写锁,即读的过程中不允许写线程去抢锁,这是一种悲观的读锁,会出现锁饥饿
- 有100个线程访问某个资源,如果有99个线程需要读锁,一个线程需要些锁,此时,写线程会很难得到执行
StampedLock改进
- 读的过程中,允许线程获取写锁后写入。但是这样就会出现数据不一致的问题。所以,需要判断读的过程中是否有写入,这种读锁是一种乐观锁
- 乐观锁的意思就是在读的过程中,大概率不会有写入操作,因此被称为乐观锁。
- 悲观锁则是读的过程中拒绝有写入操作,写入操作必须等待。
private static final StampedLock stampedLock = new StampedLock();
stampedLock.tryOptimisticRead(); // 获得一个乐观读锁
stampedLock.validate(stamp)) 检查乐观锁读锁后,是否有写锁发生,如果有,则获取悲观读锁,重写获取数据
ForkJoin框架
思想:分而治之。将一个大任务分割成若干小任务,最终汇总每个小任务的结果得到这个大任务的结果
这就是Fork/Join任务的原理:判断一个任务是否足够小。如果是,直接计算,否则,就分拆成几个小任务分别计算。这个过程可以反复“裂变”成一系列小任务。
编码实现
整个任务流程如下所示:
- 首先继承任务,重写任务的执行方法
- 通过判断阈值,判断该线程是否可以执行
- 如果不能执行,则将任务继续递归分配,利用fork方法,并行执行
- 如果是有返回值的,才需要调用join方法,汇集数据。
主要的两个类:
RecursiveAction
一个递归无结果的ForkJoinTask(没有返回值)RecursiveTask
一个递归有结果的ForkJoinTask(有返回值)
原理:
整个流程需要三个类完成
1.FrokJoinPool
- 任务是被逐渐细化的,那就需要吧这些任务存在一个池子里面,这个池子就是ForkJoinPoll
- 它与ExecutorService的区别就是 ,它存在工作窃取。
- 工作窃取:一个大任务会被划分成无数个小任务,这些任务呗分配到不同的队列,这些队列有些干活比较快,有些干活比较慢,于是,干的快的,发现自己没有活干了,。就会去其他队列里面拿活干
2.ForkJoinTask
ForkJoinTask是poll中的一个任务。它主要有2个子类:RecursiveAction和RecursiveTask。通过fork()方法去分配任务执行任务,通过join()方法汇总任务结果
总结
- Fork/Join是一种基于分治的算法,通过分解任务,并行执行,最后合并结果得到最终的结果
- ForkJoinPool线程池可以把一个大任务拆分成小任务并行执行
- 使用Fork/Join可以进行并行计算以提高效率
阻塞队列
- 阻塞队列为空时,从队列中获取元素的操作将会被阻塞
- 阻塞队列已满时,往队列里添加元素操作将会被阻塞
阻塞队列的好处
- 多线程领域中,所谓阻塞,即某些情况下会挂起线程,一旦条件满足,线程就唤醒
为什么需要阻塞队列
- 我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程了
- 在JUC包发布以前,多线程环境下,程序员需要自己控制这些细节,并且兼顾效率于线程安全
阻塞队列的使用场景
- 生产者-消费者模式
- 线程池
- 消息中间件
阻塞队列的种类
- ArrayBlockingQueue -->数组结构组成的有界阻塞队列
- LinkedBlockingQueue --> 链表结构组成的有界阻塞队列(默认大小为Integer。MAX_VALUE)
- PriorityBlockingQueue -->支持优先级排序的无届阻塞队列
- DelayQueue 使用优先级队列实现的延迟无界阻塞队列
- SynchronousQueque 不存储元素的阻塞队列,即单个元素的队列
- LinkedTransferQueue 由链表结构组成的无界队列
- LinkedBlockingDeque 由链表结构组成的双向阻塞队列
核心方法
抛出异常组
add(e) #队列满时add会抛出java.lang.IllegalStateException: Queue full
remove() #队列空时,会抛出java.util.NoSuchElementException
element() #获取队首元素,队列为空时,抛出java.util.NoSuchElementException
返回布尔值组
offer(e) 往队列插入数据,成功返回true,失败返回false
poll() 从队列中取出数据,成功时返回数据,队列为空时返回null
peek() 取出队首元素,成功时数据,队列为空返回null
阻塞方法组
put(e) 插入数据,无返回值,插入不成功时阻塞线程,直到插入成功或线程中断
take() 从阻塞队列中取出数据,成功返回数据,不成功的时候阻塞线程,直到获取成功或线程中断
超时方法组
offer(e,time,unit) 插入数据成功返回true,不成功阻塞等待超时实际,过时返回false并放弃操作
poll(time,unit) 取出数据,成功返回数据,队列为空时线程阻塞等待超时实际,过时返回false,并放弃操作
线程池
为什么使用线程池
- 降低资源消耗,避免线程重复创建和销毁
- 提高响应速度,当任务到达时,任务可以不需要等待线程的创建,就能立即执行
- 底稿线程的可管理性,线程是稀缺资源,如果无线创建,不仅消耗资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控
线程池的7大参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize 核心线程数,任务数大于核心线程数,将任务放入任务队列
- maximumPoolSize 最大线程数。当任务队列满了后,核心线程数也满了,就开启最大线程数,创建新的线程去执行任务。
- keepAliveTime 线程空闲时间 当线程池的线程数量超过核心线程数后,当空闲时间达到keepAliveTime 时,会销毁多余的线程直到剩下核心线程数为止
- TimeUnit unit, 线程空闲时间的单位
- workQueue 任务队列
- threadFactory 线程工厂
- handler 拒绝策略 当队列满了且工作线程大于最大线程数时,会触发拒绝策略
四大拒绝策略
- AbortPolicy 默认的拒绝策略,直接抛出异常
- CallerRunsPolicy 交由调用者运行 ,不抛出异常
- DiscardOldestPolicy 抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
- DiscardPolicy 直接丢弃任务,不抛出异常
线程池的工作流程
- 创建线程池,等待请求任务
- 调用execute、
- execute添加请求任务,线程池做如下判断
- 如果正在运行的线程数小于核心线程数,则创建线程运行此任务
- 如果正在运行的线程数大于或等于核心线程数,则将任务存入任务队列
- 如果任务队列满了,且正在运行的线程数小于最大线程数,则创建非核心线程执行任务
- 如果任务列表满了。且线程池线程到达最大线程数,则启动拒绝策略来执行
- 当一个线程完成任务时,从阻塞队列中取出下一个任务来执行
- 当一个线程无事可做,超过存活时间,线程池会判断
- 如果当前运行的线程数大于核心线程数,则销毁非核心线程
- 所以,线程池完成所有的任务后,最终会收缩至核心线程数大小
生产环境中使用那个线程池
阿里巴巴Java开发手册规定不允许使用Executors创建线程池,必须使用ThreadPoolExecutor的方式
FixedThreadPol和SingleThreadPool 中允许的阻塞队列容量为Integer.MAX_VALUE ,可能会堆积大量请求,导致OOM
CacheThreadPool和ScheduledThreadPool 允许创建的线程数据为Interger.MAX_VALUE,可能会创建大量线程,导致OOM
线程池合理配置参数
CPU密集型
cpu密集型,是主要用于计算,线程使用比较充分,没有阻塞,cpu会一直全速允许
CPU密集型任务只有在真真的多喝CPU上才可能得到加速
CPU密集型任务配置尽可能少的线程数量
公式:CPU核数+1个线程的先吃吃最大线程数
IO密集型
IO密集型任务线程并不是一直执行任务,会存在阻塞,则应配置尽可能多的线程来加速程序的运行
公式:CPU核酸*2
线程池的关闭
关闭有两个方法:shutdown
和shutdownNow
- 线程池的关闭,本质上执行的是
interrupt
方法 - 如果线程是空闲的,执行的是Condition的await方法
- 如果线程正在工作,该线程会被打上一个标记,等任务执行后被回收
CompleableFuture
为什么会出现CompleableFuture
- 使用Future获得异步执行结果时,要么调用阻塞的get方法,要么通过循环查看isDone是否为true,这2种方法都不是很好,因为主线程也会被迫等待/
- 从java8开始引入了CompleableFuTURE,它针对future做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。
优点
- 可以利用结果进行级联的执行
- 支持callback会自动回调给调用者
- 执行一批任务时,可以按照任务的执行顺序,获得结果
- 可以并行的获取结果,只拿最先获取的结果级联执行
方法
runAsync
:异步的执行Runnable,没有返回值。supplyAsync
:异步的执行Supplier实例,会有返回值。