咱们都知道,当多个线程并发地操做同一共享资源的时候,容易发生线程安全问题,解决这个问题的一个办法是加锁,那么问题来了:加锁就必定线程安全了吗?html
各位小伙伴,大家的答案是什么?是,仍是不是?java
其实这种面试问题,面试官可能会但愿你能根据不一样的场景展开阐述,而不是简单的回答是或不是,这既可表现出你对多线程中的线程安全问题的理解到位,同时也体现了你分析问题的能力比别的候选人强,考虑问题周到。面试
这种方式其实是将并行变成了串行,全部须要进入同步区的线程,都须要先获取到这把锁,一旦某个线程获取到了锁,其余线程就须要等待,即同时间在同步区范围内,只能容许一个线程进行共享资源的访问,所以会下降性能!数据库
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、二、4 时 new Thread(() -> { for (int i = 0; i < 10000; i++) { demo.incr(); } latch.countDown(); }).start(); } else { // threadIdx 等于 一、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,执行结果以下:api
结果正确,线程安全。缓存
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、二、4 时 new Thread(() -> { for (int i = 0; i < 10000; i++) { // 显式独占锁加锁 lock.lock(); demo.incr(); // 显式独占锁解锁 lock.unlock(); } latch.countDown(); }).start(); } else { // threadIdx 等于 一、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
),因此执行结果是同样的!安全
咱们对 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、二、4 时 new Thread(() -> { for (int i = 0; i < 10000; i++) { demo.incr(); } latch.countDown(); }).start(); } else { // threadIdx 等于 一、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); } }
执行结果以下:并发
能够看到,结果并不正确,线程不安全。oracle
那这是为何呢?其实就是由于这里有两把锁,不一样的锁,也就不能保证多线程对同一共享资源的并发操做是线程安全的。也就是说 0、二、4 线程获取的锁跟 一、3 线程获取的锁不是同一个锁,0、二、4 线程获取的锁做用的对象是调用 incr()
这个方法的对象,也就是 demo
,而 一、3 线程获取的锁做用的对象是 ThreadSafeDemo
这个类的 Class
对象,跟 synchronized (ThreadSafeDemo.class) {...}
的做用是相似的。
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();
如以上代码,在前面的代码基础上,⑴ 处第一次获取到读锁后,在释放读锁以前,对共享资源进行了修改,执行结果以下:
能够看到,由于在读锁区域内对共享资源进行了修改,致使出现了线程安全问题,而这种问题是因为不正确地使用了读写锁致使的。也就是说,在使用读写锁时,不能在读锁范围内对共享资源进行“写”操做,须要理解读写锁的适用场景而且正确地使用它。
此次经过一个面试题,简单地梳理了一下多线程的线程安全问题与锁的关系,但愿对各位能有帮助!因为我的能力所限,若是各位小伙伴在阅读文章时发现有错误的地方,欢迎反馈给我勘正,万分感谢。