Java多线程|同步与锁机制

目录

1 同步的含义

2 保证线程安全的本质

3 死锁发生的四个条件

4 JVM发生死锁如何恢复

5 乐观锁与悲观锁

(1)乐观锁

(2)悲观锁

6 活锁与死锁

(1)活锁

(2)死锁

7 锁的形式与种类

(1)内置锁(独占锁的形式)

(2)显式锁

(3)显式锁-ReentrantLock

(4)读写锁-ReadWriteLock(读写锁接口)

8 可重入锁机制

9 锁优化手段

(1)锁分解与锁分段技术(阿姆达尔定律应用)

(2)替代独占锁

(3)非阻塞同步机制

(4)原子变量

(5)CAS(Compare And Swap)机制

(6)CAS之ABA问题

(7)偏向锁


==========================【导读】[开始]========================== 

        工作中实践到了多线程与高并发应用,也踩了一些沉重的坑。
万丈高楼起于垒土,学习与总结+工作实践不可相离。分三部分总结这块知识。
知识体系详见第一张思维导图。本篇主题“同步与锁机制”。

==========================【导读】[结束]========================== 

1 同步的含义

1 原子性
2 内存可见性(一个线程修改对象的状态后,其他线程可看到状态的改变)

2 保证线程安全的本质

保证线程安全的手段
(1)不在线程之间共享该状态变量。
(2)将状态变量修改为不可变的变量。
(3)在访问状态变量时使用同步。

3 死锁发生的四个条件

互斥,持有,不可剥夺,环形等待

4 JVM发生死锁如何恢复

JVM 在解决死锁,只能重启恢复程序。

5 乐观锁与悲观锁

(1)乐观锁

对于细粒度的操作,有一种更高效的方法,既乐观锁机制。通过这种方法可以在不发生干扰的情况下完成更新操作。
这种方法需要借助冲突检查机制来判断在更新过程中是否存在来自其他线程的干扰,如果存在,这个操作将失败,
并且可以重试(也可以不重试)。

(2)悲观锁

独占锁是一项悲观锁技术一它假设最坏的情况(如果你不锁门,那么搞蛋鬼就会闯人并搞得一团糟),
并且只有在确保其他线程不会造成干扰(通过获取正确的锁)的情况下才能执行下去。

6 活锁与死锁

(1)活锁

    活锁(Livelock)是另一种形式的话跃性问题,该问题尽管不会阻塞线程,但也不能继续执行,因为线程将不断重复执行相同的操作,
而且总会失败。活锁通常发生在处理事务消息的应用程序中:如果不能成功地处理某个消息,那么消息处理机制将回滚整个事务,并
将它重新放到队列的开头,线程将不断重复执行相同的操作,而且总会失败。
    当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。这就像两个
过于礼貌的人在半路上面对面地相遇:他们彼此都让出对方的路,然而又在另一条路上相遇了。因此他们就这样反复地避让下去。
    要解决这种活锁问题,需要在重试机制中引人随机性。

(2)死锁

<2.1>致命拥抱死锁

	在单线程的Executor中,如果一个任务将另一个任务提交到同一个 Excutor,并且等待这个
被提交任务的结果,那么通常会引发死锁。第二个任务停留在工作队列中,并等待第一个任务完
成,而第一个任务又无法完成,因为它在等待第二个任务的完成。资源循环等待死锁。

<2.1>线程饥饿死锁

在线程池中的任务需要无限期地等待一些必须由池中其他任务才能提供的资源或条件,例如某个
任务等待另一个任务的返回值或执行结果,那么除非线程池足够大,否则将发生线程饥饿死锁。

7 锁的形式与种类

(1)内置锁(独占锁的形式)

     任何Java对象都可以用作同步锁,为了便于区分,将其称为内置锁。当然这个“内置锁”是
要与synchronized 配合使用的。所以内置锁指的是“synchronized”与synchronized配合使用的
任何Java对象。
    Object中对内置锁进行操作的一些方法。
Wait系列:使当前已经获得该对象锁的线程进入等待状态,并释放该对象锁。
Notify系列:唤醒那些正在等待该对象锁的线程,使其继续运行。

(2)显式锁

    Lock接口中定义了一组抽象的加锁操作。与内置加锁机制不同的是,Lock提供了一种无条件的、
可轮询的、定时的以及可中断的锁获取操作,所有加锁和解锁的方法都是显式的。在Lock的实现中
必须提供与内部锁相同的内存可见性语义,但在加锁语义、调度算法、顺序保证以及性能特性等方
面可以有所不同。
    ReentrantLock实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性。
ReentrantLock还提供了可重人的加锁语义。与synchronized相比提供了更高的灵话性。

(3)显式锁-ReentrantLock

<3.1>使用形式

//可指定公平锁,非公平锁(默认)模式
//ReentrantLock(boolean fair)
Lock lock = new ReentrantLock();
lock.lock();
//定时的与可轮询的锁获取模式tryLock
//boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
try{
	//更新对象的操作
	//捕获异常并处理
}
finally {
	lock.unlock();
}

<3.2>为什么要创建一种与内置锁如此相似的新加锁机制?

    内置锁无法中断一个正在等待获取锁的线程,或者无法在请求获取一个锁时无限地等待下去。内置锁必须在获取该锁
的代码块中释放,这就简化了编码工作,并且与异常处理操作实现了很好的交互,但却无法实现非阻塞结构的加锁规则。
    ReentrantLock使用确更加灵活。ReentrantLock不能完全替代synchronized的原因:它更加“危险”,当程序的执行控
制离开被保护的代码块时,不会自动情除锁。需要在finally 块中释放锁。

<3.3>ReentrantLock公平性

    在ReentrantLock的构造函数中提供了两种公平性选择:创建一个非公平的锁(默认)或者一个公平的锁。
在公平的锁上,线程将按照它们发出请求的顺序来获得锁,但在非公平的锁上,则允许“插队”。非公平锁
的性能要高于公平锁的性能。

<3.4>ReentrantLock定时锁与轮询机制

支持定时与轮询锁机制
可以检测死锁和从死锁中恢复过来,即显式使用Lock类中的定时tryLock 功能来代替内置锁(Synchronized)机制。
当使用内置锁时,只要没有获得锁,就会永远等待下去,而显式锁则可以指定一个超时时限( Timeout),  在等待超过
该时间后tryLock会返回一个失败信息。如果超时时限比获取锁的时间要长很多,那么就可以在发生某个意外情况后重
新获得控制权。

(4)读写锁-ReadWriteLock(读写锁接口)

    ReentrantReadWriteLock实现了一种在多个读取操作以及单个写人操作情况下的加锁规则:
如果多个读取操作都不会修改共享资源,那么这些读取操作可以同时访问该共享资源,但在执行
写人操作时必须以独占方式来获取锁。对于读取操作占多数的数据结构,ReadWriteLock能提供比
独占锁更高的并发性。

8 可重入锁机制

    当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。然而,由于内置锁是可重人的,
因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的
操作的粒度是“线程”,而不是“调用。重人的一种实现方法是,为每个锁关联一个获取计数值和一个所有
者线程。当计数值为0时,这个锁就被认为是没有被任何线程持有。  当线程请求一个未被持有的锁时,JVM
将记下锁的持有者,并且将获取计数值置为1.  如果同一个线程再次获取这个锁,计数值将递增,而当线程
退出同步代码块时,计数器会相应地递减。当计数值为0时,这个锁将被释放。重入避免了死锁的发生。
    内置锁,显式锁ReentrantLock都是可重入锁。

9 锁优化手段

(1)锁分解与锁分段技术(阿姆达尔定律应用)

阿姆达尔定律应用(降低锁的粒度)
    大多数井发程序都与农业耕作有着许多相似之处,它们都是由一系列的并行工作和串行工作组成的。
Amdahl定律描述的是: 在增加计算资源的情况下,程序在理论上能够实现最高加速比,这个值取决于程
序中可并行组件与串行组件所占的比重。假定F是必须被串行执行的部分,那么根据Amdahl定律,在包含
N个处理器的机器中,最高的加速比为:
    S<=1/(F+(1-F)/N)
    注:F为串行执行占比。
该定律的应用:降低锁的粒度。
    具体包括:锁分解(一个锁分解为两个)
    锁分段(一个锁分解为多个,效率提升更加有效)
    ConcurrentHashMap的实现中使用了一个包含16个锁的数组,每个锁保护所有散列桶的116,其中
第N个散列桶由第(N mod 16) 个锁来保护。假设散列函数具有合理的分布性,并且关键字能够实现
均匀分布,那么这大约能把对于锁的请求减少到原来的1/16. 

(2)替代独占锁

内置属于独占锁机制。可用显式锁,读写锁机制替代独占锁使用。
内置锁,在java6中得到了优化,和显式锁性能几乎相当,ReentrantLock,性能略微高于内置锁。

(3)非阻塞同步机制

    非阻塞算法被广泛地用于在操作系统和JVM中实现线程/进程调度机制、垃圾回收机制以及销和其他并发数据结构。
非阻塞算法是基于底层原子机器指令(如比较交换指令CAS)代替锁确保数据并发访问的一致性。非阻塞同步机制在多
个线程在竟争相同的数据时不会发生阻塞;不存在死销和其他活跃性问题,效率更高。

(4)原子变量

    原子变量类提供了在整数或者对象引用上的细粒度原子操作,并使用了现代处理器中提供的底层原语(如CAS)。
共有12个原子变量类,可分为4组:标量类(Scalar). 更新器类、数组类以及复合变量类。最常用的原子变量就是标量
类: AtomicInteger, AtomicLong、AtomicBoolcan以及AtomicReference,所有这些类都支持CAS,此外,AtomicInteger
和AtomicLong还支持算术运算。
    在原子变量类中同样没有重新定义hashCode或equals方法,每个实例都是不同的。与其他可变对象相同,它们也
不宜用做基于散列的容器中的键值。

(5)CAS(Compare And Swap)机制

    CAS(比较并交换)包含了3个操作数一需要读写的内存位置V、进行比较的值A和拟写人的新值B。
当且仅当V的值等于A时,CAS才会通过原子方式用新值B来更新V的值,否则不会执行任何操作。无论
位置V的值是否等于A,都将返回V原有的值。这种变化形式被称为比较并设置,无论操作是否成功都
会返回.CAS的含义是:“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的
值实际为多少”。CAS是一项乐观的技术,它希望能成功地执行更新操作,并且如果有另一个线程
在最近一次检查后更新了该变量,那么CAS能检测到这个错误。
    当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线
程都将失败。然而,失败的线程并不会被挂起,而是被告知在这次竞争中失败,并可以再次尝试。由
于一个线程在竞争CAS时失败不会阻塞,因此它可以决定是否重新尝试,或者执行一些恢复操作,也
或者不执行任何操作。这种灵话性就大大减少了与锁相关的话跃性风险。

(6)CAS之ABA问题

    ABA问题是一种异常现象: 如果在算法中的节点可以被循环使用,那么在使用“比较并交换”指令时就可能
出现这种问题(主要在没有垃圾回收机制的环境中)。在CAS操作中将判断“V的值是否仍然为A?",并且如果是的
话就继续执行更新操作。 在某些算法中,如果V的值首先由A变成B,再由B变成A,那么仍然被认为是发生了变化,
并需要重新执行算法中的某些步骤。
    如果在算法中采用自己的方式来管理节点对象的内存,那么可能出现ABA问题。在这种情况下,有一个相对
简单的解决方案: 不是更新某个引用的值,而是更新两个值,包括一个引用和一个版本号。即使这个值由A变为B,
然后又变为A,版本号也将是不同的。

(7)偏向锁

    偏向锁也是JDK 1.6 中引人的一项锁优化,它的目的是消除数据在无竞争情况下
的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用
CAS操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消
除掉,连CAS操作都不做了。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不甩锅的码农

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值