一、多线程
1、线程的生命周期
创建 -> 就绪 -> 运行 -> 阻塞 -> 就绪 -> 运行 -> 死亡。
2、你是如何理解多线程的?
-
多线程定义:
多线程是指从硬件或软件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,进而提升整体的处理性能。
-
多线程由来:
因为单个线程的处理能力低下。打个比方,一个人去搬砖与几个人去搬砖,一个人只能同时搬一车,但是几个人可以同时一起搬多个车。
-
多线程实现:
在 Java 中,实现多线程主要是依靠JUC包下的四种方式:
- 直接继承Thread类;
- 实现 Runnable 接口;
- 实现 Callable 接口配合 FutureTask 对象使用;
- 使用线程池。如 Executors 工具类。
-
需要注意的问题:
使用多线程虽然可以获得更大的吞吐量,但线程在创建与销毁时的开销很大,如创建线程需要分配空间,切换线程需要分配时间等,
所以在使用多线程时要尽可能的重复利用每一个线程,以减少线程在创建与销毁上的性能消耗,这就要用到线程池技术,
类似于数据库的连接池:先提前创建好指定数量的线程放在池中,当需要时可从池中取出,用完后又将它放回池里,如此可避免线程的重复创建与销毁。
3、多线程编程中的三个核心概念
-
原子性
与数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)在执行时,要么全部成功(生效),要么全都不成功(都不生效)。
如转账问题:包含减钱与加钱两个子操作,要么这两个操作一起成功,要么一起失败。
-
可见性
可见性是指,当多个线程并发访问共享变量时,一个线程对共享变量的修改,其它线程能够立即看到。
可见性问题是由硬件机制引起的:CPU从主内存中读数据的效率相对来说不高,现在主流的计算机中,都有几级缓存。每个线程在读取共享变量时,
都会将该变量加载进其所在CPU的高速缓存里,修改该变量后,CPU会立即更新该缓存,但并不一定会立即将其写回主内存(实际上写回主内存的时间不可预期)。
此时其它线程(尤其是不在同一个CPU上执行的线程)访问该变量时,从主内存中读到的就是旧的数据,而非是更新后的数据。
-
顺序性
顺序性指的是,**程序执行的顺序按照代码的先后顺序执行。**在实际处理中,处理器为了提高程序整体的执行效率,
可能会对代码进行优化,其中的一项优化方式就是调整代码的执行顺序,按照更高效的顺序执行代码。
4.线程之间的通信方式
-
等待通知机制有:
-
wait()
t.wait():让调用此方法的线程进入等待状态,释放CUP并释放持有的锁,只有notify()/notifyAll()可以唤醒。
-
notify()/notifyAll()
notify():唤醒等待队列中的一个线程,使其获得锁进行访问。
notifyAll():唤醒等待队列中等待该对象锁的全部线程,让其竞争去获得锁。
-
join()
t.join()方法阻塞调用此方法的线程(calling thread),直到线程t完成,此线程再继续;通常用于在main()主线程内,等待其它线程完成再结束main()主线程。
-
interrupted():
interrupt():该方法是用于中断线程的,调用该方法的线程的状态将被置为"中断"状态。
注意:**调用interrupt()方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程,需要用户自己去监视线程的状态为并做处理。
-
**这一方法实际上完成的是,在线程受到阻塞时抛出一个中断信号,这样线程就得以退出阻塞的状态。更确切的说,
如果线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,那么,它将接收到一个中断异常(InterruptedException),从而提早地终结被阻塞状态。
interrupted():测试当前线程(当前线程是指运行interrupted()方法的线程)是否已经中断,且清除中断状态。
isInterrupted():测试线程(调用该方法的线程)是否已经中断,不清除中断状态。
interrupted()、isInterrupted()区别:
interrupted 是作用于当前线程,isInterrupted 是作用于调用该方法的线程对象所对应的线程。(线程对象对应的线程不一定是当前运行的线程。
例如我们可以在A线程中去调用B线程对象的isInterrupted方法。)
这两个方法最终都会调用同一个方法-----isInterrupted(boolean ClearInterrupted),只不过参数ClearInterrupted固定为一个是true,一个是false;
下面是该方法,该方法是一个本地方法。
-
并发同步工具有:
- synchronized
- lock
- CountDownLatch
- CyclicBarrier
- Semaphore
5、锁
-
定义
锁是在不同线程竞争资源的情况下用来分配不同线程执行方式的同步控制工具,只有线程获取到锁之后才能访问同步代码,否则需等待其他线程使用结束后释放锁。
-
synchronized
隐式锁,通常和wait(),notify(),notifyAll()一块使用。
- wait:释放占有的对象锁,释放CPU,进入等待队列只能通过notify/notifyAll继续该线程。
- sleep:释放CPU,但是不释放占有的对象锁,可以在sleep结束后自动继续该线程。
- notify:唤醒等待队列中的一个线程,使其获得锁进行访问。
- notifyAll:唤醒等待队列中等待该对象锁的全部线程,让其竞争去获得锁。
-
lock
显示锁,拥有synchronize相同的语义,但是添加一些其他特性,如中断锁等候和定时锁等候。可以使用lock代替synchronize,
但必须手动加锁和释放锁。通过lock()手动加锁,通过unlock()方法手动释放锁,unlock()方法需要放到finally里保证锁能释放。
-
synchronized与lock的区别.
synchronized | lock | |
---|---|---|
性能 | 稍差【竞争激烈时】/差不多【竞争不激烈】 | 好【竞争激烈时】/差不多【竞争不激烈】 |
用法 | 代码块上,方法上 | 通过代码实现,需要手动上锁与释放,提供了多样化的同步,如公平锁、有时间限制的同步、可以被中断的同步 |
原理 | 在JVM级别实现,会在生成的字节码中加上monitorenter和monitorexit,任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。monitor是JVM的一个同步工具,synchronized还通过内存指令屏障来保证共享变量的可见性。 | 使用AQS在代码级别实现,通过Unsafe.park调用操作系统内核进行阻塞。 |
功能 | 非公平锁、要么随机唤醒一个线程要么唤醒全部线程 | ReentrantLock:可指定公平锁/非公平锁。提供Condition(条件)类,可实现分组唤醒。提供中断等待机制【lockInterruptibly()】 |
使用总结:
写同步的时候,优先考虑synchronized,如果有特殊需要,再进一步优化。ReentrantLock和Atomic如果用的不好,不仅不能提高性能,还可能带来灾难。
5.volatile
- 在主内存和工作内存交互时,直接与主内存产生交互【不通过缓存】,进行读写操作,保证数据的内存可见性【实时刷新】;
- 禁止 JVM 进行的指令重排序。
-
ThreadLocal
使用
ThreadLocal<UserInfo> userInfo = new ThreadLocal<UserInfo>()
的方式,让每个线程内部都会维护一个ThreadLocalMap,里边包含若干了 Entry(K-V 键值对),每次存取都会先获取到当前线程ID,然后得到该线程对象中的Map,然后与Map交互。
6、并发工具
1、CountDownLatch
- 计数器闭锁:
以计数器形式来处理需要等待的线程数,可用来让一个线程来等待其他多个线程完成任务后再执行,比如让主线程来计算其他子线程运行时的耗时。
-
构造器:
public CountDownLatch(int count)
count
:需要等待的线程数,当count减到0时才会执行。 -
重要方法:
await()
:等待count指定的线程线程数都完成后再执行。await(long timeout, TimeUnit unit)
:等待一段时间,不管其他线程玩不完成都执行。countDown()
:让等待的线程数-1,当一个线程完成任务后调用。
public class TestCountDownLatch { public static void main(String[] args) { /** * CountDownLatch闭锁的执行原理: latch.countDown(); 即每有一个线程完成工作后, * CountDownLatch= CountDownLatch - 1; 当 CountDownLatch 0时,表示每有其他线程在执行了 */ final CountDownLatch latch = new CountDownLatch(10); LatchDemo ld = new LatchDemo(latch); long start = System.currentTimeMillis(); for (int i = 0; i < 10; i++) { new Thread(ld).start(); } try { latch.await(); //让 main 线程执行到此处时,进行闭锁,等待其他线程完成操作后再执行后续代码。 } catch (InterruptedException e) { e.printStackTrace(); } long end = System.currentTimeMillis(); // 不用闭锁时,11个线程(1个主线程,10个子线程)同时执行,是无法计算耗时的。 System.out.println("耗时为(ms):" + (end - start)); } } class LatchDemo implements Runnable { private CountDownLatch latch;// 闭锁 public LatchDemo(CountDownLatch latch) { this.latch = latch; } @Override public void run() { //保证线程安全 synchronized (this) { try { // 打印 5W 以内的奇数 for (int i = 0; i < 50000; i++) { if ((i & 1) 1) { System.out.println(i); } } } finally { // latch.countDown();即CountDownLatch = CountDownLatch - 1;使用闭锁时要保证该代码一定执行到。 latch.countDown(); } } } }
2、CyclicBarrier
- 回环栅栏:
通过它可以实现让一组线程相互等待至某个状态(barrier)之后再全部同时执行。
- 构造器:
public CyclicBarrier(int parties)
;
public CyclicBarrier(int parties, Runnable barrierAction)
;
parties
:让多少个线程或者任务等待至barrier状态。
barrierAction
:所有线程/任务都到达barrier状态后,需要执行的附加任务。【让最后一个完成任务的线程去执行】
- 重要方法:
await()
:让当前线程等待其他线程都处于barrier状态后再执行。
await(long timeout, TimeUnit unit)
:等待到指定时间,若还有线程未到barrier状态,直接让到达barrier的线程执行后续任务。
public class TestCyclicBarrier { public static void main(String[] args) { int N = 4; CyclicBarrier barrier = new CyclicBarrier(N, new Runnable() { @Override public void run() { System.out.println("当前执行额外任务的线程" + Thread.currentThread().getName()); } }); for (int i = 0; i < N; i++) new Writer(barrier).start(); } static class Writer extends Thread { private CyclicBarrier cyclicBarrier; public Writer(CyclicBarrier cyclicBarrier) { this.cyclicBarrier = cyclicBarrier; } @Override public void run() { System.out.println("线程" + Thread.currentThread().getName() + "正在写入数据..."); try { Thread.sleep(5000); // 以睡眠来模拟写入数据操作 System.out.println("线程" + Thread.currentThread().getName() + "写入数据完毕,等待其他线程写入完毕"); cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"所有线程写入完毕,继续处理其他任务..."); } } }
3、Semaphore
-
信号量
可以控制同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。
-
构造器
Semaphore(int permits)
;public Semaphore(int permits, boolean fair)
;permits
:表示许可数目,即同时可以允许多少线程进行访问。fair
:表示是否是公平的,公平即等待时间越久的越先获取许可。 -
重要方法
availablePermits()
: 获取可用的许可数目。
acquire()
【阻塞】: 获取一个许可。若无许可能够获得,则会一直等待,直到获得许可。
acquire(int permits)
【阻塞】: 获取permits个许可。
release()
【阻塞】: 释放一个许可。不要求在释放许可之前必须先获获得许可,但如果没有获取许可,
-
就直接release许可会导致Semaphore允许的同时线程数+1(即便将Semaphore设置成final也会+1)。
release(int permits)
【阻塞】: 释放permits个许可。
boolean tryAcquire()
【非阻塞】: 尝试获取一个许可,若获取成功,则立即返回true,若获取失败,则立即返回false。
boolean tryAcquire(long timeout, TimeUnit unit)
【非阻塞,需要等待 timeout 时间】: 尝试获取一个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false。
tryAcquire(int permits)
【非阻塞】: 尝试获取permits个许可,若获取成功,则立即返回true,若获取失败,则立即返回false。
boolean tryAcquire(int permits, long timeout, TimeUnit unit)
【非阻塞,需要等待 timeout 时间】: 尝试获取permits个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false
4、区别
工具 | 作用 | 可否重用 |
---|---|---|
CountDownLatch | 用于某个线程等待若干个其他线程执行完任务之后,它才执行 | 不可 |
CyclicBarrier | 用于一组线程互相等待至某个状态,然后这一组线程再同时执行 | 可以 |
Semaphore | 和锁有点类似,它一般用于控制对某组资源的访问权限。 | / |
二、线程池
1、起源
线程池起源于 new Thread() 的各种弊端:
- 每次启动线程都需要new Thread()新建对象与线程,性能差。线程池能重用已存在的线程,减少对象创建与回收的开销。
- 线程缺乏统一管理,可以无限制的新建线程,可能导致OOM【内存溢出】。线程池可以控制可创建与执行的最大并发线程数。
- 缺少工程实践的一些高级的功能,如定期执行、线程中断等。线程池提供定期执行、并发数控制等功能。
2、线程池体系结构
1、线程池的体系结构: |--java.util.concurrent.Executor://负责线程的使用与调度的根接口 |--**ExecutorService //子接口:线程池的主要接口。 |--ThreadPoolExecutor //线程池的实现类,只有线程池的使用功能,无法进行延迟等任务。 |--ScheduledExecutorService //子接口,继承了ExecutorService:负责线程的调度。 |--ScheduledThreadPoolExecutor //继承了ThreadPoolExecutor类,实现了 ScheduledExecutorService接口,提供线程池的使用与调度功能。 2、工具类:Executors ExecutorService newCachedThreadPool();//创建缓存线程池,线程的数量无限制,会根据需求自动更改数量。 ExecutorService newFixedThreadPool(int number);//创建固定大小的线程池 ExecutorService newSingleThreadExecutor();//创建单个线程池。线程池中只有一个线程。 ScheduledThreadPoolExecutor newScheduledThreadPool(); //创建固定大小的线程池,还可以延迟或定时的执行任务。
3、线程池的核心参数
- corePoolSize:核心线程数量,线程池中常驻的线程数量。
- maximumPoolSize:线程池允许的最大线程数,非核心线程在超时之后会被清除,受限于
CAPACITY
- workQueue:任务阻塞队列,用于存储等待执行的任务。
- keepAliveTime:线程没有任务执行时可以保持的时间【非核心线程】。
- unit:时间单位
- threadFactory:线程工厂,用来创建线程。
- rejectHandler:当任务队列已满时,拒绝任务提交时的策略:
- AbortPolicy【默认】:丢掉任务,并抛RejectedExecutionException异常。
- DiscardPolicy:直接丢掉任务,不抛异常。
- DiscardOldestPolicy:丢掉最老的任务,然后调用execute立刻执行该任务(新进来的任务)。
- CallerRunsPolicy【推荐】:在调用者的当前线程去执行这个任务。
线程池的最大容量:CAPACITY
中的前三位用作标志位,也就是说工作线程的最大容量为(2^29)-1
。
4、线程池四种模型【Executors提供】
newCachedThreadPool
:创建一个可缓存的线程池,如果线程池的当前规模超过了处理需求时,那么将回收空闲的线程,- 当需求增加时,则可以添加新的线程,线程池的规模不存在任何的限制。
newFixedThreadPool
:创建一个固定大小的线程池,提交一个任务时就创建一个线程,- 直到达到线程池的最大数量,这时线程池的大小将不再变化。
newSingleThreadPool
:创建一个单线程的线程池,它只有一个工作线程来执行任务,- 可以确保按照任务在队列中的顺序来串行执行,如果这个线程异常结束将创建一个新的线程来执行任务。
newScheduledThreadPool
:创建一个固定大小的线程池,并且以延迟或者定时的方式来执行任务,类似于Timer。
4、线程池创建线程的核心逻辑
当有新任务提交时,线程池会做以下判断:
- 如果运行的线程数 < corePoolSize,直接创建新线程,即使有其他线程是空闲的。
- 如果运行的线程数 >= corePoolSize,继续判断:
- 如果插入队列成功【任务队列未满】,则完成本次任务提交,但不创建新线程。
- 如果插入队列失败,说明任务队列满了,再判读:
- 如果当前线程数 < maximumPoolSize,创建新的线程来处理该任务。
- 如果当前线程数 >= maximumPoolSize,会执行指定的拒绝策略。
5、队列阻塞策略
-
直接提交(如:SynchronousQueue)。
工作队列的默认选项是
SynchronousQueue
,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,
则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。
直接提交通常要求无界maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
-
无界队列(如:LinkedBlockingQueue)。
使用无界队列(如不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。
这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列 。
-
有界队列(如 ArrayBlockingQueue)。
使用有限的 maximumPoolSizes 时,有界队列有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:
使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。
如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,
CPU使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。