咱们都晓得,当多个线程并发地操作同一共享资源的时候,容易产生线程平安问题,解决这个问题的一个方法是加锁,那么问题来了:加锁就肯定线程平安了吗?

各位小伙伴,你们的答案是什么?是,还是不是?

其实这种面试问题,面试官可能会心愿你能依据不同的场景开展论述,而不是简略的答复是或不是,这既可体现出你对多线程中的线程平安问题的了解到位,同时也体现了你剖析问题的能力比别的候选人强,思考问题周到。

1. 加同一个内置锁或者显式独占锁,肯定线程平安

这种形式实际上是将并行变成了串行,所有须要进入同步区的线程,都须要先获取到这把锁,一旦某个线程获取到了锁,其余线程就须要期待,即同工夫在同步区范畴内,只能容许一个线程进行共享资源的拜访,因而会升高性能!

1) 加同一个内置锁

import java.util.concurrent.CountDownLatch;public class ThreadSafeDemo {    private int anInt = 0;    public synchronized void incr() {        anInt++;    }    public void decr() {        synchronized (this) {            anInt--;        }    }    public static void main(String[] args) {        CountDownLatch latch = new CountDownLatch(5);        ThreadSafeDemo demo = new ThreadSafeDemo();        for (int threadIdx = 0; threadIdx < 5; threadIdx++) {            if (threadIdx % 2 == 0) { // threadIdx 等于 0、2、4 时                new Thread(() -> {                    for (int i = 0; i < 10000; i++) {                        demo.incr();                    }                    latch.countDown();                }).start();            } else {                  // threadIdx 等于 1、3 时                new Thread(() -> {                    for (int i = 10000; i > 0; i--) {                        demo.decr();                    }                    latch.countDown();                }).start();            }        }        try {            latch.await();        } catch (InterruptedException e) {            Thread.currentThread().interrupt();        }        // 期望值:10000        System.out.println("以后 anInt 的值为:" + demo.anInt);    }}

如以上代码,开启 5 个并发线程,其中 3 个线程别离自增 10000,2 个线程别离自减 10000,所以最终冀望正确的值应该是 30000 - 20000 = 10000,执行后果如下:

后果正确,线程平安。

2) 加同一个显式独占锁

import java.util.concurrent.CountDownLatch;import java.util.concurrent.locks.ReentrantLock;public class ThreadSafeDemo {    private int anInt = 0;    public void incr() {        anInt++;    }    public void decr() {        anInt--;    }    public static void main(String[] args) {        CountDownLatch latch = new CountDownLatch(5);        ReentrantLock lock = new ReentrantLock();        ThreadSafeDemo demo = new ThreadSafeDemo();        for (int threadIdx = 0; threadIdx < 5; threadIdx++) {            if (threadIdx % 2 == 0) { // threadIdx 等于 0、2、4 时                new Thread(() -> {                    for (int i = 0; i < 10000; i++) {                        // 显式独占锁加锁                        lock.lock();                        demo.incr();                        // 显式独占锁解锁                        lock.unlock();                    }                    latch.countDown();                }).start();            } else {                 // threadIdx 等于 1、3 时                new Thread(() -> {                    for (int i = 10000; i > 0; i--) {                        // 显式独占锁加锁                        lock.lock();                        demo.decr();                        // 显式独占锁解锁                        lock.unlock();                    }                    latch.countDown();                }).start();            }        }        try {            latch.await();        } catch (InterruptedException e) {            Thread.currentThread().interrupt();        }        // 期望值:10000        System.out.println("以后 anInt 的值为:" + demo.anInt);    }}

同 1) 一样,只不过这里换成了显式的独占锁(ReentrantLock),所以执行后果是一样的!

2. 加不同的锁,肯定线程不平安

咱们对 1 中的内置锁局部代码做一些批改,留神 incr()decr() 办法:

import java.util.concurrent.CountDownLatch;public class ThreadSafeDemo {    private static int anInt = 0;    public synchronized void incr() {        anInt++;    }    public static synchronized void decr() {        anInt--;    }    public static void main(String[] args) {        CountDownLatch latch = new CountDownLatch(5);        ThreadSafeDemo demo = new ThreadSafeDemo();        for (int threadIdx = 0; threadIdx < 5; threadIdx++) {            if (threadIdx % 2 == 0) {  // threadIdx 等于 0、2、4 时                new Thread(() -> {                    for (int i = 0; i < 10000; i++) {                        demo.incr();                    }                    latch.countDown();                }).start();            } else {                  // threadIdx 等于 1、3 时                new Thread(() -> {                    for (int i = 10000; i > 0; i--) {                        ThreadSafeDemo.decr();                    }                    latch.countDown();                }).start();            }        }        try {            latch.await();        } catch (InterruptedException e) {            Thread.currentThread().interrupt();        }        // 期望值:10000        System.out.println("以后 anInt 的值为:" + anInt);    }}

执行后果如下:

能够看到,后果并不正确,线程不平安。

那这是为什么呢?其实就是因为这里有两把锁,不同的锁,也就不能保障多线程对同一共享资源的并发操作是线程平安的。也就是说 0、2、4 线程获取的锁跟 1、3 线程获取的锁不是同一个锁,0、2、4 线程获取的锁作用的对象是调用 incr() 这个办法的对象,也就是 demo,而 1、3 线程获取的锁作用的对象是 ThreadSafeDemo 这个类的 Class 对象,跟 synchronized (ThreadSafeDemo.class) {...} 的作用是相似的。

3. 加同一读写锁,不肯定线程平安

1 中应用的是独占锁,会升高性能。实际上在一些场景下,多线程也能够同时访问共享资源,而不会产生线程平安的问题。例如多线程的“读”操作与“读”操作之间。

上面以 Java 8 的 ReentrantReadWriteLock 例子作示例阐明,该示例参考了 Oracle 官网的 API 文档中的例子,>> 传送门:

import java.util.concurrent.CountDownLatch;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.ReentrantReadWriteLock;public class ThreadSafeDemo {    /**     * 数据     */    private String data = null;    /**     * 缓存是否无效     */    private volatile boolean cache = false;    public String getDataFromDb() {        // 模仿从数据库中获取数据,耗时 0.5 秒        String data = null;        try {            TimeUnit.MILLISECONDS.sleep(500L);            data = String.valueOf(System.currentTimeMillis());            System.out.println("[" + Thread.currentThread().getName()                    + "] 缓存有效,从数据库中获取数据:" + data);        } catch (InterruptedException e) {            Thread.currentThread().interrupt();        }        return data;    }    public void use() {        System.out.println("[" + Thread.currentThread().getName()                + "] 以后 data 的值为:" + data);    }    public static void main(String[] args) {        CountDownLatch latch = new CountDownLatch(5);        ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();        ThreadSafeDemo demo = new ThreadSafeDemo();        for (int threadIdx = 0; threadIdx < 5; threadIdx++) {            new Thread(() -> {                // 获取读锁:⑴                rwLock.readLock().lock();                // 如果缓存有效                if (!demo.cache) {                    // 开释读锁(读锁不能降级为写锁):⑴ 处获取的                    rwLock.readLock().unlock();                    // 获取写锁                    rwLock.writeLock().lock();                    try {                        // 再次查看缓存是否无效,因为其余线程有可能先于以后线程获取到写锁并批改了它的值                        if (!demo.cache) {                            demo.data = demo.getDataFromDb();                            // 缓存设为无效                            demo.cache = true;                        }                        // 获取读锁(在开释写锁之前,再获取读锁,进行锁降级):⑵                        rwLock.readLock().lock();                    } finally {                        // 开释写锁,此时线程仍持有读锁(⑵ 处获取的)                        rwLock.writeLock().unlock();                    }                }                try {                    // 模仿 1 秒的解决工夫,并打印出以后值                    TimeUnit.SECONDS.sleep(1);                    demo.use();                } catch (InterruptedException e) {                    Thread.currentThread().interrupt();                } finally {                    // 开释读锁:⑴ 或 ⑵ 处获取的                    rwLock.readLock().unlock();                }                latch.countDown();            }).start();        }        try {            latch.await();        } catch (InterruptedException e) {            Thread.currentThread().interrupt();        }    }}

执行后果:

乍一看,这不是正确的吗?别急,咱们再来加点货色看看:

new Thread(() -> {    // 获取读锁:⑴    rwLock.readLock().lock();    // 如果缓存有效    if (!demo.cache) {        // 谬误示范,在读锁外面批改了数据        demo.cache = true;        demo.data = demo.getDataFromDb();        demo.cache = false;        // 开释读锁(读锁不能降级为写锁):⑴ 处获取的        rwLock.readLock().unlock();        // Omit code...    }    // Omit code...}).start();

如以上代码,在后面的代码根底上,⑴ 处第一次获取到读锁后,在开释读锁之前,对共享资源进行了批改,执行后果如下:

能够看到,因为在读锁区域内对共享资源进行了批改,导致呈现了线程平安问题,而这种问题是因为不正确地应用了读写锁导致的。也就是说,在应用读写锁时,不能在读锁范畴内对共享资源进行“写”操作,须要了解读写锁的实用场景并且正确地应用它。

总结

这次通过一个面试题,简略地梳理了一下多线程的线程平安问题与锁的关系,心愿对各位能有帮忙!因为集体能力所限,如果各位小伙伴在阅读文章时发现有谬误的中央,欢送反馈给我勘正,万分感激。