通过锁可以实现受限制资源的共享,序列化共享资源的访问。java提供了一套用于锁的机制,这套机制里主要的锁就是关键字synchronized和concurrent包中的lock类。另外也需要记住这一点:多线程加锁虽然实现互斥,但是很可能降低了处理速度,带来严重的性能问题。
为了解决问题,不得不处理这样的复杂性。虽然复杂性会带来性能、可读性、可维护性上的诸多的问题。
CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。concurrent包中的原子类就是通过CAS实现的。(可以查看并发实现机制-2-互斥实现中的硬件互斥部分获得其他信息)
CAS算法涉及到三个操作数:
当且仅当 V 的值等于 A 时,CAS通过原子方式用新值B来更新V的值(“比较+更新”整体是一个原子操作),否则不会执行任何操作。一般情况下,“更新”是一个不断重试的操作。
CAS虽然很高效,但是它也存在三大问题
ABA问题
CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。**但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。**ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。
AtomicStampedReference
类来解决ABA问题,具体操作封装在compareAndSet()
中。compareAndSet()
首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
只能保证一个共享变量的原子操作
对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
AtomicReference
类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。todo: 自增计数;todo: 关于AQS请查看后面的lock小节,了解继承体系
在Java中有AQS(AbstractQueuedSynchronizer
)AQS中维护了一个同步状态status来计数重入次数,status初始值为0。
当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0表示没有其他线程在执行同步代码,则把status置为1,当前线程开始执行。如果status != 0,则判断当前线程是否是获取到这个锁的线程,如果是的话执行status+1,且当前线程可以再次获取锁。而非可重入锁是直接去获取并尝试更新当前status的值,如果status != 0的话会导致其获取锁失败,当前线程阻塞。
释放锁时,可重入锁同样先获取当前status的值,在当前线程是持有锁的线程的前提下。如果status-1 == 0,则表示当前线程所有重复获取锁的操作都已经执行完毕,然后该线程才会真正释放锁。而非可重入锁则是在确定当前线程是持有锁的线程之后,直接将status置为0,将锁释放。
(请注意:本部分参考了文末参考2,主要图文均来自该文章)
Java提供了种类丰富的锁,每种锁因其特性的不同,在适当的场景下能够展现出非常高的效率。本小节专门论述不同类型的锁和锁现象(介绍名词向的小节),例如:乐观锁、悲观锁;自旋锁、适应性自旋锁;无锁、偏向锁、轻量级锁、重量级锁;公平锁、不公平锁;可重入锁(递归锁)、不可重入锁;共享锁、排他锁(独享锁)(互斥锁)。
乐观锁与悲观锁体现了看待线程同步的不同角度。
适合的场景:
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。
为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
自旋锁的实现原理同样也是CAS。有三种常见的锁形式:TicketLock
、CLHlock
和MCSlock
。
自旋锁本身是有缺点的,即它要占用处理器时间:
所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin
来更改)没有成功获得锁,就应当挂起线程。
适应性自旋锁
自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:
如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。
如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
这四种锁是指锁的状态,专门针对synchronized的。锁状态只能升级不能降级.
对比:
ThreadID
的时候依赖一次CAS原子指令即可(todo: 依然不是很能理解)状态转换:
ReentrantLock默认使用非公平锁,也可以通过构造器来显示的指定使用公平锁
ReentrantLock和synchronized都是可重入锁
ReentrantLock
和NonReentrantLock
都继承父类AQS
doSomething
方法上的锁,doSomething
方法体中需要调用doOthers
,那么线程可以获得doOthers
的锁synchronized和JUC中Lock的实现类就是互斥锁
独享锁与共享锁也是通过AQS来实现的
(请对照上述的无锁 & 偏向锁 & 轻量级锁 & 重量级锁来理解)
java提供关键字synchronized(该关键词检查锁是否可用、然后获取锁、执行代码、释放锁)
synchronized(syncObject){//在进入此段代码之前,必须得到syncObject的锁
// this code can be accessed by only one task at a time
}
如果在this上同步即synchronized(this)那么临界区的效果就会直接缩小在同步的范围内。
有时必须在另一个对象上同步,但是如果你要这么做,就必须确保所有相关的任务都是在同一个对象上同步的(如方法A和方法B都使用了一个list变量域来进行添加删除元素,那么就要使用同一个对象来对其同步)。
下面的示例演示了两个任务可以同时进入同一个对象,只要这个对象上的方法是在不同的锁上同步的即可。下面的示例中,两个方式在同时运行,因此任何一个方法都不会因为另一个方法的同步而被堵塞。
class DualSynch {
private Object syncObject = new Object();
public synchronized void f() { // sync this
for(int i = 0; i < 5; i++) {
print("f()");
Thread.yield();
}
}
public void g() {
synchronized(syncObject) { // syncObject
for(int i = 0; i < 5; i++) {
print("g()");
Thread.yield();
}
}
}
}
todo: 进一步整理,本小节没有进行很好的整理!请注意!
在回答这个问题之前我们需要了解两个重要的概念:“Java对象头”、“Monitor”。
synchronized是悲观锁,在操作同步资源之前需要给同步资源先加锁,这把锁就是存在Java对象头里的,而Java对象头又是什么呢?
我们以Hotspot虚拟机为例,Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。
Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。
Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
现在话题回到synchronized,synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。
如同我们在自旋锁中提到的“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长”。这种方式就是synchronized最初实现同步的方式,这就是JDK 6之前synchronized效率低的原因。这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”,JDK 6中为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。
所以目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。
通过上面的介绍,我们对synchronized的加锁机制以及相关知识有了一个了解,那么下面我们给出四种锁状态对应的的Mark Word内容,然后再分别讲解四种锁状态的思路以及特点:
锁状态 | 存储内容 | 存储内容 |
---|---|---|
无锁 | 对象的hashCode、对象分代年龄、是否是偏向锁(0) | 01 |
偏向锁 | 偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 |
重量级锁 | 指向互斥量(重量级锁)的指针 | 10 |
使用显式的Lock对象。即使用lock.lock()和lock.unlock()来包围住需要加锁的区域。对于某些类型的问题更为灵活。使用lock的示例如下:
private Lock lock = new ReentrantLock();
public int next() {
lock.lock();
try {
++currentEvenValue;
Thread.yield(); // Cause failure faster
++currentEvenValue;
return currentEvenValue;
} finally {
lock.unlock();
}
}
lock的小小概况
Lock方式来获取锁支持中断、超时不获取、是非阻塞的
提高了语义化,哪里加锁,哪里解锁都得写出来
Lock显式锁可以给我们带来很好的灵活性,但同时我们必须手动释放锁
支持Condition条件对象
允许多个读线程同时访问共享资源
清理资源上:如果在使用synchronized关键字时,某些事物失败了,那么就会抛出一个异常。但是你没有机会去做任何清理工作,以维护系统使其处于良好状态。有了显式的Lock对象,你就可以使用 finally子句将系统维护在正确的状态了。
易用程度上:当你使用synchronized关键字时,需要写的代码量更少,并且用户错误出现的可能性也会降低,因此通常只有在解决特殊问题时,才使用显式的Lock对象。
获取锁这件事上:ReentrantLock允许你尝试着获取但最终未获取锁,这样如果其他人已经获取了这个锁,那你就可以决定离开去执行其他一些事情,而不是等待直至这个锁被释放(而synchronized会一直堵塞在这里)。
灵活性上:显式的Lock对象在加锁和释放锁方面,相对于内建的 synchronized锁来说,还赋予了你更细粒度的控制力。这对于实现专有同步结构是很有用的,
临界区构建上:
使用synchronized可以构建临界区。
synchronized(syncObject){//在进入此段代码之前,必须得到syncObject的锁
// this code can be accessed by only one task at a time
}
也可以使用lock对象lock.lock()
;和lock.unlock()
;包围起来一个临界区。不一定在同一个区域。但是这样设计就会增加复杂性,很可能陷入死锁和饥饿
在向读者介绍这两个问题和volatile时,可能有必要引述编程思想上这几句话,在本节结束时会再次重复以免读者忘记这一忠告。
如果你是一个并发专家,或者你得到了来自这样的专家的帮助,你才应该使用原子性来代替同步。如果你认为自己足够聪明可以应付这种玩火似的情况,那么请接受下面的测试:
- 如果你可以编写用于现代微处理器的高性能JVM,那么就有资格去考虑是否可以进免同步
- 这个测试的一个推论是:“如果某人表示线程机制很容易并且很简单,那么请确保这个人没有对你的项目做出重要的决策。如果这个人已经在这么做了,那么你就已经陷入麻烦之中了。”
请不要简单的因为炫技或者偷懒而不加思考的使用原子类和利用原子性、可视性。
在有关Java线程的讨论中,一个经常不正确的知识是“原子操作不需要进行同步控制”。
原子操作是不能被线程调度机制中断的操作;一旦操作开始,那么它一定可以在可能发生的“上下文切换”之前(切换到其他线程执行)执行完毕。依赖于原子性是很棘手且很危险的。
原子性可以应用于除long和 double之外的所有基本类型之上的“简单操作”。
但是JVM可以将64位(long和 double变量)的读取和写入当作两个分离的32位操作来执行,这就产生了在一个读取和写入操作中间发生上下文切换,从而导致不同的任务可以看到不正确结果的可能性(这有时被称为字撕裂,因为你可能会看到部分被修改过的数值)。
但是,当你定义long或 double变量时,如果使用 volatile关键字,就会获得(简单的赋值与返回操作的)原子性。
Java中++和--不是原子性的,涉及到一个读操作和写操作
可视性(易变性)
相对于单处理器系统而言,在多处理器系统上,可视性问题远比原子性问题多得多。
一个任务做出的修改,即使在不中断的意义上讲是原子性的,对其他任务也可能是不可视的(例如,修改只是暂时性地存储在本地处理器的缓存中),
另一方面,同步机制强制在处理器系统中,一个任务做出的修改必须在应用中是可视的。如果没有同步机制,那么修改时可视将无法确定。
应当和volatile关键字结合的来看待可视性问题
定义long或 double变量时,如果使用 volatile关键字,就会获得(简单的赋值与返回操作的)原子性,避免字撕裂
volatile关键字会导致相应的域向主存中刷新,这确保了应用中的可视性。
即便使用了本地缓存,情况也确实如此, volatile域会立即被写入到主存中,而读取操作就发生在主存中。
如果多个任务在同时访问某个域,那么这个城就应该是 volatile,否则,这个域就应该只能经由同步来访问。
同步也会导致向主存中刷新,因此如果一个域完全由 synchronized方法或语句块来防护,那就不必将其设置为是volatile的。
在非 volatile域上的原子操作不必刷新到主存中去,因此其他读取该域的任务也不必看到这个新值。如果多个任务在同时访问某个域,那么这个城就应该是 volatile,否则,这个域就应该只能经由同步来访问。
无法工作的情形:
一个例子
看下面一个例子,该程序将找到奇数值并终止。尽管 return i
确实是原子性操作,但是缺少同步使得其数值可以在处于不稳定的中间状态时被读取。除此之外,由于i
不是 volatile的,因此还存在可视性问题。
因此 getValue
和 evenIncrement
必须是 synchronized的。
public class AtomicityTest implements Runnable {
private int i = 0;
public int getValue() { return i; } // 注意这一句,不稳定的返回 注意:本句return是原子性操作
private synchronized void evenIncrement() { i++; i++; }
public void run() {
while(true)
evenIncrement();
}
}
基本上,如果一个域可能会被多个任务同时访问,或者这些任务中至少有一个是写入任务,那么就应该将这个域设置为 volatile的。但是, volatile并不能对递增不是原子性操作这一事实产生影响。
如下所示。对基本类型的读取和赋值操作被认为是安全的原子性操作。但是,正如你在上面的AtomicityTest
中看到的,当对象处于不稳定状态时,仍旧很有可能使用原子性操作来访问它们。
public class SerialNumberGenerator {
private static volatile int serialNumber = 0;
public static int nextSerialNumber() {
return serialNumber++; // Not thread-safe 不是线程安全的
}
} ///:~
如果你是一个并发专家,或者你得到了来自这样的专家的帮助,你才应该使用原子性来代替同步。如果你认为自己足够聪明可以应付这种玩火似的情况,那么请接受下面的测试:
- 如果你可以编写用于现代微处理器的高性能JVM,那么就有资格去考虑是否可以进免同步
- 这个测试的一个推论是:“如果某人表示线程机制很容易并且很简单,那么请确保这个人没有对你的项目做出重要的决策。如果这个人已经在这么做了,那么你就已经陷入麻烦之中了。”
请不要简单的因为炫技或者偷懒而不加思考的使用原子类和利用原子性、可视性。
这部分需要结合本文上面的CAS算法小节来了解。原子类是基于比较交换指令的,
原子类在常规编程很少会派上用场,但涉及性能调优时,他们就大有用武之地。 应该强调的是, Atomic类被设计用来构建 java util. concurrent
中的类,因此只有在特殊情况下才在自己的代码中使用它们,即便使用了也需要确保不存在其他可能出现的问题。通常依赖于锁要更安全一些(要么是 synchronized关键字,要么是显式的Lock对象)
针对原子类以及jdk8的新的原子类,在另一篇文章中有介绍,读者可以去另一篇文章深入了解。todo: 在这里应当引用另一篇文章的链接
- Java 编程思想 第四版 中文版 倒数第二章
- 美团技术团队:不可不说的Java"锁"事
原文:https://www.cnblogs.com/cheaptalk/p/12549672.html