你的接口真的线程安全了么?聊聊保证线程安全的10个小技巧
作者是名退役复学在校大学生,对jdk、spring、springboot、springcloud、mybatis等开源框架源码有一定研究,欢迎关注,和我一起交流。
前言
对于从事后端开发的同学来说,线程安全
问题是我们每天都需要考虑的问题。
线程安全问题通俗的讲:主要是在多线程的环境下,不同线程同时读和写公共资源(临界资源),导致的数据异常问题。
比如:变量a=0,线程1给该变量+1,线程2也给该变量+1。此时,线程3获取a的值有可能不是2,而是1。线程3这不就获取了错误的数据?
线程安全问题会直接导致数据异常,从而影响业务功能的正常使用,所以这个问题还是非常严重的。
那么,如何解决线程安全问题呢?
今天跟大家一起聊聊,保证线程安全的10个小技巧,希望对你有所帮助。
1. 无状态
我们都知道只有多个线程访问公共资源
的时候,才可能出现数据安全问题,那么如果我们没有公共资源,是不是就没有这个问题呢?
例如:
public class NoStatusService {public void add(String status) {System.out.println("add status:" + status);}public void update(String status) {System.out.println("update status:" + status);}
}
这个例子中NoStatusService没有定义公共资源,换句话说是无状态
的。
这种场景中,NoStatusService类肯定是线程安全的。
2. 不可变
如果多个线程访问的公共资源是不可变
的,也不会出现数据的安全性问题。
例如:
public class NoChangeService {public static final String DEFAULT_NAME = "abc";public void add(String status) {System.out.println(DEFAULT_NAME);}
}
DEFAULT_NAME被定义成了static
final
的常量,在多线程中环境中不会被修改,所以这种情况,也不会出现线程安全问题。
3. 无修改权限
有时候,我们定义了公共资源,但是该资源只暴露了读取的权限,没有暴露修改的权限,这样也是线程安全的。
例如:
public class SafePublishService {private String name;public String getName() {return name;}public void add(String status) {System.out.println("add status:" + status);}
}
这个例子中,没有对外暴露修改name字段的入口,所以不存在线程安全问题。
3. synchronized
使用JDK
内部提供的同步机制
,这也是使用比较多的手段,分为:同步方法
和 同步代码块
。
我们优先使用同步代码块,因为同步方法的粒度是整个方法,范围太大,相对来说,更消耗代码的性能。
其实,每个对象内部都有一把锁
,只有抢到那把锁的线程
,才被允许进入对应的代码块执行相应的代码。
当代码块执行完之后,JVM底层会自动释放那把锁。
例如:
public class SyncService {private int age = 1;private Object object = new Object();//同步方法public synchronized void add(int i) {age = age + i; System.out.println("age:" + age);}public void update(int i) {//同步代码块,对象锁synchronized (object) {age = age + i; System.out.println("age:" + age);} }public void update(int i) {//同步代码块,类锁synchronized (SyncService.class) {age = age + i; System.out.println("age:" + age);} }
}
4. Lock
除了使用synchronized
关键字实现同步功能之外,JDK还提供了Lock
接口,这种显示锁的方式。
通常我们会使用Lock
接口的实现类:ReentrantLock
,它包含了:公平锁
、非公平锁
、可重入锁
、读写锁
等更多更强大的功能。
例如:
public class LockService {private ReentrantLock reentrantLock = new ReentrantLock();public int age = 1;public void add(int i) {try {reentrantLock.lock();age = age + i; System.out.println("age:" + age);} finally {reentrantLock.unlock(); } }
}
但如果使用ReentrantLock,它也带来了有个小问题就是:需要在finally代码块中手动释放锁
。
不过说句实话,在使用Lock
显示锁的方式,解决线程安全问题,给开发人员提供了更多的灵活性。
5. 分布式锁
如果是在单机的情况下,使用synchronized
和Lock
保证线程安全是没有问题的。
但如果在分布式的环境中,即某个应用如果部署了多个节点,每一个节点使用可以synchronized
和Lock
保证线程安全,但不同的节点之间,没法保证线程安全。
这就需要使用:分布式锁
了。
分布式锁有很多种,比如:数据库分布式锁,zookeeper分布式锁,redis分布式锁等。
其中我个人更推荐使用redis分布式锁,其效率相对来说更高一些。
使用redis分布式锁的伪代码如下:
try{String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);if ("OK".equals(result)) {return true;}return false;
} finally {unlock(lockKey);
}
同样需要在finally
代码块中释放锁。
6. volatile
有时候,我们有这样的需求:如果在多个线程中,有任意一个线程,把某个开关的状态设置为false,则整个功能停止。
简单的需求分析之后发现:只要求多个线程间的可见性
,不要求原子性
。
如果一个线程修改了状态,其他的所有线程都能获取到最新的状态值。
这样一分析这就好办了,使用volatile
就能快速满足需求。
例如:
@Service
public CanalService {private volatile boolean running = false;private Thread thread;@Autowiredprivate CanalConnector canalConnector;public void handle() {//连接canalwhile(running) {//业务处理}}public void start() {thread = new Thread(this::handle, "name");running = true;thread.start();}public void stop() {if(!running) {return;}running = false;}
}
需要特别注意的地方是:
volatile
不能用于计数和统计等业务场景。因为volatile
不能保证操作的原子性,可能会导致数据异常。
7. ThreadLocal
除了上面几种解决思路之外,JDK还提供了另外一种用空间换时间
的新思路:ThreadLocal
。
当然ThreadLocal并不能完全取代锁,特别是在一些秒杀更新库存中,必须使用锁。
ThreadLocal的核心思想是:共享变量在每个线程都有一个副本,每个线程操作的都是自己的副本
,对另外的线程没有影响。
温馨提醒一下:我们平常在使用ThreadLocal时,如果使用完之后,一定要记得在
finally
代码块中,调用它的remove
方法清空数据,不然可能会出现内存泄露
问题。
例如:
public class ThreadLocalService {private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();public void add(int i) {Integer integer = threadLocal.get();threadLocal.set(integer == null ? 0 : integer + i);}
}
8. 线程安全集合
有时候,我们需要使用的公共资源放在某个集合当中,比如:ArrayList、HashMap、HashSet等。
如果在多线程环境中,有线程往这些集合中写数据,另外的线程从集合中读数据,就可能会出现线程安全问题。
为了解决集合的线程安全问题,JDK专门给我们提供了能够保证线程安全的集合。
比如:CopyOnWriteArrayList、ConcurrentHashMap、CopyOnWriteArraySet、ArrayBlockingQueue等等。
例如:
public class HashMapTest {private static ConcurrentHashMap<String, Object> hashMap = new ConcurrentHashMap<>();public static void main(String[] args) {new Thread(new Runnable() {@Overridepublic void run() {hashMap.put("key1", "value1");}}).start();new Thread(new Runnable() {@Overridepublic void run() {hashMap.put("key2", "value2");}}).start();try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(hashMap);}
}
在JDK底层,或者spring框架当中,使用ConcurrentHashMap保存加载配置参数的场景非常多。
比较出名的是spring的refresh
方法中,会读取配置文件,把配置放到很多的ConcurrentHashMap缓存起来。
9. CAS
JDK除了使用锁的机制解决多线程情况下数据安全问题之外,还提供了CAS机制
。
这种机制是使用CPU中比较和交换指令的原子性,JDK里面是通过Unsafe
类实现的。
CAS内部包含了四个值:旧数据
、期望数据
、新数据
和 地址
,比较旧数据 和 期望的数据,如果一样的话,就把旧数据改成新数据。如果不一样的话,当前线程不断自旋
,一直到成功为止。
不过,使用CAS保证线程安全,可能会出现ABA
问题,需要使用AtomicStampedReference
增加版本号解决。
其实,实际工作中很少直接使用Unsafe
类的,一般用atomic
包下面的类即可。
public class AtomicService {private AtomicInteger atomicInteger = new AtomicInteger();public int add(int i) {return atomicInteger.getAndAdd(i);}
}
10. 数据隔离
有时候,我们在操作集合数据时,可以通过数据隔离
,来保证线程安全。
例如:
public class ThreadPoolTest {public static void main(String[] args) {ExecutorService threadPool = new ThreadPoolExecutor(8, //corePoolSize线程池中核心线程数10, //maximumPoolSize 线程池中最大线程数60, //线程池中线程的最大空闲时间,超过这个时间空闲线程将被回收TimeUnit.SECONDS,//时间单位new ArrayBlockingQueue(500), //队列new ThreadPoolExecutor.CallerRunsPolicy()); //拒绝策略List<User> userList = Lists.newArrayList(new User(1L, "苏三", 18, "成都"),new User(2L, "苏三说技术", 20, "四川"),new User(3L, "技术", 25, "云南"));for (User user : userList) {threadPool.submit(new Work(user));}try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(userList);}static class Work implements Runnable {private User user;public Work(User user) {this.user = user;}@Overridepublic void run() {user.setName(user.getName() + "测试");}}
}
这个例子中,使用线程池
处理用户信息。
每个用户只被线程池
中的一个线程
处理,不存在多个线程同时处理一个用户的情况。所以这种人为的数据隔离机制,也能保证线程安全。
数据隔离还有另外一种场景:kafka生产者把同一个订单的消息,发送到同一个partion中。每一个partion都部署一个消费者,在kafka消费者中,使用单线程接收消息,并且做业务处理。
这种场景下,从整体上看,不同的partion是用多线程处理数据的,但同一个partion则是用单线程处理的,所以也能解决线程安全问题。
推荐阅读
告别宽表,用DQL成就新一代BI_叶秋学长的博客-CSDN博客
Web前端 ---入门教学_叶秋学长的博客-CSDN博客_web前端教程
5月末跟大家讲讲webpack(生日篇)_叶秋学长的博客-CSDN博客
昨天面了一位,见识到了Spring的天花板~_叶秋学长的博客-CSDN博客
POM文件帮助文档_叶秋学长的博客-CSDN博客
SpringBoot整合Flowable快速实现工作流(高考最后一天,高考加油)_叶秋学长的博客-CSDN博客_flowable整合springboot
瞧瞧人家用SpringBoot写的后端API接口,那叫一个优雅~_叶秋学长的博客-CSDN博客
热门开源Web开发框架推荐_叶秋学长的博客-CSDN博客_开源web框架
你的接口真的线程安全了么?聊聊保证线程安全的10个小技巧相关推荐
- 聊聊保证线程安全的10个小技巧
`` 前言 对于从事后端开发的同学来说,线程安全问题是我们每天都需要考虑的问题. 线程安全问题通俗的讲:主要是在多线程的环境下,不同线程同时读和写公共资源(临界资源),导致的数据异常问题. 比如:变量 ...
- 后端开发—10个小技巧教你保证线程安全
前言 对于从事后端开发的同学来说,线程安全问题是我们每天都需要考虑的问题. 线程安全问题通俗的讲:主要是在多线程的环境下,不同线程同时读和写公共资源(临界资源),导致的数据异常问题. 比如:变量a=0 ...
- 你一定要知道的保证线程安全的10个小技巧
导语 对于从事后端开发的同学来说,线程安全问题是我们每天都需要考虑的问题. 线程安全问题通俗的讲:主要是在多线程的环境下,不同线程同时读和写公共资源(临界资源),导致的数据异常问题. 比如:变量a=0 ...
- 多线程情况下如何保证线程安全
一.线程安全等级 其实线程安全并不是一个"非黑即白"单项选择题.按照"线程安全"的安全程度由强到弱来排序,我们可以将java语言中各种操作共享的数据分为以下5类 ...
- 到底如何保证线程安全,总结得太好了。。
一.线程安全等级 之前的博客中已有所提及"线程安全"问题,一般我们常说某某类是线程安全的,某某是非线程安全的.其实线程安全并不是一个"非黑即白"单项选择题. 按 ...
- 线程池(一):线程池参数及使用说明
目录 一.线程池是什么? 二.线程池参数说明 三.线程池生命周期 四.四种常见线程池 总结 一.线程池是什么? 线程池,是指管理一组工作线程的的资源池.线程池与任务队列密切相关,其中在任务队列work ...
- java 多线程编程(包括创建线程的三种方式、线程的生命周期、线程的调度策略、线程同步、线程通信、线程池、死锁等)
1 多线程的基础知识 1.1 单核CPU和多核CPU 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务.微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那 ...
- 接口调用导致阻塞_RocketMQ与Dubbo之间线程之间如何阻塞和唤醒
在上一篇RocketMQ与Dubbo相爱相杀引起的FullGC文章中,我们讲解了由于Dubbo接口调用耗时太久,而消息生产者发送的消息非常快,导致消息消费者不能及时消费消息,造成消息队列堆积,最终导致 ...
- 通过实现Runnable接口创建,开启,休眠和中断线程。
** 通过实现Runnable接口创建,开启,休眠和中断线程. ** 1.创建线程 在Android中,提供了两种创建线程的方法,一种是通过Thread类的构造方法创建线程对象,并重写run()方法实 ...
- java 手编线程池_死磕 java线程系列之自己动手写一个线程池
欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. (手机横屏看源码更方便) 问题 (1)自己动手写一个线程池需要考虑哪些因素? (2)自己动手写 ...
最新文章
- html怎么加载xml文档,在html中解析xml文件(javascript 读取)
- C++ 学习之旅(6)——循环loop
- HaProxy+Keepalived+Mycat高可用群集配置
- 局部内部类和匿名内部类的对比
- 收藏 |彻底搞懂感受野的含义与计算
- adduser useradd userdel /etc/password【原创】
- STM32:win10装CH340驱动、获取删除权限
- Debian 8 时间同步
- Visual Studio 2012正式版官方下载地址
- Cobalt Strike神器使用教程
- 超好用的SVN使用教程!!不看血亏!!
- 发家致富:爬取双色球信息并统计
- 打印机只能扫描图片,不能扫描成PDF解决办法
- 英语口语232之每日十句口语
- Android 蓝牙自动打开并扫描设备,以及获取对方蓝牙设备的种类
- WPF快速入门2—布局WrapPanel,DockPanel,StackPanel,Canvas
- 2018蓝桥杯C/C++ A组C组题目汇总
- Swift 基础 枚举详解(代码)
- php和c#短信接口,C#代码示例_短信接口 | 微米-中国领先的短信彩信接口平台服务商...
- 洛谷:P1033 [NOIP2002 提高组] 自由落体 C++详解
热门文章
- 620集成显卡和mx250,轻薄本的新独显!MX250现身英伟达官网,核显3.5倍性能
- 应用Matlab小波变换工具箱进行图像压缩
- linux创建目录快捷方式,linux创建快捷方式命令
- 数据结构二叉树学习1-前序序列创建二叉树
- yolov5 数据集预处理(多文件夹同时提取文件并分类)(同时随机提取一定比例的图片和txt文件到指定文件)
- python 统计图绘制,Python绘制统计图表
- 采用晶体管作为电子元器件的计算机属于,采用晶体管作为电子元器件的计算机属于(...
- matlab中对一个数求余,matlab中求余、求模运算方法总结
- nginx服务器添加微信小程序校验文件
- linux 硬盘分区,分区,删除分区,格式化,挂载,卸载笔记