Java中的线程与线程池——线程篇

484 阅读10分钟

后篇 :Java中的线程与线程池——线程池篇

线程

进程是什么?

在了解线程之前,首先需要了解的是进程。

进程是指运行中的应用程序,每个进程都有自己独立的地址空间(内存空间)。用户每启动一个进程,操作系统就会为该进程分配一个独立的内存空间。

比如用户点开桌面的浏览器,就启动了一个进程,操作系统就会为该进程分配独立的地址空间。当用户再次点开桌面的浏览器,就又启动了一个进程,操作系统将为其分配新的独立的地址空间。

线程是什么?

线程是进程中的一个实体,是被系统独立调度和分派的基本单位。线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可以与同属于一个进程的其他线程共享进程所拥有的全部资源。

线程的主要特点在于 :

  • 线程是轻量级的进程;
  • 线程没有独立的地址空间(内存空间);
  • 线程是由进程创建的,是寄生在进程的;
  • 一个进程可以拥有多个线程。

Java中如何创建线程

Java中线程的创建主要包含四种方式。

通过继承 Thread 类创建线程

首先继承 Thread 类,然后重写其 run() 方法。

public class DemoThread extends Thread {
    @Override
    public void run() {
        System.out.println("这是重写的run()方法");
    }

    public static void main(String[] args) {
        // 启动线程
        new DemoThread().start();
    }
}

通过实现 Runnable 接口创建线程

首先实现 Runnable 接口,然后重写 run() 方法。

public class DemoRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("这是重写的 run() 方法");
    }

    public static void main(String[] args) {
        // 启动线程
        new Thread(new DemoRunnable()).start();
    }
}

使用 Callable 和 Future 创建线程

首先实现 Callable 接口,然后重写其 call() 方法。

public class DemoCallable implements Callable {

    @Override
    public Object call() throws Exception {
        return Arrays.asList("1", "2", "3", "4");
    }
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
        // 定义任务
        FutureTask<List<String>> futureTask = new FutureTask<>(new DemoCallable());
        
        // 启动线程
        Thread thread = new Thread(futureTask);
        thread.start();
        
        // 获取结果
        List<String> list = futureTask.get();
        
        // 处理结果
        list.forEach(System.out::println);
    }
}

使用线程池创建线程

首先需要一个实现了 Runnable 的线程,然后通过线程池来创建它 :

public class DemoThreadPoolExecutor {

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE  = 10;
    private static final int QUEUE_CAPACITY = 100;
    private static final Long KEEP_ALIVE_TIME = 1L;

    public static void main(String[] args) {
        // 使用线程池来创建线程
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                // 核心线程数为 :5
                CORE_POOL_SIZE,
                // 最大线程数 :10
                MAX_POOL_SIZE,
                // 等待时间 :1L
                KEEP_ALIVE_TIME,
                // 等待时间的单位 :秒
                TimeUnit.HOURS,
                // 任务队列为 ArrayBlockingQueue,且容量为 100
                new ArrayBlockingQueue<>(QUEUE_CAPACITY),
                // 饱和策略为 CallerRunsPolicy
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        for(int i = 0; i < 10; i++) {
            // 创建WorkerThread对象,该对象需要实现Runnable接口
            Runnable worker = new DemoThread("任务" + i);
            // 通过线程池执行Runnable
            threadPoolExecutor.execute(worker);
        }
        // 终止线程池
        threadPoolExecutor.shutdown();
        while (!threadPoolExecutor.isTerminated()) {
            // 在这里进行终止异常处理
        }
        System.out.println("全部线程已终止");
    }
}

线程的状态

让我们首先进入 Thread.State 来看一下线程中定义的六种状态吧!

public enum State {
    // 线程刚创建
    NEW,
 
    // 在JVM中正在运行的线程
    RUNNABLE,
 
    // 线程处于阻塞状态,等待监视锁,可以重新进行同步代码块中执行
    BLOCKED,
 
    // 线程处于等待状态
    WAITING,
 
    // 线程处于定时等待状态,在等待一段时间后能够自己将自己唤醒
    TIMED_WAITING,
 
    // 线程执行完毕,已经退出
    TERMINATED;
}

总体上来说可以用这张图概括 :

新建状态

NEW : 新创建了一个线程时,线程所处于的状态。

public class Demo {
    public static void main(String[] args) {
        DemoThread demoThread = new DemoThread();
        System.out.println(demoThread.getState());
    }
}

可运行状态

RUNNABLE : 处于该状态的线程正在 JVM 中执行。但这里的“执行”不一定是真的在运行,也有可能是在等待系统为其分配 CPU 资源。

public class Demo {
    public static void main(String[] args) {
        DemoThread demoThread = new DemoThread();
        demoThread.start();
        System.out.println(demoThread.getState());
    }
}

阻塞状态

BLOCKED : 该状态多用来表示线程正在等待获取某个锁,以继续执行下面的步骤。

典型的如 synchronized 关键字,被该关键字修饰的代码或方法,在执行前需要获取到对应的锁;而未获取到锁之前,线程将一直处于阻塞状态。

首先对自定义的线程类进行改造,延长运行时间并给线程添加锁条件 :

public class DemoThread extends Thread {

    private byte[] lock = new byte[0];

    public DemoThread(byte[] lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

然后模拟线程阻塞 :

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        byte[] lock = new byte[0];
        DemoThread thread1 = new DemoThread(lock);
        thread1.start();
        DemoThread thread2 = new DemoThread(lock);
        thread2.start();
        // 使主线程休眠,从而使该时间内thread2无法拿到锁
        Thread.sleep(1000);
        System.out.println(thread2.getState());
    }
}

等待状态

WAITING : 处于这种状态的线程不会被分配 CPU 时间,它们需要等待被显示地唤醒,否则会处于无限等待的状态。

在执行了如下的一些代码后,线程会进入该状态 :

  • Object.wait()
  • Thread.join()
  • LockSupport.park()

改造自定义线程类,调用 LockSupport.park();

public class DemoThread extends Thread {

    private byte[] lock = new byte[0];

    public DemoThread(byte[] lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            // 为了线程调度,禁用当前线程,除非许可可用
            LockSupport.park();
        }
    }
}

在主线程中模拟 WAITING 状态 :

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        byte[] lock = new byte[0];
        DemoThread thread = new DemoThread(lock);
        thread.start();
        Thread.sleep(100);
        // thread禁用中,这里输出 WAITING
        System.out.println(thread.getState());
        // 解除thread1,使其可用
        LockSupport.unpark(thread);
        Thread.sleep(100);
        // 到这里执行完毕,输出 TERMINATED
        System.out.println(thread.getState());
    }
}

定时等待

TIMED_WAITING : 处于这种状态的线程不会被分配CPU执行时间,但不需要无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。

在执行了如下代码的时候,线程会进入该状态 :

  • Thread.sleep(long)
  • Object.wait(long)
  • Thread.join(long)
  • LockSupport.parkNanos()
  • LockSupport.parkUntil()

用一个简单的自定义线程类模拟一下就好啦 :

public class DemoThread extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public class Demo {
    public static void main(String[] args) throws InterruptedException {
        DemoThread thread = new DemoThread();
        thread.start();
        Thread.sleep(100);
        System.out.println(thread.getState());
    }
}

终止状态

TERMINATED : 当线程的 run() 方法完成时,或者主线程的 main() 方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。

正常执行完成的线程,最后一个状态通常都属于终止状态 :

public class DemoThread extends Thread {
    @Override
    public void run() {
        System.out.println("这是一个很快就能执行完成的线程");
    }
}
public class Demo {
    public static void main(String[] args) throws InterruptedException {
        DemoThread thread = new DemoThread();
        thread.start();
        Thread.sleep(10);
        System.out.println(thread.getState());
    }
}

线程中的常用方法

join() 方法

执行该方法的线程进入阻塞状态,直到调用该方法的线程结束后再由阻塞转为就绪状态。

interrupt() 方法

结束线程在调用 Object 类的 wait() 方法或该类的 join() 方法、sleep() 方法过程中的阻塞状态,并产生 InterruptedException 异常。

wait() 方法

使当前线程进入等待状态,并将该线程置入锁对象的等待队列中,直到接到通知或被中断为止。wait() 方法只能在同步方法或同步代码块中调用,当方法执行后,当前线程释放锁,其他线程可以竞争该锁;若调用 wait() 时没有适当的锁,会抛出异常。

notify() 方法

该方法必须在同步方法或同步代码块中调用,用来唤醒等待在该对象上的线程。如果有多个线程等待,则任意挑选一个线程进行唤醒。

需要注意的是,notify() 并不释放锁,只是告诉自己唤醒的这个线程,它可以去参与获得锁的竞争了。但至于是否能够得到锁,或是否能马上得到锁,要取决于别人是否有释放锁,以及调度系统是否有选中它。

notifyAll() 方法

notify() 相似,不过它唤醒的是在该对象上等待的所有线程。

setDaemon() 方法

用于将一个尚未调用线程 start() 方法的线程设置为守护线程。

守护线程主要用于为其他线程的运行提供服务,比如 Java 中的垃圾回收机制就是使用的一个守护线程。这种线程属于创建它的线程,守护线程随着主线程的终止而终止。

isAlive() 方法

判定该线程是否处于就绪、运行或阻塞状态,如果是则返回 true,否则返回 false。

start() 方法

使该线程开始启动,Java 虚拟机负责调用该线程的 run() 方法。多次启动一个线程是非法的。

sleep(long) 方法

Thread 类的静态方法,使线程进入休眠状态,并在指定时间 (单位为毫秒) 到达后自动唤醒。

需要注意的是,线程自动唤醒后进入的依然是就绪状态,而不是直接进入执行状态。

yield() 方法

Thread 类的静态方法,使当前线程放弃占用 CPU 资源,回到就绪状态,使其他优先级不低于此线程的线程有机会被执行。

setPriority(int) 方法

设置当前线程的优先级,线程优先级越高,线程获得执行的次数越多。Java线程的优先级用整数表示,取值范围是1~10。

Thread类有以下三个静态常量:

  • static int MAX_PRIORITY :最高优先级值为10;
  • static int NORM_PRIORITY :默认优先级值为5;
  • static int MIN_PRIORITY :最低优先级值为1。

getPriority() 方法

获得当前线程的优先级。

线程同步

这里主要介绍一下 synchronized 关键字的用法。

修饰类

作用范围为 synchronized 括起来的部分,作用的对象是该类的所有对象。

public class Demo {
    public void method() {
        synchronized(Demo.class) {
            // todo
        }
    }
}

修饰方法

直接加在方法头上,锁住的是该方法所在的那个对象。

public class Demo {
    public synchronized void method() {
        // todo
    }
}

注意事项 :

  • 若方法为抽象方法,则不能使用 synchronized 关键字修饰;
  • 构造方法不能使用 synchronized 关键字,但可以使用 synchronized 代码块进行同步;
  • synchronized 关键字不能被继承。即,若子类覆盖了父类的 被 synchronized 关键字修饰的方法,那么只要子类的该方法没有显式声明 synchronized ,就默认该方法不同步。

修饰静态方法

直接加在方法头上,锁住的是这个类的全部对象。

public class Demo {
    public synchronized static void method() {
        // todo
    }
}

修饰同步代码块

锁住的是当前对象,但其他线程可以访问其未加锁代码块。

public class Demo {
    public void method() {
        synchronized (this) {
            // todo
        }
    }
}

其他一点区分

sleep() 与 wait()

sleep() : Thread 类的静态本地方法,只能在同步环境中被调用;调用后会使当前线程进入休眠,同时释放 CPU 资源,不释放对象锁;休眠时间到后自动苏醒并继续执行。

wait() : Object 类的成员本地方法,被调用时会放弃当前持有的对象锁并进入等待队列,仅有当该对象被调用 notify()notifyAll() 方法后,才有机会再次竞争获取对象锁,进入运行状态。

sleep() 与 yield()

yield() 方法会临时暂停当前正在执行的线程,并放弃当前 CPU,来让有同样优先级的正在等待的线程有机会执行。如果没有正在等待的线程,或所有正在等待的线程的优先级都比较低,则该线程会重新拿过 CPU 继续运行。yield() 方法的具体调度与操作系统相关。

相比于 sleep() 方法,它 :

  • sleep() 方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield() 方法只会给相同优先级或更高优先级的线程以运行的机会;
  • 线程执行 sleep() 方法后转入阻塞状态,而执行 yield() 方法后转入就绪状态;
  • sleep() 方法声明抛出 InterruptedException,而 yield() 方法没有声明异常
  • sleep() 方法比 yield() 方法(与操作系统 CPU 调度相关)具有更好的可移植性