Java多线程入门


一、基础概念

1. 进程与线程

(1) 进程

进程是一个程序关于某个数据集合的一次运行活动,它是操作系统动态执行的基本单元,是并发执行的程序在执行过程中分配和管理资源的基本单位,竞争计算机系统资源的基本单位。

(2) 线程

线程是操作系统能够进行运算调度的最小单位,是进程的一个执行单元,是比进程更小的独立运行的基本单位。

2. 并行与并发

(1) 串行

串行 (serial) 指在从事某项工作时一个步骤一个步骤的去实施,与并行相对应。

(2) 并行(parallel)

并行 (parallel) 指同一时刻有多条指令在多个处理器上同时执行,无论从微观还是从宏观来看,二者都是一起执行的。

(3) 并发(concurrency)

并发 (concurrency) 指同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

二、线程创建

Java 针对单线程的创建一共提供 3 种方式,下面将依次介绍创建方式。

1. Thread

通过继承 Thread 类并重写 run() 方法实现异步逻辑。

注意启动线程时调用的为 start() 而非 run(),前者才为异步任务启动。

public void demo() { 
    Thread t1 = new MyThread();
    t1.start();
}

// 继承 Thread 类重写 run() 方法
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread is running!");
    }
}

如果只是仅需要创建一个简单线程任务可以使用 lambda 函数。

public void demo() {
    new Thread(() -> {
        // do somethings
        System.out.println("task demo!");
    }).start();
}

2. Runnable

通过实现 Runnable 接口并重写 run() 方法用于定义线程业务逻辑。

注意直接执行 Runnablerun() 其仍为同步执行,通常将业务定义于 Runnable 并将任务通过 Thread 类或线程池进行提交,从而实现异步效果。

public void demo() {
    Thread t1 = new Thread(new MyThread());
    t1.start();
}

// 实现 Runnable 接口重写 run() 方法
class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println("Thread is running!");
    }
}

3. Callable

无论是通过继承 Thread 类还是实现 Runnable 接口所创建的线程都没有返回值,当需要线程返回参数时,可通过实现 Callable 接口并重写 call() 方法。

public void demo() {
    TimeCall thread = new TimeCall();
    try {
        long l = thread.call();
        System.out.println("Result: " + l);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// 实现 Callable 接口重写 call() 方法
class TimeCall implements Callable<Long> {
    @Override
    public Long call() {
        return System.currentTimeMillis();
    }
}

4. 区别介绍

(1) Thread

ThreadRunnable 的线程区别是由于 Java 特性引起的,在 Java 中一个类可以实现多个接口,但只能继承一个父类,因此通过继承 Thread 实现的线程便无法继承其它父类。

// MyThread1 无法再继承其它类
class MyThread1 extends Thread {
    @Override
    public void run() {
        System.out.println("Thread is running!");
    }
}
(2) Runnable

Runnable 创建的线程效果和 Thread 创建的并没有根本性区别,但因为 Runnable 是通过实现接口类实现线程因此其仍可以继承其它父类。

// MyThread2 可以同时继承其它父类
class MyThread2 extends Human implements Runnable {
    @Override
    public void run() {
        System.out.println("Thread is running!");
    }
}
(3) Callable

CallableRunnable 通过是通过 implements 接口类实现,但与后者不同的是 Callable 返回值,而 Runnable 没有返回值。

class TimeCall implements Callable<Long> {
    @Override
    public Long call() {
        return System.currentTimeMillis();
    }
}

三、性质特征

1. 启动方式

Java 中对于创建的线程分别提供了 run()start() 两种启用方式,下面分别介绍二者的区别。

(1) run()

通过 run() 启动线程则要等待 run 方法体执行完毕后才可继续执行下面的代码,程序中只有主线程这一个线程,程序执行路径还是只有一条,这样就没有达到写线程的目的。

(2) start()

通过 start() 启动线程无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码,从而实现异步的效果。

(3) 示例

下面通过一个简单案例进行说明,在线程 run 方法体中利用 sleep() 方法让线程睡眠 5 秒以模拟任务耗时。再分别通过 run()start() 启动线程,并在启动线程后进行一个输出打印,根据输出的时间点即可观察二者的区别。

public void demo1() {
    Thread thread1 = new MyThread();
    thread1.run();

    // 5 秒后才会打印
    System.out.println("done.");
}

public void demo2() {
    Thread thread1 = new MyThread();
    thread1.start();
    
    // 打印同步进行
    System.out.println("done.");
}

class MyThread extends Thread {
    @Override
    public void run() {
        try {
            // 模拟任务耗时 5 秒
            Thread.sleep(5 * 1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

2. 生命周期

  • New: 新创建的线程,尚未执行。

  • Runnable: 运行中的线程,正在执行 run() 方法。

  • Blocked: 运行中的线程,因为某些操作被阻塞而挂起。

  • Waiting: 运行中的线程,因为某些操作在等待中。

  • Timed Waiting: 运行中的线程,因为执行 sleep() 方法正在计时等待。

  • Terminated: 线程已终止,因为 run() 方法执行完毕。

    public void LifeDemo() {
        Thread thread = new ThreadSon();
        // 状态为 NEW
        System.out.println("State:" + thread.getState());
    
        thread.start();
        // 状态为 RUNNABLE
        System.out.println("State:" + thread.getState());
    
        // 返回当前正在运行的线程
        // 格式:[ name, priority, group.name ]
        System.out.println("Info:" + Thread.currentThread());
    }
    
    class ThreadSon extends Thread {
        @Override
        public void run()  {
            System.out.println("thread is running with " + Thread.currentThread().getName());
        }
    }
    

3. 守护线程

守护线程即创建之后将一直处于运行状态,当所有非守护线程结束后,则其会自动结束退出。

如果直接通过 while(true) 开启线程若系统异常停止程序并无法立刻退出造成资源泄露浪费,而守护线程的创建只需通过 setDaemon() 设置即可,但需要注意不能在守护线程进行任务资源操作,因为其结束时不会释放资源。

public void DaemonDemo() {
    Thread thread = new TimerThread();
    // 设置为守护线程
    thread.setDaemon(true);
    thread.start();

    try {
        // 主线程休眠 10 秒
        Thread.sleep(10000);
    } catch (InterruptedException e) {
    }
}

class TimerThread extends Thread {
    @Override
    public void run()  {
        while(true) {
            System.out.println(LocalTime.now());
            try {
                // 每次输出后休眠 1 秒
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                break;
            }
        }
    }
}

四、线程并发

当线程启动之后,不同线程任务间交替运行,我们无法清楚得知当前运行的为哪个任务,尤其当多个线程共享变量时,就会涉及到变量值不可知的情景。

如以下代码中当 main 方法中通过 start() 启动线程后,此时为线程 main 和 线程 thread 二者异步并发执行,在 main 方法执行自增前,并不能确定 thread 中的自增是否完成,导致最终输出的结果不确定性。

private int amount = 0;

public static void main(String[] arg) {
    Thread thread = new MyThread();
    thread.start();
    System.out.println(amount);

    amount++;
    System.out.println(amount);
}

class MyThread extends Thread{
    @Override
    public void run() {
        amount ++;
    }
}

针对上述的情况,除了通过加锁的方式 Java 提供了更简单的 isAlive()join() 以应对上述问题,下面分别介绍两种方法作用。

1. isAlive()

isAlive() 方法用于判断当前线程是否处于活动状态,我们可以根据不同状态进行不同操作,从而避免共享变量冲突。

public void AliveDemo(){
    Thread thread = new MyThread();
    thread.start();
    // 判断线程是否处于活动状态
    while(thread.isAlive()) {
        System.out.println("Waiting...");
    }
    // 线程结束才会继续向下执行
    amount++;
    System.out.println(amount);
} 

2. Join()

join() 作用效果等同于 isAlive ,直到线程处于结束状态才会继续执行后续内容。

public void JoinDemo(){
    Thread thread = new MyThread();
    thread.start();
    try {
        // 等待线程结束
        thread.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    amount++;
    System.out.println("After join() and do add:" + amount);
}

五、线程休眠

在使用线程时我们常用到 sleep()wait()yeild() 等方法让线程暂时进入休眠,如上述提到的守护线程就是通过 sleep() 实现间隔时间打印。

下面就分别介绍一下两种休眠方式的使用方式和不同之处。

1. sleep()

sleep() 的使用方式很简单,通过 Thread.sleep() 即可让当前线程进入休眠。

当一个执行 sleep() 后将阻塞挂起,即线程内余下代码块将不会执行,待睡眠结束才会继续向下执行。

public static void main(String[] arg) {
    // 睡眠 5 秒
    Thread.sleep(5000);

    // 5 秒后才会输出 done
    System.out.println("done");
}

默认 sleep() 休眠时间单位是毫秒,通常使用 n * 1000 更直观的表示休眠时间,但如果需要更长时间休眠可使用 TimeUnit 替代,代码示例如下:

public static void main(String[] arg) {
    // 传入时间单位为毫秒
    Thread.sleep(1000);
    // 休眠 5 分钟
    Thread.sleep(5 * 60 * 1000);
    // 休眠 5 分钟
    TimeUnit.MINUTES.sleep(6);
    System.out.println("线程休眠结束");
}

2. wait()

wait()sleep() 不一样的地方在于前者除了可以指定休眠时间外,也可以不设置休眠时间从而进入无限期休眠,通过 notify() 方法退出休眠状态。

需要注意的是 waitnotify 同时执行者必须为同一对象,必须在同步代码块中使用,即在持有锁的情况下,否则将会抛出 IllegalMonitorStateException 异常。

如下述示例中 t1 在同步块中通过 wait() 进入无限期休眠,等待三秒后有 t2 执行 notify() 唤醒 t1,此时控制台才会打印 Been wake. 信息。

public void demo() throws InterruptedException {
    Object object = new Object();
    new Thread(() -> {
        synchronized (object) {
            try {
                // 休眠, 同步块中才能使用 wait()
                object.wait();
                System.out.println("Been wake.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }, "t1").start();
    Thread.sleep(3 * 1000);

    new Thread(() -> {
        synchronized (object) {
            // 唤醒睡眠, 同步块中才能使用 notify()
            object.notify();
        }
    }, "t2").run();
}

3. park()

JDK 中同时提供了 LockSupport 用于线程休眠,不同是其为锁无关对象,因此无需与 wait() 一样要在同步块中执行。其通过 park() 执行休眠,通过 parkNanos() 休眠执行时间,单位为纳秒。也正是因为锁无关的特性,在唤醒线程时执行 unpark(thread) 即可。

在下述示例中,线程 t1 启动后通过 park() 进行休眠,主线程等待 3 秒后执行 unpark() 唤醒线程此时将打印输出 Wake up. 信息。

public void packDemo() throws InterruptedException {
    var t1 = new Thread(() -> {
        System.out.println("Park.");
        // park(): will block here
        LockSupport.park();
        System.out.println("Wake up.");
    }, "t1");

    t1.start();
    TimeUnit.SECONDS.sleep(3);
    LockSupport.unpark(t1);
}

4. yeild()

yeild() 顾名思义即退让,即放弃当前线程的 CPU 资源,与 LockSupport 类似它同样是锁无关。

在之前已经提到了 Java 中的线程并不是真正意义上的并行而是并发,由 CPU 按照一定算法进行时间切片轮询调度,从而实现宏观上的并行。因此,当线程执行 Thread.yield() 即表明降低当前自身优先级,将 CPU 资源优先提供给其它线程。

5. 区别介绍

既然上述几者都能实现线程休眠,那么二者到底有什么区别?

区别很简单:如果线程在持有锁的同时调用 sleep() 后其并不会释放锁,其它对象在此时是无法访问锁的,在这种情况下即容易造成死锁现象。而 wait() 在持有锁的情况下进入休眠将会释放锁,其它对象此时仍能访问锁。

下面通过两个例子详细介绍二者实际生效情况。

(1) sleep()

在下述示例中,当 t1 通过 synchronized 获取锁之后通过 sleep() 进入睡眠,注意此时 t1 仍然持有锁。而同时 t2 尝试获取锁时发现锁已被其它线程持有,将进入重试等待,等到 t1 休眠结束释放锁才能获取锁并输出信息。

最终程序运行的效果就是等待 3 秒后才输出 Get lock 信息,和我们预期的结果一致。

public void SleepDemo() throws InterruptedException {
    new Thread(() -> {
        synchronized (object) {
            try {
                // 进入休眠,但未释放锁
                Thread.sleep(3 * 1000);
            } catch (InterruptedException e) {
            }
        }
    }, "t1").start();
    Thread.sleep(500);

    new Thread(() -> {
        // 等待线程 1 释放锁
        synchronized (object) {
            System.out.println("Get lock");
        }
    }, "t2").start();

    // 等待两线程结束
    Thread.sleep(5 * 1000);
}
(2) wait()

稍微改造上面的例子,运行程序后可以发现在间隔 500ms 启动线程后几乎立即输出的 Get lock ,这正是因为 t1 在进入 wait() 休眠时释放锁的原因,此时 t2 可以直接取锁无需进入等待。

public void demo2() throws InterruptedException {
    new Thread(() -> {
        synchronized (object) {
            try {
                // 进入休眠,但释放锁
                object.wait(3 * 1000);
            } catch (InterruptedException e) {
            }
        }
    }, "t1").start();

    Thread.sleep(500);
    new Thread(() -> {
        // 立即获取锁
        synchronized (object) {
            System.out.println("Get lock");
        }
    }, "t2").start();

    // 等待两线程结束
    Thread.sleep(5 * 1000);
}

六、线程状态

1. 线程中断

当一个线程在运行期间若想停止任务即可通过 interrupt() 中断线程,但中断 interrupt() 只是向线程任务发送一个中断信号,至于线程相应需要根据其自身调度,并不是一发送中断即会中断线程。

线程中断状态第一次中断后将清除中断标识,后续再读全为 false ,同时当线程处于 sleep(), wait(), join() 等阻塞状态时若中断线程同样会清除中断状态。

public void demo1() throws InterruptedException {
    Thread thread = new Thread(() -> {
        int i = 0;
        while (true) {
            System.out.println(i++);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    thread.start();
    TimeUnit.SECONDS.sleep(3);
    // 中断线程,响应由调度决定
    thread.interrupt();
}

2. 线程阻塞

(1) notify

在上面 wait() 示例中已经介绍了如何使用 notify() ,这里就不再过多介绍。

(2) Condition

在使用 synchronized 实现同步块时可以通过 wait() 实现动态唤醒的效果,而 Condition 则是针对 ReentrantLock 而言,可达到同样效果。

public void demo() throws InterruptedException {
    Lock lock = new ReentrantLock();
    // 绑定到锁对象
    Condition condition = lock.newCondition();
    // 等价于 "wait()"
    condition.await();
    // 休眠且指定时间
    condition.await(1, TimeUnit.SECONDS);
    // 等价于 "notify()"
    condition.signal();
    // 等价于 "notifyAll()"
    condition.signalAll();
}

参考: Java多线程 - 廖雪峰的官方网站


文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录