Java虚拟机--(互斥同步与非阻塞同步)和锁优化

线程安全的实现方法:

互斥同步(悲观锁):

互斥同步是常见的一种并发正确性保障手段。同步是指在多个线程并发访问数据时,保证共享数据在同一时刻只被一个(或是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。因此互斥是因,同步是果;互斥是方法,同步是目的。

synchronized关键字和可重入锁ReentrantLock是两种最为常用的互斥同步手段。

synchronized关键字:

该关键字经过编译之后会在同步块前后分别形成monitorenter和monitorexit这两个字解码命令,这两个命令都想需要一个reference类型的参数来指明要锁和解锁的对象,如果synchronized明确指定了对象参数,那么就是这个对象的reference,如果没有指定,那么根据synchronized修饰的是实例方法还是类方法去取对应的对象实例或Class对象来作为锁对象。

在执行monitorenter命令时,首先尝试获取对象的锁,如果成功获取,就把锁的计数器加一;相应的,执行monitorexit会将锁的计数器减一。如果获取对象失败,该线程就进入阻塞状态,直到对象锁被另一个线程释放为止。

注意一些情况:

  • synchronized同步块对同一线程来说是可重入的,不会出现自己把自己锁死的问题;
  • 同步块在已进入线程执行完之前,会阻塞后面线程的进入;
  • Java线程是映射到操作系统的原生线程上的,如果要阻塞和唤醒一个线程,都要操作系统来完成,这需要从用户态转到核心态,会消耗很多处理器时间,因此synchronized是一个重量级的操作。

可重入锁ReentrantLock:

在用法上,ReentrantLock和synchronized很相似都具备一样的线程重入特性,但前者表现为API层面的互斥锁,后则表现为原生语法层面的互斥锁。不过,ReentrantLock比synchronized增加了一些高级功能:

  • 等待可中断:是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以放弃等待去做其他事情。
  • 公平锁:是指多个线程在等待同一个锁时,必须按照申请锁的顺序来依次获得锁。synchronized中的锁是非公平的,ReentrantLock默认也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。
  • 锁绑定多个条件:是指一个ReentrantLock对象可以同时绑定多个Condition对象,只需多次调用newCondition()方法即可。

若要使用上面的三种功能,ReentrantLock是很好的选择。但一般情况下使用synchronized就可以了。JDK1.5之前多线程环境下ReentrantLock要比synchronized效率高,然而JDK1.6引入锁优化之后,两者效率已经很接近。

非阻塞同步(乐观锁):

互斥同步最主要的问题就是进行线程阻塞和唤醒带来的性能问题,因此互斥同步也成为阻塞同步。阻塞同步属于一种悲观锁策略,总是认为只要不做这忘却的同步措施(加锁)就肯定会出现问题。

非阻塞同步基于一种冲突检测的乐观并发策略。就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果产生了冲突,再进行补救措施(最常见的补救就是重试直到成功为止)。这种并发策略不需要把线程挂起。

非阻塞同步需要保证操作和冲突检测具有原子性,这里的原子性必须依靠“硬件指令集”完成,因为这里再使用互斥同步就毫无意义了,只能依靠硬件来完成。硬件保证一个语义上看起来需要多步操作的行为只通过一条处理器指令就可以完成,这类指令常用的有:

  • 测试并设置( Test-and-Set )
  • 获取并增加( Fetch-and-Increment )
  • 交换( Swap )
  • 比较并交换( CAS )
  • 加载链接/条件存储( LL/SC )

无同步:

要保证线程安全,并不一定要同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果代码不涉及争用数据,就无需同步。因此有写代码天生是线程安全的。比如可重入代码和线程本地存储。

锁优化:

自旋锁与自适应自旋:

因为线程阻塞和唤醒要消耗大量处理器时间,所以在一些情况下,可以让要等待的线程“稍等一下”,但不放弃处理器,看看持有锁的线程是否会马上释放锁。为了让线程占有处理器等待,只需让线程执行一个忙循环(自旋),这就是自旋锁。

自旋锁不能代替阻塞,因为它是占用处理器时间的,如果锁被占用的时间很短,自旋锁效果会很好,但如果锁被占用时间很长,那自旋线程就会白白消耗处理器资源。所以自旋锁一般会指定自选次数,默认10次。

自适应自旋锁是自选时间时间不固定,而是由前一次在同一个锁上的自旋线程的自旋时间以及锁的拥有者状态来决定。如果前一次的自旋线程刚刚成功获得锁,那么虚拟机认为这次也会容易获得锁,进而允许自旋线程多自旋几次比如100次;而如果对于某个锁自旋很少成功过,那么以后的线程可能直接忽略掉自旋过程。

锁清除:

锁清除是指虚拟机即时编译器在运行时,会将代码上要求同步,但被检测到实际上不可能出现共享数据竞争的锁进行清除。锁清除的主要判定依据来源于逃逸分析的数据支持。

锁粗化:

很多情况下,总是推荐将同步代码块的范围限制得越小越好。但在一些情况下,如果一系列连续操作都对同一个对象反复加锁和解锁,甚至加锁解锁关系出现在循环体中,那么也会消耗性能。如果虚拟机探测到有这样的操作,就会把加锁同步的范围扩展(粗化)到整个操作序列之外。

轻量级锁:

“轻量级”是相对于使用系统互斥量实现的传统锁而言的,因此传统的锁机制就是重量级锁。强调一点是:轻量级锁不是为了取代重量级锁的,而是在没有多线程竞争的前提下,减少传统重量级锁使用操作系统互斥量产生的性能消耗。

轻量级锁提升系统同步性能的依据是“对于绝大多数锁,在整个同步周期内是不存在竞争的”,这是一个经验数据。但如果存在竞争,除了传统锁互斥量的开销外,还额外发生了CAS操作,因此会更慢。

偏向锁:

如果说轻量级锁是在无竞争的情况下使用CAS操作消除同步使用的互斥量,那么偏向锁就是在无竞争的情况下把整个同步都消除掉。偏向锁的意思是这个锁会偏向于第一个获取它的线程,如果在接下来的执行过程中,该锁没有被其他县城获取,则持有偏向锁的线程将永远不需要再进行同步。当有另一个线程去尝试获取这个锁时,偏向模式结束。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Gaussic

使用 Spring HATEOAS 开发 REST 服务

原文地址:http://www.ibm.com/developerworks/cn/java/j-lo-SpringHATEOAS/

1102
来自专栏JAVA同学会

Zookeeper应用之——栅栏(barrier)

barrier的作用是所有的线程等待,知道某一时刻,锁释放,所有的线程同时执行。举一个生动的例子,比如跑步比赛,所有

923
来自专栏崔庆才的专栏

是时候抛弃print了,开始体验下logging的强大吧!

1982
来自专栏Golang语言社区

Goroutine + Channel 实践

背景 在最近开发的项目中,后端需要编写许多提供HTTP接口的API,另外技术选型相对宽松,因此选择Golang + Beego框架进行开发。之所以选择Golan...

3684
来自专栏向治洪

svn错误对照表

#, c-format msgid "Destination '%s' is not a directory" msgstr "目的 “%s” 不是目录" ...

3215
来自专栏阿杜的世界

RocketMQ学习-NameServer-1

NameServer在RocketMQ中的角色是配置中心,主要有两个功能:Broker管理、路由管理。因此NameServer上存放的主要信息也包括两类:Bro...

1223
来自专栏邹立巍的专栏

Linux进程间通信:共享内存 (下)

使用文件或管道进行进程间通信会有很多局限性,比如效率问题以及数据处理使用文件描述符而不如内存地址访问方便,于是多个进程以共享内存的方式进行通信就成了很自然要实现...

7810
来自专栏java学习

你竟敢说你懂Spring框架?有可能你是没看到这些...(上)

所以,特地去搜刮了一些关于spring的面试题,希望能帮助各位同学在升职加薪的路上,一去不复返。

1102
来自专栏用户2442861的专栏

Sping 的 BeanFactory 容器

http://wiki.jikexueyuan.com/project/spring/ioc-container/spring-bean-fatory-con...

832
来自专栏沃趣科技

MySQL中server_id一致带来的问题

简 介 我们都知道在MySQL搭建复制环境的时候,需要设置每个server的server_id不一致,如果主库与从库的server_id一致,那么复制会失败。...

3806

扫码关注云+社区