多线程是Java最基本的一种并发模型;Java语言内置了多线程支持;
进程和线程
进程和线程的关系就是:进程和线程是包含关系;一个进程可以包含一个或多个线程,但至少会有一个线程;
在计算机中,我们把一个任务称为一个进程,浏览器就是一个进程,视频播放器是另一个进程,类似的,音乐播放器和Word都是进程。
某些进程内部还需要同时执行多个子任务。例如,我们在使用Word时,Word可以让我们一边打字,一边进行拼写检查,同时还可以在后台进行打印,我们把子任务称为线程。
启动多线程
要创建一个新线程非常容易,只需要实例化一个 Thread 实例,然后调用它的 start() 方法;
package com.demo;
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = new MyThread();
thread = new Thread(new MyRunnable());
//优先级高的线程被操作系统调度的优先级较高,操作系统对高优先级线程可能调度更频繁,但我们决不能通过设置优先级来确保高优先级的线程一定会先执行。
thread.setPriority(10); //设置优先级
thread.start();
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println("my thread run...");
}
}
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("myRunnable is run....");
}
}
常见的两种方法创建 Thread 实例:
- 从 Thread 派生一个自定义类,然后覆写 run() 方法
- 创建 Thread 实例时,传入一个 Runnable 实例
线程状态
在Java程序中,一个线程对象只能调用一次 start() 方法启动新线程,并在新线程中执行 run() 方法。一旦 run() 方法执行完毕,线程就结束了。因此,Java线程的状态有以下几种:
- New:新创建的线程,尚未执行;
- Runnable:运行中的线程,正在执行 run() 方法的Java代码;
- Blocked:运行中的线程,因为某些操作被阻塞而挂起;
- Waiting:运行中的线程,因为某些操作在等待中;
- Timed Waiting:运行中的线程,因为执行 sleep() 方法正在计时等待;
- Terminated:线程已终止,因为 run() 方法执行完毕
当线程启动后,它可以在 Runnable 、 Blocked 、 Waiting 和 Timed Waiting 这几个状态之间切换,直到最后变成 Terminated 状态,线程终止。
一个线程还可以等待另一个线程直到其运行结束。例如, main 线程在启动 t 线程后,可以通过 t.join() 等待 t 线程结束后再继续运行
操作线程
中断线程两种方式:
对目标线程调用 interrupt() 方法可以请求中断一个线程,目标线程通过检测 isInterrupted() 标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到 InterruptedException ;目标线程检测到 isInterrupted() 为 true 或者捕获了InterruptedException 都应该立刻结束自身线程;
通过标志位判断需要正确使用 volatile 关键字;volatile 关键字解决了共享变量在线程间的可见性问题。
为什么要对线程间共享的变量用关键字 volatile 声明?
在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的;volatile 关键字的目的是告诉虚拟机:
每次访问变量时,总是获取主内存的最新值;
每次修改变量后,立刻回写到主内存。
线程同步synchronized
多线程模型下,要保证逻辑正确,对共享变量进行读写时,必须保证一组指令以原子方式执行:即某一个线程执行时,其他线程必须等待:
保证一段代码的原子性就是通过加锁和解锁实现的。Java程序使用 synchronized 关键字对一个对象进行加锁;
使用 synchronized的步骤 :
- 找出修改共享变量的线程代码块;
- 选择一个共享实例作为锁;
- 使用 synchronized(lockObject) { … } 。在使用 synchronized 的时候,不必担心抛出异常。因为无论是否有异常,都会在 synchronized 结束处正确释放锁;
用 synchronized 修饰方法可以把整个方法变为同步代码块, synchronized 方法加锁对象是 this ;
一个类没有特殊说明,默认不是thread-safe;
Java的 synchronized 锁是可重入锁;
死锁产生的条件是多线程各自持有不同的锁,并互相试图获取对方已持有的锁,导致无限等待;
等待和唤醒
wait() 和 notify() 用于多线程协调运行:
- 在 synchronized 内部可以调用 wait() 使线程进入等待状态;
- 必须在已获得的锁对象上调用 wait() 方法;
- 在 synchronized 内部可以调用 notify() 或 notifyAll() 唤醒其他等待线程;
- 必须在已获得的锁对象上调用 notify() 或 notifyAll() 方法;
- 已唤醒的线程还需要重新获得锁后才能继续执行。
使用 notifyAll() 将唤醒所有当前正在 this 锁等待的线程,而 notify() 只会唤醒其中一个(具体哪个依赖操作系统,有一定的随机性)。这是因为可能有多个线程正在 getTask() 方法内部的 wait() 中等待,使用 notifyAll() 将一次性全部唤醒。通常来说, notifyAll() 更安全;
线程安全包
使用 java.util.concurrent 包提供的线程安全的并发集合可以大大简化多线程编程:多线程同时读写并发集合是安全的;
使用 java.util.concurrent.atomic 提供的原子操作可以简化多线程编程:原子操作实现了无锁的线程安全;适用于计数器,累加器等
线程池
能接收大量小任务并进行分发处理的就是线程池;
简单地说,线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理
Java标准库提供了 ExecutorService 接口表示线程池;
ExecutorService 只是接口,Java标准库提供的几个常用实现类有:
- FixedThreadPool:线程数固定的线程池;
- CachedThreadPool:线程数根据任务动态调整的线程池;
- SingleThreadExecutor:仅单线程执行的线程池。
ExecutorService executorService = Executors.newFixedThreadPool(10);
executorService = Executors.newCachedThreadPool();
executorService = Executors.newSingleThreadExecutor();
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("");
}
});
线程池在程序结束的时候要关闭。使用 shutdown() 方法关闭线程池的时候,它会等待正在执行的任务先完成,然后再关闭。shutdownNow() 会立刻停止正在执行的任务, awaitTermination() 则会等待指定的时间让线程池关闭。
需要反复执行的任务,可以使用 ScheduledThreadPool 。放入 ScheduledThreadPool 的任务可以定期反复执行。
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
scheduledExecutorService.schedule(new Runnable() {
@Override
public void run() {
System.out.println("....");
}
},10, TimeUnit.MILLISECONDS);
获取线程返回值
Java标准库还提供了一个 Callable 接口,和 Runnable 接口比,它多了一个返回值:并且 Callable 接口是一个泛型接口,可以返回指定类型的结果。
当我们提交一个 Callable 任务后,我们会同时获得一个 Future 对象,然后,我们在主线程某个时刻调用 Future 对象的 get() 方法,就可以获得异步执行的结果。在调用 get() 时,如果异步任务已经完成,我们就直接获得结果。如果异步任务还没有完成,那么 get() 会阻塞,直到任务完成后才返回结果。
一个 Future 接口表示一个未来可能会返回的结果,它定义的方法有:
- get() :获取结果(可能会等待)
- get(long timeout, TimeUnit unit) :获取结果,但只等待指定的时间;
- cancel(boolean mayInterruptIfRunning) :取消当前任务;
- isDone() :判断任务是否已完成
从Java 8开始引入了 CompletableFuture ,它针对 Future 做了改进,可以传入回调对象,当异步任务完成或者发生异常时,自动调用回调对象的回调方法。