# 并发编程
# 线程和进程的区别
进程是一个可执行的程序,是系统分配资源的基本单位。线程是进程内部相对独立的可执行单元,是任务调度的基本单位。
# 多线程
通常以实现Runnable接口为主,因为实现Runnable接口相比继承Thread类可以避免继承的局限,一个类可以继承多个接口,适合于资源的共享。 最佳线程数目 = ((线程等待时间+该请求时间)/该请求时间 )* CPU数目 一个请求所消耗的时间
# 多线程的优缺点
优点: 充分利用多核CPU的优势,提高CPU的利用率和程序运行效率 缺点: 1、线程过多影响性能,CPU切换增加内存开销 2、存在线程同步和线程安全问题 3、可能会发生死锁 4、增加了开发人员的技术难度
# 多线程的5种状态:
新建,就绪,运行,阻塞和死亡
新建状态:new创建一个线程时,还没开始运行,就是新建状态。 就绪状态:新建后,调用start()方法,线程就处于就绪态,等待CPU调度。 运行状态:当线程获得了CPU时间后,进入运行状态,执行run()里的内容 阻塞状态:线程运行中随时可能被阻塞:比如调用sleep()方法;等待获取锁被阻塞;线程在等待其他触发条件。暂时让出CPU资源。 死亡状态:有两个原因导致线程死亡:run()方法正常结束;一个未捕获的异常终止了run()方法
# 创建线程的4种方法
继承Thread类,重写run()方法,利用Thread.start()启动线程 实现Runnable接口,重写run()方法,利用new Thread(Runnable a)创建线程,调用start()方法启动线程。 通过Callable和futureTask创建线程,实现Callable接口,重写call方法,利用future对象包装callable实例,通过Thread方法创建线程。 通过线程池创建线程
# sleep和wait的区别
wait只能在synchronized中调用,属于对象级别的方法,sleep不需要,属于Thread的方法。 调用wait方法会释放锁,sleep不会释放锁 wait超时之后线程进入就绪状态,等待获取cpu继续执行。
# yield和join
yield会释放cpu资源,不会释放锁,让当前线程进入就绪状态,只能使同优先级或更高优先级的线程有执行的机会。 join会释放cpu资源和锁,底层是wait()方法实现的,join会等待调用join()方法的线程执行完成之后再继续执行。
# 死锁
指两个及以上的线程在执行过程中,因争夺资源而造成的一种互相等待(饥饿)的现象。若无外力作用,他们都将无法运行下去
# 死锁原因
系统资源的争夺:系统中拥有不可剥夺的资源,其数量不足以满足多个线程运行的需要,使得线程在运行过程因为争夺资源而陷入僵局。 线程推进顺序非法:线程在获得一个锁L1的情况下再去申请另一个锁L2,也就是在没有释放锁L1的情况下又去申请锁L2,这个是产生死锁最根本的原因。
# 死锁的必要条件
互斥条件:进程要求对所分配的资源在一段时间内只能由一个进程拥有。 不可剥夺条件:资源在进程未使用完成之前,不能被其他进程夺走,除非是主动释放 请求和保持条件:进程已经保持了一个资源,又去申请另一个资源,但是该资源已经被其他进程占有。 循环等待条件:进程资源循环等待,A拥有资源1,申请资源2,B拥有资源2,申请资源1
# 如何避免死锁
加锁顺序要合理 加锁时限要适当:线程尝试获取锁要加上一定时限,超时就要放弃请求,同时释放自己的锁。 死锁检测
# 线程池
管理线程的池子。帮助我们管理线程,减少创建线程和销毁线程的资源消耗。线程池的好处很多:提高响应速度,直接从线程池中拿线程比创建线程快,而且线程可以重复利用。
#ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long keepAliveTime,TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
#参数解释
corePoolSize:核心线程数
maximumPoolSize:最大线程数
keepAliveTime:线程池中非核心线程的空闲的存活时间
TimeUnit:keepAliveTime的时间单位
workQueue:存放任务的阻塞队列
threadFactory:用于创建核心线程的线程工厂,可以给创建的线程定义名称
handler:线程池的饱和策略(拒绝策略),有四种
# 四种拒绝策略
AbortPolicy:直接拒绝,抛出一个异常,默认的拒绝策略 DiscardPolicy:直接丢弃任务 DiscardOldestPolicy:丢弃任务里最老的任务,将当前这个任务继续提交给线程池 CallerRunsPolicy:交给线程池调用所在的线程进行处理
# 阻塞队列
ArrayBlockingQueue:有界队列,是一个数组实现的有界阻塞队列,按照FIFO排序。 LindkedBlockingQueue:基于链表实现的阻塞队列,按照FIFO排序,容量可以设置,不设置的话就是一个无边界的阻塞队列(最大长度是Integer.MAX_VALUE),吞吐量要高于ArrayBlockingQueue。newFixedThreadPool就是使用的这个队列。 DelayQueue:一个任务定时周期延迟执行的队列,根据指定的执行从小到大排序,否则根据插入到队列的先后顺序,newScheduledThreadPool使用的这个队列。 PriorityBlockingQueue:优先级队列是具有优先级的无界阻塞队列。 SynchronousQueue:同步队列,一个不存储元素的阻塞队列,每个插入操作必须等待另一个线程调用移除操作,否则插入操作一直阻塞。newCachedThreadPool使用了这个队列。
# 常用线程池
#newFixedThreadPool
固定线程数目的线程池,内部使用LinkedBlockingQueue
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
核心线程数=最大线程数
没有非空闲时间,即keepAliveTime=0
阻塞队列是无界队列LinkedBlockingQueue。
#使用场景:
适用于处理CPU密集型的任务,确保CPU在长期工作线程使用的情况下,尽可能少的分配线程。
#newCachedThreadPool
可缓存线程的线程池,内部使用SynchronousBlockingQueue
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
核心线程数=0,最大线程数=Integer.Max_VALUE
非核心线程存活时间为60s
这个池也有一个问题:当提交的任务数量大于处理任务的数量时,每次提交一个任务必然会创建一个非核心线程,极端情况下会创建过多的线程,耗尽CPU和内存
#使用场景:
用于并发量大执行大量短期的小任务
#newSingleThreadPool
单线程的线程池,内部使用linkedBlockingQueue
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
核心线程数=最大线程数=1,也就是这个池子里始终都只有一个活着的线程。
keepAliveTime=0,这个参数无效
阻塞队列是无界的LinkedBlockingQueue
#使用场景:
适用于串行执行任务的场景,一个任务接一个任务执行。
#newScheduledThreadPool
定时及周期性执行的线程池,内部使用DelayQueue
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
核心线程自定义,最大线程数为Integer.Max_Value
keepAliveTime=0
#使用场景:
周期性执行任务的场景。
# 线程池5个状态
RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED
Running:该状态的线程池会接收新任务,并处理阻塞队列中的任务。调用shudown()方法可切换到SHUTDOWN状态。调用shutdownNow()方法变为STOP状态。 Shutdown:该状态线程池不会接收新任务,但会处理阻塞队列中的任务。队列为空,并且线程池中执行的任务也为空,进入TIDYING状态。 Stop:该状态的线程池不会接收新任务,也不会处理队列中的任务,而且会中断正在执行的任务。 Tidying:该状态表明所有任务运行终止,记录的任务数量为0.terminated()执行完毕进入TERMINATED状态。 Terminated:该状态表明线程池终止或死亡。
# ThreadLocal
# 四个方法
get():返回此线程局部变量当前副本的值
set(T value):将线程局部变量当前副本的值设置为指定值。
initialValue():返回此线程局部变量当前副本的初始值。
remove():移除此线程局部变量当前副本的值
# Synchronized
synchronized可以保证方法或代码执行时,同一时刻只有一个线程可以进入临界区,同时可以保证共享变量的内存可见性
优缺点:
优点:
- 简单易用,不需要显式地处理获取和释放锁的操作。
- 可以保证线程之间的内存可见性,即一个线程修改的共享数据对其他线程是可见的。
- 可以有效防止死锁,因为它支持了锁的可重入性。
缺点:
- 由于锁的粒度是整个对象,如果用于代码块同步,可能会影响性能。
- 在处理同步时,需要注意避免死锁情况的发生。
- 不适合使用在需要精细锁定(精确到代码块而非整个方法)的场景。
# Volatile
原理
- 保证可见性,不保证原子性
- 禁止指令重排序
- 底层使用内存屏障实现
内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存中。 当读一个volatile变量时,JMM会把线程本地变量的值置为无效,从主内存中读取。
重排序规则
如果第一个操作是volatile读,不管第二个操作是什么,都不允许重排序。这个操作确保volatile读之后的操作不会被编译器重排序到volatile读之前。 如果第二个操作是volatile写,不管第一个操作是什么,都不允许重排序。这个操作确保volatile写之前的操作不会被编译器重排序到volaile写之后。
优缺点:
有点:
- 可见性
- 有序性
缺点:
- 不保证原子性
- 同步性差
- 易出错
- 性能开销
# Lock
# ReentrantLock(可重入锁)
是实现Lock接口的一个类,支持重入性,表示能够对共享资源重复加锁,即当前线程再次获取该锁不会被阻塞。还支持公平锁和非公平锁两种方式。
方法 lock()和unlock()
使用场景:
- 递归锁需求:当需要在递归函数或者多个同步方法中使用同一把锁时,ReentrantLock的可重入特性非常有用。
- 避免死锁:由于ReentrantLock允许同一个线程多次获取同一把锁,而不会导致死锁。
- 灵活控制:ReentrantLock提供了可中断的锁等待、公平锁选择、多个条件变量等高级功能。
# ReentrantReadWriteLock(可重入读写锁)
读写锁允许同一时刻被多个线程访问,但是在写线程访问时,所有的读线程和其他写线程必须阻塞。
使用场景:
- 多读少写场景:允许多个线程同时读取数据,但只允许一个线程写入数据,适用于数据库连接池、缓存系统等。
- 提高并发性能:通过读写分离,减少写操作的竞争,提高程序的性能和可靠性。
# Condition(条件变量)
一个Lock对象可以绑定多个Condition对象,实现多线程间更复杂的通信需求。提供了await()、signal()、signalAll()等方法来实现线程的等待/通知机制。
使用场景:
- 线程间通信:允许线程在某些条件不满足时挂起,直到条件改变时再继续执行。
- 实现等待/通知模式:在多线程环境中,实现复杂的线程间通信和同步。
# CountdownLatch(倒计时门闩)
使用场景:
- 等待多个线程完成:在完成一组正在其他线程中执行的操作之前,允许一个或多个线程一直等待。
- 触发事件:当所有线程都完成后触发事件,以便进行后续操作。
# CyclicBarrier(循环屏障)
允许一组线程相互等待,直到所有线程都到达某个公共屏障点。
使用场景:
- 多个线程相互等待:控制多个线程相互等待,直到所有线程都到达一个共同的屏障点,然后所有线程才会继续执行。
- 可重复使用:CyclicBarrier可以在所有线程释放后重复使用,适合需要多轮同步的场景。
# Semophore(信号量)
用于控制多个线程对某个或多个共享资源的访问。
使用场景:
- 控制资源访问:用于控制对共享资源的访问,如限制同时访问某个文件的线程数量。
- 限流:在微服务架构中,用于实现服务限流,防止在高并发情况下服务被过载。
# Exchanger(交换器)
使用场景:用于在两个线程之间交换数据。
# StampedLock(戳记锁)
使用场景:读多写少,锁切换
Java 8中新增的一个读写锁,它提供了乐观读锁和悲观写锁的功能,同时支持乐观读锁和悲观写锁的转换。
# CopyOnWriteArrayList
使用场景:读多写少,写时读不阻塞
通过写时复制的机制实现线程安全,但写操作开销大且内存消耗高。
如果简单使用读写锁ReentrantReadWriteLock,当写锁被获取后,读写线程被阻塞,只有当写锁被释放后读线程才有机会获取到锁从而读取到最新的数据。但是读线程想任何时候都可以获取到最新的数据。COW就是通过Copy-On-Write,即写时复制的思想来通过延时更新的策略实现数据的最终一致性,并且保证读线程之间不阻塞。 COW就是当我们往一个容器添加元素时,不直接往当前容器添加,而是先将当前容器copy,复制出一个新容器,然后往新容器添加元素,添加完元素后,再将原容器的引用指向新的容器。所以CopyOnWrite是一种读写分离的思想,延时更新的策略是通过在写的时候针对的是不同的数据容器来实现的,放弃数据实时性,实现最终一致性。
# ConcurrentSkipListMap
使用场景:适用于需要保持键值对排序顺序,并且多个线程可能同时读写映射的场景。例如,在线购物平台中根据商品的评分对商品进行排序的场景。
内部使用跳表的数据结构实现,跳表就是一个多层链表,底层是一个普通链表,然后逐层减少,通常通过一个简单的算法实现每一层的元素是下一层元素的二分之一,这样当搜索元素时从最顶层开始搜索,可以说是另一种形式的二分查找。理论上它的查找、插入和删除的时间复杂度都是O(logN)。