# 前言

这篇简单记录一下线程,计划是先把线程记录完,越学越感觉基础的重要性,本篇中所有代码都在 Clown_java/src/main/java/JavaSE at master・clown-q/Clown_java (github.com)

# 线程

# 创建

在 java 中也支持多线程编程,一条线程指的是进程中一个单一的控制流,操作系统中可能有了解过的知道,线程的几种状态

graph TD;
  A((新建New))
  B((就绪Ready))
  C((运行Running))
  D((阻塞Blocked))
  E((终止Terminated))
  A -->|启动| B;
  B -->|获取CPU时间片| C;
  C -->|时间片用尽| B;
  C -->|阻塞| D;
  D -->|解除阻塞| B;
  C -->|执行完毕| E;

先看这样一个例子,创建一个线程

public class MultithreadingTest {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0;i<10;i++){
                    System.out.println("线程一");
                }
            }
        });
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0;i<10;i++){
                    System.out.println("线程二");
                }
            }
        });
        thread.start();
        thread1.start();
    }
}

image-20230731095442543

这里可以看一下输出的结果,可以看到打印是交替进行的,两个线程是同时运行的

image-20230731095705790

看一下调用的方法,这里有一个参数 Runnable,这里会自动生成一个新的名字作为线程名,看一下传入的参数

image-20230731100113319

它实际上是一个接口,并且使用了 @FunctionalInterface,是函数式接口的注解,可以简写为 Lambda 表达式

public class MultithreadingTest {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0;i<10;i++){
                System.out.println("线程一");
            }
        });
        Thread thread1 = new Thread(() -> {
            for (int i = 0;i<10;i++){
                System.out.println("线程二");
            }
        });
        thread.start();
        thread1.start();
    }
}

这样写起来更加简单

image-20230731100532677

start 方法调用了同类下的 start0 方法

image-20230731100617011

start0 使用 native 修饰,是用 c++ 实现的一个方法

run 方法也可以使得线程启动,但是是在当前线程执行,而不是创建一个线程执行

# 中断 & 休眠

前面简单的说了一下创建和启动,这里简单的记录一下中断 & 休眠,这里可以使用 sleep 方法,顾名思义它可以使得当前进程休眠,等待一段时间

public class SleepTest {
    public static void main(String[] args) throws RuntimeException{
        Thread thread = new Thread(() -> {
            try {
                System.out.println("线程二");
                Thread.sleep(1000);
                System.out.println("线程二");
                Thread.sleep(1000);
                System.out.println("线程二");
                Thread.sleep(1000);
                System.out.println("线程二");
                Thread.sleep(1000);
                System.out.println("线程二");
                Thread.sleep(1000);
                System.out.println("线程二");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        Thread thread1 = new Thread(() -> {
            try {
                System.out.println("线程一");
                Thread.sleep(1000);
                System.out.println("线程一");
                Thread.sleep(1000);
                System.out.println("线程一");
                Thread.sleep(1000);
                System.out.println("线程一");
                Thread.sleep(1000);
                System.out.println("线程一");
                Thread.sleep(1000);
                System.out.println("线程一");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        thread.start();
        thread1.start();
    }
}

image-20230731104311644

在一个线程等待的同时另一个线程被 cpu 选中由就绪态变为执行态,这里捕获 InterruptedException 异常,是因为只要是支持中断的操作都需要抛出异常

实际上可以使用 stop 来中断一个线程,但是当我们在进行一下例如文件操作的时候,如果直接中断可能会导致资源异常,所以 stop 这个方法实际上是已经被弃用的

image-20230731173341324

那么我们该如何去停止中断更加合适呢?在 java 中有一种更为 “优雅” 的方式来 “提醒” 线程该中断了就是 interrupt 方法,简单看一下下面这个例子

public class InterruptTestStop {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (true){
                if (Thread.currentThread().isInterrupted()){
                    System.out.println("循环中断");
                    break;
                }
            }
            System.out.println("中断成功");
        });
        Thread thread1 = new Thread(() ->{
            thread.interrupt();
        });
        thread.start();
        thread1.start();
    }
}

image-20230731174057700

可以看到,当 “提醒” 它中断后就进入了 if 语句

需要多重嵌套的情况下,可以使用 interrupted 复原

当然有时候我们不希望循环直接结束,只是需要停一会其实实现也很简单

public class InterruptPause {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for(int i = 0 ; i < 100 ; i++){
                if(i==50){
                    System.out.println("等一会");
                    Thread.currentThread().suspend();
                    System.out.println("开始运行");
                }
            }
        });
        Thread thread1 = new Thread(() -> {
            try {
                Thread.sleep(1000);
                thread.resume();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        thread.start();
        thread1.start();
    }
}

image-20230731175818543

这似乎看起来很好用?但实际上因为 suspend 方法容易导致死锁问题,这两个方法也是被弃用的

image-20230731175648792

# 线程让位

让位,也就是字面意思,将当前占有的 cpu 让给其他线程使用,操作系统中有着优先调度算法,其实每个线程都是有优先级的,当然我们也可以设置这个优先级,看下面这个例子

public class Priority {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
        });
        Thread thread1 = new Thread(() -> {
        });
        Thread thread2 = new Thread(() -> {
        });
        thread.setPriority(Thread.MAX_PRIORITY);
        thread1.setPriority(Thread.MIN_PRIORITY);
        thread2.setPriority(Thread.NORM_PRIORITY);
        System.out.println(thread.getPriority());
        System.out.println(thread1.getPriority());
        System.out.println(thread2.getPriority());
        System.out.println(Thread.currentThread().getPriority());
    }
}

image-20230731181443726

这里可以设置一个线程的优先级,main 线程的优先级默认是 5

优先级只是说抢到时间片的概率更大,并不是绝对的

优先级并不绝对能让优先级更高的有限执行完,但是我们可以干预这个竞争 cpu 的过程,也就是让位

public class Abdicate {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for(int i = 0 ; i < 100 ; i++){
                System.out.println("线程一");
                if(i%5 == 0 ){
                    System.out.println("线程一满足条件,让位");
                    Thread.yield();
                }
            }
        });
        Thread thread1 = new Thread(() -> {
           for(int i = 0 ;i < 100 ;i++){
               System.out.println("线程二");
               if(i%10 == 0 ){
                   System.out.println("线程二满足条件,让位");
                   Thread.yield();
               }
           }
        });
        thread.start();
        thread1.start();
    }
}

image-20230731183337994

看最开始,线程一得到 cpu 使用权,然后条件满足让位给线程二(线程一出奇的强大,怪)

# 线程合并

前面都是在说让两个线程去竞争 cpu,是不可控的,因为它们是并行的,合并就是将并行再次合并为串行

public class Merge {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for(int i = 0 ; i < 10 ; i++){
                try {
                    Thread.sleep(100);// 抑制线程一执行
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println("线程一");
            }
        });
        Thread thread1 = new Thread(() -> {
            for(int i = 0 ;i < 100 ;i++){
                System.out.println("线程二");
                if(i == 50 ){
                    try {
                        System.out.println("线程一合并");
                        thread.join();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        thread.start();
        thread1.start();
    }
}

image-20230731185142370

可以看见,线程一合并后,线程一先执行完成,在执行线程二

# 线程同步

线程同步是指在多线程编程中,确保多个线程按照一定的顺序执行,避免出现竞争条件和数据不一致的问题。在并发编程中,多个线程可以同时访问共享资源,如果没有合适的同步机制,可能会导致数据的不一致性,这里举一个简单的例子

public class SynchronizationTest {
    public static int sum = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
           for (int i=0;i<10000;i++){
               sum++;
           }
            System.out.println("线程一执行完成");
        });
        Thread thread1 = new Thread(() -> {
            for (int i=0;i<10000;i++){
                sum++;
            }
            System.out.println("线程二执行完成");
        });
        thread.start();
        thread1.start();
        Thread.sleep(1000);
        System.out.println(sum);
    }
}

image-20230731192153310

可以看到,虽然两个线程都已经执行完成了,但是得到的结果并不是预期的 20000,这是因为当线程从程序中取出数据加一的过程中另一个线程也同样取出数据加一,当两个线程覆盖原有数据的时候其实只加了一次

可以使用 synchronized 关键字,可以同步方法,或者同步代码块来保证在同一时刻最多只有一个线程执行该段代码

public class SynchronizationTest {
    public static int sum = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
           for (int i=0;i<10000;i++){
               synchronized (SynchronizationTest.class){
               sum++;
               }
           }
            System.out.println("线程一执行完成");
        });
        Thread thread1 = new Thread(() -> {
            for (int i=0;i<10000;i++){
                synchronized (SynchronizationTest.class){
                    sum++;
                }
            }
            System.out.println("线程二执行完成");
        });
        thread.start();
        thread1.start();
        Thread.sleep(10);
        System.out.println(sum);
    }
}

image-20230731193508170

可以看到,就能够成功加到 20000

当使用 synchronized 修饰静态方法,那么使用的是当前类作为锁,如果修饰的是成员方法,则使用的是对应的对象作为锁

# 死锁

前面也有提到,suspend 容易导致死锁,死锁是指两个或多个进程互相等待对方释放资源而无法继续执行的情况。这种情况下,进程会陷入无限等待的状态(死去的操作系统记忆突然袭击我),先看下面这个出现死锁的情况

public class DeadLock {
    public static void main(String[] args) {
        Object object1 = new Object();
        Object object2 = new Object();
        new Thread(() -> {
            synchronized (object1){
                System.out.println("线程一,获取资源1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (object2){
                    System.out.println("线程一,获取资源2");
                }
            }
        }).start();
        new Thread(() -> {
            synchronized (object2){
                System.out.println("线程二,获取资源1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (object1){
                    System.out.println("线程二,获取资源2");
                }
            }
        }).start();
    }
}

image-20230731202714724

这里有两个资源 object1 和 object2,两个线程都需要两个资源才能完成运行,但是线程一线拿到了 object1,线程二先拿到了 object2,两个线程所需要的资源都在对方手中,但是都不释放资源所以出现了死锁现象

那么怎么样才能避免这种情况呢?

java 中提供了 wait 和 notify 两个方法在 Object 类中,他们配合 synchronized 使用,可以在等待的同时释放资源

public class DeadLock {
    public static void main(String[] args) {
        Object object1 = new Object();
        Object object2 = new Object();
        new Thread(() -> {
            synchronized (object1){
                System.out.println("线程一,获取资源1");
                try {
                    Thread.sleep(1000);
                    System.out.println("开始等待,释放现有资源");
                    object1.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (object2){
                    System.out.println("线程一,获取资源2");
                }
            }
        }).start();
        new Thread(() -> {
            synchronized (object2){
                System.out.println("线程二,获取资源1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (object1){
                    System.out.println("线程二,获取资源2");
                    object1.notifyAll();// 使用 object1 调用的 wait 使用 object1 唤醒
                    System.out.println("唤醒使用wait等待的线程");
                }
            }
        }).start();
    }
}

image-20230731203618288

wait 也可以设置超时,当超时时间到了之后还是没有被唤醒就自动唤醒,这样的使用如果没有被唤醒其实和 sleep 的功能是一样的