一. 前言
1. 几个基本概念
进程和线程
- 进程:进程用来描述程序的执行过程,是资源分配的最小单位
- 线程:线程建立在进程上,是CPU调度的最小单位,线程可设置优先级
同步和异步
- 同步:方法调用一旦开始,就必须等待调用返回后,才能继续后续的代码
- 异步:方法调用一开始,其执行过程被分配到另一线程中进行,同时主线程可以继续后续的代码
并发和并行
- 并发:单核 cpu 处理多任务(一个人同时处理很多事情)
- 并行:多核 cpu 执行多任务(多个人同时执行很多事情)
线程的生命周期
- 新建:创建线程对象,并保持存在
- 就绪:线程对象调用.start()方法,等待JVM线程调度器的调度
- 运行:准备就绪的线程获取到了分配的cpu资源,开始执行
- (阻塞):线程一旦调用wait()、sleep()等方法,就会进入阻塞状态
- 死亡:线程正常运行完成或异常,就会终止
线程的优先级
Java 线程的优先级取值范围是 :
1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )
默认优先级取值:(NORM_PRIORITY)5
2. 创建多线程的方式
- 继承 Tread 类
- 实现 Runnable 接口
- 使用线程池(常用)
二. 多线程的创建与使用
1. 继承 Thread 类
//1.继承Thread类
public class MyThread extends Thread {
//2.重写run()方法,方法体写需要执行的内容
@Override
public void run() {
for(int i = 0; i <= 100; i++){
if(i % 2 ==0){
System.out.println(i);
}
}
}
}
public class MyThreadTest {
public static void main(String[] args) {
//3.new出myThread对象
MyThread myThread = new MyThread();
//4.调用start()方法,start内部最终调用run()方法
myThread.start();
}
}
Tread 类内部源码本身也是实现 Runnable 接口
2. 实现 Runnable 接口
//1.实现Runnable接口
public class MyThread implements Runnable{
//2.重写run()方法,方法体写需要执行的内容
@Override
public void run() {
for(int i = 0; i <= 100; i++){
if(i % 2 ==0){
System.out.println(i);
}
}
}
}
public class MyThreadTest {
public static void main(String[] args) {
//3.new出myThread对象
MyThread myThread = new MyThread();
//4.将myThread对象作为参数传入Tread构造器
Thread thread = new Thread(myThread);
//5.调用start()方法
thread.start();
}
}
3. 线程池
先启动若干数量的线程,并让这些线程都处于睡眠状态,当客户端有一个新请求时,就会唤醒线程池中的某一个睡眠线程,让它来处理客户端的这个请求,当处理完这个请求后,线程又处于睡眠状态
实际开发中使用 ThreadPoolExecutor 类创建线程池,其构造方法如下
public ThreadPoolExecutor(int corePoolSize, //核心线程数
int maximumPoolSize, //最大线程数
long keepAliveTime, //允许线程空闲时间
TimeUnit unit, //时间对象
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工厂
RejectedExecutionHandler handler //任务拒绝策略) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3),
Executors.defaultThreadFactory()
new ThreadPoolExecutor.AbortPolicy()
);
高并发、任务执行时间短的业务,线程数可以设置为CPU核数+1
各种任务拒绝策略区别:
- AbortPolicy:超出最大线程数,直接抛出RejectedExecutionException异常阻止系统正常运行
- CallerRunsPolicy:一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛异常,如果允许任务丢失这是最好的一种方案
三. 线程同步
线程安全问题:对于非原子性操作,由于多线程存在上下文切换,可能会出现多个线程同时读取到一个变量值,并都对变量进行了修改,导致错误结果,并且由于存在指令重排,实际情况可能更糟糕。出现这样的情况被称为存在竞态条件。
导致竞态条件产生的原因:
- 读取-修改-写入,如i++这样的操作,先读取i,修改i,其他线程介入,写入i错误
- 先检查后执行,如先 if(true),其他线程介入,if内的操作失效
1. 乐观锁
总是认为线程不会修改数据,因此更适合读多写少的操作
乐观锁的常见实现方法:版本号机制 或 CAS(Compare and Swap)
- 版本号机制
- 数据表中加上一个数据版本号version字段
- 线程A进入读取数据和当前版本号,修改数据后提交,当前版本号+1
- 线程B在线程A提交前进入,一番操作提交时发现版本号不匹配,修改失败
- 线程B重新获取新版本号,再次尝试修改操作,直到最终修改成功
- CAS指令算法(是原子操作)
- 三个参数:V-要更新变量的内存地址,E-旧值,N-新值
- 当V值等于E值时,更新V值为N值
- 当V值不等于E值时,什么都不做
- 不论是否更新成功,都返回V的旧值
总之,CAS操作通过先检查原值是否被修改过再决定是否更新的逻辑实现了同步
CAS 存在ABA问题:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,在这段时间内,V的值可能已被其他线程改为了其他值,然后又改回A,此时的CAS操作就会误认为V从来没有被修改过。加版本号来避免
2. 悲观锁
总是认为线程会修改数据,因此更适合写多读少的操作
悲观锁的常见实现方式:synchronized 和 ReentrantLock
- synchronized(保证原子性和内存可见性)
在 代码块 或 方法 前添加关键字 synchronized 可以实现锁功能,被称为监视器锁
//同步代码块,最好用于有共享数据的情况
public void test1(){
//可以传入任何java对象作为锁,这里直接使用this调用当前对象(当前对象只能是同一个对象才行)
synchronized(this){
//需要执行的代码
}
}
//同步方法(修饰普通方法时锁的是当前对象实例;修饰静态方法时锁的是整个类,即 当前类.class)
public synchronized void test2(){ //属于哪个类的方法,就调用那个类的对象锁
//需要执行的代码
}
//当代码块或方法执行完毕后会自动释放锁
synchronized 锁保证了在监视器锁定的代码范围内只能同时进入一个线程
java中每个对象都有一个内置锁,synchronized 使用这些对象的内置锁实现监视器锁功能
- ReentrantLock
ReentrantLock加锁后必须在finally手动解锁
public class SetLock implements Runnable{
//1.创建ReentrantLock对象
private ReentrantLock lock = new ReentrantLock();
//2.重写run方法
public void run(){
//3.手动上锁
lock.lock();
try{
//执行代码
}catch{
}finally{
//4.手动解锁
lock.unlock();
}
}
}
死锁:不同线程分别占用对方的资源不放,同时都在等对方放弃资源,就形成了死锁
如果所有线程以固定的顺序来获得锁,那么程序中就不会出现锁顺序死锁问题
synchronized 和 ReentrantLock 的区别
- synchronized 自动获得、释放锁,ReentrantLock 手动获得、释放锁
- synchronized 不可响应中断,ReentrantLock 可响应中断
- synchronized 是 JVM 层的,ReentrantLock 是个 API
- synchronized 是同步阻塞,悲观并发策略,lock 是同步非阻塞,乐观并发策略
- synchronized 在发生异常时会自动释放线程占有的锁,ReentrantLock 不会
3. volatile 关键字
volatile比synchronized更轻量,它不会阻塞线程
保证内存可见性:如果一个变量被声明成volatile,所有线程均可见这个变量,当一个线程修改了该变量,其他线程会知道。对于如何保证可见性,其细节如下:
- 写操作:一个线程修改了volatile变量,JVM会把该线程工作内存中的volatile变量最新值立即刷新到主内存中,这个回写主内存的操作会导致其他线程工作内存缓存行无效
- 读操作:其他线程再进行写操作前,必须先从主内存中读取最新值
对非原子操作,不能保证原子性:如 i++ ,假设线程A和线程B都开始写操作,它们都读取了volatile变量的最新值,当线程A完成写操作,线程B读取的volatile变量值变成失效值,线程B不会再重新读取最新值,而是继续错误的修改,即不保证原子性。因此,对于多线程下的非原子性操作,仍需要加锁才能保证状态更新的原子性
禁止指令重排:保证执行到volatile变量时,其前面的所有语句都执行完,后面所有语句都未执行,且前面语句的结果对volatile变量及其后面语句可见
当且仅当满足以下条件,使用volatile变量才是线程安全的,否则仍要加锁
- 对变量的写操作不依赖变量的当前值,或保证只有一个线程能更新变量
- 该变量不会与其他成员变量共同参与不变性条件
四. 线程通信
简单来讲,就是线程 A 调用了一个对象的 wait() 方法进入等待状态,当线程 B 调用此对象的 notify() 或 notifyAll() 方法,线程A则退出等待开始执行后续操作
while(true){
synchronized(this){
this.notify();
//循环代码
this.wait();
}
}
- wait():释放锁,同时进入等待状态,直到被 notify 唤醒
- wait(1000):等待1s,没收到通知就自动解除阻塞
- notify():唤醒处于等待状态的一个线程
- notify():唤醒处于等待状态的全部线程
五. ThreadLocal
ThreadLocal 为每个线程都提供了一个独立变量的副本,各线程在其内部任何地方都可以使用,使线程之间互不影响(即其目的就是隔离各线程,并非为了解决线程安全问题),常应用于解决数据库连接、session管理等
每个线程中都有一个 TheadLocalMap 类型的局部变量 threadLocals,它保存了该线程的本地变量,键为当前 ThreadLocal 变量,值为T类型的变量副本(需要先set,否则为null),即可以将ThreadLocal<T>视为包含了Map<Thread, T>对象
ThreadLocal 提供了以下方法:
- get():获取当前线程保存的变量副本
- set():设置当前线程变量的副本
- remove():移除当前线程变量的副本
- initialValue():是一个延迟加载的方法
为了避免内存泄露,需要手动 remove()