【Java SE】十三、多线程

Java 给多线程编程提供了内置的支持。 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。多线程是多任务的一种特别的形式,但多线程使用了更小的资源开销。

普通的 Java 程序至少有三个线程:主线程、垃圾收集线程、异常处理线程

线程的生命周期

线程是一个动态执行的过程,它也有一个从产生到死亡的过程。如图所示:

img
  • 新建状态:

    创建一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序启动这个线程。

  • 就绪状态:

    当该线程启动之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待 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

此时死锁产生,这里给出几点建议:

  • 用专门的算法避开死锁
  • 少定义同步资源
  • 避免嵌套同步