实现线程安全的三种基本手段

389 阅读2分钟

实现线程安全的三种基本手段

摘要

\quad\quad 本文旨在举例说明Java中多线程的不安全性,已经使用三种不同的方法去解决例子中的问题。

目录

(点击可直接跳转)
*1.举例说明
*2.避免在多线程下,出现意外结果的方法

1.前言

\quad\quad 在这个例子中,我们开启100个线程,对一个Integer类型,值为0的数,进行加减同一个数的操作。按道理,结果应该是0,那么现实中的情况真的是不是这样呢?
代码如下:

public class Counter {
    private Integer value = 0;

    public int getValue() {
        return value;
    }

    public int addAndGet(int i) {
        value += i;
        return value;
    }

    public int minusAndGet(int i) {
        value -= i;
        return value;
    }
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        for (int i = 0; i < 10; i++) {
            if (test() == 0) {
                System.out.print(true+" ");
            } else {
                System.out.print(false+" ");
            }
        }
    }

    public static Integer test() throws ExecutionException, InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(100);
        Counter counter = new Counter();

        List<Future<Void>> futures = new ArrayList<>();
        for (int i = 0; i < 100; ++i) {
            futures.add(
                    threadPool.submit(
                            () -> {
                                safeSleep();
                                counter.addAndGet(2);
                                counter.minusAndGet(2);
                                return null;
                            }));
        }
        threadPool.shutdown();

        for (Future future : futures) {
            future.get();
        }
        return counter.value;
    }

    private static void safeSleep() {
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

\quad\quad代码很简单,使用线程池开辟100个线程,每一个线程执行一次加减2操作。100次操作之后,对产生的结果进行与0比较,比较10次。从结果我们可以看到,就算是如此简单的操作,在多线程情况下都会出现问题。
结果如下:

所以,在多线程情况下,需要一些保证同步的措施。

2.避免在多线程下,出现意外结果的方法

\quad 1.使用synchornized同步块
\quad\quad 相当于给代码上锁,也就是同一时刻,只能有一个线程执行该段内容。可以在方法的签名中声明synchornized,其实相当于把这个class对象当成锁。例如:

public synchronized int minusAndGet(int i) {
        value -= i;
        return value;
    }

或者使用一个可变的对象:

private final Object lock = new Object();

public int minusAndGet(int i) {
        synchronized (lock) {
            value -= i;
            return value;
        }
    }

注意:synchornized()中锁住的内容(例子中是lock),必须保证在后面的代码中不会被修改。否则锁住的不是一个对象,结果还是会出现问题。

\quad 2.使用原子类型
\quad\quad使用AtomicInteger/AtomicBoolean/AtomicLong类代替对应的类型,也可以实现同样的效果。 不过需要使用对应的get方法获取相应类型的值,例如:

private AtomicInteger value = new AtomicInteger(0);

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

    // 加上一个整数i,并返回加之后的结果
    public int addAndGet(int i) {
        return value.addAndGet(i);
    }

    // 减去一个整数i,并返回减之后的结果
    public int minusAndGet(int i) {
        return value.addAndGet(-i);
    }

\quad\quad 注意:这里的addAndGet方法是AtomicInteger中提供的加一个整数的方法,与类中的方法无关。
\quad 3.使用ReentrentLock
\quad\quad使用ReentrentLock(可重入锁)的lock/unlock。

ReentrantLock lock = new ReentrantLock();
 public int addAndGet(int i) {
        try {
            lock.lock();
            value += i;
            return value;
        }finally {
            lock.unlock();
        }
    }