再谈线程安全

线程安全

“线程安全”不是指线程的安全,而是指内存的安全。为什么如此说呢?这和操作系统有关。

目前主流操作系统都是多任务的,即多个进程同时运行。为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程的,这是由操作系统保障的。在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存)。进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因。

假设某个线程把数据处理到一半,觉得很累,就去休息了一会,回来准备接着处理,却发现数据已经被修改了,不是自己离开时的样子了。可能被其它线程修改了。

比如把你住的小区看作一个进程,小区里的道路/绿化等就属于公共区域。你拿1万块钱往地上一扔,就回家睡觉去了。睡醒后你打算去把它捡回来,发现钱已经不见了。可能被别人拿走了。因为公共区域人来人往,你放的东西在没有看管措施时,一定是不安全的。内存中的情况亦然如此。

所以线程安全指的是,在堆内存中的数据由于可以被任何线程访问到,在没有限制的情况下存在被意外修改的风险。即堆内存空间在没有保护机制的情况下,对多线程来说是不安全的地方,因为你放进去的数据,可能被别的线程“破坏”。

那我们该怎么办呢?解决问题的过程其实就是一个取舍的过程,不同的解决方案有不同的侧重点。主要有5种解决方法。

  1. 私有的东西就不要放公共区域(堆),放在自己的私有区域(栈上的局部变量),局部变量缺点是只能在方法内用,不能被多个方法共用。
  2. 每个线程都copy一份公共的堆内存的数据,此后,各线程只处理自己的拷贝——ThreadLocal类。ThreadLocal数据是属于Thread类的成员变量(非局部变量,存储在公共堆空间),每个线程都有1份各玩各的。经常听到的“线程本地”,是从逻辑从属关系上来讲的,这些数据和线程一一对应,仿佛成了线程自己“领地”的东西了。其实从数据所在“位置”的角度来讲,它们都位于公共的堆内存中,只不过被线程认领了而已。
  3. 公共堆内存中可以放只读数据(用final修饰)
  4. 互斥锁保证公共资源被有序访问(Synchronized、ReentrantLock等)。
  5. 锁的获取与释放有代价,并发量不大时要乐观一些,用CAS(线程越多数据不安全的概率越大,线程越少数据不安全的概率就越小)。

CAS体现了乐观锁的思想,悲观锁对应的实现是Synchronized和ReentrantLock。

ThreadLocal的作用是什么,用在什么场景

ThreadLocal的作用是做数据隔离,填充的数据只属于当前线程,对其他线程隔离,防止本线程的数据被其他线线程篡改。Spring实现事务隔离级别的源码,采用了ThreadLocal来实现保证单个线程中对数据库操作使用的是同一个数据库连接,同时采用这种方式使业务层使用事务时不需要感知并管理Connection对象。通过传播级别巧妙地管理多个事务配置之间的切换、挂起和恢复。代码主要在TransactionSynchronizationManager类里面。

物流实操系统中一个线程经常要横跨若干个处理单元,要在各自处理单元间传递对象,也就是上下文Context,通常是物流单据信息等,用ThreadLocal实现这个Context防止频繁查询数据库。

CAS是怎么实现线程安全的

从思想上来说, synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守,CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。在java中,除了Atomic系列类,以及Lock系列类的底层实现,甚至在java1.6以上的版本,synchronized在变为重量级锁之前,也会采用CAS机制。

CAS是Compare and Swap,比较并交换。维护三个变量值,一个是内存值V,一个是期望的旧的值A,一个是要更新的值B。更新一个变量的时候,只有当预期值A与内存V中的值相等的时候,才会执行更新操作,把内存V的值改为B。线程在读取数据时不进行加锁,在准备写回数据时,先去查询原值,操作的时候比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程并比较。其中,比较 + 更新整体是一个原子操作。

CAS在Java中的体现是sun.misc.Unsafe类中的各个方法。 调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现原子操作。因为原语的执行必须是连续的,在执行过程中,不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成数据不一致问题。

缺点:ABA问题;循环(CPU自旋)时间长开销大;只能保证一个共享变量的原子操作

什么是AQS

AQS全称是AbstractQueuedSynchronizer,是一个用来构建锁和同步器的框架,它维护了一个状态volatile int state(作为共享资源) 和一个FIFO等待队列,底层利用了CAS机制来保证操作的原子性。state用关键字volatile修饰,代表着该共享资源的状态一更改就能被所有线程可见,而AQS的加锁方式本质上就是多个线程在竞争state。AQS实现锁的主要原理如下:

以实现独占锁为例(即当前资源只能被一个线程占有),其实现原理如下:state初始化0,在多线程条件下,线程要执行临界区的代码,必须首先获取state,某个线程获取成功之后, state加1,其他线程再获取的话由于共享资源已被占用,所以会到FIFO等待队列去等待,等占有 state的线程执行完临界区的代码释放资源(state减1,减到0)后,会唤醒 FIFO中的下一个等待线程(head 中的下一个结点)去获取state。

state由于是多线程共享变量,所以必须定义成volatile,以保证state在多线程场景下的可见性, 同时虽然volatile能保证可见性,但不能保证原子性,所以AQS提供了对state的原子操作方法,即用CAS机制来更新state的值,保证了线程安全。

另外AQS中实现的FIFO队列(CLH 队列)其实是双向链表实现的,有head、tail节点分别指向链表的头与尾节点,head结点代表当前占用的线程,其他节点由于暂时获取不到锁所以依次排队等待锁释放,新节点入队时放入链表尾部,tail指针指向新队尾的操作同样也是用CAS来实现的,保证原子性

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值