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 接口实现

  1. 得到任务对象
    1. 定义类实现 Callable 接口,重写 call 方法,封装要做的事情,泛型是方法返回的类型
    2. 用 FutureTask 把 Callable 对象封装成线程任务对象
  2. 把线程任务对象交给 Thread 处理
  3. 调用 Thread 的 start 方法启动线程,执行任务
  4. 线程执行完毕后、通过 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)

image-20211108232005852

5、定时器

定时器是一种控制任务延时调用,或者周期调用的技术
作用:闹钟、定时邮件发送
实现方式:Timer 或 ScheduledExecutorService

5.1、Timer 定时器

Timer 定时器的特点和存在的问题

  1. Timer 是单线程,处理多个任务按照顺序执行,存在延时,与设置定时器的时间有出入
  2. 可能因为其中的某个任务的异常使 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 任务之间独立, 不会相互影响
posted @ 2023-06-13 21:51  lidongdongdong~  阅读(22)  评论(0编辑  收藏  举报