三. Java并发基础知识

创建和启动Java线程

Java线程也是一个对象,与任何其他Java对象一样。线程是类 java.lang.Thread的 实例,或此类的子类的实例。除了作为对象之外,java线程还可以执行代码。在这个Java线程教程中,我将解释如何创建和启动线程。

Java线程视频教程

这里是这个Java线程教程的视频版本:
https://www.youtube.com/watch?v=9y7l6QHpoQI

创建和启动线程

下面展示如何用Java创建一个线程:

Thread thread = new Thread();

要启动Java线程,你要调用其start()方法,如下所示:

thread.start();

此示例并未指定线程要执行的代码,线程启动后会立即运行结束,然后停止。

有两种方法可以指定线程需要执行的代码。第一种是创建Thread的子类并覆盖该run()方法。第二种方法是传递一个实现了Runnable接口 (java.lang.Runnable)的实例到Thread的构造函数中。两个方法都在下面介绍。

Thread 子类

指定线程要运行的代码的第一种方法是创建Thread的子类并覆盖run()方法。run()方法是调用start()方法后线程执行的方法。以下是创建Java Thread子类的示例:

public class MyThread extends Thread {

    public void run(){
       System.out.println("MyThread running");
    }
  }

要创建并启动上述线程,您可以这样做:

MyThread myThread = new MyThread();
myTread.start();

一旦线程启动 ,start()方法调用将立即返回,它不会等到run()方法执行完成。run()方法在执行时就好像是由另一个不同的CPU在执行一样。当run()方法执行时,它将打印出文本“MyThread running”。

您还可以创建一个这样的匿名子类Thread:

Thread thread = new Thread() {
    public void run(){
        System.out.println("Thread Running");
    }
}
thread.start();

一旦run()方法被该新线程执行,此示例将打印出文本“Thread running” 。

Runnable 接口实现

指定线程要运行的代码的第二种方法是创建实现java.lang.Runnable接口的类。实现Runnable接口的Java对象可以由Java Thread来执行。如何完成这一点将在本教程稍后介绍。

Runnable接口是Java平台附带的标准Java接口。Runnable接口仅具有一个方法run(),这差不多是Runnable接口的样子:

public interface Runnable() {
    public void run();
}

无论线程在执行时要做什么,都必须包含在run()方法的实现中。有三种方法可以实现Runnable接口:

  • 创建一个实现Runnable接口的Java类。
  • 创建一个实现Runnable接口的匿名类。
  • 创建一个实现Runnable接口的Java Lambda 。

下面的部分将介绍这三个选项。

Java 类实现Runnable

实现Java Runnable接口的第一种方法是创建自己的Java类来实现Runnable接口。以下是实现Runnable接口的自定义Java类的示例:

public class MyRunnable implements Runnable {
    public void run(){
        System.out.println("MyRunnable running");
    }
}

这个Runnable的实现内容是打印 MyRunnable running文本。打印该文本后,run()方法退出,运行run()方法的线程将停止。

Runnable 的匿名实现

您还可以创建一个匿名实现Runnable的类。以下是实现Runnable接口的匿名Java类的示例:

Runnable myRunnable = new Runnable() {
    public void run(){
        System.out.println("Runnable running");
    }
}

除了是一个匿名类之外,此示例与使用自定义类实现Runnable接口的示例非常相似。

Runnable 的Java lambda实现

实现Runnable接口的第三种方法是创建一个 Java Lambda 来实现Runnable。这是可以做到的,因为Runnable接口只有一个未实现的方法,因此实际上(尽管可能是无意的)是一个 功能性Java接口。

以下是实现Runnable接口的Java lambda表达式的示例:

Runnable runnable = () -> { System.out.println("Lambda Runnable running"); };

使用Runnable启动线程

要让一个线程执行run()方法,需要将实现Runnable接口的类,匿名类或lambda表达式的实例传递给Thread 类的构造函数中:

Runnable runnable = new MyRunnable(); // or an anonymous class, or lambda...
Thread thread = new Thread(runnable);
thread.start();

线程启动时,它将调用 MyRunnable 实例的 run() 方法,而不是执行它自己的 run() 方法。上面的示例将打印出文本“MyRunnable running”。

以下是Thread类的源码:

@Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

   /* What will be run. */
    private Runnable target;

从中可以看出run()方法的默认实现就是调用Runnable接口的run()方法,如果创建线程时传递给它一个Runnable对象。这里的成员变量target为构造函数中传入的Runnable对象,具体的初始化过程可以查看Thread源码。

子类还是Runnable?

没有规定两种方法中的哪种方法最好,两种方法都能工作。就个人而言,我更喜欢实现 Runnable,将实现的实例交给 Thread 实例。当 Runnable 提交给线程池执行时,就可以很容易将 Runnable 实例排队,直到线程池的工作线程空闲然后执行(关于线程池的机制在后面章节介绍)。而使用 Thread 子类完成这种工作就会有难度。

有时你需要一起实现 RunnableThread 子类。例如,创建一个可以执行多个 RunnableThread子类 ,实现线程池时通常就是这种情况。

常见陷阱:调用run() 而不是start()

在创建和启动线程时,常见的错误是调用Thread类的run()方法而不是start(),如下所示:

Thread newThread = new Thread(MyRunnable());
newThread.run();  //should be start();

起初,你可能没有发现任何问题因为 Runnablerun() 像你期望的一样执行。但是,它不是由刚刚创建的新线程执行的,相反,该 run() 方法由创建该线程的线程执行。换句话说,就是执行上面两行代码的线程,这点可以从前面的Thread源码中看出。要使新创建的线程调用 MyRunnable 实例的 run() 方法,必须调用 newThread.start() 方法。因为线程的状态不止有运行态,还会有等待,睡眠等状态以及线程的上下文切换,直接调用run()方法只会执行其中的代码,不会正常的创建一个线程,也不会是真正的多线程执行,关于线程的基本理论会在后面说明。

线程名称

创建Java线程时,可以为其命名。该名称可以帮助您区分不同的线程。例如,如果多个线程写入 System.out ,可以很方便地查看哪个线程写入文本。这是一个例子:

Thread thread = new Thread("New Thread") {
    public void run(){
        System.out.println("run by: " + getName());
    }
};

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

注意字符串"New Thread"作为参数传递给 Thread 构造函数。该字符串是线程的名称。该名称可以通过 ThreadgetName() 方法获得。当你使用Runnable 实现时你也可以传递名称 :

MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable, "New Thread");

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

但请注意,由于 MyRunnable 类不是 Thread 子类,因此它无权访问执行线程的 getName() 方法。

以下代码是错误的:

MyRunnable runnable = new MyRunnable();
runnable.getName();  //error

启动一个线程前,最好为这个线程设置名称,因为这样在使用jstack分析程序或者进行问题排查时,就会给开发人员一点提示,自定义的线程最好的起个名字。

Thread.currentThread()

Thread.currentThread() 方法返回执行 currentThread() 方法的 Thread 实例的引用。这样,你就可以在run()方法中访问执行自己的Java Thread 对象。以下是如何使用 Thread.currentThread() 的示例:

Thread thread = Thread.currentThread();

一旦有了 Thread 对象的引用,就可以在其上调用方法。例如,您可以获取正在执行当前代码的线程的名称,如下所示:

String threadName = Thread.currentThread().getName();

Java线程示例

这是一个小例子。首先,它打印出执行 main() 方法的线程的名称。该线程由JVM分配。然后它启动10个线程并给它们一个数字("" + i)作为名称。然后每个线程打印出其自己名称,然后停止执行。

public class ThreadExample {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
        for(int i=0; i<10; i++) {
            new Thread("" + i) {
                public void run() {
                    System.out.println("Thread: " + getName() + " running");
                }
            }.start();
        }
    }
}

请注意,即使线程按顺序启动(1,2,3等),它们也可能不会按启动的顺序执行,这意味着线程1可能不是第一个将其名称写入 System.out 的线程。这是因为线程原则上并行执行而不是顺序执行,由JVM和操作系统确定执行线程的顺序,顺序不必与它们的启动顺序相同​​。

暂停一个 Thread

线程可以通过调用静态方法 Thread.sleep() 来暂停自身。 sleep() 方法需要毫秒数作为参数。sleep() 方法将在线程恢复执行之前让线程休眠指定的毫秒数。sleep() 方法不是100%精确,但它已经足够使用了。以下是通过调用 Thread.sleep() 方法暂停Java线程3秒(3,000毫秒)的示例:

try {
    Thread.sleep(3L * 1000L);
} catch (InterruptedException e) {
    e.printStackTrace();
}

执行上述Java代码的线程将休眠大约3秒(3,000毫秒)。

停止一个 Thread

停止Java线程需要准备一些线程实现代码。Java Thread类 包含一个 stop() 方法,但不推荐使用它。原始 stop() 方法不会提供有关线程停止状态的任何保证。这意味着,线程在执行期间访问的所有Java对象都将处于未知状态。如果应用程序中的其他线程也访问相同的对象,则应用程序可能会意外地和不可预测地失败。

代替调用 stop() 方法,你需要实现你自己的线程代码,以便可以停止它。下面是一个实现 Runnable 接口的类的示例,其中包含一个额外的方法doStop(),该方法被调用来通知 Runnable 以停止。Runnable 会检查这个信号,如果信号符合就停止。

public class MyRunnable implements Runnable {
    private boolean doStop = false;

    public synchronized void doStop() {
        this.doStop = true;
    }

    private synchronized boolean keepRunning() {
        return this.doStop == false;
    }

    @Override
    public void run() {
        while(keepRunning()) {
            // keep doing what this thread should do.
            System.out.println("Running");

            try {
                Thread.sleep(3L * 1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

注意 doStop()keepRunning() 方法。 doStop() 旨在被另一个线程调用执行,而不是在 MyRunnablerun() 方法中使用。 keepRunning() 方法由执行 MyRunnablerun() 方法的线程内部调用。只要 doStop()尚未调用,keepRunning() 方法将永远返回true - 意味着执行该 run() 方法的线程将继续运行。

下面是一个执行上述 MyRunnable 类实例的Java线程的示例,主线程在经过一段延迟后停止该子线程:

public class MyRunnableMain {

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

        try {
            Thread.sleep(10L * 1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        myRunnable.doStop();
    }
}

此示例首先创建一个 MyRunnable 实例,然后将该实例传递给一个线程并启动该线程。然后执行main()方法的线程(主线程)休眠10秒,休眠结束后调用 MyRunnable 实例的doStop()方法。这将导致执行 MyRunnable 的线程停止,因为 keepRunning() 将在 doStop() 调用之后返回 false

请记住,如果你的 Runnable 实现不只需要 run() 方法(例如还需要一个 stop()pause() 方法),那么您就不能再使用Java lambda表达式创建实现 Runnable 。Java lambda只能实现单个方法。相反,您必须使用自定义类或继承 Runnable 的自定义接口,该接口可以具有额外的方法,然后由匿名类实现。

还有两个类似stop()的方法是suspend()resume()方法,第一个用于暂停线程,另一个用于恢复线程,但是这些方法都和stop()方法一样被废弃了。废弃的原因主要有:以suspend()方法为例,调用后,线程不会释放已经占用的资源(比如锁),而是占有资源进行睡眠状态,这样容易引发死锁问题。同样,stop()方法在终结一个线程的时候也不会保证线程的资源正常释放,通常是没有给予线程完成资源释放工作的机会。

理解中断

中断可以理解为线程的一个标识位属性,他表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt()方法对其进行中断操作。

线程通过检查自身是否被中断来进行响应,线程可以通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位。如果该线程已经处于终结状态,即使该线程被中断过,在调用该线程对象的isInterrupted()方法时依然会返回false。

许多声明抛出InterruptedException的方法(例如Thread.sleep())在抛出异常前,Java虚拟机会先将该线程的中断标志位清除,然后抛出异常,此时isInterrupted()方法将返回false。

public class InterruptedTest {
    /**
     * result: SleepThread interrupted is false
     *         BusyThread interrupted is true
     * SleepThread throw InterruptedException
     * @throws Exception InterruptedException
     */
    @Test
    public void test() throws Exception {
        Thread sleepThread = new Thread(()->{
            while(true) {
                SleepUtils.sleep(10);
            }
        }, "SleepThread");
        sleepThread.setDaemon(true);
        Thread busyThread = new Thread(()->{
            while (true) { }
        },"BusyThread");
        busyThread.setDaemon(true);
        sleepThread.start();
        busyThread.start();

        //休眠,使两个线程充分运行
        SleepUtils.sleep(5);
        sleepThread.interrupt();
        busyThread.interrupt();
        System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
        System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());
        //防止两个线程立即退出
        SleepUtils.sleep(2);
    }
}

输出结果如下:

java.lang.InterruptedException: sleep interrupted
at java.base/java.lang.Thread.sleep(Native Method)
at java.base/java.lang.Thread.sleep(Thread.java:340)
at java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:403)
at util.SleepUtils.sleep(SleepUtils.java:8)
at p4.InterruptedTest.lambda$test$0(InterruptedTest.java:22)
at java.base/java.lang.Thread.run(Thread.java:844)
SleepThread interrupted is false
BusyThread interrupted is true

从结果可以看出,抛出InterruptedException的线程SleepThread,中断标识位被清除了,而一直忙碌的线程BusyThread,中断标识位没有被清除。

安全的停止线程

在前面已经说过一个正确停止线程的方法,不过在讲解了中断之后,也因此多了一种新的方式。现在对其进行一点修改:

/**
 * 中断操作是一种简便的线程交互方式,最适合用来取消或停止任务
 * 除了中断以外,还可以用一个boolean变量来控制是否需要停止任务并终止该线程
 * 这两种方法都可以在线程终止时释放资源,而废弃的stop()则不可以
 */
public class ShutDownTest {
    @Test
    public void test() throws Exception {
        Runner one = new Runner();
        Thread thread = new Thread(one, "CountThread");
        thread.start();

        SleepUtils.sleep(1);
        thread.interrupt();

        Runner two = new Runner();
        thread = new Thread(two, "CountThread");
        thread.start();

        SleepUtils.sleep(1);
        two.cancel();
    }

    private static class Runner implements Runnable {
        private long i;
        private volatile boolean on = true;

        @Override
        public void run() {
            while (on && !Thread.currentThread().isInterrupted()) {
                ++i;
            }
            System.out.println("count i : " + i);
        }

        void cancel() {
            on = false;
        }
    }
}

输出结果如下:

count i : 364312261
count i : 353178215

可以看出通过cancel()方法和中断操作都可以将CountThread得以终止。这两种方式可以使线程在终止时有机会去清理资源,而不是武断的直接将线程停止,因此这两种方式显得更加安全和优雅。

竞争条件和临界区

一个竞争条件 是一个可能在临界区内出现的特殊情况。临界区 是被多个线程执行的代码区域,并且由于临界区的同步执行会导致线程执行顺序会出现差别。

当多个线程执行临界区产生的结果因为线程执行的顺序而最终不同时,临界区被称为包含竞争条件。竞争条件一词源于线程正在竞争通过临界区的比喻,并且该竞争的结果影响执行临界区的结果。

这可能听起来有点复杂,因此我将在以下章节中详细介绍竞争条件和临界区。

临界区

在同一个应用程序中运行多个线程不会导致问题,但是当多个线程访问相同的资源时会出现问题。例如,相同的内存(变量,数组或对象),系统(数据库,Web服务等)或文件。

实际上,只有一个或多个线程写入这些资源时才会出现问题。只要资源不改变,让多个线程读取相同的资源是安全的。

这是一个临界区Java代码示例,如果同时由多个线程执行,则可能会失败:

public class Counter {
     protected long count = 0;

     public void add(long value){
         this.count = this.count + value;
     }
}

想象一下,如果两个线程A和B正在同一个 Counter 类实例上执行add方法,我们无法知道操作系统何时在两个线程之间切换。 add() 方法中的代码不是被Java虚拟机作为单个原子指令执行的,而是将其作为一组较小的指令执行,类似于:

  1. 从内存中读取this.count到寄存器中。
  2. 添加值到寄存器。
  3. 将寄存器写入存储器。

观察以下线程A和B的混合执行会发生什么:

   this.count = 0;

   A:  Reads this.count into a register (0)
   B:  Reads this.count into a register (0)
   B:  Adds value 2 to register
   B:  Writes register value (2) back to memory. this.count now equals 2
   A:  Adds value 3 to register
   A:  Writes register value (3) back to memory. this.count now equals 3

两个线程想要将值2和3添加到counter。因此,在两个线程完成执行后,该值应该为5。但是,由于两个线程的执行是交错的,因此结果会不同。

在上面列出的执行序列示例中,两个线程都从内存中读取值0。然后,他们分别将他们自己的值2和3添加到其中,然后将结果写回内存。 this.count 中存储的值将是最后一个线程写入其中的值而不是5。在上面的例子中它是线程A,但如果执行顺序改变它也可以是线程B.

临界区中的竞争条件

前面示例中 add() 方法中的代码包含一个临界区。当多个线程执行此临界区时,会出现竞争条件。

更正式地说,遇到两个线程竞争相同资源的情况,其中访问资源的顺序是重要的,称为竞争条件。导致竞争条件的代码部分称为临界区。

防止竞争条件

为了防止竞争条件发生,您必须确保临界区作为原子指令执行。这意味着一旦一个线程执行它,在第一个线程离开临界区之前,没有其他线程可以执行它。

通过在临界区使用适当的线程同步机制可以避免竞争条件,可以使用同步的Java代码块来实现线程同步。线程间的同步也可以使用其他同步结构来实现,例如锁或原子变量,如java.util.concurrent.atomic.AtomicInteger。

临界区吞吐量

对于较小的临界区,将整个临界区包含在同步块可以起作用。但是,对于较大的临界区,将它分解为几个较小的临界区可能是有益的,这将允许多个线程同时执行多个较小的临界区,可以减少对共享资源的竞争,从而增加总临界区的吞吐量。

这是一个非常简单的Java代码示例,用于表达我的意思:

public class TwoSums {
    private int sum1 = 0;
    private int sum2 = 0;

    public void add(int val1, int val2){
        synchronized(this){
            this.sum1 += val1;   
            this.sum2 += val2;
        }
    }
}

注意该add()方法将值添加到两个不同的sum成员变量。为了防止竞争条件,求和在Java同步块内执行。使用此实现,只有一个线程可以执行求和。

但是,由于两个sum变量彼此独立,因此可以将它们的求和分成两个独立的同步块,如下所示:

public class TwoSums {
    private int sum1 = 0;
    private int sum2 = 0;

    private Integer sum1Lock = new Integer(1);
    private Integer sum2Lock = new Integer(2);

    public void add(int val1, int val2){
        synchronized(this.sum1Lock){
            this.sum1 += val1;   
        }
        synchronized(this.sum2Lock){
            this.sum2 += val2;
        }
    }
}

现在两个线程可以同时执行该add()方法,第一个同步块内有一个线程,第二个同步块内有另一个线程。两个同步块在不同对象上同步,因此两个不同的线程可以独立的分别执行这两个块。这样线程执行该add()方法就可以彼此等待更少的时间。

当然,这个例子非常简单。在现实生活中的共享资源中,临界区的分解可能要复杂得多,并且需要对执行顺序可能性进行更多分析。

线程安全和共享资源

可以被多个线程同时安全调用的代码称为线程安全。如果一段代码是线程安全的,那么它不包含竞争条件,仅当多个线程更新共享资源时才会出现竞争条件。因此,了解Java线程在执行时需要共享的资源非常重要。

局部变量

局部变量存储在每个线程自己的堆栈中,这意味着线程之间永远不会共享局部变量,这也意味着所有原始类型局部变量(primitive variable,例如int,long等)都是线程安全的。以下是线程安全局部原始类型变量的示例:

public void someMethod(){
    long threadSafeInt = 0;
    threadSafeInt++;
}

局部对象引用

局部对象引用和原始类型变量有点不同,引用自己本身不共享。但是,引用的对象不存储在每个线程的本地堆栈中,所有对象都存储在共享堆中。

如果一个方法创建的对象永远不会离开创建它的方法,那么它是线程安全的。事实上,您也可以将其传递给其他方法和对象,只要这些方法或对象都不会使此对象能够被其他线程使用。

以下是线程安全局部对象的示例:

public void someMethod(){
    LocalObject localObject = new LocalObject();
    localObject.callMethod();
    method2(localObject);
}

public void method2(LocalObject localObject){
    localObject.setValue("value");
}

此示例中LocalObject的实例不从方法返回,也不会传递给可从someMethod()方法外部访问的任何其他对象。执行someMethod()方法的每个线程将创建自己的LocalObject实例并将其分配给localObject引用。因此,这里LocalObject的使用是线程安全的。

实际上,整个方法someMethod()都是线程安全的。即使LocalObject实例作为参数传递给同一个类或其他类中的其他方法,它的使用也是线程安全的。

当然,唯一的例外是,如果一个方法使用LocalObject作为调用参数,并且以允许其他线程访问的方式存储这个LocalObject实例。

下面这个示例展示了上面描述的例外情况:

import org.junit.Test;

import static java.lang.Thread.sleep;

public class LocalObjectTest {
    private LocalObject sharedLocalObject;
    private int num;

    @Test
    public void test() throws Exception {
        method1();
        for(int i = 0; i < 4; ++i) {
            new Thread(() -> sharedLocalObject.setText("" + num++)).start();
        }
        sleep(500);
        System.out.println(sharedLocalObject.text);
    }

    /**
     * 创建一个局部对象引用,并引用一个实例
     */
    private void method1() {
        LocalObject localObject = new LocalObject();
        method2(localObject);
    }

    /**
     * 使一个局部对象逃逸,以此被其他线程访问
     * @param object LocalObject
     */
    private void method2(LocalObject object) {
        sharedLocalObject = object;
    }


    private static class LocalObject {
        private String text;

        void setText(String text) {
            this.text = text;
        }
    }
}

sharedLocalObject即是可从someMethod方法外部访问的对象,此对象可以被其他线程访问(因为此对象是类的成员变量,而线程和嵌套子类一样,可以在run()中访问此对象)。这个示例解释了上面这句比较抽象的话:

此示例中LocalObject的实例不从方法返回,也不会传递给可从someMethod()方法外部访问的任何其他对象。

对象成员变量

对象成员变量(字段)与对象一起存储在堆上。因此,如果两个线程在同一对象实例上调用方法,并且此方法更新成员变量,则该方法不是线程安全的。以下是非线程安全方法的示例:

public class NotThreadSafe{
    StringBuilder builder = new StringBuilder();

    public add(String text){
        this.builder.append(text);
    }
}

如果两个线程在同一个NotThreadSafe实例上同时调用add()方法,则会导致竞争条件。例如:

NotThreadSafe sharedInstance = new NotThreadSafe();

new Thread(new MyRunnable(sharedInstance)).start();
new Thread(new MyRunnable(sharedInstance)).start();

public class MyRunnable implements Runnable{
  NotThreadSafe instance = null;

  public MyRunnable(NotThreadSafe instance){
    this.instance = instance;
  }

  public void run(){
    this.instance.add("some text");
  }
}

注意两个MyRunnable实例共享同一个NotThreadSafe实例。因此,当他们在NotThreadSafe实例上调用add()方法时,会导致竞争条件。

但是,如果两个线程在不同的实例上同时调用add()方法 ,那么它不会导致竞争条件。以下是之前的示例,但略有修改:

new Thread(new MyRunnable(new NotThreadSafe())).start();
new Thread(new MyRunnable(new NotThreadSafe())).start();

现在两个线程各有自己的NotThreadSafe实例,因此它们对add方法的调用不会彼此干扰,代码不再具有竞争条件。因此,即使对象不是线程安全的,它仍然可以以不会导致竞争条件的方式使用。

线程控制逃逸规则

在尝试确定您的代码对某个资源的访问是否是线程安全时,您可以使用线程控制逃逸规则:

If a resource is created, used and disposed within
the control of the same thread,
and never escapes the control of this thread,
the use of that resource is thread safe.

如果资源的创建,使用和回收在同一个线程控制下进行,
并且永远不会从这个线程的控制下逃逸,那么这个资源的使用是线程安全的。

资源可以是任何共享资源,如对象,数组,文件,数据库连接,套接字等。在Java中,您并不需要显式地回收对象,因此“回收”意味着丢失对象的引用(引用另一个对象)或将引用置为null。

即使对象的使用是线程安全的,但是如果该对象指向共享资源(如文件或数据库),则整个应用程序可能不是线程安全的。例如,如果线程1和线程2各自创建自己的数据库连接,连接1和连接2,则每个连接本身的使用是线程安全的。但是连接指向的数据库的使用可能不是线程安全的。例如,如果两个线程都执行如下代码:

check if record X exists
if not, insert record X

检查记录X是否存在
如果没有,插入记录X.

如果两个线程同时执行此操作,并且它们正在检查的记录X恰好是相同的记录,则存在两个线程最终都插入它的风险。这是一个示例:

Thread 1 checks if record X exists. Result = no
Thread 2 checks if record X exists. Result = no
Thread 1 inserts record X
Thread 2 inserts record X

线程1检查记录X是否存在。结果为否
线程2检查记录X是否存在。结果为否
线程1插入记录X.
线程2插入记录X.

对于在文件或其他共享资源上进行操作的线程也可能发生这种情况。因此,区分由线程控制的对象是资源还是仅仅引用这个资源(如数据库连接所做的)是很重要的。

线程安全和不变性

仅当多个线程正在访问同一资源,并且一个或多个线程写入资源时,才会出现竞争条件。如果多个线程读取相同的资源, 竞争条件不会发生。

我们可以确保线程之间共享的对象永远不会被任何线程更新,方法是使共享对象不可变,从而保证线程安全。这是一个例子:

public class ImmutableValue{
    private int value = 0;

    public ImmutableValue(int value){
        this.value = value;
    }

    public int getValue(){
        return this.value;
    }
}

注意ImmutableValue实例的值是在构造函数中传递的,另请注意没有setter方法。一旦ImmutableValue实例被创建,你将不能改变它的值,它是不可变的。但是,您可以使用getValue()方法读取它。

如果需要对ImmutableValue实例执行操作,可以通过返回带有该操作产生的值的新实例来执行此操作。以下是添加操作的示例:

public class ImmutableValue{
    private int value = 0;

    public ImmutableValue(int value){
        this.value = value;
    }

    public int getValue(){
        return this.value;
    }

    public ImmutableValue add(int valueToAdd){
        return new ImmutableValue(this.value + valueToAdd);
    } 
}

注意add()方法返回了一个带有add操作结果的新ImmutableValue实例,而不是将值添加到自身。

引用不是线程安全的!

要记住,即使对象是不可变的并且因此线程安全,该对象的引用也可能不是线程安全的。看看这个例子:

public class Calculator{
    private ImmutableValue currentValue = null;

    public ImmutableValue getValue(){
        return currentValue;
    }

    public void setValue(ImmutableValue newValue){
        this.currentValue = newValue;
    }

    public void add(int newValue){
        this.currentValue = this.currentValue.add(newValue);
    }
}

Calculator类持有一个对ImmutableValue实例的引用。注意可以通过setValue()add()方法更改该引用。因此,即使Calculator类在内部使用不可变对象,它本身也不是不可变的,因此不是线程安全的,这和前面的累加器实现一样,add()和setValue()方法都不是原子操作,而是几个操作组成。换句话说:ImmutableValue是线程安全的,但使用它不是。在尝试通过不变性实现线程安全时,请记住这一点。

为了使Calculator类线程安全,你可以使用synchronized关键词声明getValue()setValue()add()方法 ,那将可以做到。

原网页:

  • Creating and Starting Java Threads
  • Race Conditions and Critical Sections
  • Thread Safety and Shared Resources
  • Thread Safety and Immutability

你可能感兴趣的:(三. Java并发基础知识)