并发与多线程(二) --- 锁

什么是锁

在计算机信息世界里,单机单线程时代没有锁的概念. 自从出现了资源竞争,人们才意识到需要对部分场景的执行现场加锁,昭告天下,表明自己的"短暂" 拥有(其实对于任何有形或无形的东西,拥有都不可能是永恒的). 计算机的锁也是开始的悲观锁,发展到后来的乐观锁,偏向锁,分段锁等. 锁主要提供了两种特性: 互斥性 和 不可见性 . 因为锁的存在,某些操作对外界来说是黑箱进行的,只有锁的持有者才知道对变量进行了什么修改.


Java 中常用锁实现的方式有两种:

  1. 用并发包中的锁类
  2. 利用永不代码块

用并发包中的锁类

并发包的类族中,Lock 是JUC 包的顶层接口,它的实现逻辑并未用到synchronized , 而是利用了volatile 的可见性.
先通过Lock 来了解JUC 包的一些基础类,如下图所示:
在这里插入图片描述
上图为Lock 的继承类图,ReentrantLock 对于Lock 接口的实现主要依赖了Sync , 而Sync 继承了AbstractQueuedSynchronizer (AQS) ,它是JUC 包实现同步的基础工具 . 在AQS 中,定义了一个volatile int state 变量作为共享资源, 如果线程获取资源失败,则进入同步 FIFO 队列中等待; 如果成功获取资源就执行临界区代码. 执行完释放资源时,会通知同步队列中的等待线程来获取资源后出队并执行.
AQS 是抽象类,内置自旋锁实现的同步队列,封装入队和出队的操作,提供独占, 共享, 中断等特性的方法. AQS 的子类可以定义不同的资源实现不同性质的方法.
比如可重入锁 ReentrantLock ,定义state 为0 时可以获取资源并置为1 . 若已获得资源,state 不断加1 , 在释放资源时state 减1 ,直至为0 ; CountDownLatch 初始时定义了资源总量state = count , countDown() 不断将state 减1 , 当state = 0 时才能获得锁,释放后state 就一直为0 . 所有线程调用await() 都不会等待,所以CountDownLatch 是一次性的,用完后如果再想用就只能重新创建一个; 如果希望循环使用,推荐使用基于ReentrantLock 实现的 CyclicBarrier .
Semaphore 与 CountDownLatch 略有不同,同样也是定义了资源总量state = permits ,当state > 0 时就能获得锁,并将state 减1, 当state = 0时只能等待其他线程释放锁,当释放时state 加1 ,其他等待线程又能获得这个锁 . 当Semphore 的permits 定义为1 时,就是互斥锁, 当permits > 1 就是共享锁 .

在JDK8 提出了一个新的锁: StampedeLock , 改进了读写锁ReentrantReadWriteLock . 这些新增的锁相关类不断丰富了JUC 包的内容,降低了并发编程的难度,提高了锁的性能和安全性 .

利用同步代码块

同步代码块一般使用Java 的 synchronized 关键字来实现,有两种方式对方法进行加锁操作:
第一,在方法签名处加synchronized 关键字;
第二,使用synchronized (对象或类) 进行同步 ;
这里的原则是锁的范围尽可能的小,锁的时间尽可能短,即能锁对象,就不要锁类; 能锁代码块,就不要锁方法 .
synchronized 锁特性由JVM 负责实现. 在JDK 的不断优化迭代中,synchronized 锁的性能得到极大提升,特别是偏向锁的实现,使得synchronized 已经不是昔日哪个低性能且笨重的锁了 . JVM 底层是通过监视锁来实现 synchronized 同步的. 监视锁即 monitor , 是每个对象与生俱来的一个隐藏字段。 使用synchronized 时,JVM会根据synchronized 的当前使用环境,找到对应对象的 monitor ,再根据monitor 的状态进行加、解锁的判断
例如,线程在进入同步方法或代码块时,会获取该方法或代码块所属对象的monitor ,进行加锁判断,如果成功加锁就成为该monitor 的唯一持有者 。 monitor 在被释放前,不能再被其他线程获取。

synchronized 字节码分析

下面是通过字节码分析synchronized 锁的实现:

public void testSynchronized();
	descriptor: ()V 
	flages: ACC_PUBLIC
	Code:
		statck = 2,locals=2,args_size=1
			0: getstaic      #13
			// Field mutex:Ljava/lang/Object
			3: dup
			4: astore_1
			5: monitorenter
			6: getstatic     #39
			// Field java/lang/System.out:Ljava/io/PrintStream;
			9: ldc
			// String hello world
			11: invokevirtual #47
			// Method java/io/PrintStream.println:(Ljava/lang/String;)V
			14: aload_1
			15: monitorexit
			16: goto 22
			19: aload_1
			20: monitorexit
			21: athrow
			22: return
Exception table:
	from	to	target type
		6	16	19		any
		19	21	19		any
		LineNumberTable:
			line 26: 0
			line 27: 6
			line 26: 14
			line 29: 22
		LocalVariableTable:
			Start	Length	Slot	Name	Signature
				0		23		0	this	Ltest/Test;

方法元信息中会使用ACC_SYNCHRONIZED 标识该方法是一个同步方法.同步代码块中会使用monitorentermonitorexit 两个字节码指令获取和释放monitor
如果使用monitorenter 进入时monitor 为0 ,表示该线程可以有monitor 后续代码,并将monitor 加1;如果当前线程已经持有了monitor ,那么monitor 继续加1; 如果monitor 非0,其他线程就会进入阻塞状态
JVM 对synchronized 的优化主要在于对 monitor 的加锁、解锁上。JDK6后不断优化使得synchronized 提供三种锁的实现,包括偏向锁、轻量级锁、重量级锁,还提供了自动的升级和降级机制。JVM 就是利用CAS 在对象头上设置线程ID ,表示这个对象偏向于当前线程,这就是偏向锁

偏向锁

偏向锁是为了在资源没有被多线程竞争的情况下尽量减少锁带来的性能开销。在锁对象的对象头中有一个ThreadId 字段,当第一个线程访问锁时,如果该线程没有被其他线程访问过,即ThreadId 字段为空,那么JVM 让其持有偏向锁,并将ThreadId 字段的值设置为该线程的ID 。当下一次获取锁时,会判断当前线程的Id 是否与锁对象的ThreadId 一致。如果一致,那么该线程不会再重复获取锁,从而提高了程序的运行效率。如果出现锁的竞争情况,那么偏向锁会被撤销并升级为轻量级锁。如果资源的竞争非常激烈,会升级为重量级锁。偏向锁可用降低无竞争开箱,它不是互斥锁,不存在线程竞争的情况,省去了再次同步判断的步骤,提高了性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值