共计 4564 个字符,预计需要花费 12 分钟才能阅读完成。
背景
先上图
由此可见,非自旋锁如果拿不到锁会把线程阻塞,直到被唤醒;自旋锁拿不到锁会始终尝试
为什么要这样?
益处
阻塞和唤醒线程都是须要昂扬的开销的,如果同步代码块中的内容不简单,那么可能转换线程带来的开销比理论业务代码执行的开销还要大。
在很多场景下,可能咱们的同步代码块的内容并不多,所以须要的执行工夫也很短,如果咱们仅仅为了这点工夫就去切换线程状态,那么其实不如让线程不切换状态,而是让它自旋地尝试获取锁,期待其余线程开释锁,有时我只须要稍等一下,就能够防止上下文切换等开销,进步了效率。
用一句话总结自旋锁的益处,那就是自旋锁用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节俭了线程状态切换带来的开销。
AtomicLong 的实现
getAndIncrement 办法
public final long getAndIncrement() {return unsafe.getAndAddLong(this, valueOffset, 1L);
}
复制代码
public final long getAndAddLong(Object o, long offset, long delta) {
long v;
do {v = getLongVolatile(o, offset);
// 如果批改过程中遇到其余线程竞争导致没批改胜利,死循环,直到批改胜利为止
} while (!compareAndSwapLong(o, offset, v, v + delta));
return v;
}
复制代码
试验
package com.reflect;
import java.util.concurrent.atomic.AtomicReference;
class ReentrantSpinLock {private AtomicReference<Thread> owner = new AtomicReference<>();
private int count = 0;
public void lock() {Thread t = Thread.currentThread();
if (t == owner.get()) {
++count;
return;
}
while (!owner.compareAndSet(null, t)) {System.out.println("自旋了");
}
}
public void unlock() {Thread t = Thread.currentThread();
if (t == owner.get()) {if (count > 0) {--count;} else {owner.set(null);
}
}
}
public static void main(String[] args) {ReentrantSpinLock spinLock = new ReentrantSpinLock();
Runnable runnable = new Runnable() {
@Override
public void run() {System.out.println(Thread.currentThread().getName() + "开始尝试获 取自旋锁");
spinLock.lock();
try {System.out.println(Thread.currentThread().getName() + "获取到 了自旋锁");
Thread.sleep(4000);
} catch (InterruptedException e) {e.printStackTrace();
} finally {spinLock.unlock();
System.out.println(Thread.currentThread().getName() + "开释了 了自旋锁");
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();}
}
复制代码
很多 “ 自旋了 ”,阐明自旋期间 CPU 仍然在不停运行
毛病
尽管防止了线程切换的开销,然而在防止线程切换开销的同时带来新的开销:不停尝试获取锁,如果这个锁始终不能被开释那么这种尝试常识无用的尝试,节约处理器资源,就是说一开始自旋锁开销低于线程切换,然而随着工夫减少,这种开销前期甚至超过线程切换的开销,得失相当
实用场景
并发不是特地高的场景
临界区比拟短小的状况,利用防止线程切换提高效率
如果临界区很大,线程拿到锁很久才开释,那自旋会始终占用 CPU 但无奈拿到锁,浪费资源
JVM 对锁做了哪些优化?
相比于 JDK 1.5,在 JDK 1.6 中 HotSopt 虚拟机对 synchronized 内置锁的性能进行了很多优化,包含自适应的自旋、锁打消、锁粗化、偏差锁、轻量级锁等。有了这些优化措施后,synchronized 锁的性能失去了大幅提高,上面咱们别离介绍这些具体的优化。
自适应的自旋锁
在 JDK 1.6 中引入了自适应的自旋锁来解决长时间自旋的问题。自适应意味着自旋的工夫不再固定,而是会依据最近自旋尝试的成功率、失败率,以及以后锁的拥有者的状态等多种因素来独特决定。自旋的持续时间是变动的,自旋锁变“聪慧”了。比方,如果最近尝试自旋获取某一把锁胜利了,那么下一次可能还会持续应用自旋,并且容许自旋更长的工夫;然而如果最近自旋获取某一把锁失败了,那么可能会省略掉自旋的过程,以便缩小无用的自旋,提高效率。
锁打消
public class Person {
private String name;
private int age;
public Person(String personName, int personAge) {
name = personName;
age = personAge;
}
public Person(Person p) {this(p.getName(), p.getAge());
}
public String getName() {return name;}
public int getAge() {return age;}
}
class Employee {
private Person person;
public Person getPerson() {return new Person(person);
}
public void printEmployeeDetail(Employee emp) {Person person = emp.getPerson();
System.out.println("Employee's name: "+ person.getName() +"; age: " + person.getAge());
}
}
复制代码
在这段代码中,咱们看到下方的 Employee 类中的 getPerson () 办法,这个办法中应用了类外面的 person 对象,并且新建一个和它属性完全相同的新的 person 对象,目标是避免办法调用者批改原来的 person 对象。然而在这个例子中,其实是没有任何必要新建对象的,因为咱们的 printEmployeeDetail () 办法没有对这个对象做出任何的批改,仅仅是打印,既然如此,咱们其实能够间接打印最开始的 person 对象,而无须新建一个新的。
如果编译器能够确定最开始的 person 对象不会被批改的话,它可能会优化并且打消这个新建 person 的过程。依据这样的思维,接下来咱们就来举一个锁打消的例子,,通过逃逸剖析之后,如果发现某些对象不可能被其余线程拜访到,那么就能够把它们当成栈上数据,栈上数据因为只有本线程能够拜访,天然是线程平安的,也就无需加锁,所以会把这样的锁给主动去除掉。
例如,咱们的 StringBuffffer 的 append 办法如下所示:
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
复制代码
从代码中能够看出,这个办法是被 synchronized 润饰的同步办法,因为它可能会被多个线程同时应用。
然而在大多数状况下,它只会在一个线程内被应用,如果编译器能确定这个 StringBuffffer 对象只会在一个线程内被应用,就代表必定是线程平安的,那么咱们的编译器便会做出优化,把对应的 synchronized 给打消,省去加锁和解锁的操作,以便减少整体的效率。
锁粗化
开释了锁,紧接着什么都没做,又从新获取锁
public void lockCoarsening() {synchronized (this) { }
synchronized (this) { }
synchronized (this) {}}
复制代码
那么其实这种开释和从新获取锁是齐全没有必要的,如果咱们把同步区域扩充,也就是只在最开始加一次锁,并且在最初间接解锁,那么就能够把两头这些无意义的解锁和加锁的过程打消,相当于是把几个 synchronized 块合并为一个较大的同步块。这样做的益处在于在线程执行这些代码时,就毋庸频繁申请与开释锁了,这样就缩小了性能开销。
不过,咱们这样做也有一个副作用,那就是咱们会让同步区域变大。如果在循环中咱们也这样做,如代码所示:
for (int i = 0; i < 1000; i++) {synchronized (this) {}}
复制代码
也就是咱们在第一次循环的开始,就开始扩充同步区域并持有锁,直到最初一次循环完结,才完结同步代码块开释锁的话,这就会导致其余线程长时间无奈取得锁。所以,这里的锁粗化不适用于循环的场景,仅实用于非循环的场景。
锁粗化性能是默认关上的,用 -XX:-EliminateLocks 能够敞开该性能
偏差锁 / 轻量级锁 / 重量级锁
这三种锁是特指 synchronized 锁的状态,通过在对象头中的 mark word 来表明锁的状态
偏差锁
对于偏差锁而言,它的思维是如果从头至尾,对于这把锁都不存在竞争,那么其实就没必要上锁,只有打个标记就行了。一个对象在被初始化后,如果还没有任何线程来获取它的锁时,它就是可偏差的,当有第一个线程来拜访它尝试获取锁的时候,它就记录下来这个线程,如果前面尝试获取锁的线程正是这个偏差锁的拥有者,就能够间接获取锁,开销很小。
轻量级锁
JVM 的开发者发现在很多状况下,synchronized 中的代码块是被多个线程交替执行的,也就是说,并不存在理论的竞争,或者是只有短时间的锁竞争,用 CAS 就能够解决。这种状况下,重量级锁是没必要的。轻量级锁指当锁原来是偏差锁的时候,被另一个线程所拜访,阐明存在竞争,那么偏差锁就会降级为轻量级锁,线程会通过自旋的形式尝试获取锁,不会阻塞
重量级锁
这种锁利用操作系统的同步机制实现,所以开销比拟大。当多个线程间接有理论竞争,并且锁竞争工夫比拟长的时候,此时偏差锁和轻量级锁都不能满足需要,锁就会收缩为重量级锁。重量级锁会让其余申请却拿不到锁的线程进入阻塞状态。
锁降级
偏差锁性能最好,防止了 CAS 操作。而轻量级锁利用自旋和 CAS 防止了重量级锁带来的线程阻塞和唤醒,性能中等。重量级锁则会把获取不到锁的线程阻塞,性能最差。
JVM 默认会优先应用偏差锁,如果有必要的话才逐渐降级,这大幅提高了锁的性能
残缺附件:点击此处下载附件