文章目录
- 一、多线程
- 1.多线程实现方式
- 2.线程安全
- 3.JUC开发框架
- 4.volatile关键字
- 5.CAS
- 二、Threadlocal
- 1.ThreadLocal介绍
- 2.ThreadLocal实现原理
- 3.ThreadLocal如何避免内存泄漏
- 4.ThreadLocal使用场景
- 5.NamedInheritableThreadLocal
- 三、AQS
- 1.AQS介绍及应用
- 2.AQS实现方式
- 3.如何设计一个互斥的lock
- 4.ReentrantLock
- 5.读写锁ReadWriteLock
- 6.CountDownLactch
- 7.CyclicBarrier
- 四、线程池
- 1.线程池简述
- 2.线程池参数
- 3.线程池实现机制(线程池原理)
- 4.线程池配置方案及调优
- 5.自定义线程池代码编写
- 五、锁
- 1.锁的分类
- 2.锁(sychronized)的优化
一、多线程
1.多线程实现方式
(1)继承Thread
(2)实现Runnable接口
(3)实现Callable接口,有返回值,需要借助FutureTask类
(4)使用线程池
2.线程安全
(1)线程安全概念:无论单线程还是多线程,调用一个对象的行为都能得到正确结果。
(2)线程安全实现
①将对象设置成不可变的,比如声明成private final 并不设置set访问器;
②消除同一资源这个概念,让每一个资源都有一份数据的拷贝;
③使用互斥锁,同一时间只有一个线程在使用共享资源;
④使用乐观锁,处理数据的时候判断数据是否被篡改,如果被篡改可以回滚然后重试
3.JUC开发框架
介绍:JUC(java.util.concurrent包)是Java5.0开始提供的一组专门实现多线程并发处理的开发框架。
JUC核心类:
(1)Executor:具体Runnable任务的执行者
(2)ExecutorService:一个线程池管理者,能把Runnable,Callable提交到池中进行调度
(3)Semaphore:一个计数信号量
(4)ReentrantLock:一个可重入的互斥锁定Lock,功能类似synchronized,但要强大很多
(5)Future:是与Runnable,Callable进行交互的接口,比如一个线程执行结束取返回的结果等等,还提供了cancel终止线程
(6)BlockingQueue:阻塞队列
(7)CompletionService:ExecutorService的扩展,可以获得线程执行结果
(8)CountDownLatch:一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等
(9)Callable:用于创建线程的Callable接口,与Runnable相比有返回值
(10)CyclicBarrier:一个同步辅助类,它允许一组线程互相等待,直到达到某个公共屏障点
(11)ScheduledExecutorService:一个ExecutorService,可安排在给定的延迟后运行或定期执行的命令
4.volatile关键字
从并发编程三要素方面分析 :
a.可见性(最重要的特性)。对一个volatile变量的读,总是能看到任意线程对这个volatile变量最后的写入。
b.原子性。对任意(包括64位long类型和double类型)单个volatile变量的读/写具有原子性。但是类型于a++/ a = b这种复合操作不具有原子性,AtomicInteger(volatile和CAS结合)可以保证原子性。
c.有序性,也就是防止指令重排序。
volatile关键字通过“内存屏障”来防止指令被重排序
内存屏障插入策略:
在每个 volatile 写操作的前面插入一个 StoreStore 屏障(禁止前面的写与volatile写重排序)。
在每个 volatile 写操作的后面插入一个 StoreLoad 屏障(禁止volatile写与后面可能有的读和写重排序)。
在每个 volatile 读操作的后面插入一个 LoadLoad 屏障(禁止volatile读与后面的读操作重排序)。
在每个 volatile 读操作的后面插入一个 LoadStore 屏障(禁止volatile读与后面的写操作重排序)。
其中StoreLaod屏障,它是确保可见性的关键,因为它会将屏障之前的写缓冲区中的数据全部刷新到主内存中。
Sychronized、lock可以解决可见性、原子性和有序性的问题,volatile可以解决可见性和有序性atomic可以解决原子性问题。
5.CAS
CAS: Compare And Swap , CAS(V,E,N) ,其中v表示要更新的变量,E表示预期值,N表示新值。当期望值E与当前线程的变量值V相同时,说明还没线程修改该值,当前线程可以将内存中值修改成N,也就是执行CAS操作,但如果期望值与当前线程不符,则说明该值已被其他线程修改,此时不执行更新操作。
过程是原子的。有个ABA问题:比如有个变量值为1,A读了1之后挂起,B改成了0,然后又改成了1,A唤醒之后发现值和预期的相同,通过CAS修改成功了,对于某些场景看重过程的就有问题了。可以添加版本号解决,即使数据一样,版本号不一样也不允许修改。
synchronized、volatile、CAS比较:
synchronized是悲观锁,属于抢占式,会引起其他线程阻塞。
volatile提供多线程共享变量可见性和禁止指令重排序优化。
CAS是基于冲突检测的乐观锁(非阻塞)
LongAdder与AtomicLong(CAS):LongAdder一般应用于高并发计数场景,在并发度不高的情况下直接对base进行累加,高的时候采用了分段CAS的机制,将核心数据分离成一个Cell数组,每个cell独立维护内部的值,最终的值由每个cell和base组成,将热点数据分离来提高并发度。缺点是并发更新可能会有误差。
二、Threadlocal
1.ThreadLocal介绍
用于解决多线程访问同一个共享变量的时候出现并发问题。
2.ThreadLocal实现原理
set时将值放入ThreadLocalMap中。每个线程自身都维护着一个ThreadLocalMap,用来存储线程本地的数据,可以简单理解成ThreadLocalMap的key是ThreadLocal变量,value是线程本地的数据。就这样很简单的实现了线程本地数据存储和交互访问。
3.ThreadLocal如何避免内存泄漏
ThreadLocalMap中的key的是一个Entry数据是一个弱引用(extends WeekReference)可以被回收,如果数据初始化好之后,一直不调用get、set等方法,value却存在一条从Current Thread过来的强引用链,这就会导致内存泄漏,可以在使用完后调用remove方法来避免。(但是实际上在ThreadLocalMap中的set/getEntry方法中,会对key为null进行判断,如果为null的话,那么是会对value置为null的)
4.ThreadLocal使用场景
①可以用于保存用户登录信息:第一次登录的时候保存在session中,然后放到Threadlocal中。
②可以用于解决SimpleDateFormat线程不安全问题:如format底层有个setTime方法,多线程下可能A设置的值会给B。
5.NamedInheritableThreadLocal
1.NamedInheritableThreadLocal是ThreadLocal的一个子类,它允许子线程继承父线程中的ThreadLocal变量。在多线程环境下,每个线程都可以拥有自己独立的变量副本,而InheritableThreadLocal则提供了一种机制,使得子线程可以获取到父线程中定义的ThreadLocal变量的值。
2.NamedInheritableThreadLocal在Spring框架中主要用于解决跨线程的本地变量传递问题。例如,在Web开发中,可以使用NamedInheritableThreadLocal来传递HTTP请求相关的上下文信息,如用户身份、会话信息等。这些信息在请求处理过程中需要在多个线程之间共享,而NamedInheritableThreadLocal提供了一种简单而有效的方式来实现这一点。
三、AQS
1.AQS介绍及应用
(1)介绍:AQS 是 AbustactQueuedSynchronizer 的简称, 它是一个 Java 提高的底层同步工具 类, 用一个 int 类型的变量表示同步状态, 并提供了一系列的 CAS 操作来管理这个同步 状态。
(2)应用
AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大 量的同步器,主要使用继承的方式来使用(设计模式为模板模式), 比如我们提到的 ReentrantLock, Semaphore, 其他的诸如 ReentrantReadWriteLock, SynchronousQueue, FutureTask 等等皆是基于AQS 的。
2.AQS实现方式
AQS支持两种同步方式:1、独占式 2、共享式
这样方便使用者实现不同类型的同步组件, 独占式如 ReentrantLock, 共享式如 Semaphore,CountDownLatch,组合式的如 ReentrantReadWriteLock。总之, AQS 为使用提供了底层支撑, 如何组装实现, 使用者可以自由发挥。
3.如何设计一个互斥的lock
(前三步为加锁,后两步为释放锁)
1.互斥性:int state=0;==0表示空闲,>0表示被占用
2.存储没有抢占到锁的线程 用数组、集合、链表(reentrantLock)、队列等存储Thread对象。
3.阻塞没有抢占到锁的对象 wait();LockSupport.park()(reentrantLock)
4.释放锁 state–
5.唤醒阻塞的线程 notify()/notifyAll()/LockSupport.unpark()
4.ReentrantLock
可重入锁:就是一个线程如果抢占到了互斥锁资源,在锁释放之前再去竞争同一把锁的时候,不需要等待,只需要记录重入次数。常见的有Synchronized、ReentrantLock。主要解决的问题是避免线程死锁的问题。如果一个锁不可重入,一个已经获得同步锁X的线程,在释放锁X之前再去竞争锁X的时候,相当于会出现自己要等待自己释放锁。
ReentrantLock 和 Synchronized的对比:
ReentrantLock通过AQS实现,Synchronized通过监视器模式;
ReentrantLock通过使用lock、unlock加锁解锁,更灵活;
ReentrantLock有公平模式和非公平模式,Synchronized是非公平锁;
ReentrantLock可以关联多个条件队列,Synchronized只能关联一个条件队列。
5.读写锁ReadWriteLock
有公平策略和非公平策略(默认)。用全局变量state来表示同步状态,前16位表示读锁状态,读锁成功则加1,释放则减一,后16位表示写锁状态。读锁是一个支持重进入的共享锁,写锁状态为0时,读锁总会成功获取,如果被其他线程获取写锁则会等待(防止写锁被饿死);写锁是一个支持重进入的排它锁。
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。
6.CountDownLactch
(1)实现原理:①CountDownLatch是一个同步计数器,他允许一个或者多个线程在另外一组线程执行完成之前一直等待,基于AQS共享模式实现的
②是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作来。
(2)CountDownLatch 的一个非常典型的应用场景是: 有一个任务想要往下执行, 但必须要 等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务 调用一个 CountDownLatch 对象的 await() 方法, 其他的任务执行完自己的任务后调用 同一个 CountDownLatch 对象上的 countDown()方法, 这个调用await()方法的任务将 一直阻塞等待, 直到这个 CountDownLatch 对象的计数值减到 0 为止。
(3)CountDownLatch与join
CountDownLatch可以手动控制在n个线程里调用n次countDown()方法使计数器进行减一操作,也可以在一个线程里调用n次执行减一操作。
而 join() 的实现原理是不停检查join线程是否存活,如果 join 线程存活则让当前线程永远等待。所以两者之间相对来说还是CountDownLatch使用起来较为灵活。
(4)CountDownLatch和CyclicBarrier
CyclicBarrier 可以重复使用, 而 CountdownLatch 不能重复使用。
7.CyclicBarrier
介绍:一个同步辅助类, 它允许一组线程互相等待, 直到到达某个公共屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互 相等待, 此时 CyclicBarrier 很有用。因为该 barrier 在释放等待线程后可以重用, 所以 称它为循环 的 barrier。
四、线程池
1.线程池简述
线程池中,当需要使用线程时,会从线程池中获取一个空闲线程,线程完成工作时,不会直接关闭线程,而是将这个线程退回到池子,方便其它人使用。
优点:降低资源消耗;提高响应速度;提高线程的可管理性。
2.线程池参数
(1)线程池创建方式有两种:executors创建方式(不推荐,里面参数配置的不太好,容易内存溢出,后面会提到)和ThreadPoolExecutor创建方式,下面介绍ThreadPoolExecutor创建方式
(2)ThreadPoolExecutor线程池参数
①corePoolSize 指定了线程池里的线程数量,核心线程池大小
②maximumPoolSize 指定了线程池里的最大线程数量
③keepAliveTime 当线程池线程数量大于corePoolSize时候,多出来的空闲线程,多长时间会被销毁。
④unit 时间单位。TimeUnit
⑤workQueue 任务队列,用于存放提交但是尚未被执行的任务。
队列选择:ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。
LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。
SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作,反之亦然。
PriorityBlockingQueue:具有优先级别的阻塞队列。
⑥threadFactory 线程工厂,用于创建线程,一般可以用默认的
⑦handler 拒绝策略,所谓拒绝策略,是指将任务添加到线程池中时,线程池拒绝该任务所采取的相应策略。
什么时候拒绝?当向线程池中提交任务时,如果此时线程池中的线程已经饱和了,而且阻塞队列也已经满了,则线程池会选择一种拒绝策略来处理该任务,该任务会交给RejectedExecutionHandler(自定义策略的时候需要实现该策略) 处理。
线程池提供了四种拒绝策略,也可以自定义拒绝策略:
AbortPolicy:直接抛出异常,默认策略;
CallerRunsPolicy:用调用者所在的线程来执行任务;
DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
DiscardPolicy:直接丢弃任务;
选择拒绝策略时,需要考虑以下几点:
业务需求:关键业务推荐使用AbortPolicy,及时反馈程序运行状态;无关紧要业务推荐使用DiscardPolicy。
系统性能:CallerRunsPolicy适合可以容忍任务在调用者线程中执行的业务场景,但需注意调用者线程的性能。
任务重要性:DiscardOldestPolicy适合允许丢弃老任务的场景,需根据实际业务衡量。
3.线程池实现机制(线程池原理)
(1)线程池小于核心线程数时,新提交任务将创建一个新线程执行;
(2)达到到核心线程数时,新任务提交至任务队列workqueue中;
(3)任务队列已满时,若最大线程数小于核心线程数,新任务会创建新线程执行任务;
(4)任务数超过最大线程数时,新任务提交有拒绝策略处理
(5)线程销毁机制:当设置allowCoreThreadTimeOut(true)时,关闭达到KeepAliveTime(TimeOut)的线程,可以销毁至0; 设置为false时,超过核心线程数且有线程TimeOut,会销毁空闲线程,最低可达到核心线程数之后则不再销毁。超过最大线程数且没有TimeOut时,会销毁线程,只保留最大线程数数量的线程。
注:
(1)放入队列中的线程是非核心线程,不被运行
(2)实际场景中核心线程数和最大线程数经常是设置成一样的(固定大小线程池)
4.线程池配置方案及调优
(1)线程数设置:
CPU密集(解密、压缩、计算)
CPU核心数获取:Runtime.getRuntime().availableProcessors();
理论上为CPU核心数,一般为CPU核心数+1(多一个可以用于切换已使用完成的CPU资源)
IO密集(数据库、文件读写、网络通信)
公式1:CPU核心数*(1-阻塞系数),阻塞系数一般取0.8-0.9 也就是核心数乘以5-10
公式2:CPU核心数*(1+IO耗时/CPU耗时)
公式3:CPU核心数*2+1
(2)Executor自带的几种线程池(一般不用这个,通过这几种自带的给你提供下怎么设计线程池的思路):
new ThreadPoolExecutor:参数依次为核心线程数、最大线程数、线程存活时间、时间单位、队列
可以看到下面的参数设置有队列无限大,最大线程数无限大的情况,所以容易内存溢出。
可缓冲线程池:(0,Integer.max,60L,TimeUtils.SECONDS,new SynchronousQueue())
核心线程数为0,说明默认任务放入队列;最大线程数为Integer.max,工作线程的创建数量几乎没有限制。SynchronousQueue是blockingqueue的一种实现,队列内部只含一个元素,元素空了才能存入,存在才能取出。空闲线程存活时间为60秒,可灵活回收空闲线程(核心线程数为0说明线程可销毁至0个)。
使用场景为很多短期异步的(最大线程数几乎没限制) 小程序或负载较轻的服务器(不如容易内存溢出)
固定大小线程池:(num,num,0,TimeUtils.MILLISECONDS,new BlockingQueue())
核心线程和最大线程数相同,线程池大小达到最大值时,新提交的任务放入无界阻塞队列中,
线程空闲时,再从队列中取出任务执行。
使用场景为长期的任务。
定时任务线程池:(num,Integer.max,0,NANOSECONDS,new DelayWorkQueue()):
支持定时或周期性任务执行,DelayWorkQueue是一种按照延时时间排列的队列,线程都繁忙时,会将延时时间最短的放在前面。每次队列中取出的都是超期的任务。
使用场景为周期性执行的任务
单个线程池:(1,1,0,TimeUtils.MILLISECONDS,new BlockingQueue())
单一线程化的线程池,所有任务会按照制定顺序执行。在线程池内部是单线程,但在主线程提交任务的时候,没有阻塞,仍是异步的。(至少提交任务时主线程不阻塞)
场景:一个任务一个任务执行的场景。
单个线程定时任务线程池:上面的定时任务线程池,核心线程数设置为1
窃取线程池:会创建足够多线程的线程池,通过工作窃取的方式,使多核CPU不闲置
new ForkJoinPool(parallelism,ForkJoinPool.defaultForkJoinWorkerThreadFactory,null,ture)
(3)实践中用过的自定义线程池(有点像上面的固定大小线程池)
核心线程数和最大线程数相同,KeepAliveTime=0,使用Blockingqueue(定长,否则可能内存溢出),自定义拒绝策略:
xxxPolicy implements RejectedExecutionHandler (Runnable r,ThreadPoolExecutor exe){
Exe.getQueue().put®;//BlockingQueue的put方法–大于最大size时阻塞,因为任务不能丢
}
5.自定义线程池代码编写
1.创建一个服务类,一个线程类,在服务类中创建线程池:设置各种参数及拒绝策略;execute方法放入线程类(加任务);
2.在服务类中加入任务数c1、错误数c2等参数(volitile修饰),在加入任务后c1++,在线程类中执行完c–,报错时c2++,一旦报错抛出异常;
3.最后校验任务数是否为0判断任务是否执行完。
代码如下
package com.example.springb_web.utils.ThreadPool;
public class ThreadPoolTestMain {
public static void main(String[] args) {
new ThreadPoolService().calTask(5,10,20);
}
}
package com.example.springb_web.utils.ThreadPool;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadPoolService {
public static volatile int job_count;
public static volatile int error_count;
public static AtomicInteger handle_count = new AtomicInteger();
public boolean calTask(int poolSize,int batchCount,int taskCount){
try {
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(poolSize,poolSize,0, TimeUnit.HOURS,new ArrayBlockingQueue<>(poolSize),new ForceQueuePolicy());
CallTaskThread thread = new CallTaskThread();
thread.setBatchCount(batchCount);
for(int i = 0;i<taskCount;i++){
poolExecutor.execute(thread);
synchronized (this){
job_count ++;
}
if(error_count > 0){
error_count = 0;
return false;
}
}
while (true){
if(job_count == 0){
break;
}
Thread.sleep(10000);
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
public class ForceQueuePolicy implements RejectedExecutionHandler{
@Override
public void rejectedExecution(Runnable runnable, ThreadPoolExecutor threadPoolExecutor) {
try {
threadPoolExecutor.getQueue().put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void doTask() throws Exception{
System.out.println(Thread.currentThread().getName()+":done"+":"+handle_count.incrementAndGet());
Thread.sleep(1000);
}
}
package com.example.springb_web.utils.ThreadPool;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
public class CallTaskThread implements Runnable{
private int batchCount;
/* @Autowired
private ThreadPoolService service;*/
@Override
public void run() {
try {
for(int i = 0;i<batchCount ; i++){
ThreadPoolService service = new ThreadPoolService();
service.doTask();
}
} catch (Exception e) {
ThreadPoolService.error_count ++;
e.printStackTrace();
}finally {
ThreadPoolService.job_count --;
}
}
public int getBatchCount() {
return batchCount;
}
public void setBatchCount(int batchCount) {
this.batchCount = batchCount;
}
}
五、锁
1.锁的分类
可重入锁:可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。如sychronized/Reetrantlock。
共享锁:如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。
排他锁:如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。所有线程都能得到资源,但是吞吐效率低,其他线程都阻塞了,唤醒的开销大
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。吞吐效率高但是有些线程获取不到资源可能饿死。
2.锁(sychronized)的优化
锁有四种状态-无锁、偏向锁、轻量级锁、重量级锁,他们会随竞争的激烈逐渐升级,不会降级。偏向锁、轻量级锁是乐观锁,重量级锁是悲观锁。
偏向锁:创建对象时,对象头有一个偏向锁标志,线程持有锁时会将这个标志置为一,表示获取到锁了(类似于可重入锁),两个线程来竞争锁时升级
轻量级锁:自旋获取锁,一定次数还没成功之后升级成重量级锁。
重量级锁:也是互斥锁、悲观锁,阻塞或唤醒线程需要从用户态转换到内核态,非常消耗资源。
适应性自旋锁:JDK1.6引入,自旋次数不在固定,更好的使用资源。如果线程上次自旋成功了,会认为他这次自旋还会成功,允许他自旋更多的次数,若某个锁很少获取成功就会减少自旋次数甚至直接阻塞。