对java中无锁方式保证同步的一些总结

无锁(乐观锁)

无锁不会对资源进行锁定,相比加锁方式开销更小,但是加锁也有加锁的好处,比如当竞争十分激烈时往往加锁方式效率更好更方便

无锁的实现方式主要有:
1、CAS
所有的线程都能访问并修改同一个资源,但保证只有一个线程能修改成功,其他线程的修改无效
2、ThreadLocal
将变量拷贝多份为每个线程私有,每个线程操作自己私有的变量副本,彼此之间互不干扰
3、volatile
保证内存可见性+内存屏障禁止指令重排

CAS

CAS全称 Compare And Swap(比较并更新),是一种基于乐观锁思想的自旋锁实现方式。在不使用锁的情况下实现多线程之间的变量同步

2、原理
CAS算法涉及到三个操作数:
当前的内存值 V
先前读取的值 A
要写入的新值 B
当且仅当 V 的值等于 A 时(说明没有被修改),CAS用将V更新为B

思考:有没有可能我在判断了V等于A之后,正准备更新它的新值的时候,被其它线程更改了V的值呢?
答案是不会。因为CAS的比较+更新原子操作
底层是通过增加一个Lock指令对缓存或者总线加锁,从而保证比较+替换这两个指令的原子性的。

3、特点

  • 当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起阻塞,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。
  • CAS必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果

CAS的问题
1、ABA问题
CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A(此时记为A1),那么CAS进行检查时会误以为值没有发生变化,但是实际上是有变化的,是不能更新的。
解决思路:
添加版本号,每次变量更新的时候都把版本号+1,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。

2、自旋时间长开销大。
CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
解决思路:
设定一个自旋上限,超过之后不再自旋。

CAS的使用场景主要有两个:

  • 第一个是J.U.C里面Atomic的原子实现,比如AtomicInteger,AtomicLong。
  • 第二个是实现多线程对共享资源竞争的互斥性质,比如在AQS、ConcurrentHashMap、ConcurrentLinkedQueue等都有用到。

test and set指令

类似的还有test and set指令也可以实现自旋锁
一个Test-and-Set(TAS)指令包括两个子步骤,把给定的内存地址设置为1,然后返回之前的旧值。
这两个子步骤在硬件上实现为一个原子操作,执行期间不会被其他处理器打断。
下面是使用TAS实现自旋锁的伪代码:

lock = 0 //shared state
while(test_and_set(lock)==0){ //try lock
       //do nothing   
}
// 临界区代码
lock = 0   //release 

当第一个线程执行这段代码时,TAS指令会立即把lock设置为1,并返回0 ,线程退出while循环进入临界区。
如果另一个线程尝试进入临界区,TAS会把lock设置为1,但是也会返回1(由第一个线程的TAS指令设置为1),
此时第二个线程会一直while循环(忙等待),直到第一个线程退出临界区代码,执行了lock=0,即释放了锁。
这种通过while-loop等待获取锁的实现称为自旋锁(spin lock)。

volatile

保证可见性
禁止指令重排(通过内存屏障防止多个指令之间的重排序)
不保证原子性

volatile 只能修饰变量,而且是全局变量,不能修饰局部变量,因为局部变量是线程私有的,没有保证让其他线程看到的意义。

volatile如何做到保证可见性 ?
对于增加了volatile关键字修饰的共享变量,JVM虚拟机会自动增加一个** #Lock汇编指令**,这个指令会根据CPU型号自动添加总线锁和缓存锁

  • 总线锁是锁定了CPU的前端总线,从而导致在同一时刻只能有一个线程去和内存通信,这样就避免了多线程并发造成的可见性。

  • 缓存锁是对总线锁的优化,因为总线锁导致了CPU的使用效率大幅度下降,所以缓存锁只针对CPU三级缓存中的目标数据加锁,缓存锁是使用MESI缓存一致性来实现的。

  • 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中

  • 读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

为什么不保证原子性?
修改volatile变量分为下面四步:
1)读取volatile修饰的变量到本地内存

2)修改变量值

3)变量值写回主内存

4)变量值让其他线程可见

前三步都是不安全的,取值和写回之间,不能保证没有其他线程修改。所以不能保证原子性.

ThreadLocal

ThreadLocal是一种线程隔离机制,它提供了多线程环境下对于共享变量访问的安全性。

在多线程访问共享变量的场景中,一般的解决办法是对共享变量加锁,但是加锁会带来性能的下降,所以ThreadLocal用了一种空间换时间的设计思想,也就是说在每个线程里面,都有一个容器来存储共享变量的副本,然后每个线程只对自己的变量副本来做更新操作,这样既解决了线程安全问题,又避免了多线程竞争加锁的开销。

ThreadLocal的具体实现原理是,在Thread类里面有一个成员变量ThreadLocalMap,它专门来存储当前线程的共享变量副本,后续这个线程对于共享变量的操作,都是从这个ThreadLocalMap里面进行变更,不会影响全局共享变量的值。

InheritableThread
因为ThreadLocal是不可继承的,同一个 ThreadLocal 变量在父线程中被设置值后,在子线程中是获取不到的。InheritableThread可以让子线程访问在父线程中设置的本地变量。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值