目录
程序、进程、线程、管程
- 管程即为监视器(monitor),就是作为锁的锁对象本身
- 在 JVM 中,通过创建管程对象实现同步管理
- 只有获取到了管程对象才能执行同步代码,执行结束后释放管程对象
单核CPU和多核CPU的理解
- 单核CPU:其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务,但是因为CPU时间单元特别短,因此感觉不出来
- 多核CPU:可以理解为多个单核CPU,同一个时间单元内可以同时运行N个线程的任务(N为核数)
并行与并发
- 并行:多个CPU同时执行多个任务
- 并发:一个CPU执行多个任务,但是因为多个任务之间的切换时间很短(采用时间片),感觉上像是在同时执行多个任务
线程的创建和启动
Thread类
- Java语言的JVM允许程序运行多个线程,它通过
java.lang.Thread
类来体现 - 每个线程都是通过某个特定Thread对象(线程实例)的
run()
方法来完成操作的,经常把run()
方法的主体称为线程体 - 通过该Thread对象的
start()
方法来启动这个线程,而非直接调用run()
- Thread类实现了Runnable接口
Thread类构造器
Thread()
:创建新的线程实例对象,需要重写线程体Thread(String threadname)
:创建线程并指定线程实例名Thread(Runnable target)
:创建新的线程实例对象,且该对象的线程体为指定的target
中的run()
主体,target
实现了Runnable接口中并重写了run()
方法Thread(Runnable target, String name)
:在上一步的基础上,指定了线程实例名
Thread类有关方法
void start()
: 启动线程run()
: 线程在被调度时执行的操作String getName()
: 返回线程的名称void setName(String name)
:设置该线程名称static Thread currentThread()
: 返回当前线程,在Thread子类中就是this,通常用在主线程和Runnable实现类static void yield()
:线程让步,暂停当前正在执行的线程进入Ready,把执行机会(CPU)让给优先级相同或更高的线程,若线程等待队列中没有同优先级的线程,忽略此方法
线程等待队列:由Ready状态的线程组成,根据CPU的调度规则,随时有可能被CPU执行进入Running状态
join()
:当某个程序执行流中调用其他线程的join()
方法时,调用线程将被阻塞,直到join()
方法加入的 join 线程执行完为止,就算是低优先级的线程也可以获得执行
比如:在线程A中调用B线程的
join()
方法,那么线程A让出CPU进入Wating状态,并且让B线程进入Ready状态
static void sleep(long millis)
:令当前活动线程在指定时间段内放弃对CPU控制进入TimedWating状态,使其他线程有机会被执行,时间到后重排队进入Ready状态thread.setDaemon(true)
:线程的属性daemon
默认为false
,设置为true
即把一个用户线程(当前线程)变成一个守护线程
后台守护线程一定是在主线程结束后立即结束,
daemon
作为后台守护线程标志,决定JVM优雅关闭
stop()
: 强制线程生命期结束,不推荐使用boolean isAlive()
:判断线程是否还活着- 中断相关的方法,后面详细记录
创建线程
继承Thread类的方式
- 定义子类继承Thread类
- 子类中重写Thread类中的run方法
- 创建Thread子类对象,即创建了线程对象
- 调用线程对象start方法:启动线程
/**
* 创建线程的方式一:继承Thread类
*/
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello MyThread!");
}
}
/**
* 使用方式一创建的线程使用方式
*/
new MyThread().start();
实现Runnable接口的方式
- 定义实现类实现Runnable接口
- 实现类中重写了Runnable接口的run方法
- 创建实现类对象
- 通过
Thread(Runnable target)
或Thread(Runnable target, String name)
创建线程对象 - 调用线程对象start方法:启动线程
避免了单继承的局限性,多个线程可以共享同一个Runnable接口实现类的对象,非常适合多个相同线程来处理同一份资源
/**
* 创建线程的方式二:实现Runnable接口
*/
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Hello MyRunnable!");
}
}
/**
* 使用方式二创建的线程使用方式
*/
new Thread(new MyRunnable()).start();
实现Callable接口的方式
- 定义实现类实现Callable接口
- 重写Callable接口的
call()
方法 - 创建实现类对象
- 创建 FutureTask 对象,上一步的实现类对象作为FutureTask 构造器的入参
- 通过
Thread(Runnable target)
或Thread(Runnable target, String name)
创建线程对象 - 调用线程对象start方法:启动线程
- FutureTask 是 Future 接口的唯一的实现类
- 可以对具体的 Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等
- FutureTask 同时实现了 Runnable, Future 接口。它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值
/**
* 创建线程的方式三:实现 Callable 接口。 --- JDK 5.0新增
*
* 如何理解实现 Callable 接口的方式创建多线程,比实现 Runnable 接口创建多线程方式强大?
* 1. call() 可以有返回值的。
* 2. call() 可以抛出异常,被外面的操作捕获,获取异常的信息
* 3. Callable 是支持泛型的
* 4. 需要借助 FutureTask 类,比如获取返回结果
*
*/
class MyCallable implements Callable {
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i % 2 == 0){
sum += i;
}
}
System.out.println("Hello MyCallable!");
return sum;
}
}
/**
* 使用方式三创建的线程使用方式
* Callable 实现类配合 FutureTask 使用
* 1. FutureTask 是 Future 接口的唯一的实现类
* 2. 可以对具体的 Runnable、Callable 任务的执行结果进行取消、查询是否完成、获取结果等
* 3. FutureTask 同时实现了 Runnable, Future 接口。它既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值
*/
MyCallable myCallable = new MyCallable();
FutureTask futureTask = new FutureTask(myCallable);
new Thread(futureTask).start();
try {
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
通过JDK线程池
/**
* 创建线程的方式四:使用线程池
* 好处:
* 1.提高响应速度(减少了创建新线程的时间)
* 2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
* 3.便于线程管理,以下参数都是可以设置的
* corePoolSize:核心池的大小
* maximumPoolSize:最大线程数
* keepAliveTime:线程没有任务时最多保持多长时间后会终止
*
* ExecutorService:线程池接口
* 1. 常见子类 ThreadPoolExecutor
* 2. 常用方法:
* void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行 Runnable
* <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行 Callable
* void shutdown() :关闭线程池
* 3. Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
* Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
* Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
* Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
* Executors.newScheduledThreadPool():创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行
*/
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
// 以下 API 只有 ExecutorService 的实现子类才有
service1.setCorePoolSize(5);
service1.setKeepAliveTime(1,TimeUnit.MILLISECONDS);
service1.execute(new Runnable() {
@Override
public void run() {
System.out.println("通过线程池创建线程");
}
});
Future<?> future = service1.submit(futureTask);
System.out.println("通过线程池创建线程执行的结果" + future);
Future<?> future1 = service1.submit(new FutureTask<String>(new Callable<String>() {
@Override
public String call() throws Exception {
return "可以向同一个线程池提交多个任务";
}
}));
System.out.println(future1);
service1.shutdown(); // 关闭线程池
线程的启动
- 想要启动多线程,必须调用
start()
方法,但是线程的执行(run()
方法)由JVM调用,什么时候调用,执行的过程控制都由操作系统的CPU调度决定 - 一个线程对象只能调用一次
start()
方法启动,如果重复调用了,则将抛出以上的异常IllegalThreadStateException
线程的调度与线程的优先级
调度策略
-
时间片的方式
-
抢占式:高优先级的线程抢占CPU
同优先级线程组成 先进先出 队列(先到先服务),使用时间片策略
对高优先级的线程,使用优先调度的抢占式策略
线程的优先级
线程的分类
线程状态与线程的生命周期
线程状态
- 强行使线程状态进入Terminated状态,可以在该线程线程外部使用想要终结的线程的interrupt()方法
Thread.sleep(long millis)
,一定是当前线程调用此方法,当前线程进入 TIMED_WAITING 状态,但不释放对象锁,millis 毫秒后线程自动苏醒进入就绪状态。作用:给其它线程执行机会的最佳方式。Thread.yield()
,一定是当前线程调用此方法,当前线程放弃获取的 CPU 时间片,但不释放锁资源,由运行状态变为就绪状态,让 OS 再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield() 达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield() 不会导致阻塞。该方法与sleep() 类似,只是不能由用户指定暂停多长时间。t.join()/t.join(long millis)
,当前线程里调用其它线程 t 的 join 方法,当前线程进入WAITING/TIMED_WAITING 状态,内部调用了 t.wait,线程 t 执行完毕或者 millis 时间到,当前线程进入就绪状态。其中,wait 操作对应的 notify 是由 jvm 底层的线程执行结束前触发的。obj.wait()/obj.wait(long timeout)
,当前线程调用对象的 wait() 方法,进入等待队列。依靠notify()/notifyAll()
唤醒或者 timeout 时间到自动唤醒。唤醒后,线程恢复到 Ready状态。obj.notify()
唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()
唤醒在此对象监视器上等待的所有线程。
需要注意的是,因为
join()
方法或wait()
方法进入阻塞状态的线程,会释放当前现场所持有的锁,意味着当前线程恢复到Ready状态时不但需要重新获取CPU,还需要重新获取需要持有的锁才能进入Running状态
线程的生命周期
JDK中用Thread.State
类定义了线程的几种状态,要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源
就绪即为Ready状态
- 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能
运行即为Running状态
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态
阻塞即为Wating、TimedWating、Blocked状态
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束
死亡即为Terminated状态
发生线程安全的条件
- 多个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。比如线程A要求访问变量c的原值,线程B会不定时更新变量c的值,此时就存在访问顺序的问题
- 导致竞态条件发生的代码区称作临界区。
- 对于上述 竞态条件、临界区 不进行恰当的控制,会导致线程安全问题
解决线程安全
- Java对于多线程的安全问题提供了专业的解决方式:同步机制
1.使用同步synchronized/volatile/final
2.利用Java锁机制,显式的加锁
synchronized 的使用
不管是同步代码块还是同步方法,都意味着进入该区域的线程对象都必须获得相应的
synchronized
的锁,否则进入阻塞状态
当线程执行完同步方法或同步代码块后,线程会自动释放对应的synchronized
的锁
synchronized
的锁即为同步锁
- 使用同步关键字,需要持有的锁对象不能是String字符串、包装类,因为String字符串是常量,影响范围广,包装类内部的实现机制是每次赋值产生新对象,所以也不能用来作为锁对象
释放锁与不释放锁
volatile
- 保证不同线程之间对于某一变量的可见性:每次读写都强制从主内存刷数据,利用了MESI-缓存一致性协议
- volatile可见性仅保证最浅级别,也就是说如果volatile修饰的是个对象引用变量,某线程仅改变该对象内部的成员属性字段,此时对其他线程来说依然是不可见的
- volatile 能够实现禁止指令重排序(有序性),通过内存屏障实现
- 适用场景: 单个线程写,多个线程读
- 原则: 能不用就不用,不确定的时候也不用
- 可选替代方案: Atomic 原子操作类
final
- final 声明的引用数据类型与原生数据类型在处理时区别:
1、final的原生类型变量,在编译时会被常量替代,且等同于final static修饰
2、final修饰的引用类型变量指向的对象不能改,但是指向的对象的内部属性可以改
三者的区别
- 使用final,利用的是Java中常量不能再次更改的原则,解决竞态条件,但是依然有线程安全问题,final仅能保证原生数据类型或者对象引用不被改变
- 利用violatile,利用了可见性,解决竞态条件,但是依然会有线程安全问题,因为不保证原子性的情况下,一些线程对volatile修饰变量修改的中间状态,会被其他线程所读取
- 使用synchronized,利用了可见性和原子性,解决竞态条件和临界区
Lock锁
注意加锁的位置,因为显式的加锁并不会自动释放锁,所以程序中必然会有释放锁的步骤,如果将加锁的位置放到了容易发生异常的代码之后,则会出现没有加锁却有释放锁的操作
synchronized 与 Lock 的对比
死锁
排查死锁
方式一,命令
- 通过
jps -l
查看所有 java 进程(与ps -ef
类似)得到进程ID,然后通过 JVM命令jstack Java进程的ID
查看堆栈的信息(会打印到控制台)
方式二,图形界面
- 在命令行输入
jconsole
,打开 JMX 客户端工具
线程的通信
- 被再次唤醒的线程,需要等待执行唤醒的线程释放锁
- 唤醒和等待都必须在
synchronized
orLock
范围之内
使用 synchronized
wait()
必须要在notify()/notifyAll()
之前执行,否则notify()/notifyAll()
不生效
图片中所说的对象,调用
wati()
方法的对象与调用notify() 、 notifyAll()
的对象均为同一个对象
当前线程具有该对象的监控权:即当前线程持有该锁对象,即作为锁的对象的对象头中的锁标记字中的线程ID是当前线程的ID
使用 Lock
- 类比
使用 synchronized
Condition
的使用必须在lock()
与unlock()
之间- 唤醒必须要在等待之后执行
//创建Lock
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
//+1
public void incr() throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while (number != 0) {
condition.await();
}
//干活
number++;
System.out.println(Thread.currentThread().getName()+" :: "+number);
//通知
condition.signalAll();
}finally {
//解锁
lock.unlock();
}
}
使用 工具类
- 常用的 JUC 工具
CountDownLatch、CyclicBarrier、Semaphore
等,它们底层都是通过使用 AQS 实现, Lock 底层的实现原理也是AQS,所以这些工具类和 Lock 一样都是 AQS 框架应用的一部分
线程池
线程池的关键参数
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.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
1.最小线程数:当线程池初始化创建时,线程池中的线程数为0,当有任务被提交到线程池时才会根据任务数量逐渐增加线程数,这个增加的过程是依次递增,直到此时线程池中的线程数=最小线程数,停止创建新线程,也就是说线程池中的 线程数<最小线程数 时,线程数=任务数
2.最大线程数:当线程池中的线程数=最小线程数,且工作队列已满的情况下,会继续增加线程数,这也是递增的过程,只有当任务队列再次满的情况下才会继续再新增一个线程,直至线程池里的线程数=最大线程数
拒绝策略
JDK自带的线程池
//Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
//创建一个可根据需要创建新线程的线程池
Executors.newCachedThreadPool();
//创建一个可重用固定线程数的线程池
Executors.newFixedThreadPool(n);
//创建一个只有一个线程的线程池
Executors.newSingleThreadExecutor();
//创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行
Executors.newScheduledThreadPool();
- 为什么不推荐使用JDK线程池:共有4种 JDK 线程池,其中每一种都有以下两者或两者之一的问题,会导致OOM
1.线程数:Integer.MAX_VALUE
2.工作队列长度:Integer.MAX_VALUE
分支合并框架
-
ForkJoinPool
由ForkJoinTask
数组和ForkJoinWorkerThread
数组组成
-
ForkJoinTask
数组负责将存放任务以及将任务提交给ForkJoinPool
-
ForkJoinWorkerThread
负责执行这些任务
CompletableFuture
Future
Future 的主要缺点
CompletableFuture
- 构造的方法(不推荐直接使用构造方法)
线程中断
线程中断协商
中断正在运行的线程
ThreadLocal
Thread、ThreadLocal、ThreadLocalMap关系
- Thread 中含有成员变量 ThreadLocal.ThreadLocalMap,意味着每个线程实例,都拥有各自的 ThreadLocalMap
- ThreadLocalMap 是 ThreadLocal 的静态内部类
- 总结来说:每个
Thread
都有各自的ThreadLocalMap
,这些Thread
用ThreadLocal
实例(可能相同,可能不同)作为各自的ThreadLocalMap
的 key
关于强、软、弱、虚引用
- 需要注意的是,这里的引用指的是引用的类型,不是对象的类型
- 从下图可以看出,四种引用类型,本身也是对象
- 被强引用指向的对象,是强引用的对象
- 注意区分强引用对象与强引用的对象,强引用对象指的是 强引用类 的实例
- 被弱引用指向的对象,是弱引用的对象
- 注意区分弱引用对象与弱引用的对象,弱引用对象指的是 弱引用类 的实例
- 注意区分弱引用对象与弱引用的对象,弱引用对象指的是 弱引用类 的实例
- 被不同类型引用指向的对象,被垃圾回收的时间不同
- 强引用的对象,根可达时不可以被回收
- 一般的引用都是强引用,被强引用指向的对象就是强引用的对象
-软引用的对象,内存不足时,根可达也没用,照样回收
- 弱引用的对象,不管是否根可达,都回收
- 虚引用的对象,都不知道它是否还存在,只能通过引用队列判断是否还存活
- 虚引用的对象被垃圾回收器回收后,会放入引用队列
各种引用在和根对象有联系的时候的被回收情况
ThreadLocal 内存泄露问题
ThreadLocal
中的ThreadLocalMap
由Entry
数组构成,而Entry
继承了弱引用类,通过这种方式,将作为key
的ThreadLocal
对象变成由弱引用类型的引用所指向的对象(即弱引用的对象)- 注意区分弱引用类的实例(弱引用对象)与弱引用的对象(弱引用所指向的对象),
Entry
继承了弱引用类,但不是弱引用的对象(是弱引用实例),Entry
是强引用的对象,因为它组成了ThreadLocalMap
的Entry
数组,是Entry
数组对象的成员变量,是被强引用所指向的
- ThreadLocalMap 使用弱引用对象
Entry
存储数据,作为key
的ThreadLocal
对象,在创建Entry
时,直接被包装成了弱引用对象
- 就算作为
key
的ThreadLocal
对象被回收,也有可能导致如下图所示的情况发生,即出现无法访问的value
,但是根据根可达不会被回收,由此造成内存泄漏,这也是为什么要求在使用完ThreadLocal
后,在finally
中手动调用remove()
ThreadLocal
类中,无论set()、get()
还是remove()
都会有调用expungeStaleEntry()
专门用于清除脏Entry
,就是把key==null
的Entry
的value=null
,但是使用的时候还是要求remove()
ThreadLocal使用总结