当前位置:网站首页>聊聊保证线程安全的 10 个小技巧
聊聊保证线程安全的 10 个小技巧
2022-07-04 12:51:00 【51CTO】
前言
对于从事后端开发的同学来说,线程安全
问题是我们每天都需要考虑的问题。
线程安全问题通俗的讲:主要是在多线程的环境下,不同线程同时读和写公共资源(临界资源),导致的数据异常问题。
比如:变量a=0,线程1给该变量+1,线程2也给该变量+1。此时,线程3获取a的值有可能不是2,而是1。线程3这不就获取了错误的数据?
线程安全问题会直接导致数据异常,从而影响业务功能的正常使用,所以这个问题还是非常严重的。
那么,如何解决线程安全问题呢?
今天跟大家一起聊聊,保证线程安全的10个小技巧,希望对你有所帮助。
1. 无状态
我们都知道只有多个线程访问公共资源
的时候,才可能出现数据安全问题,那么如果我们没有公共资源,是不是就没有这个问题呢?
例如:
这个例子中NoStatusService没有定义公共资源,换句话说是无状态
的。
这种场景中,NoStatusService类肯定是线程安全的。
2. 不可变
如果多个线程访问的公共资源是不可变
的,也不会出现数据的安全性问题。
例如:
DEFAULT_NAME被定义成了static
final
的常量,在多线程中环境中不会被修改,所以这种情况,也不会出现线程安全问题。
3. 无修改权限
有时候,我们定义了公共资源,但是该资源只暴露了读取的权限,没有暴露修改的权限,这样也是线程安全的。
例如:
这个例子中,没有对外暴露修改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);
}
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
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();
}
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
但如果使用ReentrantLock,它也带来了有个小问题就是:需要在finally代码块中手动释放锁
。
不过说句实话,在使用Lock
显示锁的方式,解决线程安全问题,给开发人员提供了更多的灵活性。
5. 分布式锁
如果是在单机的情况下,使用synchronized
和Lock
保证线程安全是没有问题的。
但如果在分布式的环境中,即某个应用如果部署了多个节点,每一个节点使用可以synchronized
和Lock
保证线程安全,但不同的节点之间,没法保证线程安全。
这就需要使用:分布式锁
了。
分布式锁有很多种,比如:数据库分布式锁,zookeeper分布式锁,redis分布式锁等。
其中我个人更推荐使用redis分布式锁,其效率相对来说更高一些。
使用redis分布式锁的伪代码如下:
同样需要在finally
代码块中释放锁。
如果你对redis分布式锁的用法和常见的坑,比较感兴趣的话,可以看看我的另一篇文章《 聊聊redis分布式锁的8大坑》,里面有更详细的介绍。
6. volatile
有时候,我们有这样的需求:如果在多个线程中,有任意一个线程,把某个开关的状态设置为false,则整个功能停止。
简单的需求分析之后发现:只要求多个线程间的可见性
,不要求原子性
。
如果一个线程修改了状态,其他的所有线程都能获取到最新的状态值。
这样一分析这就好办了,使用volatile
就能快速满足需求。
例如:
public CanalService {
private volatile boolean running = false;
private Thread thread;
private CanalConnector canalConnector;
public void handle() {
while( running) {
}
}
public void start() {
thread = new Thread( this:: handle, "name");
running = true;
thread. start();
}
public void stop() {
if( ! running) {
return;
}
running = false;
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
需要特别注意的地方是:
volatile
不能用于计数和统计等业务场景。因为volatile
不能保证操作的原子性,可能会导致数据异常。
7. ThreadLocal
除了上面几种解决思路之外,JDK还提供了另外一种用空间换时间
的新思路:ThreadLocal
。
当然ThreadLocal并不能完全取代锁,特别是在一些秒杀更新库存中,必须使用锁。
ThreadLocal的核心思想是:共享变量在每个线程都有一个副本,每个线程操作的都是自己的副本
,对另外的线程没有影响。
温馨提醒一下:我们平常在使用ThreadLocal时,如果使用完之后,一定要记得在
finally
代码块中,调用它的remove
方法清空数据,不然可能会出现内存泄露
问题。
例如:
如果对ThreadLocal感兴趣的小伙伴,可以看看我的另一篇文章《 ThreadLocal夺命11连问》,里面有对ThreadLocal的原理、用法和坑,有非常详细的介绍。
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() {
public void run() {
hashMap. put( "key1", "value1");
}
}). start();
new Thread( new Runnable() {
public void run() {
hashMap. put( "key2", "value2");
}
}). start();
try {
Thread. sleep( 50);
} catch ( InterruptedException e) {
e. printStackTrace();
}
System. out. println( hashMap);
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
在JDK底层,或者spring框架当中,使用ConcurrentHashMap保存加载配置参数的场景非常多。
比较出名的是spring的refresh
方法中,会读取配置文件,把配置放到很多的ConcurrentHashMap缓存起来。
9. CAS
JDK除了使用锁的机制解决多线程情况下数据安全问题之外,还提供了CAS机制
。
这种机制是使用CPU中比较和交换指令的原子性,JDK里面是通过Unsafe
类实现的。
CAS内部包含了四个值:旧数据
、期望数据
、新数据
和 地址
,比较旧数据 和 期望的数据,如果一样的话,就把旧数据改成新数据。如果不一样的话,当前线程不断自旋
,一直到成功为止。
不过,使用CAS保证线程安全,可能会出现ABA
问题,需要使用AtomicStampedReference
增加版本号解决。
其实,实际工作中很少直接使用Unsafe
类的,一般用atomic
包下面的类即可。
10. 数据隔离
有时候,我们在操作集合数据时,可以通过数据隔离
,来保证线程安全。
例如:
public class ThreadPoolTest {
public static void main( String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor( 8,
10,
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;
}
public void run() {
user. setName( user. getName() + "测试");
}
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
这个例子中,使用线程池
处理用户信息。
每个用户只被线程池
中的一个线程
处理,不存在多个线程同时处理一个用户的情况。所以这种人为的数据隔离机制,也能保证线程安全。
数据隔离还有另外一种场景:kafka生产者把同一个订单的消息,发送到同一个partion中。每一个partion都部署一个消费者,在kafka消费者中,使用单线程接收消息,并且做业务处理。
这种场景下,从整体上看,不同的partion是用多线程处理数据的,但同一个partion则是用单线程处理的,所以也能解决线程安全问题。
边栏推荐
- R language uses dplyr package group_ The by function and the summarize function calculate the mean and standard deviation of the target variables based on the grouped variables
- [R language data science]: cross validation and looking back
- R语言使用dplyr包的group_by函数和summarise函数基于分组变量计算目标变量的均值、标准差
- R语言ggplot2可视化:gganimate包创建动画图(gif)、使用anim_save函数保存gif可视化动图
- Leetcode 61: 旋转链表
- Xcode 异常图片导致ipa包增大问题
- 吃透Chisel语言.08.Chisel基础(五)——Wire、Reg和IO,以及如何理解Chisel生成硬件
- ARouter的使用
- 吃透Chisel语言.12.Chisel项目构建、运行和测试(四)——Chisel测试之ChiselTest
- Oppo find N2 product form first exposure: supplement all short boards
猜你喜欢
92.(cesium篇)cesium楼栋分层
TestSuite and testrunner in unittest
Ruichengxin micro sprint technology innovation board: annual revenue of 367million, proposed to raise 1.3 billion, Datang Telecom is a shareholder
去除重複字母[貪心+單調棧(用數組+len來維持單調序列)]
吃透Chisel语言.11.Chisel项目构建、运行和测试(三)——Chisel测试之ScalaTest
C # WPF realizes the real-time screen capture function of screen capture box
392. Judgement subsequence
Mask wearing detection based on yolov1
Apple 5g chip research and development failure: continue to rely on Qualcomm, but also worry about being prosecuted?
Data warehouse interview question preparation
随机推荐
2022 game going to sea practical release strategy
Understand chisel language thoroughly 10. Chisel project construction, operation and testing (II) -- Verilog code generation in chisel & chisel development process
Test evaluation of software testing
Learning projects are self-made, and growth opportunities are self created
sql优化之查询优化器
吃透Chisel语言.11.Chisel项目构建、运行和测试(三)——Chisel测试之ScalaTest
China Post technology rushes to the scientific innovation board: the annual revenue is 2.058 billion, and the postal group is the major shareholder
Use of tiledlayout function in MATLAB
吃透Chisel语言.04.Chisel基础(一)——信号类型和常量
How to package QT and share exe
docker-compose公网部署redis哨兵模式
What is the real meaning and purpose of doing things, and what do you really want
go语言中的文件创建,写入,读取,删除(转)
R语言使用dplyr包的group_by函数和summarise函数基于分组变量计算目标变量的均值、标准差
Fs4059c is a 5V input boost charging 12.6v1.2a. Inputting a small current to three lithium battery charging chips will not pull it dead. The temperature is 60 ° and 1000-1100ma is recommended
Test process arrangement (3)
MySQL之详解索引
R language uses dplyr package group_ The by function and the summarize function calculate the mean and standard deviation of the target variables based on the grouped variables
[FAQ] Huawei Account Service Error Report 907135701 Common reasons Summary and Solutions
R language uses bwplot function in lattice package to visualize box plot and par Settings parameter custom theme mode