java arraymap线程安全吗_关于HashMap是否线程安全的问题

前两天和一个朋友聊天,聊到找工作面试的时候,随口问了下HashMap是否是线程安全的?相信大多数人都能脱口而出:“肯定是线程不安全的”。那我接着又问了,为什么线程不安全呢?朋友说是没有同步。那继续追问,为什么没有同步就不安全?说存放到Map的值可以被多个线程同时访问,所以不安全。

那我又问,AtomicInteger等原子类,也没有使用synchrionzed的同步手动,是线程安全的吗?答:因为Atomic有volatile和自旋,所以安全。那为什么volatile+自旋就安全了呢?到此,可能大部分已经不知道要怎么回答了。那我们今天就来说说线程安全的问题,以及没有同步为什么线程不安全。

本文不会深入到实现细节,只是大致讲解线程安全的理论部分,可能说的会有偏差或错误的地方,欢迎大家指正。

首先,为什么会存在线程安全的问题,这个得从JMM(Java Memory Model)说起。大家都知道,计算机在执行一个指令的时候,会先将数据和指令从硬盘或网络等地方加载到内存,然后CPU在从内存加载指令和数据到CPU缓存,然后到寄存器,最终计算的结果,也会写回到内存,最终保存到磁盘或者其他地方(所有的读写都会用到读缓冲区和写缓冲区)。因为现在基本都是多核CPU,内存是所有CPU共享的,但每个CPU内部的缓存和寄存器是私有的,这就造成了同一个数据,由不同的CPU同时访问一个内存地址,就会存在一个数据有多个副本(内存一份,每个访问到的CPU内部一份),所以造成了线程安全,即数据可能出现不一致的情况。其实面试和工作中经常遇到的缓存和数据库的一致性问题,也是因为同一份数据有了多个副本导致的。

但JMM为了屏蔽计算机硬件结构体系的复杂度,为我们出了一个规范,具体来说就是大名鼎鼎的JSR133,可以参考 http://www.jcp.org/en/jsr/detail?id=133。我的理解大意是说,JMM规定了内存由线程私有的工作内存(类似的可以对照CPU缓存和寄存器)和所有线程共享的主内存(类似计算机的内存)组成。

每个线程在访问变量的时候,都要从主内存中去读取,然后在将变量的值copy一份到自己的工作内存中。那么在多线程并发访问同一个共享变量的时候,就会导致同一个共享变量,会存在于一个主内存+N个工作内存的多个副本。如果此时一个线程修改了变量的值,新值首先会写入该线程的工作内存,然后在写入主内存,而其他线程是不知道主内存的值以及被修改了,所以其他线程在做后续的操作的时候,都是用的原来的值,就造成了数据不一致,即线程安全的问题。

那为什么我们在使用诸如synchrionzed或者Lock工具类等同步手段后,就说线程安全了呢?这两个也要分别来说明。

synchrionzed是由JVM底层已经帮我们做了很多事情,JVM会在执行synchrionzed修饰的代码时,自动取获取一个互斥排他锁,保证同一时刻只能有一个线程能执行这段代码,也就是说,只要你代码写的不出问题,那它就不会出现多线程并发的访问同一个共享变量的情况,并且JMM规定在synchrionzed写之后,要立即将结果写回到主内存,不使用写缓冲区,从根本上避免了多线程并发访问同一个共享内存。

而Lock等工具类,本质上来说,也是避免了多线程同时并发的访问同一个共享变量,只是它的底层实现变成了volatile + CAS,其中CAS(Compare And Swap)是操作系统和CPU硬件级别提供的一个原子操作。而volatile 也是JMM的内容,简单来说就是volatile 可以保证线程可见性和通过插入内存屏障来使指令不重排(由于编译器或操作系统,CPU等的优化,会出现指令重排序,来提高性能),但它并不保证原子性,所以需要CAS来保证。

以及上面提到的AtomicInteger等原子类,它也是线程安全的,其实它就是用到了volatile + CAS + 自旋重试来保证线程安全的。那么除了这几种方式,还有哪些可以保证线程安全的方式方法呢?

当然还有了,比如Java提供的final关键字。我们都知道final修饰基本类型变量时,可以保证该变量的值在后续的使用中不会被改变,而修饰引用类型时,仅保证该引用的地址不变。所以如果我们想使用final来做线程安全的话,需要注意:如果是修饰的引用类型,那么我们一定要注意该引用类型本身是否是线程安全的,如String就是典型的线程安全的引用类型。如果我们自己来实现一个线程安全的类型的话,可以参照String的方法来实现。

除了 final,我们也可以通过ThreadLocal 来保证线程安全。那么ThreadLocal 又是如何实现的呢? 通过查看源码或者其他博客,ThreadLocal 通过在线程内部Thread内的成员变量 threadLocalMap来避免了多线程共享,用ThreadLocal 储存的值,它只能被当前线程所访问到,不存在并发问题,所以它的安全的,但需要注意使用完毕后及时remove,避免内存泄漏的危险。

通过上面的ThreadLocal ,我们也可以发现,要保证线程安全,根本上就是避免多线程共享,所以我们的局部变量等线程私有对象,也是线程安全的。

总结一下,线程安全的手段有:synchrionzed ,Lock, volatile , final 以及拒绝共享的ThreadLocal 和局部变量等方式。当然,JDK也为我们提供了很多的并发容器来保证线程安全,如 ConcurrentHashMap, CopyOnWriteArrayList, ConcurrentLinkedList, ArrayBlockingQueue, LinkedBlockingQueue等以及Collections#synchronizedList, Collections#synchronizedMap等方法返回的同步容器,具体的实现,大家也可以自己去看看。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值