【JAVA多线程】001 线程安全的实现方法

 1.互斥同步

 互斥同步(Mutual Exclusion &Synchronization )是常见的一种并发正确性保障手段。

同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻指被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical Section )、互斥量(Mutex) 和信号量(Semaphore) 都是只要的互斥实现方式。因此在这4个字里面,互斥是因,同步是果;互斥是手段,同步是目的。

在JAVA中,最基本的互斥同步手段就是synchronized 关键字,synchronized 关键字经过编译后,会在同步块的代码前后分别形成monitorenter 和monitorrexit 这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定的和解锁的对象。如果JAVA程序中的synchronized 修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作锁定向。

根据虚拟机规范的要求,在执行monitorenter指令时,首先要尝试获取对象的锁。如果找个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应的,在执行monitorext指令时会将锁计数器减1,当计数器为0时,锁就释放。如果获取对象锁失败,那么当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止

在虚拟机规范中,执行monitorenter和monitorexit的行为描述中,有两个点是需要特别注意的。首先,synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己所示的问题。其次,同步块在已经进入的线程执行完毕之前,会阻塞后面其他线程的进入。

JAVA线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一个线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。对于代码简单的同步块 (如被synchorized修饰的getter()或setter()方法),状态转换消耗的时间有可能比用户代码执行的实际还要长。所以synchronized是JAVA语言中一个重量级的操作,有经验的程序员都会在确实有必要的情况下才使用这种操作。而虚拟机本身也会进行一些优化,譬如在操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入到核心态中。

除了synchronized之外,我们还可以使用JUC包中的 重入锁(ReentrantLock)来实现同步,在基本用法上,ReentrantLock与synchronized很相似,他们都具备一样的线程重入特性,只是代码写法上有点区别。一个表现为API层面的互斥 (lock() 和unlock())方法需要配合try/finally 语句来完成。另一个表现为原生语言层面的互斥锁。不过,相比synchronized,ReentrantLock增加了些高级功能主要有以下3项:等待可中断、可实现公平锁、以及锁可以绑定多个条件。

1.等待可中断

       等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助

      2.公平锁

      公平锁是指多线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得所;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得所。synchronized中的锁是非公平的,ReentrantLock默认情况下也是非公平的,但是可以通过带布尔值的构造函数要求使用公平锁。

     3.锁绑定多个条件

锁绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象,而在synchronized中,锁对象的wait()和notify()或者notifyall()方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外的添加一个锁,而ReentrantLock则无需这样做,只需要多次调用newCondition()方法即可。

2.非阻塞同步

互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步(Blocking Synchronization).从处理问题的方式来说,互斥同步属于一种悲观的并发策略,总是认为所有人都是坏人,只要不去做正确的同步策略(例如 加锁),那就肯定出问题,无论共享数据是否真的会出现竞争,它都需要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。随着硬件直连集的发展,我们有另外一种选择:基于冲突检测的乐观并发策略,通俗讲,就是先进行操作,如果没有其他线程争用共享数据,那么操作成功;如果共享数据有争用,产生了冲突,那就再采取其的补偿措施(最常见的补偿措施就是不断的重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步。(Non-Blocking Synchroinzed).

为啥锁乐观并发策略需要”硬件指令集的发展“才能进行呢?因为我们需要操作和冲突检测这两个步骤具备原子性,靠什么来保证呢?如果这里再使用互斥同步来保证就失去了意义,所以我们只能靠硬件来完成这件事,硬件保证一个从语义上看起来需要多次操作的行为只通过一条处理器指令就能完成,这类指令常用的有:

1.测试并设置(Test-and-Set)

2.获取并增加(Fetch-and-Increment)

3.交换(Swap)

4.比较并交换(Compare -and -swap) 江湖人称 CAS

5.加载链接/条件存储(Load-Linked/Store -Condition)

CAS指令需要是三个操作数,分别是内存位置(在JAVA中可以简单的理解为变量的内存地址,用V表示)、旧的预期值(用A表示),新值(用B表示)。CAS指令执行时,当且仅当V符合旧预期值A的时候,处理器用新值B更新V的值,否则就不执行更新,但是无论是否更新了V值,都会返回V的旧值,上述的处理过程是一个原子操作。  如何解决ABA问题呢?

JUC包为了解决ABA问题,提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性。不过目前来说这个类比较鸡肋,大部分情况下ABA问题不会影响程序并发的正确性,如果要解决ABA问题改为出传统的互斥同步可能会比原子类更高效。

3.无同方案

 要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段而已。如果一个方法本来就不涉及共享数据,那它自然就无需任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的。

 1.可重入代码

 2.线程本地存储

 

参考《深入理解JAVA虚拟机》 《UNIX 编程》

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值