Java并发学习笔记(2)-线程的同步和停止

在多线程环境下,我们必须要保证数据的一致性,否则可能会出现程序运行结果和我们预期不符的情况。我们将在本篇中介绍如果对线程进行同步以保证数据的安全性,也会介绍 JDK 提供的使运行中的线程停止的方法,通过运行这个方法找出他的缺陷并给出弥补方案。

一、线程的同步

为了完成一个工作量巨大且逻辑复杂的工作,我们通常需要开启很多个线程来协同完成,为了保证工作的完成结果和质量是符合预期的,我们就必须保证这些线程之间的协同是正确的,这就是线程的同步。狭义的讲,线程同步就是规定线程之间的执行顺序,在一个特定的情况下,哪些线程可以执行,哪些线程不能执行。

先来看一个不对线程进行同步的例子,示例代码如下:

public class TheadNotSync {

    public static class User {
        private String text1;
        private String text2;
        public User() {
            this.text1 = "a";
            this.text2 = "a";
        }
        // 省略 getter&setter&toString 方法
    }
    // 读写线程共同操作的对象
    public static User u = new User();
    // 写线程
    public static class WriteThread extends Thread {
        public void run() {
            while (true) {
                String threadName = Thread.currentThread().getName();
                u.setText1(threadName);
                u.setText2(threadName);
            }
        }
    }
    // 读线程
    public static class ReadThread extends Thread {
        public void run() {
            while (true) {
                if (!u.getText1().equals(u.getText2())) {
                    System.err.println(u);
                } else {
                    System.out.println("====================");
                }
                Thread.yield();
            }
        }
    }
    
    public static void main(String[] args) {
        // 启动一个读线程
        new ReadThread().start();
        // 启动无数个写线程
        while (true) {
            WriteThread wt = new WriteThread();
            wt.start();
        }
    }
    
}

我们创建一个自定义类 User 的实例 u,初始化时他的两个成员变量 text1text2 的值都默认为字符串 "a",然后我们启动一个读线程去不断的读取实例 u,如果 u 的两个成员变量的值不相等,则将 u 输出到标准错误流,如果相等,则输出一串等号。另外,我们启动无数个写线程,他们不断的去改变实例 u 的两个成员变量的值。

按照常理,每个线程会将自己的线程名称设置为 text1text2 的值,读线程读到的 text1text2 的值也会保持一致,如果真的是这样,我写这篇博客干嘛?请看运行结果截图:

Java并发学习笔记(2)-线程的同步和停止_第1张图片

从上图我们可以看到,读线程会读到 text1text2 的值不相等的情况,这是为什么呢?原因很简单,线程要能够执行,必须获得 CPU 的执行权,但是在获得 CPU 的执行权后也有可能随时失去这个执行权,线程失去了执行权就不能再继续执行线程任务,而是由另外的线程来继续执行,这就可能出现前一个线程刚改变了 text1 的值就失去了 CPU 的执行权,接着另外一个线程又来继续改变 text2 的值,从而出现 text1text2 不一致的情况。这就是在多线程环境下,对线程不进行同步会出现的结果,导致数据不一致。

如果我们对线程进行了同步,还会有这种数据不一致的情况吗?我们将上例修改为线程同步,示例代码如下:

public class TheadSync {

    public static class User {
        // 类成员和上例相同
    }
    
    public static User u = new User();
    
    public static class WriteThread extends Thread {
        public void run() {
            while (true) {
                // 在进入同步代码块时,线程首先需要拿到锁
                synchronized (u) {
                    String threadName = Thread.currentThread().getName();
                    u.setText1(threadName);
                    u.setText2(threadName);
                }
            }
        }
    }
    
    public static class ReadThread extends Thread {
        public void run() {
            while (true) {
                // 在进入同步代码块时,线程首先需要拿到锁
                synchronized (u) {
                    if (!u.getText1().equals(u.getText2())) {
                        System.err.println(u);
                    } else {
                        System.out.println("====================");
                    }
                }
                Thread.yield();
            }
        }
    }
    
    public static void main(String[] args) {
        // main 方法体和上例相同
    }
}

从上面的代码可以看到,我们仅仅是在读写线程的读写操作外围添加了 synchronized 块,这样就可以保证线程是同步的了。对于 synchronized 块中的代码,简单来说,就是被同步的代码,这些代码在同一时刻只能由一个线程访问。另外,线程要想进入这个同步代码块,必须先拿到一个锁,这个锁可以是任何一个对象(我们这里直接使用 User 对象的实例,您也可以使用其他任何类型的实例),他仅仅作为一个标志,拿到这个锁的线程才可以执行同步代码块中的内容,并且线程在执行完同步代码块中的代码后才会释放这个锁,也就是说,别的线程才可以拿到这个锁接着进入同步代码块。这样我们就保证了当一个线程在执行同步代码块中的代码时,不会丢失 CPU 的执行权,从而不会不出数据不一致的情况,而实际上结果也是如此,请看运行结果截图:

Java并发学习笔记(2)-线程的同步和停止_第2张图片

我们看到,程序并没有出现数据不一致的情况,这表示我们的同步是正确的。另外,如果您的 JVM 可用内存配置的太小,可能会出现上图中的内存溢出错误,解决方案是增大 JVM 的可用内存,具体方法请自行查阅相关信息。

从上面的两个例子我们可以看出,在多线程情况下,对线程进行同步是非常有必要的,也是数据一致性的可靠保证。但是由于我们对代码进行了同步,所有的线程变成了串行执行,即只有等前一个执行完,后一个才能继续执行,这样很明显是增加了系统的消耗,也降低了程序的性能。所以我们需要把握同步代码块的范围,只在有可能导致数据一致性被破坏的节点进行同步,而不是粗暴的对整个线程任务进行同步。如果需要对整个线程任务进行同步,可以直接在方法声明时使用关键字 synchronized,示例如下:

public synchronized void run() {
    while (true) {
        if (!u.getText1().equals(u.getText2())) {
          System.err.println(u);
        } else {
          System.out.println("====================");
        }
        Thread.yield();
    }
}

二、线程的停止

我们可以使用线程对象的 start() 方法来启动一个线程,有启动就有停止,那么我们怎么来停止一个正在执行中的线程呢?线程实例对象为我们提供了一个 stop() 方法来完成这一操作,示例代码如下:

public class ThreadStop {

    public static class User {
        // User 类成员与上例相同
    }
    
    public static User u = new User();
    
    public static class WriteThread extends Thread {
        public void run() {
            while (true) {
                synchronized (u) {
                    // 这里进行字符串拼接是为了消耗时间,默认真实情况下的耗时操作
                    String threadName = Thread.currentThread().getName() + "--" + new Random().nextInt(10);
                    u.setText1(threadName);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    u.setText2(threadName);
                }
            }
        }
    }
    
    public static class ReadThread extends Thread {
        public void run() {
            while (true) {
                synchronized (u) {
                    if (!u.getText1().equals(u.getText2())) {
                        System.err.println(u);
                    } else {
                        System.out.println("====================");
                    }
                }
                Thread.yield();
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        new ReadThread().start();
        while (true) {
            WriteThread wt = new WriteThread();
            wt.start();
            // 让主线程睡眠 0.15s,默认实际情况中的某些耗时操作
            Thread.sleep(150);
            wt.stop();
        }
    }
}

我们每次启动一个线程后,都让主线程睡眠 0.15s,然后调用 stop() 方法将刚才启动的线程停止。而在写线程的线程任务中,我们让线程在给 text1 赋值后睡眠 0.1s,然后再继续给 text2 赋值。我们运行这个示例,会发现即使我们使用了线程同步,数据不一致的情况还是出现了,示例运行结果截图如下:

Java并发学习笔记(2)-线程的同步和停止_第3张图片

我们上面讲过,同步可以避免数据不一致的情况,那么现在又出现了不一致的情况是为什么呢?原因就出在 stop() 方法身上。当我们在如 Eclipse 这种 IDE 中使用 stop() 方法时,编译器会立即报告一个提示,告诉我们这个方法是已经被官方废弃了的,不再受到支持。因为当我们调用 stop() 方法停止一个线程时,无论这个线程是否执行完线程任务,都会立即强制退出并释放锁,其他线程就有可能抢占到锁进入同步区域执行,最终出现数据不一致的情况。所以,我们无论什么时候,都尽量不要作死的去调用 stop() 方法来停止一个线程。

既然我们不能使用 stop() 方法来停止一个线程,那么又该使用什么方式来弥补 stop() 的缺陷呢?很简单,我们可以定义一个线程是否被中断的标识并加入一段自定义的逻辑来实现,示例代码如下:

public class SafeThreadStop {

    public static class User {
        // User 类成员与上例相同
    }
    
    public static User u = new User();
    
    public static class WriteThread extends Thread {
        // 线程是否已被停止的标识
        public volatile boolean stop = false;

        // 停止当前线程的方法
        public void stoped() {
            this.stop = true;
        }

        public void run() {
            while (true) {
                // 执行具体的任务前下能判断线程是否已被停止
                if (stop) {
                    System.out.println("=====线程已被停止=====");
                    break;
                }

                synchronized (u) {
                    // 这里进行字符串拼接是为了消耗时间,默认真实情况下的耗时操作
                    String threadName = Thread.currentThread().getName() + "--" + new Random().nextInt(10);
                    u.setText1(threadName);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    u.setText2(threadName);
                }
            }
        }
    }
    
    public static class ReadThread extends Thread {
        public void run() {
            while (true) {
                synchronized (u) {
                    if (!u.getText1().equals(u.getText2())) {
                        System.err.println(u);
                    } else {
                        System.out.println("====================");
                    }
                }
                Thread.yield();
            }
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        new ReadThread().start();
        while (true) {
            WriteThread wt = new WriteThread();
            wt.start();
            // 让主线程睡眠 0.15s,默认实际情况中的某些耗时操作
            Thread.sleep(150);
            // 调用自定义停止线程的方法
            wt.stoped();
        }
    }
}

我们在写线程中添加了一个 stop 成员变量,他用于表示当前的线程是否已经被停止,默认为 false,表示当前线程并没有被停止。请注意我们使用了一个新的修饰符 volatile,这个修饰符表示其修饰的变量不会被复制到线程实例对象的内存空间,而是仅驻留在主存(main memory)中,所有线程的实例对象对这个变量的操作结果都保存在主存中,这就保证了多个线程读到的状态永远是一致的。

然后我们提供了一个 stoped() 方法,他将 stop 的值修改为 true,表示当前线程已经被停止。接着我们在线程任务一开始,加入一段逻辑,基于 stop 的值判断当前线程是否被中断,如果被中断就跳出线程任务,当前线程也不会再去抢夺锁,进入临界区。

这样的设计就保证了我们能够安全的停止一个线程,不会出现数据不一致的情况,示例运行结果截图如下:

Java并发学习笔记(2)-线程的同步和停止_第4张图片

我们可以看到,数据要么是一致的,要么线程被停止直接退出,不会造成数据不一致的情况。

三、后记

通过本篇的学习,我们了解了怎么实现线程任务的同步来保证数据的一致性,但是需要再次强调,一定要把控好同步的粒度,即同步的代码范围。范围越小,对性能和效率的影响越低。我们也学习了怎么停止正在运行的线程,请牢记不要轻易使用 stop() 方法来停止线程,这可能会导致灾难性的后果。

你可能感兴趣的:(Java并发学习笔记(2)-线程的同步和停止)