目录

Multi-Threading(多线程)

进程(process):进程是正在运行的程序,会主动为程序分配一块内存空间。

线程(thread):进程里面一定会有个线程用于执行任务。一个进程同一时间只能执行一个任务有点浪费多核心 CPU 资源,可以创建多个线程,让程序同时执行多条任务线。

线程通信:堆(Heap)和方法区(Method Area)是共享的,每个线程都是独立的栈空间、程序计数器,不共享。

多线程:CPU 4 核心 16 线程,可以创建 16 个线程,每个线程都独自做自己的事,如果 CPU 只有 8 个线程,那么也去创建 16 个线程的任务,那么其实是 8 个线程定时来回切换任务执行。就像领导和老板各自分配给你一个任务,要求你在 1 小时内完成,就只能一会儿做这个一会儿做另一个任务来回切,本质上还是只有一个人力资源。

Java 应用运行会存在 main 主线程、gc 垃圾回收线程、异常处理线程这三个线程。

并行:多个 CPU 执行一起多个任务。

并发:单个 CPU 同时执行多个任务。

使用线程

继承 java.lang.Thread 类

继承 java.lang.Thread 类,并重写 run() 方法,在此方法里面编写线程要执行的代码,最终调用 start() 方法自动创建线程(开辟独立栈空间)同时执行任务,缺点是没法再继承其他类。

public class threadTest01 {
    public static void main(String[] args) {
        // 3. 创建 Thread 子类对象
        threadTest011 threadTest011 = new threadTest011();
        // 4. 调用 Thread 中 start 方法开启线程并执行线程 run 方法代码。
        threadTest011.start();
        for (var i = 0; i < 10; i++) {
            System.out.println("main() 主线程");
        }
    }
}

// 1. 继承 Thread 类
class threadTest011 extends Thread {
    public void printStr() {
        for (var i = 0; i < 10; i++) {
            System.out.println("子线程" + System.nanoTime());
        }
    }

    @Override
    public void run() {
        // 2. 重写 run 方法,并在方法体放入线程要执行的代码。
        printStr();
    }
}

只有调用了 start() 线程才开启,与主线程一起执行任务。

运行输出:

main() 主线程
main() 主线程
main() 主线程
main() 主线程
main() 主线程
main() 主线程
main() 主线程
main() 主线程
main() 主线程
main() 主线程
子线程14291721584800
子线程14291733160000
子线程14291733181200
子线程14291733201100
子线程14291733224600
子线程14291733244600
子线程14291733266300
子线程14291733286100
子线程14291733306700
子线程14291733329100

一个更简便的写法是使用匿名类方式实现 run 方法,它放在 main 方法里也可以叫匿名内部类:

public class threadTest01 {
    public static void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                for (var i = 0; i < 10; i++) {
                    System.out.println("子线程" + System.nanoTime());
                }
            }
        }.start();
        for (var i = 0; i < 10; i++) {
            System.out.println("main() 主线程");
        }
    }
}

实现 java.lang.Runnable 接口

实现 java.lang.Runnable 接口 run 方法,实例化 Thread 类并传入 Runnable 接口实现类 Thread(Runnable target),之后调用 start()。实现接口的方式优点是可以多个线程对象共用同一个数据对象:

public class threadTest02 {
    public static void main(String[] args) {
        Runnable runnable = new threadTest022();
        // 启动线程
        new Thread(runnable).start();

        for (var i = 0; i < 10; i++) {
            System.out.println("main 主线程" + System.nanoTime());
        }
    }
}

class threadTest022 implements Runnable {
    @Override
    public void run() {
        for (var i = 0; i < 10; i++) {
            System.out.println("子线程:" +  + System.nanoTime());
        }
    }
}

也可以通过匿名内部类的方式创建 new Runnable() { ... }

public class threadTest02 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (var i = 0; i < 10; i++) {
                    System.out.println("子线程" + System.nanoTime());
                }
            }
        };
        // 启动线程
        new Thread(runnable).start();

        for (var i = 0; i < 10; i++) {
            System.out.println("main 主线程" + System.nanoTime());
        }
    }
}

使用线程注意事项

不能直接调用 run 方法,这是直接在 main 线程执行 run 方法,不会通过线程执行。

不能多次调用 start 方法,一旦运行后状态会更改就不能再次启动,将抛出 IllegalThreadStateException 异常,非要重复执行此线程,请创建多个 Thread 对象去调 start 方法。

线程生命周期

线程生命周期由五个状态构成:

  1. 新建
  2. 就绪
  3. 运行
  4. 阻塞
  5. 死亡

线程生命周期.jpg

新建状态是线程被创建出来,就绪状态是跟 CPU 抢时间片(就是获得任务执行权和能够执行多长时间的任务),抢到时间片后就进行任务运行,是运行状态,在运行过程中如果遇到网络延迟或者处理用户输入等情况就会进入阻塞状态,当阻塞已经解除就重新回到就绪状态去拿时间片,后续就继续回到运行状态执行上次的任务,任务全部执行完成后就销毁进程,不再占有栈空间内存资源。

Java 里 Thread.State 内部类定义线程状态:

  • NEW,new Thread 对象。

  • RUNNABLE,可运行状态,调用 start() 方法后进入就绪状态,可能线程运行,也许是没抢到 CPU 时间片进入就绪状态(可以调用 yield() 方法从运行状态恢复到就绪状态)。

  • BLOCKED,多个线程操作一个数据,只要有线程操作此数据就先将其锁定只将给此线程操作,其他线程进入等待状态,这是等待同步锁。

  • WAITING,调用 join() 让当前线程阻塞。

    TIMED_WAITING,调用 sleep() 方法进入关于时间的阻塞。

  • TERMINATED,线程终止。

线程新建

方法:

  • public Thread()
  • public Thread(String name)
  • public Thread(Runnable target)
  • public Thread(Runnable target, String name)

使用 public Thread.State getState() 查看线程状态。

创建 ThreadTest02.java:

public class ThreadTest02 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                Thread subThread = Thread.currentThread();
                System.out.println("子线程名称:" + subThread.getName());
            }
        };
        Thread thread = new Thread(runnable);
        Thread mainThread = Thread.currentThread();

        // 获取线程状态
        Thread.State mainThreadStatus = mainThread.getState();
        Thread.State subThreadStatus = thread.getState();

        System.out.println("sub 线程状态:" + subThreadStatus);
        System.out.println("main 线程状态:" + mainThreadStatus);
    }
}

运行输出:

sub 线程状态:NEW
main 线程状态:RUNNABLE

线程名称设置获取

方法:

  • String getName(),获取线程名称,默认线程名称是 Thread-X,X 是 数字第一个线程是 0,后续自增。
  • void setName(String name),设置线程名称。
  • static Thread currentThread(),获取线程对象,在哪个线程内使用就获取哪个线程对象

更改 ThreadTest08.java:

public class ThreadTest08 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                Thread subThread = Thread.currentThread();
                System.out.println("子线程名称:" + subThread.getName());
            }
        };
        Thread thread = new Thread(runnable);
        Thread mainThread = Thread.currentThread();

        System.out.println(thread.getName());
        System.out.println(mainThread.getName());
    }
}

运行输出:

Thread-0
main
子线程名称:Thread-0

默认主线程名称是 main,子线程是 Thread-0,第一个线程编号从 0 开始,没建立一个线程此数字自增 1。启动子线程后发现和 mian 线程获取的线程名称一致。

线程名称也可自定义,需在启动线程前调用 setName() 设置:

public class ThreadTest08 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                Thread subThread = Thread.currentThread();
                System.out.println("子线程名称:" + subThread.getName());
            }
        };
        Thread thread = new Thread(runnable);
        Thread mainThread = Thread.currentThread();

        // void setName(String name) 设置线程名称
        thread.setName("setSubThreadName");
        mainThread.setName("setMainThreadName");

        System.out.println(thread.getName());
        System.out.println(mainThread.getName());
        thread.start();
    }
}

运行输出:

setSubThreadName
setMainThreadName
子线程名称:setSubThreadName

最简单的方式还是创建线程对象时通过构造方法指定线程名称, public Thread(Runnable target, String name) 用法如下:

public class ThreadTest08 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                Thread subThread = Thread.currentThread();
                System.out.println("子线程名称:" + subThread.getName());
            }
        };

        // 通过构造方法传入线程名称
        Thread thread = new Thread(runnable, "setSubThreadName");
        Thread mainThread = Thread.currentThread();

        // 主线程还是要手动
        mainThread.setName("setMainThreadName");

        System.out.println(thread.getName());
        System.out.println(mainThread.getName());
        thread.start();
    }
}

public Thread(String name) 用法如下:

public class ThreadTest08 {
    public static void main(String[] args) {


        // 通过构造方法传入线程名称
        Thread thread = new ThreadTest081("setSubThreadName");
        Thread mainThread = Thread.currentThread();

        // 主线程还是要手动
        mainThread.setName("setMainThreadName");

        System.out.println(thread.getName());
        System.out.println(mainThread.getName());
        thread.start();
    }
}

class ThreadTest081 extends Thread {
    public ThreadTest081() {
    }

    public ThreadTest081(String name) {
        super(name);
    }

    @Override
    public void run() {
        Thread subThread = Thread.currentThread();
        System.out.println("子线程名称:" + subThread.getName());
    }
}

线程就绪&运行

方法:

  • public void start(),启动线程后先进入就绪状态,抢到时间片后进入运行状态。

  • public static void yield(),进入就绪状态,让出已经抢到的 CPU 时间片,再次和其他线程一起抢(抢到了但先放弃,放弃后再一起抢)。

  • int getPriority(),获取线程优先级,Thread 类里存在线程优先级常量,MIN_PRIORITY = 1,NORM_PRIORITY = 5,MAX_PRIORITY = 10。高优先级的线程抢到时间片的几率大,可能会优先执行此线程任务,默认优先级是 5。

  • void setPriority(int newPriority),设置线程优先级,具体用法传优先级常量即可,如:setPriority(Thread.MIN_PRIORITY)

创建 ThreadTest11.java 使用 yield()

public class ThreadTest11 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                Thread subThread = Thread.currentThread();
                // 放弃 CPU 时间片
                Thread.yield();
                for (int i = 0; i < 150; i++) {
                    System.out.println(subThread.getName());
                }
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();

        for (int i = 0; i < 150; i++) {
            System.out.println(Thread.currentThread().getName());
        }
    }
}

yield() 说是放弃 CPU 时间片,但是到底放不放弃是由操作系统来调度,运行一两次代码后不能明确看出是 main 优先执行,可以尝试把 yield() 注释运行观察,就能发现 main 确实优先执行。

使用 setPriority(int newPriority)

public class ThreadTest11 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                Thread subThread = Thread.currentThread();
                for (int i = 0; i < 150; i++) {
                    System.out.println(subThread.getName());
                }
            }
        };

        Thread thread = new Thread(runnable);
        System.out.println("子线程默认优先级:" + thread.getPriority());
        System.out.println("主线程默认优先级:" + Thread.currentThread().getPriority());
        thread.setPriority(Thread.MAX_PRIORITY);
        Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
        System.out.println("子线程优先级修改:" + thread.getPriority());
        System.out.println("主线程优先级修改:" + Thread.currentThread().getPriority());

        thread.start();

        for (int i = 0; i < 150; i++) {
            System.out.println(Thread.currentThread().getName());
        }
    }
}

运行你会发现根本每次都是随机的,无法自己控制,还是由 JVM 调度。

线程阻塞

方法:

  • void join() throws InterruptedException,当线程 b 中调用线程 a 的 join() 方法,就释放线程 b 抢到的 CPU 时间片进入阻塞状态,先执行完线程 a,再恢复运行 b 线程。相当于主动让其他线程插个队先执行。使用时线程 a 一定要 start() 后调 join(),线程 a 没运行起来怎么让线程 b 运行运行呢?

  • static void sleep(long millis) throws InterruptedException,调用此方法的线程休眠指定毫秒,进入阻塞状态,在睡眠时间内放弃 CPU 时间片。

  • void interrupt(),解除 sleep() 方法休眠,终止睡眠。

创建 ThreadTest09.java 练习 join()

public class ThreadTest09 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                Thread subThread = Thread.currentThread();
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程名称:" + subThread.getName());
                }
            }
        };

        Runnable runnable1 = new Runnable() {
            @Override
            public void run() {
                Thread subThread = Thread.currentThread();
                for (int i = 0; i < 15; i++) {
                    System.out.println("子线程名称:" + subThread.getName());
                }
            }
        };

        Thread thread = new Thread(runnable);
        Thread thread1 = new Thread(runnable1);

        thread.start();
        thread1.start();
        try {
            // 在 main 线程调用 thread1 对象 join 方法,
            // 让 main 放弃 CPU 时间片,先让 trhead1 线程运行完。
            thread1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "运行");
        }
    }
}

运行输出:

子线程名称:Thread-0
子线程名称:Thread-1
子线程名称:Thread-0
子线程名称:Thread-1
子线程名称:Thread-0
子线程名称:Thread-1
子线程名称:Thread-0
子线程名称:Thread-1
子线程名称:Thread-0
子线程名称:Thread-1
子线程名称:Thread-1
子线程名称:Thread-1
子线程名称:Thread-1
子线程名称:Thread-1
子线程名称:Thread-1
子线程名称:Thread-1
子线程名称:Thread-1
子线程名称:Thread-1
子线程名称:Thread-1
子线程名称:Thread-1
main运行
main运行
main运行
main运行
main运行

看输出结果虽然 main 让子线程 Thread-1 先执行完成,但 Thread-0 也运行了,这是因为 main 等 Thread-1 时阻塞住 Thread-0 不受影响,要是 Thread-1 不停止,main 就不执行。使用场景是线程 a 需要依赖线程 b 数据时,让线程 a 调用 join() 等待线程 b 执行完成即可拿到数据。

创建 ThreadTest10.java 练习 sleep(long millis)interrupt()

public class ThreadTest10 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                Thread subThread = Thread.currentThread();
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程名称:" + subThread.getName());
                    try {
                        // 子线程 sleep() 进入阻塞状态,每此输出睡眠 1 秒。
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };


        Thread thread = new Thread(runnable);
        thread.start();

        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "运行");
        }
    }
}

运行输出:

main运行
子线程名称:Thread-0
main运行
main运行
main运行
main运行
子线程名称:Thread-0
子线程名称:Thread-0
子线程名称:Thread-0
子线程名称:Thread-0

通过 thread 对象调 sleep() 会怎么样呢?

public class ThreadTest10 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                Thread subThread = Thread.currentThread();
                for (int i = 0; i < 5; i++) {
                    System.out.println("子线程名称:" + subThread.getName());
                }
            }
        };


        Thread thread = new Thread(runnable);
        thread.start();

        try {
            // 通过 thread 调 sleep
            thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "运行");
        }
    }
}

运行发现还是 main 进入阻塞状态,而不是 thread 线程对象,这是因为 thread.sleep() 最终还是转换为 Thread.sleep()

子线程要是睡眠时间过长可以终止:

public class ThreadTest10 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                Thread subThread = Thread.currentThread();
                System.out.println("子线程名称:" + subThread.getName());
                try {
                    // 睡眠 100 年
                    Thread.sleep(1000 * 60 * 60 * 24 * 365 * 100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println("子线程睡眠被终止");
                }
            }
        };


        Thread thread = new Thread(runnable);
        thread.start();

        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "运行");
        }

        // 主线程执行完成子线程被 interrupt() 强制唤醒
        thread.interrupt();
    }
}

运行输出:

子线程名称:Thread-0
main运行
main运行
main运行
main运行
main运行
java.lang.InterruptedException: sleep interrupted
        at java.base/java.lang.Thread.sleep(Native Method)
        at learnThread.ThreadTest10$1.run(ThreadTest10.java:12)
        at java.base/java.lang.Thread.run(Thread.java:834)
子线程睡眠被终止

要是循环睡眠就不行,将永远阻塞住:

public class ThreadTest10 {
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                Thread subThread = Thread.currentThread();
                System.out.println("子线程名称:" + subThread.getName());
                for (int i = 0; i < 2; i++) {
                    try {
                        System.out.println("子线程开始睡眠");
                        // 睡眠 100 年
                        Thread.sleep(1000 * 60 * 60 * 24 * 365 * 100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        System.out.println("子线程睡眠被终止");
                    }
                }
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();

        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "运行");
        }

        // 主线程执行完成子线程被 interrupt() 强制唤醒
        thread.interrupt();
    }
}

运行输出:

main运行
main运行
main运行
main运行
main运行
子线程Thread-0开始睡眠
java.lang.InterruptedException: sleep interrupted
    at java.base/java.lang.Thread.sleep(Native Method)
    at learnThread.ThreadTest10$1.run(ThreadTest10.java:13)
    at java.base/java.lang.Thread.run(Thread.java:834)
子线程Thread-0睡眠被终止
子线程Thread-0开始睡眠

为什么使用 sleep 和 join 一定要处理 InterruptedException 异常?是因为有对应 interrupt() 方法中断会产生 InterruptedException 异常。

线程退出

方法:

  • void stop(),终止线程也可叫杀死线程,方法已经过时,终止时没法保存数据。最好是设置 boolean 标记,在 false 时进行数据保存完再 return 结束线程。
  • boolean isAlive(),获取线程状态,判断当前线程是否死亡。
  • void setDaemon(boolean on),参数 on 为 true 代表守护进程。

进程何时结束运行?守护线程结束后,或者说所有用户线程结束,这涉及 Java 两种线程:

  • 守护线程,守护线程守护的是用户线程,所有用户线程退出守护线程自动结束。比如 Java 程序运行中的垃圾回收线程。

  • 用户线程,main 线程和主动创建的普通线程都是用户线程。

验证线程退出

创建 ThreadTest13.java 验证 main 线程退出不会导致子线程结束:

package learnThread;

public class ThreadTest13 {
    private static boolean getThreadStatus() {
        return Thread.currentThread().isAlive();
    }

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                Thread subThread = Thread.currentThread();
                for (int i = 0; i < 15; i++) {
                    try {
                        Thread.sleep(5000);
                        System.out.println(subThread.getName());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("main 线程存活状态:" + getThreadStatus());
            }
        };

        Thread thread = new Thread(runnable);
        thread.start();

        // 主线程任务
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName());
        }
    }
}

运行程序时打开 JDK 安装目录里 jconsole 可以看到 main 和 Thread-0 线程正在运行,当 main 线程执行完成时自动退出而 Thread-0 执行完程序结束。

守护线程

如果我有个需求,是线程 a 一直运行直到其他所有线程任务结束,此时线程 a 才停止,那么可以将线程 a 设置为设置守护线程,具体操作是在 start 线程前调 setDaemon(true) 方法。创建 ThreadTest14.java:

public class ThreadTest14 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                Thread subThread = Thread.currentThread();
                while (true) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(subThread.getName() + "守护线程一直守护你");
                }
            }
        };

        Thread thread = new Thread(runnable);
        // 启动线程前设置为守护线程。
        thread.setDaemon(true);
        thread.start();

        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName());
        }
    }
}

运行输出:

main
Thread-0守护线程一直守护你
main
Thread-0守护线程一直守护你
main
Thread-0守护线程一直守护你
main
Thread-0守护线程一直守护你
main

在 ThreadTest14.java 里把 Thread-0 线程设置为守护线程,因为只有 main 一个用户线程,当执行完毕后 Thread-0 线程就自动退出。

手动设置 flag 退出线程

有没可能主动退出线程?思路是给线程类设置个 flag,当线程 flag 为 false 就退出。创建 ThreadTest12.java 练习退出进程:

public class ThreadTest12 {
    public static void main(String[] args) {
        ThreadTest122 runnable = new ThreadTest122();
        Thread thread = new Thread(runnable);
        thread.start();

        for (int i = 0; i < 2; i++) {
            System.out.println(Thread.currentThread().getName());
        }
        // 在 main 线程执行完任务后退出子线程
        runnable.stopThreadRunning();

        // 检查子线程存活状态
        // 延时检查的原因是可能刚设置完状态,循环还没开始导致线程没有退出。
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("子线程存活状态:" + thread.isAlive());
    }
}

class ThreadTest122 implements Runnable {
    private boolean status = true;

    /**
     * 开放接口用于停止线程。
     * 设置状态为 true 代表存活,为 false 线程即死亡。
     */
    public void stopThreadRunning() {
        this.status = false;
    }

    @Override
    public void run() {
        Thread subThread = Thread.currentThread();

        for (int i = 0; i < 5; i++) {
            if (!status) {
                // 状态为 false 就结束线程运行
                // 在这里保存正在操作的数据。
                System.out.println("子线程退出,已保存数据。");
                return;
            }
            System.out.println(subThread.getName());
        }
    }
}

运行输出:

main
main
Thread-0
子线程退出,已保存数据。
子线程存活状态:false

main 线程的任务是输出两次线程名称,之后调 stopThreadRunning 方法设置子线程状态,当子线程执行任务时发现状态为 false 就 return 结束 run 方法运行,等同于结束线程运行。

这里有个问题是如果 main 线程执行的任务时间比子线程任务短,很可能发生 main 线程运行完成后子线程还没运行就退出。可以多运行几次看看输出结果区别。

interrupt() 退出线程

方法:

  • public static boolean interrupted(),调用后会重置 interrupt 状态为 false。
  • public boolean isInterrupted(),返回线程中断状态。
  • public void interrupt(),中断线程,设置中断状态为 true。

又见 interrupt() 方法,前面用它中断 sleep() 睡眠,除此之外还可中断 join()、wait() 调用,如果有使用这些方法那么就会触发异常并设置中断标志为 false,没使用就仅仅设置中断标志为 true。这个标志可以用来判断当前线程中断情况,一旦中断为 ture 可以直接退出线程。

创建 ThreadTest16.java:

public class ThreadTest16 {
    public static void main(String[] args) {
        ThreadTest166 runnable = new ThreadTest166();
        Thread thread = new Thread(runnable);
        thread.start();

        for (int i = 0; i < 2; i++) {
            System.out.println(Thread.currentThread().getName() + "线程");
        }

        // 在 main 线程执行完 for 循环后退出子线程
        thread.interrupt();

        // 检查子线程存活状态
        System.out.println("子线程" + thread.getName() + "存活状态:" + thread.isAlive());
    }
}

class ThreadTest166 implements Runnable {
    private void dataBackup(int num) {
        System.out.println("子线程" + Thread.currentThread().getName() + "退出,已保存数据。" + "第" + num + "循环");
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            if (Thread.currentThread().isInterrupted()) {
                // 状态为 true 就结束线程运行
                // 在这里保存正在操作的数据。
                dataBackup(i);
                return;
            } else {
                System.out.println(Thread.currentThread().getName() + " 线程中断状态是:" +
                        Thread.currentThread().isInterrupted() + "。第" + i + "循环");
            }
        }
    }
}

运行输出:

main线程
main线程
子线程Thread-0存活状态:true
Thread-0 线程中断状态是:false。第0循环
子线程Thread-0退出,已保存数据。第1循环

输出结果子线程启动后中断状态为 false 是因为 main for 循环任务和子线程并行执行,子线程执行速度太快导致的,因为此时还没执行到 interrupt()

线程同步

线程安全举例:银行系统账户剩余 1000 块,手机银行 App 和 ATM 同时取 1000 块钱,假设银行系统应用先收到 App 发来请求,检查余额通过,因为网络的问题账户扣款操作延迟了些,同时又收到 ATM 请求,因为没有执行扣款所以余额检查通过执行扣款 -1000,此时银行系统账户余额为 0 元,网络也恢复正常了 App 扣款也成功执行,账户余额变成 -1000。

创建 ThreadTest03.java:

public class ThreadTest03 {
    public static void main(String[] args) {
        Withdrawal mobileBankingWithdrawalThread = new Withdrawal("手机银行");
        Withdrawal atmWithdrawalThread = new Withdrawal("ATM 机");

        new Thread(mobileBankingWithdrawalThread, "线程1").start();
        new Thread(atmWithdrawalThread, "线程2").start();
    }
}

class Withdrawal implements Runnable {
    private static float bankAccountBalance = 2000.0F;
    private final String withdrawalChannels;

    public Withdrawal(String withdrawalChannels) {
        this.withdrawalChannels = withdrawalChannels;
    }

    @Override
    public void run() {
        if (bankAccountBalance >= 2000) {
            // 模拟网络阻塞
            if ("ATM 机".equals(withdrawalChannels)) {
                try {
                    Thread.sleep(1500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            bankAccountBalance -= 2000;
            System.out.println(withdrawalChannels + " 提现 2000 成功银行账户余额:" + bankAccountBalance + " " +
                    Thread.currentThread().getName());
        }
    }
}

运行输出:

手机银行 提现 2000 成功银行账户余额:0.0 线程1
ATM 机 提现 2000 成功银行账户余额:-2000.0 线程2

可以发现因为两个线程对同一个数据进行操作,而没有做出限制导致的扣款重复发生,这种情况就是线程不安全。

进一步抽象概括,多个线程对同一个数据操作,可能导致数据存在问题。

synchronized statements

如何解决,就要使用线程同步,在 Java 里称作锁机制。原理很简单,在 App 执行扣款时我使用锁表示这个数据被线程操作中,只有此线程操作完毕,后续线程才能使用。相当于排着队一个个来,避免同时操作。

具体实现使用 synchronized 。

synchronized 语法:

private Object lock = new Object();
synchroized(lock) {
    // 操作数据的代码
}

synchroized 形参称作 monitor,需要接收一个对象作为锁,代码块则包住要操作数据的表达式,这意味着当线程在操作数据时因为上锁原因,当前线程操作完成释放锁后,后续线程抢到时间片后才能继续使用此数据。

使用 synchroized 重点在于所有线程对象必须使用相同 monitor 对象,方法体不能包裹过多或过少代码,不然可能出现逻辑错误导致无法实现需求。当程序出现问题时可以向这两个方向排查。

继承 Thread 使用 synchronized statements

创建 ThreadTest04.java:

public class ThreadTest04 {
    public static void main(String[] args) {
        Withdrawal1 atmWithdrawalThread = new Withdrawal1();
        Withdrawal1 mobileBankingWithdrawalThread = new Withdrawal1();

        atmWithdrawalThread.setName("ATM 机");
        mobileBankingWithdrawalThread.setName("手机银行");
        atmWithdrawalThread.start();
        mobileBankingWithdrawalThread.start();
    }
}

class Withdrawal1 extends Thread {
    private static float bankAccountBalance = 2000.0F;
    // 锁1
    private static final Object lock = new Object();

    @Override
    public void run() {
        // 同步
        // 锁2(更方便)
        // synchronized (Withdrawal1.class) {
        synchronized (lock) {
            if (bankAccountBalance >= 2000) {
                // 模拟网络阻塞
                if ("ATM 机".equals(getName())) {
                    try {
                        sleep(2500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                bankAccountBalance -= 2000;
                System.out.println(getName() + " 提现 2000 成功银行账户余额:" + bankAccountBalance);
            }
        }
    }
}

在使用时多注意锁的用法,这里也可以用 Withdrawal1.class 作为对象,因为类只加载一次对象唯一。

对一段代码使用 synchronized 包裹起来可以用 IDEA 快捷键 Ctrl + Alt + T 调出 Surround With 面板,选 9 即可自动包住。

实现 Runnable 使用 synchronized statements

创建 ThreadTest05.java:

public class ThreadTest05 {
    public static void main(String[] args) {
        Withdrawal2 withdrawal2 = new Withdrawal2();

        new Thread(withdrawal2, "ATM 机").start();
        new Thread(withdrawal2, "手机银行").start();
    }
}

class Withdrawal2 implements Runnable {
    private static float bankAccountBalance = 2000.0F;

    @Override
    public void run() {
        // 同步
        // 锁1
        synchronized (this) {
            if (bankAccountBalance >= 2000) {
                // 模拟网络阻塞
                if ("ATM 机".equals(Thread.currentThread().getName())) {
                    try {
                        Thread.sleep(2500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                bankAccountBalance -= 2000;
                System.out.println(Thread.currentThread().getName() + " 提现 2000 成功银行账户余额:" + bankAccountBalance);
            }
        }
    }
}

在实现接口我使用没创建类变量作为锁,而是使用 this ,因为只用创建一个 Withdrawal2 对象,将它作为参数传入 Thread,start 方法会自动调用同一个对象 run 方法,任何修改都是一个对象。

使用 synchronized (obj) { ... } 确实可以避免线程安全问题,但是同时只有一个线程操作数据,效率会比较低,这也是没办法的。单并不是单线程,其实所有线程只有运行到此处时才会进行排队,线程都是一起运行的。

此外还有一种情况,要是有一直没办法执行完毕呢?其他线程岂不是只能等待,这将导致程序宕机。

synchronized methods

使用 synchronized methods 是另一种实现线程同步的方式,使用场景是在操作线程的方法上加 synchronized 修饰——学集合时遇到过此关键字,此方法将自动做同步:

public static synchronized accountWithdrawal() {
    ......
}

那么监视器是什么呢?在实例方法上修饰是 this,静态方法则是当前类示例 className.class

继承 Thread 使用 synchronized methods

创建 ThreadTest06.java:

public class ThreadTest06 {
    public static void main(String[] args) {
        Withdrawal3 atmWithdrawalThread = new Withdrawal3();
        Withdrawal3 mobileBankingWithdrawalThread = new Withdrawal3();

        atmWithdrawalThread.setName("ATM 机");
        mobileBankingWithdrawalThread.setName("手机银行");
        atmWithdrawalThread.start();
        mobileBankingWithdrawalThread.start();
    }
}

class Withdrawal3 extends Thread {
    private static float bankAccountBalance = 2000.0F;

    // synchronized 使用在方法上做同步,监视器是 Withdrawal3.class
    public static synchronized void accountWithdrawal() {
        if (bankAccountBalance >= 2000) {
            // 模拟网络阻塞
            if ("ATM 机".equals(currentThread().getName())) {
                try {
                    sleep(2500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            bankAccountBalance -= 2000;
            System.out.println(currentThread().getName() + " 提现 2000 成功银行账户余额:" + bankAccountBalance);
        }
    }

    @Override
    public void run() {
        accountWithdrawal();
    }
}

将操作线程提炼出静态方法j,用 synchronized 后就不会出现线程安全问题,监视器获取的是 Withdrawal3.class。

如果你使用的是普通实例方法,那么监视器将自动是 this。

实现 Runnable 使用 synchronized methods

创建 ThreadTest07.java

public class ThreadTest07 {
    public static void main(String[] args) {
        Withdrawal4 withdrawal4 = new Withdrawal4();

        new Thread(withdrawal4, "ATM 机").start();
        new Thread(withdrawal4, "手机银行").start();
    }
}

class Withdrawal4 implements Runnable {
    private static float bankAccountBalance = 2000.0F;

    // synchronized 使用在方法上做同步,当前监视器是 this
    public synchronized void accountWithdrawal() {
        if (bankAccountBalance >= 2000) {
            // 模拟网络阻塞
            if ("ATM 机".equals(Thread.currentThread().getName())) {
                try {
                    Thread.sleep(2500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            bankAccountBalance -= 2000;
            System.out.println(Thread.currentThread().getName() + " 提现 2000 成功银行账户余额:" + bankAccountBalance);
        }
    }

    @Override
    public void run() {
        accountWithdrawal();
    }
}

synchronized 同步监视器默认是 this。

继承 Thread 使用 synchronized methods 相比接口在实例方法上会麻烦很多,因为必须设置为静态方法,这很可能导致设计上的问题,明明一个方法是对象的,因为语法问题你必须设置为静态不合逻辑。使用接口则没此问题,因为只有一个对象。

Deadlock

多个线程都在等要执行的任务释放锁好完成当前线程的任务,一旦无法释放就产生互相僵持的现象。

创建 DeadLock.java 演示死锁情况:

public class DeadLock {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        Runnable task1 = new Runnable() {
            @Override
            public void run() {
                synchronized (lock1) {
                    System.out.println(Thread.currentThread().getName() + " 获得 lock1");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (lock2) {
                        System.out.println(Thread.currentThread().getName() + " 获得 lock2");
                    }
                }
            }
        };

        Runnable task2 = new Runnable() {
            @Override
            public void run() {
                synchronized (lock2) {
                    System.out.println(Thread.currentThread().getName() + " 获得 lock2");
                    synchronized (lock1) {
                        System.out.println(Thread.currentThread().getName() + "获得 lock1");
                    }
                }
            }
        };

        Thread task1Obj = new Thread(task1,"task1");
        Thread task2Obj = new Thread(task2,"task2");

        task1Obj.start();
        task2Obj.start();

        try {
            task1Obj.join();
            task2Obj.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("主线程结束");
    }
}

运行输出:

task1 获得 lock1
task2 获得 lock2

造成此局面的原因是 task1 和 task2 同时启动,task1 获取 lock1,task2 获取 lcok2,双方都在等待对方释放所需要的锁。

另一个可能是运行不会出现死锁,尝试把 task1 sleep 去除,这时并行执行任务,恰巧 task1 运行完任务释放 lock1 而 task2 也运行完释放 lock2。

避免死锁注意事项:

  1. 减少嵌套同步
  2. 减少共享数据定义

ReentrantLock

从 Java 1.5 引入 java.util.concurrent.locks 接口,它对应实现类是 java.util.concurrent.locks.ReentrantLock,具体操作是手动上锁和释放锁。

方法:

  • void lock(),加锁。
  • void unlock(),释放锁。

官方建议用法:

class X {
    private final ReentrantLock lock = new ReentrantLock();
    // ...

    public void m() {
        lock.lock();  // block until condition holds
        try {
        // ... method body
        } finally {
            lock.unlock()
        }
    }
}

将操作共享数据的方法放入 try 里,就算 try 出现异常,finally 无论什么情况都会释放锁。

改写 ThreadTest07.java 示例,创建 ThreadTest15.java:

import java.util.concurrent.locks.ReentrantLock;

public class ThreadTest15 {
    public static void main(String[] args) {
        Withdrawal5 withdrawal5 = new Withdrawal5();

        new Thread(withdrawal5, "ATM 机").start();
        new Thread(withdrawal5, "手机银行").start();
    }
}

class Withdrawal5 implements Runnable {
    private static float bankAccountBalance = 2000.0F;
    // 创建锁
    private final ReentrantLock lock = new ReentrantLock();

    public void accountWithdrawal() {
        String threadName = Thread.currentThread().getName();

        lock.lock();
        try {
            if (bankAccountBalance >= 2000) {
                // 模拟网络阻塞
                if ("ATM 机".equals(threadName)) {
                    try {
                        Thread.sleep(2500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                bankAccountBalance -= 2000;
                System.out.println(threadName + " 提现 2000 成功银行账户余额:" + bankAccountBalance);
            }
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void run() {
        accountWithdrawal();
    }
}

转账后账户余额没有出现负数。

线程同步练习题

练习题1:

https://www.bilibili.com/video/BV1Kb411W75N?p=440

银行有一个账户。

有两个出乎分别向同一个账户存 3000 元,每次存1000,存 3 次。每次存完打印账户余额。

问题:该程序是否有安全问题,如果有,如何解决?

【提示】

  1. 明确哪些代码是多线程运行代码,需写入 run() 方法
  2. 明确什么是共享数据
  3. 明确多线程运行代码中哪些语句是操作共享数据的。

扩展问题:可否实现两个出乎交替存钱的操作。

练习题 2:

https://www.bilibili.com/video/BV1zB4y1A7rb?p=19

编程题 HomeWork01.java 5min

  1. 在 main 方法中启动两个线程
  2. 第 1 个线程循环随机打印 100 以内的证书
  3. 直到第 2 个线程从键盘读取了 “Q” 命令。

练习题 3:

https://www.bilibili.com/video/BV1zB4y1A7rb?p=20

编程题 HomeWork02.java

  1. 有 2 个用户分别从同一个卡上取钱(总额:10000)
  2. 每次都取 1000,当余额不足时,就不能取款了
  3. 不能出现超取现象 => 线程同步问题

练习题 4:

https://leetcode-cn.com/tag/concurrency/problemset/

线程通信

方法:

  • wait(),进入阻塞状态并释放锁。
  • notify(),唤醒一个线程,具体唤醒哪个是随机的。
  • notifyall(),唤醒所有线程。

这三个方法都定义在 java.lang.Object 类里,每个对象都有这些方法。调用必须在同步代码 synchronized statemenets 和 synchronized methods 里。调的是同步监视器的方法,原因是要释放同步监视器锁,直接写方法名去调用默认对象是 this,如果调的不是当前监视器方法会抛 java.lang.IllegalMonitorStateException 异常。

Producer/Consumer Models(生产者消费者模型)

作业 1:

https://www.bilibili.com/video/BV1Rx411876f?p=809

使用生产者和消费者模式实现交替数据:

假设只有两个线程,输出以下结果:

t1-->1
t2-->2
t1-->3
t2-->4
t1-->5
t2-->6

要求:必须交替,并且t1线程负责输出奇数。t2线程输出偶数。两个线程共享一个数字,每个线程执行时都要对这个数字进行:++

创建 ProducerConsumerModel.java:

public class ProducerConsumerModel {
    /**
     * 共享数据
     */
    private int num;

    /**
     * 生产者内部类输出奇数
     */
    class Producer implements Runnable {
        @Override
        public void run() {
            try {
                while (true) {
                    synchronized (ProducerConsumerModel.class) {
                        if (ProducerConsumerModel.this.num % 2 == 0) {
                            System.out.println("Producer -> " + ++ProducerConsumerModel.this.num);
                            Thread.sleep(1000);
                            // 先唤醒再等待,以避免多个线程之间都在等待造成死锁。
                            ProducerConsumerModel.class.notify();
                            ProducerConsumerModel.class.wait();
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 消费者内部类输出偶数
     */
    class Consumer implements Runnable {
        @Override
        public void run() {
            try {
                while (true) {
                    synchronized (ProducerConsumerModel.class) {
                        if (ProducerConsumerModel.this.num % 2 != 0) {
                            System.out.println("Consumer -> " + ++ProducerConsumerModel.this.num);
                            Thread.sleep(1000);
                            // 先唤醒再等待,以避免多个线程之间都在等待造成死锁。
                            ProducerConsumerModel.class.notify();
                            ProducerConsumerModel.class.wait();
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ProducerConsumerModel producerConsumerModel = new ProducerConsumerModel();
        new Thread(producerConsumerModel.new Consumer()).start();
        new Thread(producerConsumerModel.new Producer()).start();
    }
}

运行输出:

Producer -> 1
Consumer -> 2
Producer -> 3
Consumer -> 4
Producer -> 5
Consumer -> 6
Producer -> 7
Consumer -> 8

创建了俩内部类 Producer 复测输出奇数,Consumer 输出偶数。它俩要共同操作私有实例变量 num。

Producer 输出奇数逻辑很简单,num 取模余数为 0 代表当前是 0 那么就将前自增输出,后面调 wait() 是释放掉锁让其他线程操作,要是调的其他对象 wait() 方法 那么当前线程永远也没法释放锁,产生死锁状态。Consumer 输出偶数也是一样,num 取模不是 0 那么就自增成偶数,其余操作一致。

获取线程执行返回值

Java 5.0 新增 java.util.concurrent.Callable 接口,使用它可执行代码可以获得返回值,配合 java.util.concurrent.FutureTask 使用,可以拿到线程运行完成的返回值。

使用步骤:

  1. 实现 Callable 重写 call 方法,call 方法可以返回 Object 类型 value。Collable 可以用匿名内部类方式创建,或者 implements 实现。

  2. 创建 java.util.concurrent.FutureTask 任务。

  3. 将 Callable 作为参数传进 FutureTask。

  4. 创建 Thread(),将 FutureTask 作为参数传入。

  5. Thread().start() 启动线程。

  6. FutureTask 对象 get() 方法获取线程返回值,要注意的是执行 get() 会等到此线程结束才能拿到值,要是不结束就会阻塞导致下面程序无法执行。

创建 threadTest17.java。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

// 1. 实现 Callable 接口,并且指定泛型类型为 Integer,
// 因为 Callable 只有一个 call 方法,本质上是约束它的返回值类型。
public class threadTest17 implements Callable<String> {
    // 1.1 重写 call 方法并必须指定返回参数类型为 Object 或其子类。由于签名实现接口指定了 Integer 所以只能返回类型只能选 Integer。
    // 还需要抛出异常,万一获取过程中出现问题需要往上告诉原因。
    @Override
    public String call() throws Exception {
        System.out.println("子线程名称:" + Thread.currentThread().getName());
        return "1";
    }

    public static void main(String[] args) {
        // 2. 创建 FutureTask 任务,将 Callable 对象传入。
        // FutureTask 类也创建对象也需要指定泛型类型 <V>,它构造方法 FutureTask(Callable<V> callable) 刚好就使用了 <V>,
        // 一旦传递过来的不是 Callable<String>就会说类型不一致,如 Callable<Integer>。
        // 有一个情况例外,当 Callable 不指定泛型就会提示 Unchecked assignment,因为需要一个 Callable<String>,
        // 但你只传了原始类型(raw type) Callable 本身。
        FutureTask<String> futureTaskObj = new FutureTask<>(new threadTest17());

        // 3. 创建 Thread 对象,将 FutureTask 对象传入,并执行线程
        // FutureTask 类实现了 RunnableFuture 接口,而 RunnableFuture 接口继承 Runnable 接口,所以可以作为 Runnable 对象传入。
        new Thread(futureTaskObj, "subThreadNameIs test").start();

        // 4. 取返回值
        try {
            System.out.println("线程返回值:" + futureTaskObj.get());
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }
    }
}

运行输出。

子线程名称:subThreadNameIs test
线程返回值:1

定时器(待补充)

就像 Linux 里 crontab 一样。

线程池(待补充)

实际开发线程池用的多。

最近更新:

发布时间:

讨论讨论

Asia/Shanghai