【Java SE】十三、多线程
Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。
普通的 Java 程序至少有三个线程:主线程、垃圾收集线程、异常处理线程
线程的生命周期
线程是一个动态执行的过程,它也有一个从产生到死亡的过程。如图所示:
-
新建状态:
创建一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序启动这个线程。
-
就绪状态:
当该线程启动之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待 JVM 里线程调度器的调度。
-
运行状态:
如果就绪状态的线程获取 CPU 资源,就可以运行,此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
-
阻塞状态:
如果一个线程执行了 sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
- 等待阻塞:运行状态中的线程执行
wait()
方法,使线程进入到等待阻塞状态。 - 同步阻塞:线程在获取
synchronized
同步锁失败(因为同步锁被其他线程占用)。 - 其他阻塞:通过调用线程的
sleep()
或join()
发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep()
状态超时,join()
等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
- 等待阻塞:运行状态中的线程执行
-
死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到死亡状态。
创建线程
继承 Thread 类
Java 提供了一个 Thread 类来处理线程,我们可以用以下方式创建一个线程:
public class MyThread extends Thread { // 第一种方式:必须继承Thread类,受限于单继承性
@Override
public void run() {
// 这里写你要运行的代码
}
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start(); // 使用start()方法,而不是直接调用run()方法
// 下面是使用匿名方式创建线程
new MyThread() {
@Override
public void run() {...}
}.start();
}
实现 Runnable 接口
然鹅,开发中优先使用下面的方式创建线程:
class MyThread implements Runnable { // 第二种方式:实现Runnable接口
@Override
public void run() { // 只用实现这一个方法
// 这里写你要运行的代码
System.out.println(Thread.currentThread().getName()) // 不能用this关键字或直接调用,只能静态调用
}
}
public class ThreadTest1 {
public static void main(String[] args) {
MyThread sample = new MyThread();
Thread t1 = new Thread(sample); // 将实现类的对象作为Thread构造器的参数
t1.start();
Thread t2 = new Thread(sample);
t2.start();
}
}
第二种方式适用于多线程共用一个数据的情况*(实例变量);若用第一种数据,则需要将共用数据声明为静态(类变量)*
实现 Callable 接口
在 JDK 5.0 中新增了通过实现 Callable
接口来创建线程的方式,如下:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
// 因为Callable是一个原始类型。对泛型类型Callable<V>的引用应该参数化,所以这里我们使用<Integer>指明类型
public class MyThread implements Callable<Integer> { // 第三种方式:实现Callable接口
public static void main(String[] args) {
MyThread ctt = new MyThread(); // 创建实例
FutureTask<Integer> ft = new FutureTask<Integer>(ctt); // 使用FutureTask类来包装Callable对象
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " 的循环变量i的值" + i);
if (i == 20) {
new Thread(ft, "有返回值的线程").start(); // 使用FutureTask对象作为参数创建且命名并启动新线程
}
}
try {
System.out.println("子线程的返回值:" + ft.get()); // 调用FutureTask对象的get()方法来获得子线程的返回值
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
@Override
public Integer call() throws Exception { // 实现call()方法,以下为线程执行体
int i;
for (i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
return i;
}
}
使用线程池
在 JDK 5.0 中新增了线程池,它可以提前创建好线程存放在池中,使用时直接获取,用完放回池中,优点如下:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建和销毁)
- 线程池提供了一些方法便于管理线程
import java.util.concurrrrent.Executors;
class NumberThread implements Runnable {
@Override
public void run() {
// 这里写你要运行的代码
}
}
public class ThreadPoll {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
// ThreadPoolExecutor control = (ThreadPoolExecutor) service;
// control.setCorePoolSize(...) 设置核心池的大小
// control.setKeepAliveTime(...) 设置线程闲置存活时间
service.execute(new NumberThread()); // 加入并运行线程,不用手动start(),适用于Runnable
// service.submit(); 适用于Callable
service.shutdown(); // 关闭线程池
}
}
线程池常用于开发,目前不要求掌握,仅了解即可
注意事项:
- 第三种方式虽功能强大,但较复杂,如果没有对返回值的要求,则一般使用前两种方式
- 通过 Thread 和 Runnable 创建的线程,须用重写的方式将该线程的操作写在
run()
方法里,用 Callable 创建则是在call()
中 - 通过实现接口创建的线程类只能以静态方式调用线程的常用方法
- 启动线程使用
start()
方法,而不是直接调用run()
方法,使用线程池则用execute()
方法 - 除非用线程池,否则不能调用已经调用过
start()
的线程,就是说同一线程实例不能调用两次 - 第一种方式创建的多个线程共用一个 MyThread 类,而第二、三种方式则是共用一个 MyThread 对象
一些常用方法
- start():启动线程,并执行对象的
run()
方法 - run():线程在被调度时执行的操作
- getName():返回线程的名称
- setName(String name):设置该线程名称
- Thread.currentThread():返回当前线程。在 Thread 子类中就是
this
,通常用于主线程和 Runnable 实现类 - yield(): 线程让步
- 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
- 若队列中没有同优先级的线程,忽略此方法
- join():当某个程序执行流中调用其他线程的
join()
方法时,调用线程将被阻塞,直到join()
方法加入的 join 线程执行完为止- 低优先级的线程也可以获得执行
- sleep(long millis): (毫秒级)
- 令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队,但不会释放锁
- 会抛出
InterruptedException
异常
- wait():让进程进入阻塞状态并释放锁,在同步结构中使用且调用者必须是同步监视器,
notify() & notifyAll()
也是如此 - notify():随机唤醒一个在此对象监视器上等待的线程,常与
wait()
配合使用- notifyAll():唤醒所有在此对象监视器上等待的线程
- stop():强制线程生命期结束,已经过时,不推荐使用
- isAlive():判断线程是否还活着
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
try {
sleep(10); // 让线程暂停10ms,也就是进入阻塞状态
} catch (InterruptedException e) { // 该方法会抛出异常,必须准备捕获
e.printStackTrace();
}
System.out.println(getName() + ":" + i);
} // Java14 加入了yield关键字,所以必须通过类调用
if (i % 20 == 0) {Thread.yield();} // 提前释放占用的CPU资源,重新接受调度(也有可能还是分配到该线程,与优先级有关)
}
}
public MyThread(String name) {super(name);} // 这是用构造器命名,也可以用setName()
}
public class Test {
public static void main(String[] args) {
MyThread t1 = new MyThread("线程一"); // 构造器命名
t1.start();
Thread.currentThread().setName("主线程"); // 给主线程命名
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
if (i == 0) {
try {
t1.join(); // 让t1先运行,等t1运行完后在运行调用该方法的线程
} catch (InterruptedException e) { // 同上
e.printStackTrace();
}
}
}
System.out.println(t1.isAlive());
}
}
线程的运行具有不确定性,所以运行结果可能有所不同,故不展示。
线程的优先级
Thread 类中,优先级使用 1 ~ 10 的整数表示:
- 最低优先级 1:
Thread.MIN_PRIORITY
- 最高优先级 10:
Thread.MAX_PRIORITY
- 普通优先级 5:
Thread.NORM_PRIORITY
我们可以使用下面两个方法来设置线程的优先级:
- getPriority():获取线程的优先级
- setPriority():设置线程的优先级
public static void main(String[] args) { // 以下均为设置主线程的优先级
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
System.out.println(Thread.currentThread().getPriority()); // 1
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
System.out.println(Thread.currentThread().getPriority()); // 10
Thread.currentThread().setPriority(8); // 1 ~ 10
System.out.println(Thread.currentThread().getPriority()); // 8
}
注意:高优先级的线程要抢占低优先级线程 CPU 的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只有当高优先级的线程执行完以后,低优先级的线程才执行。
线程的同步
使用多线程的一大隐患就是安全问题,概括地说就是,多个线程“同时”访问一个数据。为了解决该问题,我们需要使用同步机制。
同步代码块
用 synchronized
关键字修饰的代码块,就叫同步代码块,它会被加上内置锁,使得多线程无法同时执行该代码块内的代码
// 以第二种方式展示
class MyThread implements Runnable {
// Object obj = new Object(); // 该类为实例属性,用第二种方式创建线程可以满足共用同一把锁
@Override
public void run() {
// synchronized(this) 通常这样用更方便
synchronized(obj) { // 括号内为同步监视器,俗称:锁
// 操作共享数据的代码
}
}
}
// 以第一种方式展示
public class MyThread extends Thread {
// private static Object obj = new Object(); // 该类为类属性,用第一种方式创建线程可以满足共用同一把锁
@Override
public void run() {
// synchronized(MyThread.class) 通常这样用更方便
synchronized(obj) {
// 操作共享数据的代码
}
}
}
注意事项:
- 任何一个类的实例对象都能充当锁,但要求多个线程共用同一把锁,即同一对象
- 使用同步机制即是把多线程的过程限制为单线程的过程,效率会降低
- 因为方式二共用一个的对象,所以更简便的方法是用当前对象充当锁,也就是
this
;方式一不适用 - 因为类也算对象 (Class 类的实例 ),所以方式一也可以用
MyThread.class
充当锁,方式二也适用
同步方法
如果操作共享数据的代码完整地存在于一个方法中,我们就可以将其声明为同步方法,如下:
// 同步方法不一定要run(),也可以自己定义
class MyThread implements Runnable { // 实现
@Override
public synchronized void run() { // 同步监视器默认为:this
// 操纵共享数据的代码
}
}
class MyThread extends Thread { // 继承
@Override
public static synchronized void run() { // 要声明为静态!同步监视器默认为:MyThread.class
// 操纵共享数据的代码
}
}
注意:确保操纵共享数据的代码被完整地包含在同步方法里,不能多,也不能少,同步代码块也一样
Lock 锁
在 JDK 5.0 中新增了一种解决线程安全的措施:Lock 锁。它可以得到和 synchronized
一样的效果,但不同的是,Lock 锁可手动获取锁和释放锁、可中断的获取锁、超时获取锁。
Lock 是一个接口,两个直接实现类:ReentrantLock(重入锁), ReentrantReadWriteLock(读写锁)。
示例如下:
import java.util.concurrent.locks.ReentrantLock;
public class MyLockStudy implements Runnable {
private int count;
ReentrantLock l = new ReentrantLock(); // 可传入参数fair,用来确保按先后顺序执行线程,
@Override
public void run() {
l.lock(); // 手动开启锁
for (int i = 0; i < 2; i++) {
System.out.println(Thread.currentThread().getName() + ": ");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
l.unlock(); // 手动解锁
}
public static void main(String args[]) {
MyLockStudy runn = new MyLockStudy();
Thread thread1 = new Thread(runn, "thread1");
Thread thread2 = new Thread(runn, "thread2");
Thread thread3 = new Thread(runn, "thread3");
thread1.start();
thread2.start();
thread3.start();
}
}
运行结果如下:
thread1:
thread1:
thread2:
thread2:
thread3:
thread3:
死锁
死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
java 死锁产生的四个必要条件:
- 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
- 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
- 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
- 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。
解决死锁问题的方法是:一种是用 synchronized,一种是用 Lock 显式锁实现。
而如果不恰当的使用了锁,且出现同时要锁多个对象时,会出现死锁情况,如下:
import java.util.Date;
public class Test {
public static String obj1 = "obj1";
public static String obj2 = "obj2";
public static void main(String[] args) {
LockA la = new LockA();
new Thread(la).start();
LockB lb = new LockB();
new Thread(lb).start();
}
}
class LockA implements Runnable {
public void run() {
try {
System.out.println(new Date().toString() + " LockA 开始执行");
while (true) {
synchronized (Test.obj1) {
System.out.println(new Date().toString() + " LockA 锁住 obj1");
Thread.sleep(3000); // 此处等待是给B能锁住机会
synchronized (Test.obj2) {
System.out.println(new Date().toString() + " LockA 锁住 obj2"); // 多半不会执行,因为死锁
Thread.sleep(60 * 1000); // 为测试,占用了就不放
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class LockB implements Runnable {
public void run() {
try {
System.out.println(new Date().toString() + " LockB 开始执行");
while (true) {
synchronized (Test.obj2) {
System.out.println(new Date().toString() + " LockB 锁住 obj2");
Thread.sleep(3000); // 此处等待是给A能锁住机会
synchronized (Test.obj1) {
System.out.println(new Date().toString() + " LockB 锁住 obj1");
Thread.sleep(60 * 1000); // 为测试,占用了就不放
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果如下:
Tue May 05 10:51:06 CST 2015 LockB 开始执行
Tue May 05 10:51:06 CST 2015 LockA 开始执行
Tue May 05 10:51:06 CST 2015 LockB 锁住 obj2
Tue May 05 10:51:06 CST 2015 LockA 锁住 obj1
此时死锁产生,这里给出几点建议:
- 用专门的算法避开死锁
- 少定义同步资源
- 避免嵌套同步