7、多线程
对变量的访问和修改应该做到 "原子性",一气呵成
concurrent:并发
volatile:易变的
synchronized:同步的
1、概念
StringBuffer 类是线程安全的类,但 StringBuilder 类不是线程安全的类
Vector 类和 Hashtable 类是线程安全的类,但 ArrayList 类和 HashMap 类不是线程安全的类
Collections.synchronizedList() 和 Collections.synchronizedMap() 等方法实现安全
1.1、进程与线程
- 进程:是 "操作系统" 中的概念,一个独立运行的程序就是一个 "进程"
多进程:操作系统可以同时允许多个 "应用程序" 运行,可以提高用户的体验度 - 线程:是由 "进程" 创建,指一个进程中,将一段代码分离出来,与 "主进程" 同时运行
多线程:一个进程可以开启多个线程,同时 "做多件事",可以提高程序的效率,提高用户的体验度
1.2、并发与并行
- 并发:一颗 CPU,两个线程,频繁切换线程 concurrent
- 并行:两颗 CPU,两个线程
并发:CPU 分时轮询的执行线程;并行:同一个时刻同时在执行
2、多线程的创建
一个线程对象,只能 start() 一次,不能多次 start()
继承 Thread 类和实现 Runnable 接口的缺点
- run() 方法不能返回值
- run() 方法不能抛出 "编译时" 异常
2.1、继承 Thread 类
定义一个子类 MyThread 继承线程类 java.lang.Thread,重写 run() 方法
创建 MyThread 类的对象,调用线程对象的 start() 方法启动线程(启动后还是执行 run 方法的)
线程有执行结果是不可以直接返回的
把子线程放在主线程之前
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
MyThread t = new MyThread();
t.start();
2.2、实现 Runnable 接口
定义一个线程任务类 MyRunnable 实现 Runnable 接口,重写 run() 方法
创建 MyRunnable 任务对象,并把 MyRunnable 任务对象交给 Thread 处理
调用线程对象的 start() 方法启动线程
编程多一层对象包装,如果线程有执行结果是不可以直接返回的
也可以通过匿名内部类的方式来实现
构造器 | 说明 |
---|---|
public Thread(String name) | 可以为当前线程指定名称 |
public Thread(Runnable target) | 封装 Runnable 对象成为线程对象 |
public Thread(Runnable target, String name ) | 封装 Runnable 对象成为线程对象,并指定线程名称 |
MyRunnable target = new MyRunnable();
Thread t = new Thread(target);
t.start();
Runnable r = new Runnable() {
@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println(i);
}
}
};
Thread t = new Thread(r);
t.start();
2.3、JDK 5.0 新增:实现 Callable 接口
利用 Callable、FutureTask 接口实现
- 得到任务对象
- 定义类实现 Callable 接口,重写 call 方法,封装要做的事情,泛型是方法返回的类型
- 用 FutureTask 把 Callable 对象封装成线程任务对象
- 把线程任务对象交给 Thread 处理
- 调用 Thread 的 start 方法启动线程,执行任务
- 线程执行完毕后、通过 FutureTask 的 get 方法去获取任务执行的结果
方法名 | 说明 |
---|---|
public FutureTask<>(Callable call) | 把 Callable 对象封装成 FutureTask 对象 |
public boolean isDone() | 返回任务是否已完成 |
public V get() throws Exception | 获取线程执行 call 方法返回的结果 |
Callable<String> myCall = new Callable<String>() {
@Override
public String call() throws Exception {
for (int i = 0; i < 30; i++) {
System.out.println(Thread.currentThread().getName() + i);
}
return "ok";
}
};
FutureTask<String> f = new FutureTask<>(myCall);
Thread t = new Thread(f);
t.start();
while (!f.isDone()) {
System.out.println("子线程任务还没结束, 主线程先干点别的");
}
String result = f.get(); // 它会阻塞, 必须等线程结束来获取结果
System.out.println(result);
3、Thread 常用方法
Thread 常用方法:获取线程名称 getName()、设置名称 setName()、获取当前线程对象 currentThread()
至于 Thread 类提供的诸如:yield、join、interrupt、不推荐的方法 stop 、守护线程、线程优先级等线程的控制方法,在开发中很少使用
currentThread() 这个方法是在哪个线程执行中调用的,就会得到哪个线程对象,主线程的名称叫 main
- 继承 Thread 类,创建构造器调用父类构造器(String name)
- 继承 Thread 类,用 setName(String name)
- 实现 Runnable 接口,创建对象,交给 Thread 处理的时候传入 name
- Thread.currentThread().getName()
方法名 | 说明 |
---|---|
String getName() | 获取当前线程的名称,默认线程名称是 Thread-索引 |
void setName(String name) | 将此线程的名称更改为指定的名称,通过构造器也可以设置线程名称 |
public void run() | 线程任务方法 |
public void start() | 线程启动方法 |
public final void setPriority(int newPriority) | 设置线程优先级别(1 ~ 10,默认为 5) |
public final void join() | 当一个线程调用了 join 方法,这个线程就会先被执行,它执行结束以后才可以去执行其余的线程 先 start 后 join |
public final void setDaemon(boolean on) | 设置伴随线程,先设置再启动,主线程停止的时候子线程也不要继续执行了 |
方法名 | 说明 |
---|---|
public static Thread currentThread() | 返回对当前正在执行的线程对象的引用 |
public static void sleep(long time) | 让当前线程休眠指定的时间后再继续执行,单位为毫秒 |
构造器 | 说明 |
---|---|
public Thread(String name) | 可以为当前线程指定名称 |
public Thread(Runnable target) | 封装 Runnable 对象成为线程对象 |
public Thread(Runnable target, String name ) | 封装 Runnable 对象成为线程对象,并指定线程名称 |
3.1、中断
Thread.currentThread().interrupt(); // 设置当前线程的中断状态为中断
// Thread.interrupted():判断当前线程是否被中断,并清除中断状态
// Thread.currentThread().isInterrupted():判断当前线程是否被中断,而不清除中断状态
// 如果线程已经处于终结状态,即使线程被中断过,在调用该线程对象的 isInterrupted() 时依旧会返回 false
if (Thread.interrupted()) {
System.out.println("线程被中断");
} else {
System.out.println("线程未被中断");
}
在 Java 的 API 中可以看到,许多声明抛出 InterruptedException 的方法(例如 Thread.sleep(long millis) 方法)
这些方法在抛出 InterruptedException 之前,Java 虚拟机会将该线程的中断标识位清除,然后抛出 InterruptedException,此时调用 isInterrupted() 将会返回 false
public class SleepUtils {
public static final void second(long seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
}
}
}
public class Interrupted {
static class SleepRunner implements Runnable {
@Override
public void run() {
while (true) {
SleepUtils.second(10);
}
}
}
static class BusyRunner implements Runnable {
@Override
public void run() {
while (true) {
}
}
}
public static void main(String[] args) throws Exception {
// sleepThread 不停的尝试睡眠
Thread sleepThread = new Thread(new SleepRunner(), "SleepThread");
sleepThread.setDaemon(true);
// busyThread 不停的运行
Thread busyThread = new Thread(new BusyRunner(), "BusyThread");
busyThread.setDaemon(true);
sleepThread.start();
busyThread.start();
// 休眠 5 秒, 让 sleepThread 和 busyThread 充分运行
TimeUnit.SECONDS.sleep(5);
sleepThread.interrupt();
busyThread.interrupt();
System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted()); // false
System.out.println("BusyThread interrupted is " + busyThread.isInterrupted()); // true
// 防止 sleepThread 和 busyThread 立刻退出
TimeUnit.SECONDS.sleep(2);
}
}
3.2、安全地终止线程
public class Shutdown {
private static class Runner implements Runnable {
private long i;
private volatile boolean on = true;
@Override
public void run() {
while (on && !Thread.currentThread().isInterrupted()) {
i++;
}
System.out.println("Count i = " + i);
}
public void cancel() {
on = false;
}
}
public static void main(String[] args) throws Exception {
Runner one = new Runner();
Thread countThread = new Thread(one, "CountThread");
countThread.start();
// 睡眠 1 秒, main 线程对 Runner one 进行中断, 使 CountThread 能够感知中断而结束
TimeUnit.SECONDS.sleep(1);
countThread.interrupt();
Runner two = new Runner();
countThread = new Thread(two, "CountThread");
countThread.start();
// 睡眠 1 秒, main 线程对 Runner two 进行取消, 使 CountThread 能够感知 on 为 false 而结束
TimeUnit.SECONDS.sleep(1);
two.cancel();
}
}
4、线程池
4.1、简介
线程池就是一个可以复用线程的技术
如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能
JDK 5.0 起提供了代表线程池的接口:java.util.concurrent.ExecutorService
- 可以缓存 "线程对象",并且可以 "重用线程对象",不需要每次都 new 一个新的线程对象,提高效率
- 可以控制多个线程的 "并发数量"
如何得到线程池对象
- 使用 ExecutorService 的实现类 ThreadPoolExecutor 自己创建一个线程池对象
- 使用 Executors(线程池的工具类)调用方法返回不同特点的线程池对象
线程池常见面试题
- 临时线程什么时候创建?新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程
- 什么时候会开始拒绝任务?核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝
public ThreadPoolExecutor(int corePoolSize, // 指定线程池的线程数量(核心线程) 不能小于 0
int maximumPoolSize, // 指定线程池可支持的最大线程数 最大数量 >= 核心线程数量
long keepAliveTime, // 指定临时线程的最大存活时间 不能小于0
TimeUnit unit, // 指定存活时间的单位(秒、分、时、天) 时间单位
BlockingQueue<Runnable> workQueue, // 指定任务队列 不能为 null
ThreadFactory threadFactory, // 指定用哪个线程工厂创建线程 不能为 null
RejectedExecutionHandler handler) // 指定线程忙, 任务满的时候, 新任务来了怎么办 不能为 null
4.2、ExecutorService 的常用方法
方法名 | 说明 |
---|---|
void execute(Runnable command) | 执行任务 / 命令,没有返回值,一般用来执行 Runnable 任务 |
Future<T> submit(Callable<T> task) | 执行任务,返回未来任务对象获取线程结果,一般拿来执行 Callable 任务 |
void shutdown() | 等任务执行完毕后关闭线程池 |
List<Runnable> shutdownNow() | 立刻关闭,停止正在执行的任务,并返回队列中未执行的任务 |
4.3、新任务拒绝策略
策略 | 说明 |
---|---|
ThreadPoolExecutor.AbortPolicy | 丢弃任务并抛出 RejectedExecutionException 异常(是默认的策略) |
ThreadPoolExecutor.DiscardPolicy | 丢弃任务,但是不抛出异常 这是不推荐的做法 |
ThreadPoolExecutor.DiscardOldestPolicy | 抛弃队列中等待最久的任务,然后把当前任务加入队列中 |
ThreadPoolExecutor.CallerRunsPolicy | 由主线程负责调用任务的 run() 方法从而绕过线程池直接执行 |
4.4、Executors 工具类实现线程池
Executors:线程池的工具类通过调用方法返回不同类型的线程池对象
Executors 的底层其实也是基于线程池的实现类 ThreadPoolExecutor 创建线程池对象的
Executors 得到线程池对象的常用方法
方法名 | 说明 |
---|---|
public static ExecutorService newCachedThreadPool() | 线程数量随着任务增加而增加 如果线程任务执行完毕且空闲了一段时间则会被回收掉 |
public static ExecutorService newFixedThreadPool(int nThreads) | 创建固定线程数量的线程池,如果某个线程因为执行异常而结束 那么线程池会补充一个新线程替代它 |
public static ExecutorService newSingleThreadExecutor () | 创建只有一个线程的线程池对象 如果该线程出现异常而结束,那么线程池会补充一个新线程 |
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 创建一个线程池 可以实现在给定的延迟后运行任务,或者定期执行任务 |
Executors 使用可能存在的陷阱:大型并发系统环境中使用 Executors 如果不注意可能会出现系统风险
方法名 | 存在问题 |
---|---|
public static ExecutorService newFixedThreadPool(int nThreads) | 允许请求的任务队列长度是 Integer.MAX_VALUE |
public static ExecutorService newSingleThreadExecutor() | 可能出现 OOM 错误 (java.lang.OutOfMemoryError) |
public static ExecutorService newCachedThreadPool() | 创建的线程数量最大上限是 Integer.MAX_VALUE, 线程数可能会随着 |
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 任务 1 : 1 增长,也可能出现 OOM 错误 (java.lang.OutOfMemoryError) |
5、定时器
定时器是一种控制任务延时调用,或者周期调用的技术
作用:闹钟、定时邮件发送
实现方式:Timer 或 ScheduledExecutorService
5.1、Timer 定时器
Timer 定时器的特点和存在的问题
- Timer 是单线程,处理多个任务按照顺序执行,存在延时,与设置定时器的时间有出入
- 可能因为其中的某个任务的异常使 Timer 线程死掉,从而影响后续任务执行
方法名 | 说明 |
---|---|
public Timer() | 创建 Timer 定时器对象 |
public void schedule(TimerTask task, long delay) | 在指定的延迟后执行指定的任务 |
public void schedule(TimerTask task, Date time) | 在指定的时间执行指定的任务 |
public void schedule(TimerTask task, long delay, long period) | 开启一个定时器,间隔重复处理 TimerTask 任务 |
public void schedule(TimerTask task, Date firstTime, long period) | 开启一个定时器,间隔重复处理 TimerTask 任务 |
Timer timer = new Timer(); // 定时器本身就是一个单线程
// A 任务
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行 AAA");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 3000, 2000);
// B 任务
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "执行 BBB");
}
}, 3000, 2000);
// 结果: A 任务会影响 B 任务
5.2、ScheduledExecutorService 定时器
ScheduledExecutorService 是 jdk 1.5 中引入了并发包,目的是为了弥补 Timer 的缺陷,ScheduledExecutorService 内部为线程池
优点:基于线程池,某个任务的执行情况不会影响其他定时任务的执行
Executors 的方法 | 说明 |
---|---|
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) | 得到线程池对象 |
ScheduledExecutorService 的方法 | 说明 |
---|---|
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) | 周期调度方法 |
ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
// A 任务
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 执行输出: AAA");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 3, 2, TimeUnit.SECONDS);
// B 任务
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 执行输出: BBB");
}
}, 3, 2, TimeUnit.SECONDS);
// 结果: A 任务和 B 任务之间独立, 不会相互影响
本文来自博客园,作者:lidongdongdong~,转载请注明原文链接:https://www.cnblogs.com/lidong422339/p/17478809.html