Java多线程~线程基础

99 阅读6分钟

1. 概念

1.1 线程与进程

  • 进程:系统进行资源分配和调度的基本单位,可以看作一个程序的实体,表示一个执行单元;一般包含多个线程;多个进程拥有互相独立的内存单元,数据是独立不共享的
  • 线程:系统能够进行运算调度的最小单位;线程不具备任何的系统资源,它在同一个进程里与其他线程共享全部资源

1.2 并发与并行

  • 并发:指两个或多个事件在同一时段内发生,即交替做不同事的能力,多线程是并发的一种形式。例如垃圾回收时,用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上
  • 并行:同一时刻多个程序在运行(多个或多核CPU才存在并行),多个任务间不会互相抢占资源

1.3 线程的生命周期

  • 初始状态(NEW):线程被创建,但是还没有调用 start() 方法
  • 运行状态(RUNNABLE):将操作系统的就绪和运行状态称为 - 运行中
  • 阻塞状态(BLOCKED):线程阻塞于锁
  • 等待状态(WAITING):当前线程需要等待其他线程作出一些特定动作(通知或中断)
  • 超时等待(TIME_WAITING):指定时间自行返回
  • 终止状态(TERMINATED):线程已经执行完毕

2. 多线程的创建方式

2.1 继承Thread类

static class MyThread extends Thread {
    @Override
    public void run() {
        super.run();
        System.out.println("继承Thread类的方式创建线程:" + Thread.currentThread().getName());
    }
}
public static void main(String[] args) {
    System.out.println("当前线程:" + Thread.currentThread().getName());
    // 继承Thread类的方式创建并启动线程
    MyThread myThread = new MyThread();
    myThread.start();
}
运行结果:
当前线程:main
继承Thread类的方式创建线程:Thread-0

2.2 实现Runable接口

static class MyRunnable implements Runnable{

    @Override
    public void run() {
        System.out.println("实现Runnable接口的方式创建线程:" + Thread.currentThread().getName());
    }
}
public static void main(String[] args) {
    System.out.println("当前线程:" + Thread.currentThread().getName());
    // 实现Runnable接口的方式创建并启动线程
    MyRunnable myRunnable = new MyRunnable();
    new Thread(myRunnable).start();
}
运行结果:
当前线程:main
继承Thread类的方式创建线程:Thread-0

2.3 实现Callable接口

static class MyCallable implements Callable<String> {
    @Override
    public String call() {
        System.out.println("实现Callable接口的方式创建线程:" + Thread.currentThread().getName());
        return "hello,Callable";
    }
}
public static void main(String[] args) {
    System.out.println("当前线程:" + Thread.currentThread().getName());
    // 实现Callable接口的方式创建并启动线程
    MyCallable myCallable = new MyCallable();
    // 需要借助FutureTask
    FutureTask<String> futureTask = new FutureTask<String>(myCallable) {
        @Override
        protected void done() {
            System.out.println("线程执行前");
            super.done();
            System.out.println("线程执行后");
            try {
                System.out.println("done获取Call方法的返回值:" + get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }

        }
    };
    new Thread(futureTask).start();

    try {
        System.out.println("外部获取Call方法的返回值"+futureTask.get());
    } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
    }
}
当前线程:main
实现Callable接口的方式创建线程:Thread-0
线程执行前
外部获取Call方法的返回值hello,Callable
线程执行后
done获取Call方法的返回值:hello,Callable

2.4 三种方式比较

  • 继承Thread需要手动重写run方法,启动直接使用该对象调用start方法即可
  • 实现Runnable也是重写run方法,启动时需要将该对象当参数传入Thread构造,再使用该Thread对象调用start方法启动
  • 实现Callable<*>方式需要重写call方法,可以获取call方法的返回值,启动时需要借助FutureTask对象,将Callable实现类对象作为参数传入FutureTask构造器,最终将该FutureTask对象传入Thread对象,调用Thread对象的start方法即可启动

2.5 Thread.start与Thread.run的区别

  • Thread.start会启动线程并调用Thread.run方法,而Thread.run只会回调run方法

3. 线程中断

  • 当开启的线程运行的代码没有无限循环代码时,代码运行结束线程即可自动终止
  • 假如是无限循环时,常见的中断方式有Thread.Interrupt配合isInterrupted设置停止的标识符(Thread.stop()不安全,可能导致线程持有的锁突然释放而不受控制,已被废弃)

3.1 Thread.Interrupt配合isInterrupted

  • 线程运行后,调用Thread.Interrupt()中断线程,while的循环条件必须判断当前线程是否被中断
/**
 * 线程中断
 *
 * @author BTPJ  2022/2/15
 */
public class StopThread {
    private static int mCount = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new MyRunnable());
        thread.start();

        // 延迟三秒中断线程
        Thread.sleep(3000);
        // 调用interrupt方法请求中断线程
        thread.interrupt();
    }

    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            // 循环条件为当前线程不被中断
            while (Thread.currentThread().isInterrupted()) {
                mCount++;
                System.out.println("当前count值:" + mCount);
            }
            System.out.println("线程执行已结束");
        }
    }
}
  • 注意:当循环的过程中添加了Thread.sleep时,由于sleep方法会捕获中断信号,抛出InterruptedException,并且将中断信号改为false,最终导致中断失败,所以需在catch中再次请求中断即可
static class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 循环条件为当前线程不被中断
        while (!Thread.currentThread().isInterrupted()) {
            mCount++;
            System.out.println("当前count值:" + mCount);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                // 捕获到异常后需再次调用interrupt方法请求中断线程
                Thread.currentThread().interrupt();
                e.printStackTrace();
            }
        }
        System.out.println("线程执行已结束");
    }
}

3.2 修改标志位状态退出

  • 添加全局标志位变量,设置为循环条件,修改该标志位即可完成中断
/**
 * 线程中断
 *
 * @author BTPJ  2022/2/15
 */
public class StopThread {
    private static int mCount = 0;
    private static boolean mRecycler = true;

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
        // 延迟三秒中断线程
        Thread.sleep(3000);
        // 修改标志位
        mRecycler = false;
    }

    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            // 循环条件为当前线程不被中断
            while (mRecycler) {
                mCount++;
                System.out.println("当前count值:" + mCount);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程执行已结束");
        }
    }
}
运行结果:
当前count值:1
当前count值:2
当前count值:3
当前count值:4
当前count值:5
当前count值:6
线程执行已结束

4. 线程串行Thread.join()

  • 多线程执行的先后顺序是由CPU调度决定的,所以每次运行可能先后顺序都不同
public static void main(String[] args) throws InterruptedException {
    Thread thread1 = new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "执行开始");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "执行结束");
    }, "线程1");

    Thread thread2 =  new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "执行开始");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "执行结束");
    }, "线程2");

    Thread thread3 =  new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + "执行开始");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "执行结束");
    }, "线程3");

    // 分别开启
    thread1.start();
    thread2.start();
    thread3.start();
}
  • 开启后调用Thread.join(),会将线程串行化依次执行,必须等上一个线程执行完毕才会执行,类似同步执行的效果
...略...

// 分别开启
thread1.start();
// 调用join串行执行
thread1.join();
thread2.start();
thread2.join();
thread3.start();
运行结果:
线程1执行开始
线程1执行结束
线程2执行开始
线程2执行结束
线程3执行开始
线程3执行结束

5. 线程礼让Thread.yield()

  • 当调用Thread.yield()方法时,会给线程调度器一个当前线程愿意让出CPU使用的暗示,但是线程调度器可能会忽略这个暗示,也就是说:最终抢占到CPU的还是取决于线程调度器,只是其他线程抢到CPU的概率更大
public static void main(String[] args) {
    Runnable runnable = () -> {
        for (int i = 0; i < 4; i++) {
            System.out.println(Thread.currentThread().getName() + "执行到:" + i);
            if (i == 2) {
                Thread.yield();
            }
        }
    };

    Thread thread1 = new Thread(runnable,"线程1");
    Thread thread2 = new Thread(runnable,"线程2");
    Thread thread3 = new Thread(runnable,"线程3");
    thread1.start();
    thread2.start();
    thread3.start();
}
执行结果:
线程1执行到:0
线程2执行到:0
线程1执行到:1
线程1执行到:2
线程3执行到:0
线程3执行到:1
线程3执行到:2
线程2执行到:1
线程2执行到:2
线程3执行到:3
线程2执行到:3
线程1执行到:3
  • 实测多次运行虽然每次都不一样,但基本一个线程执行到2时就换到了另一个线程,而不用Thread.yield()时则线程完全没有在执行到2时切换到另一个线程的现象