线程安全问题的 3 种解决方案!

a5deafb8ea183146b5f27262a0c2d4c9.png

作者 | 磊哥

来源 | 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);}
}

以上程序的执行结果如下图所示:b10092e31cff3a1e25989f0b1035e9d8.png从上述执行结果可以看出,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());}
}

以上程序的执行结果如下图所示:b80a1ad5ab0aaac018857754527c767c.png

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);}
}

以上程序的执行结果如下图所示:5ab43ba7869d100fdf1a20a871e6861b.png

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);}
}

以上程序的执行结果如下图所示:d476a718e5ed0b7ae47d0ecf95b03cdf.png

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);}
}

以上程序的执行结果如下图所示:7556cd3f67acfbcaa8e912fd2c4313b5.png

总结

在 Java 中,解决线程安全问题的手段有 3 种:

1.使用线程安全的类,如 AtomicInteger 类;

2.使用锁 synchronized 或 ReentrantLock 加锁排队执行;

3.使用线程本地变量 ThreadLocal 来处理。

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

公众号:Java面试真题解析

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

806b807e85bbc48799aaa869719b7532.gif

往期推荐

b9bb12bca20bb5d74184a49ded1b9ce4.png

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


8bf9d512d611248245c377dc42951c6f.png

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


ed3bc490adb20897dffb3ef5b74cb91e.png

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


8d8edb1ea78f3629972c627c1e316c6d.gif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/544225.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

黑色30s高并发IIS设置

在这篇博文中&#xff0c;我们抛开对阿里云的怀疑&#xff0c;完全从ASP.NET的角度进行分析&#xff0c;看能不能找到针对问题现象的更合理的解释。 “黑色30秒”问题现象的主要特征是&#xff1a;排队的请求&#xff08;Requests Queued&#xff09;突增&#xff0c;到达HTTP.…

我们可以覆盖Java中的main()方法吗?

The question is that "Can we override main() method in Java?" 问题是“我们可以覆盖Java中的main()方法吗&#xff1f;” No, we cant override the main() method in java. 不&#xff0c;我们不能覆盖java中的main()方法 。 First, we will understand what …

一文读懂MySQL查询语句的执行过程

需要从数据库检索某些符合要求的数据&#xff0c;我们很容易写出 Select A B C FROM T WHERE ID XX 这样的SQL&#xff0c;那么当我们向数据库发送这样一个请求时&#xff0c;数据库到底做了什么&#xff1f;我们今天以MYSQL为例&#xff0c;揭示一下MySQL数据库的查询过程&a…

angularJS的$http.post请求,.net后台接收不到参数值的解决方案

JS通用部分var shoppingCartModule angular.module(starter, [ionic], function ($httpProvider) {// Use x-www-form-urlencoded Content-Type$httpProvider.defaults.headers.post[Content-Type] application/x-www-form-urlencoded;charsetutf-8;/*** The workhorse; conve…

带有示例的Python列表reverse()方法

列出reverse()方法 (List reverse() Method) reverse() method is used to reverse the elements of the list, the method is called with this list (list in which we have to reverse the elements) and it reverses all elements in the list. reverse()方法用于反转列表中…

复杂度O(n)倒转链表

1 public class ListNode {2 int val;3 ListNode next;4 ListNode(int x) { val x; }5 ListNode(){}6 7 public static ListNode revese(ListNode input)8 {9 ListNode head new ListNode();//头插法的头 10 ListNode cur in…

synchronized底层是如何实现的?

作者 | 磊哥来源 | Java面试真题解析&#xff08;ID&#xff1a;aimianshi666&#xff09;转载请联系授权&#xff08;微信ID&#xff1a;GG_Stone&#xff09;想了解 synchronized 是如何运行的&#xff1f;就要先搞清楚 synchronized 是如何实现&#xff1f;synchronized 同步…

java sublist_Java Vector subList()方法与示例

java sublist向量类subList()方法 (Vector Class subList() method) subList() method is available in java.util package. subList()方法在java.util包中可用。 subList() method is used to return a set of sublist [it returns all those elements exists in a given rang…

单例模式 4 种经典实现方法

0.前言 如果你去问一个写过几年代码的程序员用过哪些设计模式&#xff0c;我打赌&#xff0c;90%以上的回答里面会带【单例模式】。甚至有的面试官会直接问&#xff1a;说一下你用过哪些设计模式&#xff0c;单例就不用说了。你看&#xff0c;连面试官都听烦了&#xff0c;火爆…

CSRF简单介绍及利用方法-跨站请求伪造

0x00 简要介绍 CSRF&#xff08;Cross-site request forgery&#xff09;跨站请求伪造&#xff0c;由于目标站无token/referer限制&#xff0c;导致攻击者可以用户的身份完成操作达到各种目的。根据HTTP请求方式&#xff0c;CSRF利用方式可分为两种。 0x01 GET类型的CSRF 这种类…

java setsize_Java Vector setSize()方法与示例

java setsize向量类setSize()方法 (Vector Class setSize() method) setSize() method is available in java.util package. setSize()方法在java.util包中可用。 setSize() method is used to set the new size of this vector and when new size (n_size) > current size …

虾皮二面:什么是零拷贝?如何实现零拷贝?

前言 零拷贝是老生常谈的问题啦&#xff0c;大厂非常喜欢问。比如Kafka为什么快&#xff0c;RocketMQ为什么快等&#xff0c;都涉及到零拷贝知识点。最近技术讨论群几个伙伴分享了阿里、虾皮的面试真题&#xff0c;也都涉及到零拷贝。因此本文将跟大家一起来学习零拷贝原理。1.…

设计模式2:工程模式(1)

什么是工厂模式? 提供一个创建一系列或相互依赖对象的接口&#xff0c;而不需指定它们具体的类。 通俗的讲就是定义了多个产品的类&#xff0c;且只有一个工厂类&#xff0c;而这个工厂类根据需求的不同&#xff0c;可以产生不同产品类的对象。 作用:主要为创建对象提供过度接…

java indexof_Java Vector indexOf()方法与示例

java indexof向量类indexOf()方法 (Vector Class indexOf() method) Syntax: 句法&#xff1a; public int indexOf(Object ob);public int indexOf(Object ob, int indices);indexOf() method is available in java.util package. indexOf()方法在java.util包中可用。 indexO…

各大框架都在使用的Unsafe类,到底有多神奇?

前言 几乎每个使用 Java开发的工具、软件基础设施、高性能开发库都在底层使用了sun.misc.Unsafe&#xff0c;比如Netty、Cassandra、Hadoop、Kafka等。Unsafe类在提升Java运行效率&#xff0c;增强Java语言底层操作能力方面起了很大的作用。但Unsafe类在sun.misc包下&#xff0…

Codis 分布式缓存部署

为什么80%的码农都做不了架构师&#xff1f;>>> 环境介绍: 1:机器三台 ,IP/hostname 如下, hostname的设置很重要zookeeper / codis的通信都会用到,所以要配置好三台机器的hosts文件. 10.221.8.220 机器的hostname为 Redis1 10.221.8.221 机器的hostname为 Redis…

treeset java_Java TreeSet Higher()方法与示例

treeset javaTreeSet类Higher()方法 (TreeSet Class higher() method) higher() method is available in java.util package. Higher()方法在java.util包中可用。 higher() method is used to return the lowest element in this TreeSet that is higher than the specified el…

怎么解决MySQL死锁问题的?

咱们使用 MySQL 大概率上都会遇到死锁问题&#xff0c;这实在是个令人非常头痛的问题。本文将会对死锁进行相应介绍&#xff0c;对常见的死锁案例进行相关分析与探讨&#xff0c;以及如何去尽可能避免死锁给出一些建议。话不多说&#xff0c;开整&#xff01;什么是死锁死锁是并…

strictmath_Java StrictMath cos()方法与示例

strictmathStrictMath类cos()方法 (StrictMath Class cos() method) cos() method is available in java.lang package. cos()方法在java.lang包中可用。 cos() method is used to return the trigonometric cosine of an angle of the given parameter in the method. Here, c…

Apache cxf JaxRs基本应用

2019独角兽企业重金招聘Python工程师标准>>> 在前一篇中&#xff0c;我们完成了《Apache cxf JaxWs基本应用》 的编写&#xff0c;我们现在实现一个Restful风格的Cxf 。 一、我们首先依旧是基于Maven project配置pom.xml的依赖 [html] view plaincopyprint? <pr…