进程与线程的定义
进程是操作系统进行资源分配的最小单位,线程是进行运算调度的最小单位。一个进程在执行过程中可产生多个线程,这些线程共享进程的堆和方法区资源。
Java创建线程的方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口(线程返回值/异常通过FutureTask封装)
线程的生命周期
wait()和sleep()的区别
- 两者都暂停了线程的执行,但
wait()
释放了锁,而sleep()
没有。 wait()
的线程不会主动苏醒,需要其他线程调用同一对象的notify()
或者notifyAll()
来唤醒;而sleep()
执行完毕后线程会自动苏醒。wait()
常用于线程间通信,而sleep()
常用于暂停执行。
死锁
定义:多个线程因等待无法释放的资源而无期限地阻塞。
必要条件:
①互斥:同一资源任意时刻只被一个线程占用;
(可使用ThreadLocal
创建线程变量副本打破该条件)
②请求和保持:线程因请求资源而阻塞时,不会释放已有的资源;
(可使用ReenTrantLock.tryLock
超时机制打破该条件)
③不剥夺:线程已有的资源在使用结束前不会被强行剥夺;
④循坏等待:若干线程间形成一种头尾相接的循坏等待资源关系。
(通过固定资源申请顺序来打破该条件)
synchronized
synchronized
关键字通过保证被修饰的代码块在任意时刻只有单个线程能执行,来解决多线程之间访问资源的同步性。
用法
- 修饰实例方法(锁的是对象实例)
- 修饰静态方法(锁的是类)
- 修饰代码块(可指定锁的目标,灵活性高)
- 不能修饰构造方法(构造方法本身就是线程安全的)
- 不能被继承
底层原理
每个对象都内置了一个对象监视器ObjectMonitor
(C++实现),执行synchronized
命令时,线程会尝试获取对象监视器。获得时,锁计数器+1;释放时,锁计数器-1。
synchronized 和 ReentrantLock
- 两者都是可重入锁(即可再次重复获取自己已有的锁)
synchronized
关键字基于JVM层面,会自动解锁,而Lock
接口基于API层面,需手动解锁ReentrantLock
在保持和synchronized
相同的并发性和内存语义基础上,新增了高级功能:- 支持中断等待: 等待锁的线程可选择放弃,从而处理其他事情。
- 支持公平锁:先等待的线程先获得锁。synchronized只能非公平锁,ReentrantLock默认非公平锁。
- 支持选择性通知:结合
Condition
接口,同一个锁可以绑定多个条件,从而实现分组等待/通知。(synchronized的锁相当于只对一个Condition进行wait和notify)
针对读多写少的场景,为了允许多个线程同时读,而只当有线程写时才禁止其他线程读写,Java提供了ReentrantReadWriteLock读写锁。原理是维护了两个锁,读相关的称为共享锁,写相关的称为排他锁。
volatile
Java内存模型(JMM)
Java的所有变量都存储在主内存中,但每个线程会把主内存中的变量,复制一个副本存在自己独立的本地内存中。线程只能直接操作自己本地内存中的变量副本,这就可能导致同一变量在不同线程间的副本数据不一致。
为了解决这个问题,Java提供了volatile
关键字,表示该变量是共享且不稳定的,每次使用它需要去主内存中读取。
JMM内存间的交互操作
Java 内存模型定义了 8 个操作来完成主内存和本地内存的交互操作:
• read:把一个变量的值从主内存传输到本地内存中
• load:在 read 之后执行,把 read 得到的值放入本地内存的变量副本中
• use:把本地内存中一个变量的值传递给执行引擎
• assign:把一个从执行引擎接收到的值赋给本地内存的变量
• store:把本地内存的一个变量的值传送到主内存中
• write:在 store 之后执行,把 store 得到的值放入主内存的变量中
• lock:作用于主内存的变量,标识为某一线程独占。
• unlock:释放锁定状态的变量。
JMM三大特性
- 原子性
操作的执行不会被中断。即要么操作完全成功,要么不执行操作。
JMM保证上述8个操作具有原子性。但单操作的原子性,无法保证线程安全。 - 可见性
当一个线程对共享变量进行修改,其他线程可以立马得知。
使用volatile
就可以保证共享变量的可见性。 - 有序性
JMM允许编译器和处理器对指令进行重排序,且保证重排序不会影响单线程程序的执行。
但并发编程会受重排序的影响,使用volatile
可以禁止指令进行重排序优化。
synchronized和volatile的综合应用
synchronized
关键字和volatile
关键字是两个互补的存在。
volatile
用于保证变量的可见性和有序性,轻量级,性能好。
synchronized
用于保证代码块的原子性、可见性和有序性。
Q:使用双重检验锁实现单例模式
A:
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton(){}
public static Singleton getInstance() {
if (uniqueInstance == null) {
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
Q1:为什么不直接对getInstance
加synchronized
?
A:直接同步方法的话,每次调用该方法都要进行同步,而实际上只有在创建对象实例前的调用,才有同步的必要。通过双重检查的方式,实例创建完毕后的调用,在synchronized块外的第一次检查就返回了,避免了性能损失。
Q2:为什么要将uniqueInstance
申明为volatile
变量?
A:uniqueInstance = new Singleton();
这行代码其实分为三步执行:1.为uniqueInstance
分配内存地址 2.执行构造方法 3. 将uniqueInstance
指向分配的内存地址。由于JVM具有指令重排的特性,执行顺序可能变成1→3→2。假设有两个线程,T1执行了1和3,此时T2进入,判断uniqueInstance != null
,于是返回了一个初始化还未完毕的对象。因此,需要使用volatile
来禁止JVM重排指令。
ThreadLocal
通常情况下,我们创建的变量可以被任意线程访问并修改。如果想让每个线程都拥有自己专属的本地变量,则可以使用ThreadLocal
类。
// SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
private static final ThreadLocal<SimpleDateFormat> formatter;
原理
每个Thread
有一个ThreadLocalMap
,存储着以ThreadLocal
为key,Object对象为value的键值对。
这样每个线程都保存了一份变量的副本,避免了线程安全问题。
内存泄漏
ThreadLocalMap
的key是ThreadLocal
的弱引用,而value是强引用。于是可能出现key被回收,而value永远无法回收的情况,造成内存泄漏。为了解决这个问题,ThreadLocal
在调用set()
、get()
、remove()
方法时,会自动清理key为null的Entry。
线程池
池化技术(线程池、数据库连接池、Http 连接池)的思想就是减少每次获取资源的消耗,提高资源的利用率。
核心属性
threadFactory
:创建工作线程的工厂。corePoolSize
:核心线程数,即可同时运行的最小线程数量。workQueue
:新任务到来时,会判断当前运行的线程数是否已达核心线程数,若已达到则将新任务放入队列中。maximumPoolSize
:当队列存放的任务达到容量上限时,可同时运行的最大线程数量。keepAliveTime
:当线程数大于核心线程数时,没有任务可执行的线程会等待keepAliveTime
才会被回收销毁。unit
:keepAliveTime
参数的时间单位。handler
:往线程池添加任务时,将在下面两种情况触发拒绝策略:1)线程池运行状态不是 RUNNING;2)线程池已经达到最大线程数,并且阻塞队列已满时。ThreadPoolExecutor定义了一些拒绝策略:AbortPolicy
:抛出 RejectedExecutionException 来拒绝新任务的处理。(默认策略)CallerRunsPolicy
:在本线程(调用 execute 方法的线程)运行新任务。如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略DiscardPolicy
:丢弃新任务。DiscardOldestPolicy
:丢弃队列中最早的未处理任务。
线程池状态
- RUNNING:接受新任务并处理队列的任务
- SHUTDOWN:不接受新任务,但处理队列的任务
- STOP:不接受新任务,不处理队列的任务,并中断进行中的任务
- TIDYING:所有任务已终止(
workerCount
=0),将执行terminated()
方法 - TERMINATED:
terminated()
方法已完成
阻塞队列
- ArrayBlockingQueue:基于数组的有界阻塞队列,FIFO
- LinkedBlockingQueue:基于链表,FIFO,吞吐量一般高于ArrayBlockingQueue。
- PriorityBlockingQueue:具有优先级的无界队列
- SynchronousQueue:线程间移交的机制
Q:核心线程如何保持一直存活的?非核心线程如何实现在keepAliveTime
后死亡?
A:通过阻塞队列的take()
方法实现无任务时阻塞;通过阻塞队列的poll(time,unit)
方法在获取任务时延迟死亡。
创建方法
- 通过ThreadPoolExecutor构造方法创建(阿里巴巴规范推荐使用的方式)
- 通过Executors工具类创建(对上述构造方法的封装)
- newFixedThreadPool : 固定线程数。corePoolSize = maximumPoolSize,keepAliveTime = 0,使用无界的LinkedBlockingQueue。适用于负载较重的服务器。
- newSingleThreadExecutor:单线程。corePoolSize = maximumPoolSize = 1,keepAliveTime = 0, 使用无界的LinkedBlockingQueue。适用于需要保证顺序的执行各个任务的场景。
- newCachedThreadPool:按需创建新线程。corePoolSize = 0,maximumPoolSize = Integer.MAX_VALUE,keepAliveTime为60秒,使用SynchronousQueue。适用于执行很多的短期异步任务,或者是负载较轻的服务器。
- newScheduledThreadPool:创建一个以延迟或定时的方式来执行任务的线程池,工作队列为 DelayedWorkQueue。适用于需要多个后台线程执行周期任务。
ctl
ctl是一个打包存储2个数据的原子整数:
①runState:线程池状态,占高3位
②workerCount:线程的有效数量,占低29位
打包的好处是把对两个变量的操作封装成了一个原子操作。因为对这两个变量的读写必须保证同一时刻,这样设计避免了加锁的额外开销,只需一个AtomicInteger变量加位操作就能控制并发。
Q:线程池的大小怎么配置合适?
A:计算密集型任务:线程数 = CPU数 + 1;I/O密集型任务:线程数 = CPU数 * 2
Atomic原子类
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS
CAS指令有三个操作数:内存地址V,旧的预期值A,将要更新的目标值B。当且仅当V的值等于A时,才将V的值修改为B。
CAS高效地解决了原子操作的问题,但存在三个缺点:
1.循环时间长CPU开销大:因为CAS失败时会一直重试,占用CPU
2.只能保证单个共享变量的原子操作
3.ABA问题:内存值变化为A→B→A时,CAS操作会误认为它从未被改变过。
自旋锁
基于CAS的锁,获取锁时不会被阻塞,而是循环地尝试获取锁。
AQS
AbstractQueuedSynchronizer,是一个用来构建锁和同步器的框架,ReentrantLock、FutureTask等都是基于AQS。
原理
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。