Java线程核心
1.进程和线程
进程:进程的本质是一个正在执行的程序,程序运行时系统会创建一个进程,并且给每个进程分配独立的内存地址空间保证每个进程地址不会相互干扰。同时,在 CPU 对进程做时间片的切换时,保证进程切换过程中仍然要从进程切换之前运行的位置出开始执行。所以进程通常还会包括程序计数器、堆栈指针。
线程:有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。线程是程序中一个单一的顺序控制流程。进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派 CPU 的基本单位指运行中的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
2.线程的创建方式
创建的方式有三种:
- 继承Thread
- 实现Runnable接口
- Callable/Future
import java.util.concurrent.CountDownLatch;
public class ThreadDemo1 extends Thread {
CountDownLatch countDownLatch;
public ThreadDemo1(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ":my thread ");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}
public static void main(String[] args) {
// 第一种:使用extends Thread方式
CountDownLatch countDownLatch1 = new CountDownLatch(2);
for (int i = 0; i < 2; i++) {
ThreadDemo1 myThread1 = new ThreadDemo1(countDownLatch1);
myThread1.start();
}
try {
countDownLatch1.await();
System.out.println("thread complete...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
import java.util.concurrent.CountDownLatch;
public class ThreadDemo2 implements Runnable{
CountDownLatch countDownLatch;
public ThreadDemo2(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + ":my runnable ");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}
public static void main(String[] args) {
// 第二种:使用implements Runnable方式
CountDownLatch countDownLatch2 = new CountDownLatch(2);
ThreadDemo2 myRunnable = new ThreadDemo2(countDownLatch2);
for (int i = 0; i < 2; i++) {
new Thread(myRunnable).start();
}
try {
countDownLatch2.await();
System.out.println("runnable complete...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ThreadDemo3 implements Callable<Integer> {
public static void main(String[] args) {
ThreadDemo3 threadDemo03 = new ThreadDemo3();
//1、用futureTask接收结果
FutureTask<Integer> futureTask = new FutureTask<>(threadDemo03);
new Thread(futureTask).start();
//2、接收线程运算后的结果
try {
//futureTask.get();这个是堵塞性的等待
Integer sum = futureTask.get();
System.out.println("sum="+sum);
System.out.println("-------------------");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <101 ; i++) {
sum+=i;
}
return sum;
}
}
3.线程的生命周期
- 新建状态(New):线程对象被创建后,但还没有调用start()方法时的状态。
- 就绪状态(Runnable):线程对象调用start()方法后进入就绪状态,表示线程可以被调度执行。
- 运行状态(Running):线程被调度执行后进入运行状态。
- 等待状态(WAITING): 线程需要等待其他线程做出一些特定动作(通知或中断)
- 阻塞状态(Blocked):线程在执行过程中可能因为某些原因被阻塞,例如等待输入输出、线程休眠等。
- 结束状态(Terminated):线程执行完任务后进入结束状态。
4.线程的启动和终止
4.1 启动线程
启动线程是start方法,非run方法
4.2 线程的终止
使用退出标志退出线程
一般 run()方法执行完,线程就会正常结束,然而,常常有些线程是伺服线程。它们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程。使用一个变量来控制循环
例如:最直接的方法就是设一个boolean 类型的标志,并通过设置这个标志为 true或 false 来控制 while循环是否退出。定义了一个退出标志 exit,当 exit 为 true 时,while 循环退出,exit 的默认值为 false.在定义 exit时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。
class FlagThread extends Thread { // 自定义中断标识符 public volatile boolean isInterrupt = false; @Override public void run() { // 如果为 true -> 中断执行 while (!isInterrupt) { System.out.println("业务处理1...."); // 业务逻辑处理 Thread.sleep(10000); System.out.println("业务处理2...."); } } }
Interrupt方法结束线
线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。
public static void main(String[] args) throws InterruptedException { // 创建可中断的线程实例 Thread thread = new Thread(() -> { while (!Thread.currentThread().isInterrupted()) { System.out.println("thread 执行步骤1:线程即将进入休眠状态"); try { // 休眠 1s Thread.sleep(1000); } catch (InterruptedException e) { System.out.println("thread 线程接收到中断指令,执行中断操作"); // 中断当前线程的任务执行 break; } System.out.println("thread 执行步骤2:线程执行了任务"); } }); thread.start(); // 启动线程 // 休眠 100ms,等待 thread 线程运行起来 Thread.sleep(100); System.out.println("主线程:试图终止线程 thread"); // 修改中断标识符,中断线程 thread.interrupt(); // 首先会改变当前线程的阻塞状态 true 。同时如何线程调用 sleep wait 这些阻塞的方法。那么会抛出 InterruptedException 异常 }
线程未处于阻塞状态:使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。
stop 方法终止线程
程序中可以直接使用 thread.stop()来强行终止线程,但是 stop 方法是很危险的,就象突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,不安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出 ThreadDeatherror 的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误。因此,并不推荐使用 stop 方法来终止线程。
5.线程间通信
- wait()和notify()方法:wait()方法使线程进入等待状态,直到其他线程调用notify()或notifyAll()方法将其唤醒。notify()方法唤醒一个等待中的线程,notifyAll()方法唤醒所有等待中的线程。
- wait(long timeout)和notify()方法:wait(long timeout)方法使线程进入等待状态,直到其他线程调用notify()方法将其唤醒,或者等待时间超过指定的timeout时间。notify()方法唤醒一个等待中的线程。
- join()方法:join()方法使一个线程等待另一个线程执行完毕。当一个线程调用另一个线程的join()方法时,当前线程将被阻塞,直到另一个线程执行完毕。
- Lock和Condition接口:Lock接口提供了比synchronized关键字更灵活的锁机制,Condition接口提供了更灵活的等待/通知机制。通过Lock接口的lock()方法获取锁,unlock()方法释放锁;通过Condition接口的await()方法使线程等待,signal()方法唤醒一个等待中的线程,signalAll()方法唤醒所有等待中的线程。
- BlockingQueue阻塞队列:BlockingQueue是一个支持阻塞操作的队列,当队列为空时,获取元素的线程将被阻塞,直到队列中有可用元素;当队列满时,插入元素的线程将被阻塞,直到队列有空闲位置。
6.线程池
6.1 线程池原理
提交一个任务到线程池中,线程池的处理流程如下:
- 判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
- 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程
- 判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
线程池的好处:
- 降低资源消耗:通过重复利用已创建的线程降低线程的创建和销毁造成的不必要资源消耗
- 提高响应速度:当任务到达时,任务可以不需要等到线程创建就立即开始执行
- 提高线程的可管理性:统一分配、监控、调优
6.2 常见的线程池实现
线程池 | 说明 |
---|---|
newCachedThreadPool | 创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行 很多短期异步任务的程序而言,这些线程池通常可提高程序性能。调用 execute 将重用以前构造 的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并 从缓存中移除那些已有 60 秒钟未被使用的线程。因此,长时间保持空闲的线程池不会使用任何资 源。 |
newFixedThreadPool | 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大 多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务, 则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何 线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之 前,池中的线程将一直存在。 |
newScheduledThreadPool | 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。 |
newSingleThreadExecutor | Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程),这个线程 池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去! |
6.3 ThreadPoolExecutor
系统提供的线程池的实现都有各自的优缺点。我们在实际的使用中更多的是自定义线程池的实现
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
各个参数的含义:
corePoolSize:线程池核心线程数量
maximumPoolSize:线程池最大线程数量
keepAliverTime:当活跃线程数大于核心线程数时,空闲的多余线程最大存活时间
unit:存活时间的单位
workQueue:存放任务的队列
handler:超出线程范围和队列容量的任务的处理程序
使用的是阻塞队列:
- ArrayBlockingQueue
- LinkedBlockingQueue
- SynchronousQueue
- PriorityBlockingQueue
拒绝策略分类:
- AbortPolicy:直接抛出异常,默认策略
- CallerRunsPolicy:用调用者所在的线程来执行任务
- DiscardOldestPolicy:丢弃阻塞对类中靠最前的任务,并执行当前任务
- DiscardPolicy:直接丢弃任务
提交一个任务到线程池中,线程池的处理流程如下:
- 判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
- 线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
- 判断线程池里的线程是否达到最大线程数,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
6.4 线程池调优
要想合理地配置线程池,就必须首先分析任务特性,可以从以下几个角度来分析。
-
任务的性质:CPU密集型任务、IO密集型任务和混合型任务
-
任务的优先级:高中低
-
任务的执行时间:长中短
-
任务 的依赖性;是否依赖其他系统资源
-
CPU密集型任务应配置尽可能小的线程,如配置NCPU+1个线程的线程池,
-
IO密集型任务线程并不是一直在执行任务 ,则应配置尽可能多的线程,如2*NCPU 。
可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理,它可以让优先级高的任务先执行。
7.Synchronized
同步、重量级锁,synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入临界区,同时还可以保证共享变量的内存可见性(单台JVM内)
synchronized锁对象:
- 普通同步方法,锁的是当前实例对象
- 静态同步方法,锁的是当前类的class对象
- 同步代码块,锁的是括号里的对象(实例对象或者class对象)
synchronized的锁优化
- 无锁
- 偏向锁:为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,主要尽可能避免不必须要的CAS操作,如果竞争锁失败,则升级为轻量级锁
- 轻量级锁:自旋方式,该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁(循环方式)
- 重量级锁:阻塞方式
- 锁消除: 有些代码明明不用加锁,结果还给加上锁,这时编译器会判断锁没有什么必要,就直接把锁去掉了
- 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。例如for循环内部获取锁
8.AQS
AbstractQueuedSynchronizer,同步器,实现JUC核心基础组件,解决了子类实现同步器时涉及到的大量细节性问题,例如:同步状态、FIFO同步队列等,采用模板方法模式,AQS实现了大量的通用方法,子类通过继承方式实现其抽象方法来管理同步状态
同步锁获取与释放:
- 独占式
- 共享式
package com.boge.flow.aqs;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class AQSDemo {
// 共享的资源
private static int count = 0;
private static Lock lock = new ReentrantLock();
/**
* 造成数据安全问题的原因
* 1.原子性:执行的最小单元 要么都执行。要么都不执行
* 2.可见性:
* 3.有序性:
*/
// 定义一个方法去操作资源
public static void incr(){
try {
Thread.sleep(10);
lock.lock(); // 加锁
// ...
lock.lock();
count ++; // 原子 可见性
}catch (Exception e){
}finally {
lock.unlock();
lock.unlock(); // 释放锁
}
}
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(100);
// 多个线程去操作共享资源
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
incr();
latch.countDown();
}
}).start();
}
latch.await(); // 阻塞
// 获取 共享的数据
System.out.println("count = " + count);
}
}
9.ThreadLocal
一种解决多线程环境下成员变量的问题的方案,但是与线程同步无关,其思路就是为每个线程创建一个单独的变量副本。从而每个线程都可以独立的改变自己所拥有的变量副本,而不会影响其他线程对应的变量副本 , ThreadLocal不是用于解决共享变量的问题,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制
ThreadLocal可以理解为线程本地变量,他会在每个线程都创建一个副本,那么在线程之间访问内部副本变量就行了,做到了线程之间互相隔离,相比于synchronized的做法是用空间来换时间。
ThreadLocal有一个静态内部类ThreadLocalMap,ThreadLocalMap又包含了一个Entry数组,Entry本身是一个弱引用,他的key是指向ThreadLocal的弱引用,Entry具备了保存key value键值对的能力。
弱引用的目的是为了防止内存泄露,如果是强引用那么ThreadLocal对象除非线程结束否则始终无法被回收,弱引用则会在下一次GC的时候被回收。
但是这样还是会存在内存泄露的问题,假如key和ThreadLocal对象被回收之后,entry中就存在key为null,但是value有值的entry对象,但是永远没办法被访问到,同样除非线程结束运行。
但是只要ThreadLocal使用恰当,在使用完之后调用remove方法删除Entry对象,实际上是不会出现这个问题的。