作者 | 磊哥

来源 | Java面试真题解析(ID:aimianshi666)

转载请联系授权(微信ID:GG_Stone)

线程安全是指某个方法或某段代码,在多线程中能够正确的执行,不会出现数据不一致或数据污染的情况,我们把这样的程序称之为线程安全的,反之则为非线程安全的。在 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 {// 创建 AtomicIntegerprivate 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;// 创建 ReentrantLockprivate 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

往期推荐

面试突击36:线程安全问题是如何产生的?

每周汇总 | Java面试题(共35篇)2022版

面试突击35:如何判断线程池已经执行完所有任务了?

线程安全问题的 3 种解决方案!相关推荐

  1. 解决线程安全问题的两种办法

    package com.xt.java;/*** 创建3个窗口卖票,总票数为100张 使用实现Runnable接口的方式* 问题一:买票过程中,出现了重票.错票-->出现了线程安全问题* 问题出 ...

  2. hashmap是线程安全的吗?怎么解决?_线程安全及三种解决方案

    线程安全:如果有多个线程在同时运行,而这些线程可能会同时运行这段代码.程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的. 线程安全问题都是由全局变量及静 ...

  3. 线程安全问题的原因及解决方案

    线程安全问题:简单来说,就是在多线程的调度下,导致出现了一些随机性,随机性使代码出现 了一些bug =>线程不安全 造成线程不安全的原因有哪些呢? 1)抢占式执行,调度过程随机(也是万恶之源,无 ...

  4. 线程安全问题的原因和解决方案

    一.首先产生线程安全的原因: (一)站在开发者的角度看:①多个线程之间操作同一个数据②至少有一个线程修改这个数据(不是读操作而是写操作) (二)站在系统的角度看:一条语句对应多个指令,线程调度可以发生 ...

  5. SimpleDateFormat类的线程安全问题和解决方案

    摘要:我们就一起看下在高并发下SimpleDateFormat类为何会出现安全问题,以及如何解决SimpleDateFormat类的安全问题. 本文分享自华为云社区<SimpleDateForm ...

  6. iOS多线程全套:线程生命周期,多线程的四种解决方案,线程安全问题,GCD的使用,NSOperation的使用(上)

    2017-07-08 remember17 Cocoa开发者社区 目的 本文主要是分享iOS多线程的相关内容,为了更系统的讲解,将分为以下7个方面来展开描述. 多线程的基本概念 线程的状态与生命周期 ...

  7. 同步锁-线程安全问题解决方案

    1 同步锁 1.1 前言 经过前面多线程编程的学习,我们遇到了线程安全的相关问题,比如多线程售票情景下的超卖/重卖现象. 上节笔记点这里-进程与线程笔记 我们如何判断程序有没有可能出现线程安全问题,主 ...

  8. 如何解决线程安全问题?有几种方式?synchronized与lock有什么异同?

    1.谈谈你对同步代码块中同步监视器和共享数据的理解以及各自要求? 同步监视器:俗称锁.①任何一个类的对象都可以充当:②多个线程共用同一把锁. 共享数据:多个线程共同操作的数据. 2.什么情况会产生线程 ...

  9. SimpleDateFormat线程不安全了?这里有5种解决方案

    摘要:我们知道SimpleDateFormat是线程不安全,本文会介绍多种解决方案来保证线程安全. 本文分享自华为云社区<java的SimpleDateFormat线程不安全出问题了,虚竹教你多 ...

最新文章

  1. 大牛市中大师们如何选股长袖善舞——兼谈本周经济与股市
  2. 【NLP】Transformer自注意机制代码实现
  3. 做系统ghost步骤图解_Ghost 博客搭建超全指南
  4. 使用ueditor实现多图片上传案例——ServiceImpl层(ShoppingServiceImpl)
  5. 数据值、列类型和数据字段属性
  6. “驱网核心技术丛书”创作团队访谈
  7. drupal 迁移_关于如何迁移到Drupal的4个技巧
  8. linux内核 快速分片,linux内核学习笔记------ip报文的分片
  9. oracle 索引 lob 迁移,Oracle 11g到19c迁移TB级lob表的酸爽
  10. excel函数手册_一个函数高手的成长之路
  11. 每周分享第 34 期
  12. 集合涉及到的排序方式
  13. java读取propertiies文件例子
  14. SEO助手-免费万能SEO网站优化小助手
  15. 【Monogdb】MongoDB之十大应用设计技巧
  16. js 数字转为千分符格式
  17. Skeleton Screen -- 骨架屏
  18. Java 递归查询部门树形结构数据
  19. 学科实践活动感悟50字_初中学科实践活动50字
  20. Python time库、random库概览+Python里面有趣的东西

热门文章

  1. 最新变态传奇android,新开变态传奇单职业
  2. 第一章、第一节 Angular基础
  3. Problem E: 高于均分的学生
  4. Cow Contest【最短路-floyd】
  5. Html富文本编辑器
  6. 【转】MyEclipse快捷键大全
  7. 实数历史无穷小能否带领我们直接走向今日科学之辉煌?
  8. 解决wordpress无法离线发布(远程发布)的故障
  9. [你必须知道的.NET] 第五回:深入浅出关键字---把new说透(转载)
  10. Chrome浏览器多开,亲测有效