线程平安是指某个办法或某段代码,在多线程中可能正确的执行,不会呈现数据不统一或数据净化的状况,咱们把这样的程序称之为线程平安的,反之则为非线程平安的。在 Java 中,解决线程平安问题有以下 3 种伎俩:

  1. 应用线程安全类,比方 AtomicInteger。
  2. 加锁排队执行

    1. 应用 synchronized 加锁。
    2. 应用 ReentrantLock 加锁。
  3. 应用线程本地变量 ThreadLocal。

接下来咱们一一来看它们的实现。

线程平安问题演示

咱们创立一个变量 number 等于 0,之后创立线程 1,执行 100 万次 ++ 操作,同时再创立线程 2 执行 100 万次 -- 操作,等线程 1 和线程 2 都执行完之后,打印 number 变量的值,如果打印的后果为 0,则阐明是线程平安的,否则则为非线程平安的,示例代码如下:

public class ThreadSafeTest {    // 全局变量    private static int number = 0;    // 循环次数(100W)    private static final int COUNT = 1_000_000;    public static void main(String[] args) throws InterruptedException {        // 线程1:执行 100W 次 ++ 操作        Thread t1 = new Thread(() -> {            for (int i = 0; i < COUNT; i++) {                number++;            }        });        t1.start();        // 线程2:执行 100W 次 -- 操作        Thread t2 = new Thread(() -> {            for (int i = 0; i < COUNT; i++) {                number--;            }        });        t2.start();        // 期待线程 1 和线程 2,执行完,打印 number 最终的后果        t1.join();        t2.join();        System.out.println("number 最终后果:" + number);    }}

以上程序的执行后果如下图所示:

从上述执行后果能够看出,number 变量最终的后果并不是 0,和预期的正确后果不相符,这就是多线程中的线程平安问题。

解决线程平安问题

1.原子类AtomicInteger

AtomicInteger 是线程平安的类,应用它能够将 ++ 操作和 -- 操作,变成一个原子性操作,这样就能解决非线程平安的问题了,如下代码所示:

import java.util.concurrent.atomic.AtomicInteger;public class AtomicIntegerExample {    // 创立 AtomicInteger    private static AtomicInteger number = new AtomicInteger(0);    // 循环次数    private static final int COUNT = 1_000_000;    public static void main(String[] args) throws InterruptedException {        // 线程1:执行 100W 次 ++ 操作        Thread t1 = new Thread(() -> {            for (int i = 0; i < COUNT; i++) {                // ++ 操作                number.incrementAndGet();            }        });        t1.start();        // 线程2:执行 100W 次 -- 操作        Thread t2 = new Thread(() -> {            for (int i = 0; i < COUNT; i++) {                // -- 操作                number.decrementAndGet();            }        });        t2.start();        // 期待线程 1 和线程 2,执行完,打印 number 最终的后果        t1.join();        t2.join();        System.out.println("最终后果:" + number.get());    }}

以上程序的执行后果如下图所示:

2.加锁排队执行

Java 中有两种锁:synchronized 同步锁和 ReentrantLock 可重入锁。

2.1 同步锁synchronized

synchronized 是 JVM 层面实现的主动加锁和主动开释锁的同步锁,它的实现代码如下:

public class SynchronizedExample {    // 全局变量    private static int number = 0;    // 循环次数(100W)    private static final int COUNT = 1_000_000;    public static void main(String[] args) throws InterruptedException {        // 线程1:执行 100W 次 ++ 操作        Thread t1 = new Thread(() -> {            for (int i = 0; i < COUNT; i++) {                // 加锁排队执行                synchronized (SynchronizedExample.class) {                    number++;                }            }        });        t1.start();        // 线程2:执行 100W 次 -- 操作        Thread t2 = new Thread(() -> {            for (int i = 0; i < COUNT; i++) {                // 加锁排队执行                synchronized (SynchronizedExample.class) {                    number--;                }            }        });        t2.start();        // 期待线程 1 和线程 2,执行完,打印 number 最终的后果        t1.join();        t2.join();        System.out.println("number 最终后果:" + number);    }}

以上程序的执行后果如下图所示:

2.2 可重入锁ReentrantLock

ReentrantLock 可重入锁须要程序员本人加锁和开释锁,它的实现代码如下:

import java.util.concurrent.locks.ReentrantLock;/** * 应用 ReentrantLock 解决非线程平安问题 */public class ReentrantLockExample {    // 全局变量    private static int number = 0;    // 循环次数(100W)    private static final int COUNT = 1_000_000;    // 创立 ReentrantLock    private static ReentrantLock lock = new ReentrantLock();    public static void main(String[] args) throws InterruptedException {        // 线程1:执行 100W 次 ++ 操作        Thread t1 = new Thread(() -> {            for (int i = 0; i < COUNT; i++) {                lock.lock();    // 手动加锁                number++;       // ++ 操作                lock.unlock();  // 手动开释锁            }        });        t1.start();        // 线程2:执行 100W 次 -- 操作        Thread t2 = new Thread(() -> {            for (int i = 0; i < COUNT; i++) {                lock.lock();    // 手动加锁                number--;       // -- 操作                lock.unlock();  // 手动开释锁            }        });        t2.start();        // 期待线程 1 和线程 2,执行完,打印 number 最终的后果        t1.join();        t2.join();        System.out.println("number 最终后果:" + number);    }}

以上程序的执行后果如下图所示:

3.线程本地变量ThreadLocal

应用 ThreadLocal 线程本地变量也能够解决线程平安问题,它是给每个线程单独创立了一份属于本人的公有变量,不同的线程操作的是不同的变量,所以也不会存在非线程平安的问题,它的实现代码如下:

public class ThreadSafeExample {    // 创立 ThreadLocal(设置每个线程中的初始值为 0)    private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);    // 全局变量    private static int number = 0;    // 循环次数(100W)    private static final int COUNT = 1_000_000;    public static void main(String[] args) throws InterruptedException {        // 线程1:执行 100W 次 ++ 操作        Thread t1 = new Thread(() -> {            try {                for (int i = 0; i < COUNT; i++) {                    // ++ 操作                    threadLocal.set(threadLocal.get() + 1);                }                // 将 ThreadLocal 中的值进行累加                number += threadLocal.get();            } finally {                threadLocal.remove(); // 革除资源,避免内存溢出            }        });        t1.start();        // 线程2:执行 100W 次 -- 操作        Thread t2 = new Thread(() -> {            try {                for (int i = 0; i < COUNT; i++) {                    // -- 操作                    threadLocal.set(threadLocal.get() - 1);                }                // 将 ThreadLocal 中的值进行累加                number += threadLocal.get();            } finally {                threadLocal.remove(); // 革除资源,避免内存溢出            }        });        t2.start();        // 期待线程 1 和线程 2,执行完,打印 number 最终的后果        t1.join();        t2.join();        System.out.println("最终后果:" + number);    }}

以上程序的执行后果如下图所示:

总结

在 Java 中,解决线程平安问题的伎俩有 3 种:1.应用线程平安的类,如 AtomicInteger 类;2.应用锁 synchronized 或 ReentrantLock 加锁排队执行;3.应用线程本地变量 ThreadLocal 来解决。

是非审之于己,毁誉听之于人,得失安之于数。

公众号:Java面试真题解析

面试合集:https://gitee.com/mydb/interview