Java中锁的理解

一、什么是锁?

        在计算机科学中,锁(lock)或互斥(mutex)是一种同步机制,用于在有许多执行线程的环境中强制对资源的访问限制。锁旨在强制实施互斥排他、并发控制策略。

        锁通常需要硬件支持才能有效实施。这种支持通常采取一个或多个原子指令的形式,如"test-and-set", "fetch-and-add" or "compare-and-swap"”。这些指令允许单个进程测试锁是否空闲,如果空闲,则通过单个原子操作获取锁。

锁的一个重要属性 粒度

在引入锁粒度之前,需要了解关于锁的三个概念:

1、锁开销 lock overhead 锁占用内存空间、 cpu初始化和销毁锁、获取和释放锁的时间。程序使用的锁越多,相应的锁开销越大

2、锁竞争 lock contention 一个进程或线程试图获取另一个进程或线程持有的锁,就会发生锁竞争。锁粒度越小,发生锁竞争的可能性就越小

3、死锁 deadlock 至少两个任务中的每一个都等待另一个任务持有的锁的情况锁粒度是衡量锁保护的数据量大小,通常选择粗粒度的锁(锁的数量少,每个锁保护大量的数据),在当单进程访问受保护的数据时锁开销小,但是当多个进程同时访问时性能很差。因为增大了锁的竞争。相反,使用细粒度的锁(锁数量多,每个锁保护少量的数据)增加了锁的开销但是减少了锁竞争。例如数据库中,锁的粒度有表锁、页锁、行锁、字段锁、字段的一部分锁

相关术语 Critical Section(临界区)、 Mutex/mutual exclusion(互斥体)、 Semaphore/binary semaphore(信号量)

锁的种类

1.独享锁/共享锁

独享锁是指该锁一次只能被一个线程所持有。 (ReentrantLock、 Synchronized)

共享锁是指该锁可被多个线程所持有。 (ReadWriteLock)

互斥锁/读写锁

        独享锁/共享锁这是广义上的说法,互斥锁/读写锁就分别对应具体的实现。在Java中如ReentrantLock就是互斥锁(独享锁), ReadWriteLock就是读写锁(共享锁)。 独享锁与共享锁也是通过AQS来实现的

锁升级:读锁到写锁 (不支持)

锁降级:写锁到读锁 (支持)

2.读写锁 ReentrantReadWriteLock

低16位代表写锁,高16位代表读锁

3.公平锁/非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁。

非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能会造成饥饿现象。

        对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。

        对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的控制线程对锁的获取, 所以并没有任何办法使其变成公平锁。

4.可重入锁

可重入锁又名递归锁,是指同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

ReentrantLock和Synchronized都是可重入锁。可重入锁的一个好处是可一定程度避免死锁

需要注意的是,可重入锁加锁和解锁的次数要相等。

5.乐观锁/悲观锁

乐观锁/悲观锁不是指具体类型的锁,而是看待并发的角度。

悲观锁认为存在很多并发更新操作,采取加锁操作,如果不加锁一定会有问题

乐观锁认为不存在很多的并发更新操作,不需要加锁。数据库中乐观锁的实现一般采用版本号,Java中可使用CAS实现乐观锁。

6.分段锁

分段锁是一种锁的设计,并不是一种具体的锁。对于ConcuttentHashMap就是通过分段锁实现高效的并发操作。

7.自旋锁

自旋锁是指尝试获取锁的线程不会阻塞,而是采用循环的方式尝试获取锁。好处是减少上下文切换,缺点是一直占用CPU资源。

8.偏向锁/轻量级锁/重量级锁

这是jdk1.6中对Synchronized锁做的优化,首先了解下对象头(Mark Word):

运行时JVM内存布局

从jdk1.6开始为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。锁共有四种状态,级别从低到高分别是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。随着竞争情况锁状态逐渐升级、锁可以升级但不能降级。

偏向锁的获取和撤销:

HotSpot作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入偏向锁。

线程1检查对象头中的Mark Word中是否存储了线程1,如果没有则CAS操作将Mark Word中的线程ID替换为线程1。此时,锁偏向线程1,后面该线程进入同步块时不需要进行CAS操作,只需要简单的测试一下Mark Word中是否存储指向当前线程的偏向锁,如果成功表明该线程已经获得锁。如果失败,则再需要测试一下Mark Word中偏向锁标识是否设置为1(是否是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将偏向锁指向当前线程

偏向锁的竞争结果:

根据持有偏向锁的线程是否存活

1.如果不活动,偏向锁撤销到无锁状态,再偏向到其他线程

2.如果线程仍然活着,则升级到轻量级锁

偏向锁在Java6和Java7中默认是开启的,但是在应用程序启动几秒后才激活,如果有必要可以关闭延迟:

-XX:BiasedLockingStartupDelay=0

如果确定应用程序中所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:

-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁。

-XX:BiasedLockingStartupDelay=0 -XX:+TraceBiasedLocking

轻量级锁膨胀:

1.线程在执行同步块之前,JVM会在当前栈桢中创建用于存储锁记录的空间(Lock record),并将对象头中的Mark Word复制到锁记录中(Displaced Mark Word)。

2.然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针

3.如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程尝试使用自旋来获取锁

偏向锁、轻量级锁、重量级锁的优缺点

1.偏向锁是为了避免某个线程反复获得/释放同一把锁时的性能消耗,如果仍然是同个线程去获得这个锁,尝试偏向锁时会直接进入同步块,不需要再次获得锁。

2.而轻量级锁和自旋锁都是为了避免直接调用操作系统层面的互斥操作,因为挂起线程是一个很耗资源的操作。

为了尽量避免使用重量级锁(操作系统层面的互斥),首先会尝试轻量级锁,轻量级锁会尝试使用CAS操作来获得锁,如果轻量级锁获得失败,说明存在竞争。但是也许很快就能获得锁,就会尝试自旋锁,将线程做几个空循环,每次循环时都不断尝试获得锁。如果自旋锁也失败,那么只能升级成重量级锁。

3.可见偏向锁,轻量级锁,自旋锁都是乐观锁。

常见锁汇总:

Synchronized是一个:非公平,悲观,独享,互斥,可重入的重量级锁
Lock的实现包括:ReentrantLock和ReentrantReadWriteLock下的ReadLock和writeLock.
ReentrantLock是一个:默认非公平但可实现公平的,悲观,独享,互斥,可重入,重量级锁。
ReentrantReadWriteLocK是一个,默认非公平但可实现公平的,悲观,写独享,读共享,读写,可重入,重量级锁。

由并发引起的死锁、活锁

  • 死锁是多个线程相互竞争资源,导致僵死,线程处于阻塞状态。
  • 活锁是线程一直在尝试获取锁,但都没有获取成功,导致整个程序虽然没有阻塞,但不能正常执行下去。

发生死锁的4个条件:

  • 互斥条件:线程之间对所需资源进行排它性互斥
  • 不可剥夺:线程获取到锁后,资源在没有释放之前是不能被剥脱的。
  • 请求和保持:线程在请求别的资源时,对已经占用的资源保持不释放
  • 循环等待:存在一种资源的循环等待链。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值