java线程安全的实现方法总结学习

5 篇文章 0 订阅
3 篇文章 0 订阅
java语言中的线程安全
  1. 将安全程度由强到弱排序:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立,共五种情况
  2. 不可变:典型的就是String类,这个就不多赘言。还有枚举类,以及Number类的部分子类,如Long/Double/BigInteger等大数类型。
  3. 绝对线程安全:不论运行时环境如何,调用者都不用采取额外的同步措施。
  4. 相对线程安全:这是大家平常所提到的“线程安全”的级别,如集合类中的Vector/HashTable这类。
  5. 线程兼容:本身不是线程安全的,当时可以在调用时采取一些手段保证安全,比如典型的HashMap
  6. 线程对立:不论人如何采用手段都不能保证线程安全。典型:已经废弃的Thread.suspend/resume方法。这两个一旦同时调用就会有死锁的风险,因为它们的操作是对立的。
线程安全的实现方法
  1. 最基本的:synchronized关键字。这个方法是最常用的,它通过互斥的方式保证同步。我们知道java中有几个操作是可以保证原子性的,其中lock/unlock就是一对。虽然java没有提供这两个字节码的接口,但是我们可以通过monitorenter/monitorexit,而synchronized会在块的前后调用两个字节码指令。同时synchronize对于同一条线程来说是可重入的;其次它也是阻塞的。我们知道java线程是映射到操作系统上的,而且是混用的内核态线程和用户态线程(N:M),而将线程从阻塞/唤醒,需要将线程从用户态转换到内核态,这样会消耗太亮的资源,所以synchronize是一个重量级锁
  2. 另外一种和synchronize类似的方法:ReentrantLock。它们两个的区别:(1)synchronize是隐式的,只要块内的代码执行完,就会释放当前的锁;而后者需要显式的调用unlock()方法手动释放,所以经常搭配try/finally方法(忘记在finally中unlock是非常危险的) (2)后者可以选择等待中断——即在当前持有锁线程长期不释放锁的情况下,正在等待的线程可以选择放弃等待选择处理其他的事情。 (3) 后者可以选择公平锁(虽然默认是非公平的,因为公平锁的吞吐量很受影响)即先来后到,按申请的顺序获得锁。 (4)可以绑定多个条件
  3. 前面提到的两种方式都是通过互斥来达到同步的目的,这其实是悲观锁的一种。下面介绍的是乐观锁,基于冲突检测的并发策略,不需要将线程挂起,因此又被成为非阻塞同步。
  4. 典型:CAS(Compare And Swap),通过Unsafe类提供。有三个操作数,内存位置、旧的预期值、和新的值;当且仅当内存地址V符合预期值A时,执行将值更新为新的预期值B。  存在的问题:“ABA”情况,即原值为A,但在检测之前发生了改变,变成了B,同时也在检测时变回了A;即不能保证这个值没有被其他线程更改过。
  5. 接下来是无同步方案:
  6. 可重入代码(纯代码):是一种无同步方案,在执行的任何时候去中断,转而执行其他的代码;在重新返回代码后,不会出现任何的错误。可重入性->线程安全,充分不必要条件。即可重入性的代码都是线程安全的,但反之不一定。简单判断原则:一个方法的返回结果是可预测的,只要输入了相同的数据,就都能返回相同的结果。
  7. 线程本地存储:即利用ThreadLocal类;每个Thread类中都有一个变量ThreadLocalMap,默认是为null的。它将为每一个线程创立一个该变量的副本。这样线程之间就不存在数据征用的问题了。适用情况:(1)数据库的Connection连接 (2)WEB中的“一个请求对应一个服务器线程”,在知乎上看到一个回答,解释的蛮清晰的。(3)Spring中创建的默认模式是Singleton单例 (4)“生产者-消费者问题” 

    ThreadLocal就是变量在不同线程上的副本,不同线程不共享,所以对变量改动时就不需要考虑线程间同步的问题了

    ThreadLocal在web应用开发中是一种很常见的技巧,当web端采用无状态写法时(比如stateless session bean和spring默认的singleton),就可以考虑把一些变量放在ThreadLocal中

    举个简单例子,以理解意思为主:你有两个方法A和B都要用到变量userId,又不想传来传去,一个很自然的想法就是把userId设为成员变量,但是在无状态时,这样做就很可能有问题,因为多个request在同时使用同一个instance,userId在不同request下值是不一样的,就会出现逻辑错误
    但由于同一个request下一般都是处于同一个线程,如果放在ThreadLocal的话,这个变量就被各个方法共享了,而又不影响其他request,这种情况下,你可以简单把它理解为是一种没有副作用的成员变量


    作者:卡斯帕尔
    链接:https://www.zhihu.com/question/21709953/answer/19066232
    来源:知乎
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  8. Volidate+原子类:我们知道volidate是可以满足可见性和禁止指令重排序,不能保证同步是因为很多时候,比如++自加操作不是原子性的。
锁优化

自旋锁:
  1. 解决问题:在互斥同步中,最为影响性能的操作是阻塞的实现;而同时大多数的共享数据的锁定只持续很短的时间,因此而阻塞不值得。因此设立了自旋锁。
  2. 出现时间:JDK 1.4.2 于JDK 1.6中默认开启 并调整为自适应的自旋锁(以前是默认10次)
  3. 定义:假设物理机器拥有多个处理器(现在基本都有,多核CPU),能够并行处理两个及以上的线程,那么可以让后面的那个线程“稍微等一下”,即占用CPU资源但不进行处理,观察持有锁的线程是否很快就会放弃锁。而为了达到等待的效果,让线程执行一个忙循环(即自旋)
  4. 优缺点:对于前面很快释放锁的线程来说,性能提高很明显;而如果前面的一直不释放,就很尴尬(占着茅坑不拉屎)。
锁消除
  1. 定义:在代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除
  2. 适用:如果堆上的数据都不会逃逸出去,从而无法被其他线程访问到,那么实际上它是线程隔离的,这时候可以把它当做栈上的数据对待。
  3. 例子:String类的string s=s1+s2;在JDK 1.5之后实际上会被优化成StringBuffer类对象两次append()操作。我们知道StringBuffer是线程安全的,append()操作是synchronize同步了的。而实际上这个对象并不会逃逸出方法,自然可以消除掉。
锁粗化
  1. 这个概念比较相对于上面的锁消除,我们在一般情况下推荐将同步块写的尽可能小,使得需要同步 的操作数量尽可能小。但在对一个对象反复加锁和解除的情况下,也会导致不必要的操作损耗。
  2. 例子:还是上面那个例子,几次append其实都是针对StringBuffer的对象实例,那么可以将其同步的范围扩展到第一次append----最后一次append之间,这样只需要加一次锁就可以了。即锁的粗化。
轻量级锁和偏向锁
  1. 适用:在同步对象无竞争的情况下(只有一个线程需要同步对象)
  2. 采取方法:轻量级锁采取CAS操作替代synchronize(在无竞争情况下),有竞争则会膨胀回重量级锁;偏向锁直接就不要同步操作了,CAS也不要了,每次这个线程进入同步块都不再进行任何同步操作,直到另外一个对象尝试去获取这个对象(竞争)
  3. 启用版本:JDK 1.6之后 这两个锁都默认开启
  4. 局限:在竞争情况下,比重量级锁更慢。注意,这并不是来替代重量级的,而是优化。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值