基础
并发相关概念
并行与并发:
并行:表示两个或多个任务一起执行;
并发:多个任务交替执行。
临界区
临界区表示一种公共资源或者共享数据,可以被多个线程使用,但每次只能有一个线程使用它。临界区一旦被占用,其他线程就必须等待这个资源的释放。
阻塞和非阻塞
阻塞:线程等待资源释放,就是被阻塞了。
非阻塞:没有一个线程可以妨碍其他线程的执行。
死锁
只两个或两个以上的线程在执行过程中,因为争夺资源而造成的一种相互等待的现象,无外力作用下,将会一直等下去。
死锁的四个条件:
- 互斥条件:线程对资源的访问是排他性的;
- 请求和保护条件:线程1占用了资源x,同时请求被其他线程占用的资源y,但不释放自身的资源x;
- 不剥夺条件:线程获得的资源在未使用完之前不被其他线程剥夺,只能等使用完自行释放。
- 环路等待条件:死锁发生时,必然存在一个进程-资源环形链。
饥饿
一个或多个线程因为种种原因无法获得所需资源,导致一致无法执行。比如优先级太低,一直被优先级高的线程占用资源。
活锁
线程之间因为互相礼让,主动将资源释放给他人,导致资源不断在线程间跳动,导致没有一个线程可以同时拿到所有资源正常执行。
并发级别
- 阻塞:一个线程在其他线程释放资源之前无法继续执行,处于阻塞状态;
synchronized关键字、重入锁; - 无饥饿
饥饿是因为资源的分配不公平,如优先级线程,优先级低的线程可能会一直等待;
非公平锁会造成饥饿,因为系统允许优先级高的线程插队;
但公平锁不允许插队,就能实现无饥饿了。 - 无障碍
是一种乐观策略,所有线程都可以进入临界区修改数据,发生数据竞争就会进行回滚。
“一次性标记”:线程在操作之前,先读取这个标记,操作完成之后,再次读取这个标记,对比标记是否被更改过,如果没有,说明资源访问没有冲突;如果两次标记不一致,说明资源在操作过程中与其他线程冲突,需要重新操作。任何对资源有修改操作的线程,都需要在修改数据前更新这个一次性标记,表示数据不再安全。 - 无锁
所有的线程都尝试对临界区进行访问,但不同的是,无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区。
在资源进程中不断地重新尝试获取资源,直到有一个线程修改资源成功。(存在饥饿问题) - 无等待
所有线程都必须在有限步内完成。
RCU(Read Copy Update):对数据的读不加控制。对数据的修改时通过获取原始数据副本,修改副本数据,修改完之后在何时时机回去写数据。
JMM:java的内存模型
三大特性
- 原子性:指一个操作时不可中断的。即使多个线程一起执行,一个操作一旦开始,就不会被其他线程干扰;
- 可见性:当一个线程修改了某个共享变量的值时,其他线程能够立即知道这个修改。
- 有序性:在并发的时候,存在指令重排的情况,可能会导致乱序。
指令重排
为什么进行指令重排:为了提升程序执行的效率,减少设备中断。
Happen-Before 规则:不能重排的指令
- 程序顺序原则:一个线程内保证语义的串行性;
- volatile规则:volitile变量的写先于读发生,这保证了volatile变量的可见性。
- 锁规则:解锁(unlock)必然发生在加锁(lock)前;
- 传递性:A先于B,B先于C,那么A必然先于C;
- 线程的start()先于它的每一个动作;
- 线程所有的操作先于线程的终结;
- 线程的中断先于被中中断线程的代码;
- 对象的构造函数的执行、结束先于finalize()方法。
进程 VS 线程
【进程】是系统进行资源分配和调度的基本单位,是操作系统结构的基础。是正在执行的程序,其实程序的实体。
【程序】是指令、数据及其组织形式的描述。
【线程】是轻量级进程,是程序执行的最小单位。
线程的生命周期
- 线程的创建
1):继承Thread,重写run()方法自定义线程。
Thread t1 = new Thread(){
@override
public void run(){
//线程启动后的操作
}
t1.start();
}
2)实现Runnable结构,重写run()方法。将实例传入线程Thread中。
public class CreateThread implements Runnable {
public static void main(String[] args){
Thread t1 = new Thread(new CreateThread());
t1.start();
}
@override
public void run(){
//执行线程工作
}
}
3)使用Callable 和 Future创建线程
4)使用线程池创建线程
- 线程终止
定义一个变量,用于只是线程是否需要退出。 - 线程中断
public void Thread.interrupt() //线程中断
public boolean Thread.isInterrupted() //判断线程是否中断
public static boolean Thread.interrupted() //判断线程是否中断,并清除当前中断状态。
- 等待(wait)和 通知(notify)
wait() 方法 和 notify() 方法数据Object类,表示任何对象都可以调用这两个方法。
wait和notify工作流程:假设有T1 和T2两个线程,如下代码所示:
public class Simple{
final static Object object = new Object();
public static class T1 extends Thread{
public void run(){
synchronized(object){
System.out.println(System.currentTimeMillis()+": T1 start!");
try{
System.out.println(System.currentTimeMillis()+"T1 wait for Object");
object.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
System.out.println(System.currentTimeMillis()+": T1 end!");
}
}
public static class T2 extends Thread{
public void run(){
synchronized(object){
System.out.println(System.currentTimeMillis()+": T2 start! notify one thread");
object.notify();
System.out.println(System.currentTimeMillis()+": T2 end!");
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
public static void main(String[] args){
Thread t1 = new T1();
Thread t2 = new T2();
t1.start();
t2.start();
}
}
代码执行结果:
由上可知:wait() 方法必须包含在对应的synchronized语句中,且无论是wait还是notify 都必须获得目标对象的一个监视器
- 挂起(suspend)和继续执行(resume)
suspend一般不建议使用,因为线程在挂起的同时并不会释放任何锁资源。而且对于被挂起的线程,从它的状态上来看,是Runnable,很难对系统当前状态进行判断。
解决办法:使用一个标识,加上notify和wait来实现挂起和继续执行。
public class GoodSuspend{
public static class Object u = new Object();
public static class ChangeObjectThread extends Thread{
volatile boolean suspendme = false;
public void suspendMe(){
suspendme = true;
}
public void resumeMe(){
suspendme = false;
synchronized(this){
notify();
}
}
@override
public void run(){
while(true){
synchronized(this){
while(suspendme){
try{
wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
synchronized(u){
System.out,println("in changeObjectThread");
}
Thread.yield();
}
}
}
public static class ReadObjectThread extends Thread{
@override
public void run(){
while(true){
synchronized(u){
System.out,println("in ReadObjectThread");
}
Thread.yield();
}
}
}
public static void main(String[] args) throws InterruptedException{
ChangeObjectThread t1 = new ChangeObjectThread();
ReadObjectThread t2 = new ReadObjectThread();
t1.start();
t2.start();
Thread.sleep(1000);
t1.suspendMe();
System.out,println("suspend t1 2 sec");
Thread.sleep(2000);
System.out.println("resume t1");
t1.resumeMe();
}
}
- 等待线程结束(join)和谦让(yeild)
public final void join() throws InterruptedException
//表示无线等待,会一直阻塞当前线程,知道目标程序执行完毕。
public final synchronized void join(long millis) throws InterruptedException
//给定一个最大等待时间,如果超过给定时间线程还在执行,当前线程也会因为超时而继续执行下去。
yiled:一旦执行,它会使当前线程让出CPU。但并不是不执行了,而是与其他线程进行CPU资源竞争。
其他线程相关
关键字 volatile
是一个变量修饰符,只能用来就是变量。
可见性:当一个线程修改了声明为volatile变量的值,新值对其他线程来说是立即可见的。
有序性:volatile变量的所谓有序性也就是被声明为volatile的变量的临界区代码的执行是有顺序的,即禁止指令重排序。
受限原子性:它用来修饰变量,对于单个volatile变量的读/写操作都具有原子性,但类似于volatile++这种复合操作不具有原子性。所以volatile的原子性是受限制的
线程组 ThreadGroup
可以将相同功能的线程放置在一个线程组中。
ThreadGroup tg = new ThreadGroup("PrintGroup");
Thread t1 = new Thread(tg,new ThreadGroupName(),"T1");
Thread t2 = new Thread(tg,new ThreadGroupName(),"T2");
t1.start();
t2.start();
//获得活动线程总数
System.out.println(tg.activeCount());
//打印线程组中所有线程信息
tg.list();
//停止所有的线程
tg.stop();
守护线程 Daemon
指的是在后台默默完成一些系统性的服务,如垃圾回收线程、JIT线程等。
当Java应用内只有守护线程时,java虚拟机就会自然退出。
线程优先级
优先级高的线程在竞争资源时会更有优势,更可能抢占资源,但这只是一个概率问题。
java有三个静态标量:
MIN_PRIORITY = 1;
NORM_PRIORITY = 5;
MAX_PRIORITY = 10;
数字越大,优先级越高,用户可以通过setPriority()来设置线程的优先级。
关键字synchronized
作用:实现线程间的同步,对同步代码加锁,是的每一次都只能有一个线程进入同步块,保证线程间的安全性。
用法:
· 指定加锁对象:对给定对象加锁,进入公布代码前要获得给定对象的锁。
· 直接作用于实例方法:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁。
· 直接作用于静态方法:相当于给当前类加锁,进入同步代码前要获得当前类的锁。
synchronized可以保证线程间的可见性、有序性。
同步控制
重入锁 ReentrantLock
重入锁可以完全代替关键字synchronized。
与synchronized相比,重入锁有显示操作过程,必须手动指定何时加锁,何时释放锁。所以灵活性远优于关键字synchronized。
对于同一个线程,是允许连续多次或得同一把锁。
与synchronized进行对比
- 中断响应:
对于synchronized来说,一个线程在等在锁,只能有两个情况,要么获得锁继续执行,要么保持等待;
对于重入锁,还提供另外一种可能,就是中断。 - 锁申请等待时间。
这是重入锁另外一种避免死锁的方法,也就是限时等待。
使用方法:tryLock()
例如:tryLock(5,TimeUnit.SECONDS):表示等待5s,如果5s内没有获得锁,就会返回false。
如果没有设置时间,就表示不会等待,如果获得锁,立即返回true;否则立即返回false。 - 公平锁
重入锁是可以设置为公平的,不会产生饥饿现象;而synchronized是非公平的。//构造函数 public ReentrantLock(boolean fair); //使用 ReentrantLock fairLock = new ReentrantLock(true);
重入锁搭档:Condition
public static ReentrantLock lock = new ReentrantLock();
public static Condition condition = lock.newCondition();
//Condition提供方法:
await() : 当前线程等待,同时释放当前锁,与Object.wait()类似。等待线程被唤醒则继续执行,或者当前线程被中断时跳出等待。
awaitUninterruptibly() : 类似于await(),但不会在等待过程中响应中断。
singal():唤醒一个在等待中的线程;
singalAll():唤醒所有在等待中的线程。
信号量 Semaphore
信号量可以指定多个线程,同时访问某一个资源。
public Semaphore(int permits)
public Semaphore(int permits, boolean fair)
//permits:信号量准入数,即同时能申请多少个许可;
//fair:指定是否公平
相关方法:
public void acquire():尝试获取一个准入许可,若无法获得,则线程等待,直到有线程释放一个许可或者当前线程被中断。
public void acquireUninterruptibly():类似于acquire,但不响应中断。
public boolean tryAcquire():尝试获取一个许可,成功返回true,失败返回false,不会等待。
public boolean tryAcquire(long timeout,TimeUnit unit):会等待一段时间,超时返回false,在时间内获得许可返回ture;
public void release():线程访问资源结束后释放一个许可。
使用例子:
读写锁 ReadWriteLock
读写锁的访问约束情况:
在系统中,读操作的次数远大于写操作,所以读写锁可以发挥最大的功能,提升系统性能。
ReenTrantReadWriteLock readWriteLock= new ReenTrantReadWriteLock();
Lock readLock = readWriteLock.readLock();
Lock writeLock = readWriteLock.writeLock();
倒计数器 CountDownLatch
表示当前线程必须得在其他一定数量的线程完成后再执行。
CountDownLatch end = new CountDownLatch(10);
//表示等待10个线程执行完后,执行当前线程。
end.countDown();
//表示执行完一个线程,倒计数器-1.
end.await();
//等待所有其他线程完成。
循环栅栏 CyclicBarrier
类似于CountDownLatch。但可以反复使用。
public CyclicBarrier(int parties, Runnable barrierAction);
线程阻塞工具 LockSupport
LockSupport弥补了suspend()方法被挂起而无法继续执行的情况,也弥补了wait()方法必须要先获得某个对象的锁,同时也不会抛出InterruptedException异常。
park() :阻塞;
parkNanos() 、parkUntil() 显示等待;
unpark() :唤醒线程
限流
漏桶算法:利用一个缓存区,当请求进入系统时,无论请求的速度如何,都先在缓存区中保存,再以固定的流速流出缓冲区进行处理。
令牌桶算法:桶中存放的是令牌,处理程序只有在拿到令牌后,才能进行请求处理。如果没有令牌,处理程序要么等待可用令牌,要么丢弃请求。(在每个单位时间内生产一定量的令牌存入桶中)
线程池
线程池创建
newFixedThreadPool() : 创建一个固定线程数量的线程池。当有一个新的任务提交时,如果线程池中有空闲线程,则立即执行;如果没有,则新的任务会进入一个任务队列中等待。
newSingleThreadExecutor() : 返回一个只有一个线程的线程池。若有多余的任务提交到线程池,则会进入任务队列中等待线程空闲,按先进先出的顺序执行任务。
newCachedThreadPool() : 返回一个可根据实际情况调整线程量的线程池。线程池的线程数量不确定,但若有空闲线程可复用,则会优先使用可复用的线程。当所有线程都在处理工作,又有新的任务提交,则会创建新的线程来处理任务。所有线程在执行完当前任务后,返回线程池进行复用。
newSingleThreadScheduledExecutor() : 返回一个ScheduledExecutorService对象,线程池大小为1,。可执行定时任务。
newScheduledThreadPool() : 返回一个ScheduledExecutorService对象,但可指定线程池的线程数量。
内心线程池的内部实现
上述的不同线程池工厂都是ThreadPoolExecutor类的封装。
public ThreadPoolExecutor(int corePoolSize,
int maximunPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数说明:
参数 | 说明 |
---|---|
corePoolSize | 指定了线程池中的线程数量 |
maximunPoolSize | 指定了线程池中的最大线程数 |
keepAliveTime | 当线程池数量超过了corePoolSize,多余的空闲线程的存活时间,超过这个时间,线程将被销毁 |
unit | keepAliveTime的单位 |
workQueue | 任务队列,被提交但尚未被执行的任务 |
threadFactory | 线程工厂,用于创建线程,一般为默认 |
handler | 拒绝策略,当任务不能被及时处理时,如何拒绝任务 |
任务队列 workQueue
是一个BlockingQueue接口对象
- 直接提交的队列:由SynchronousQueue对象提供。SynchronousQueue没有容量,每一个插入操作就到等待一个相应的删除操作, 反之每一个删除操作都要等待一个相对应的插入操作。如果使用SynchronousQueue,则提交的线程不会被真实的保存,而是总将新任务提交给线程执行,如果没有空闲线程,创建新线程,如果线程总数已经达到最大值则执行拒绝策略。
- 有界的任务队列:使用ArrayBlockingQueue类实现。
当有新的任务提交时,如果当前线程池的实际线程数少于corePoolSize,则会优先创建新的线程。如果大于corePoolSize,则会将新任务加入等待队列。若等待队列已满,且当前线程总数不大于maximumPoolSize,则创建新的线程执行任务。如过大于maximumPoolSize,则执行拒绝策略。public ArrayBlockingQueue(int capacity) //指定队列的最大容量。
- 无界的任务队列:通过LinkedBlockingQueue类实现。当有新的任务提交时,如果线程数小于corePoolSize时,则会创建新的线程执行任务。当系统线程数达到corePoolSize后,且当先无空闲线程,新任务会直接进入队列等待。
- 优先任务队列:通过PriorityBlockingQueue类实现,可以控制任务的先后顺序,是一个特殊的无界队列。它能确保高优先级的任务先执行。
拒绝策略 handler
- AbortPolicy : 直接抛出异常,阻止系统正常工作;
- CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。
- DiscardOldestPolicy:丢弃最老的任务请求,并尝试从重新提交当前任务。
- DiscardPolicy:默默丢弃无法执行的任务,不予任何处理。
自定义线程创建 threadFactoy
一般选择是默认线程池创建:Executors.defaultThreadFactory();
但也可以进行自定义:Thread newThread(Runnable r);