作者是名退役复学在校大学生,对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. 分布式锁

如果是在单机的情况下,使用synchronizedLock保证线程安全是没有问题的。

但如果在分布式的环境中,即某个应用如果部署了多个节点,每一个节点使用可以synchronizedLock保证线程安全,但不同的节点之间,没法保证线程安全。

这就需要使用:分布式锁了。

分布式锁有很多种,比如:数据库分布式锁,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个小技巧相关推荐

  1. 聊聊保证线程安全的10个小技巧

    `` 前言 对于从事后端开发的同学来说,线程安全问题是我们每天都需要考虑的问题. 线程安全问题通俗的讲:主要是在多线程的环境下,不同线程同时读和写公共资源(临界资源),导致的数据异常问题. 比如:变量 ...

  2. 后端开发—10个小技巧教你保证线程安全

    前言 对于从事后端开发的同学来说,线程安全问题是我们每天都需要考虑的问题. 线程安全问题通俗的讲:主要是在多线程的环境下,不同线程同时读和写公共资源(临界资源),导致的数据异常问题. 比如:变量a=0 ...

  3. 你一定要知道的保证线程安全的10个小技巧

    导语 对于从事后端开发的同学来说,线程安全问题是我们每天都需要考虑的问题. 线程安全问题通俗的讲:主要是在多线程的环境下,不同线程同时读和写公共资源(临界资源),导致的数据异常问题. 比如:变量a=0 ...

  4. 多线程情况下如何保证线程安全

    一.线程安全等级 其实线程安全并不是一个"非黑即白"单项选择题.按照"线程安全"的安全程度由强到弱来排序,我们可以将java语言中各种操作共享的数据分为以下5类 ...

  5. 到底如何保证线程安全,总结得太好了。。

    一.线程安全等级 之前的博客中已有所提及"线程安全"问题,一般我们常说某某类是线程安全的,某某是非线程安全的.其实线程安全并不是一个"非黑即白"单项选择题. 按 ...

  6. 线程池(一):线程池参数及使用说明

    目录 一.线程池是什么? 二.线程池参数说明 三.线程池生命周期 四.四种常见线程池 总结 一.线程池是什么? 线程池,是指管理一组工作线程的的资源池.线程池与任务队列密切相关,其中在任务队列work ...

  7. java 多线程编程(包括创建线程的三种方式、线程的生命周期、线程的调度策略、线程同步、线程通信、线程池、死锁等)

    1 多线程的基础知识 1.1 单核CPU和多核CPU 单核CPU,其实是一种假的多线程,因为在一个时间单元内,也只能执行一个线程的任务.微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那 ...

  8. 接口调用导致阻塞_RocketMQ与Dubbo之间线程之间如何阻塞和唤醒

    在上一篇RocketMQ与Dubbo相爱相杀引起的FullGC文章中,我们讲解了由于Dubbo接口调用耗时太久,而消息生产者发送的消息非常快,导致消息消费者不能及时消费消息,造成消息队列堆积,最终导致 ...

  9. 通过实现Runnable接口创建,开启,休眠和中断线程。

    ** 通过实现Runnable接口创建,开启,休眠和中断线程. ** 1.创建线程 在Android中,提供了两种创建线程的方法,一种是通过Thread类的构造方法创建线程对象,并重写run()方法实 ...

  10. java 手编线程池_死磕 java线程系列之自己动手写一个线程池

    欢迎关注我的公众号"彤哥读源码",查看更多源码系列文章, 与彤哥一起畅游源码的海洋. (手机横屏看源码更方便) 问题 (1)自己动手写一个线程池需要考虑哪些因素? (2)自己动手写 ...

最新文章

  1. html怎么加载xml文档,在html中解析xml文件(javascript 读取)
  2. C++ 学习之旅(6)——循环loop
  3. HaProxy+Keepalived+Mycat高可用群集配置
  4. 局部内部类和匿名内部类的对比
  5. 收藏 |彻底搞懂感受野的含义与计算
  6. adduser useradd userdel /etc/password【原创】
  7. STM32:win10装CH340驱动、获取删除权限
  8. Debian 8 时间同步
  9. Visual Studio 2012正式版官方下载地址
  10. Cobalt Strike神器使用教程
  11. 超好用的SVN使用教程!!不看血亏!!
  12. 发家致富:爬取双色球信息并统计
  13. 打印机只能扫描图片,不能扫描成PDF解决办法
  14. 英语口语232之每日十句口语
  15. Android 蓝牙自动打开并扫描设备,以及获取对方蓝牙设备的种类
  16. WPF快速入门2—布局WrapPanel,DockPanel,StackPanel,Canvas
  17. 2018蓝桥杯C/C++ A组C组题目汇总
  18. Swift 基础 枚举详解(代码)
  19. php和c#短信接口,C#代码示例_短信接口 | 微米-中国领先的短信彩信接口平台服务商...
  20. 洛谷:P1033 [NOIP2002 提高组] 自由落体 C++详解

热门文章

  1. 620集成显卡和mx250,轻薄本的新独显!MX250现身英伟达官网,核显3.5倍性能
  2. 应用Matlab小波变换工具箱进行图像压缩
  3. linux创建目录快捷方式,linux创建快捷方式命令
  4. 数据结构二叉树学习1-前序序列创建二叉树
  5. yolov5 数据集预处理(多文件夹同时提取文件并分类)(同时随机提取一定比例的图片和txt文件到指定文件)
  6. python 统计图绘制,Python绘制统计图表
  7. 采用晶体管作为电子元器件的计算机属于,采用晶体管作为电子元器件的计算机属于(...
  8. matlab中对一个数求余,matlab中求余、求模运算方法总结
  9. nginx服务器添加微信小程序校验文件
  10. linux 硬盘分区,分区,删除分区,格式化,挂载,卸载笔记