JDK6 synchronized优化

1. CAS

1.1 目标

  • 学习CAS的作用

  • 学习CAS的原理

CAS我在之前大厂面试题中详细讲解过,需要的去看看就行了,

因为这里需要对比一波所以再次复习一波。

1.2 CAS概述和作用- oiuytr

CAS的全成是: Compare And Swap(比较相同再交换)。是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。

CAS的作用:CAS可以将比较和交换转换为C原子操作,这个原子操作直接由CPU保证。CAS可以保证共享变量赋值时的原子操作。CAS操作依赖3个值:内存中的值V,旧的预估值X,要修改的新值B,如果旧的预估值X等于内存中的值V,就将新的值B保存到内存中。

CAS和volatile实现无锁并发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
目标:演示原子性问题
1.定义一个共享变量number
2.对number进行1000的++操作
3.使用5个线程来进行
*/
public class Demo01 {
// 1.定义一个共享变量number
private static AtomicInteger atomicInteger = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
// 2.对number进行1000的++操作
Runnable increment = () -> {
for (int i = 0; i < 1000; i++) {
atomicInteger.incrementAndGet(); // 变量赋值的原子性
}
};

List<Thread> list = new ArrayList<>();
// 3.使用5个线程来进行
for (int i = 0; i < 5; i++) {
Thread t = new Thread(increment);
t.start();
list.add(t);
}
for (Thread t : list) {
t.join();
}
System.out.println("atomicInteger = " + atomicInteger.get());
}
}

1.3 CAS原理

通过AtomicInteger的源码我们可以看到,Unsafe类提供了原子操作。

Unsafe类介绍

Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使 用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。Unsafe对象不能直接调用,只能通过反射获得。

1
2
3
4
5
6
7
8
9
public class Test {
private static Unsafe UNSAFE;
public static void main(String[] args) throws Exception {

Field theUnsafe = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
UNSAFE = (Unsafe) theUnsafe.get(null);
}
}

image-20200827172727849

`

Unsafe实现CAS

image-20200827173223250

乐观锁和悲观锁

悲观锁从悲观的角度出发:

总是假设坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这 样别人想拿这个数据就会阻塞。因此synchronized我们也将其称之为悲观锁。JDK中的ReentrantLock 也是一种悲观锁。性能较差!

乐观锁从乐观的角度出发:

总是假设好的情况,每次去拿数据的时候都认为别人不会修改,就算改了也没关系,再重试即可。所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去修改这个数据,如何没有人修改则更新,如果有人修改则重试。

CAS这种机制我们也可以将其称之为乐观锁。综合性能较好!

CAS获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。结合CAS和volatile可以实现无锁并发,适用于竞争不激烈、多核 CPU 的场景下。

  1. 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一。
  2. 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。

1.4 小结

CAS的作用?

Compare And Swap,CAS可以将比较和交换转换为原子操作,这个原子操作直接由处理器保证。

CAS的原理?

CAS需要3个值:内存地址V,旧的预期值A,要修改的新值B,如果内存地址V和旧的预期值 A相等就修改内存地址值为B

2. synchronized锁升级过程

高效并发是从JDK 5到JDK 6的一个重要改进,HotSpot虛拟机开发团队在这个版本上花费了大量的精力 去实现各种锁优化技术,包括偏向锁( Biased Locking )、轻量级锁( Lightweight Locking )和自适应性自旋(Adaptive Spinning)、锁消除( Lock Elimination)、锁粗化( Lock Coarsening )等,这些技术都是为 了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。

无锁–》偏向锁–》轻量级锁–》重量级锁

3. Java对象的布局

3.1 目标

学习Java对象的布局

术语参考: http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下图所示:

image-20200827182531986

3.2 对象头

当一个线程尝试访问synchronized修饰的代码块时,它首先要获得锁,那么这个锁到底存在哪里呢?是 存在锁对象的对象头中的。

HotSpot采用instanceOopDesc和arrayOopDesc来描述对象头,arrayOopDesc对象用来描述数组类型。instanceOopDesc的定义的在Hotspot源码的 instanceOop.hpp 文件中,另外,arrayOopDesc 的定义对应 arrayOop.hpp 。

  • src/share/vm/oops/arrayOop.hpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
class arrayOopDesc : public oopDesc {
friend class VMStructs;

// Interpreter/Compiler offsets

// Header size computation.
// The header is considered the oop part of this type plus the length.
// Returns the aligned header_size_in_bytes. This is not equivalent to
// sizeof(arrayOopDesc) which should not appear in the code.
static int header_size_in_bytes() {
size_t hs = align_size_up(length_offset_in_bytes() + sizeof(int),
HeapWordSize);
#ifdef ASSERT
// make sure it isn't called before UseCompressedOops is initialized.
static size_t arrayoopdesc_hs = 0;
if (arrayoopdesc_hs == 0) arrayoopdesc_hs = hs;
assert(arrayoopdesc_hs == hs, "header size can't change");
#endif // ASSERT
return (int)hs;
}

public:
// The _length field is not declared in C++. It is allocated after the
// declared nonstatic fields in arrayOopDesc if not compressed, otherwise
// it occupies the second half of the _klass field in oopDesc.
static int length_offset_in_bytes() {
return UseCompressedClassPointers ? klass_gap_offset_in_bytes() :
sizeof(arrayOopDesc);
}

// Returns the offset of the first element.
static int base_offset_in_bytes(BasicType type) {
return header_size(type) * HeapWordSize;
}

// Returns the address of the first element.
void* base(BasicType type) const {
return (void*) (((intptr_t) this) + base_offset_in_bytes(type));
}

// Tells whether index is within bounds.
bool is_within_bounds(int index) const { return 0 <= index && index < length(); }

// Accessors for instance variable which is not a C++ declared nonstatic
// field.
int length() const {
return *(int*)(((intptr_t)this) + length_offset_in_bytes());
}
void set_length(int length) {
*(int*)(((intptr_t)this) + length_offset_in_bytes()) = length;
}

// Should only be called with constants as argument
// (will not constant fold otherwise)
// Returns the header size in words aligned to the requirements of the
// array object type.
static int header_size(BasicType type) {
size_t typesize_in_bytes = header_size_in_bytes();
return (int)(Universe::element_type_should_be_aligned(type)
? align_object_offset(typesize_in_bytes/HeapWordSize)
: typesize_in_bytes/HeapWordSize);
}

// Return the maximum length of an array of BasicType. The length can passed
// to typeArrayOop::object_size(scale, length, header_size) without causing an
// overflow. We also need to make sure that this will not overflow a size_t on
// 32 bit platforms when we convert it to a byte size.
static int32_t max_array_length(BasicType type) {
assert(type >= 0 && type < T_CONFLICT, "wrong type");
assert(type2aelembytes(type) != 0, "wrong type");

const size_t max_element_words_per_size_t =
align_size_down((SIZE_MAX/HeapWordSize - header_size(type)), MinObjAlignment);
const size_t max_elements_per_size_t =
HeapWordSize * max_element_words_per_size_t / type2aelembytes(type);
if ((size_t)max_jint < max_elements_per_size_t) {
// It should be ok to return max_jint here, but parts of the code
// (CollectedHeap, Klass::oop_oop_iterate(), and more) uses an int for
// passing around the size (in words) of an object. So, we need to avoid
// overflowing an int when we add the header. See CRs 4718400 and 7110613.
return align_size_down(max_jint - header_size(type), MinObjAlignment);
}
return (int32_t)max_elements_per_size_t;
}

// for unit testing
#ifndef PRODUCT
static bool check_max_length_overflow(BasicType type);
static int32_t old_max_array_length(BasicType type);
static void test_max_array_length();
#endif
};

从instanceOopDesc代码中可以看到 instanceOopDesc继承自oopDesc,oopDesc的定义载Hotspot 源码中的 oop.hpp (src/share/vm/oops/oop.hpp )文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;

// Fast access to barrier set. Must be initialized.
static BarrierSet* _bs;
 // 省略其他代码
}

image-20200827182637546

在普通实例对象中,oopDesc的定义包含两个成员,分别是 _mark_metadata

_mark 表示对象标记、属于markOop类型,也就是接下来要讲解的Mark World,它记录了对象和锁有 关的信息

_metadata 表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中Klass表示 普通指针、 _compressed_klass 表示压缩类指针。

对象头由两部分组成,一部分用于存储自身的运行时数据,称之为 Mark Word,另外一部分是类型指 针,及对象指向它的类元数据的指针。

3.3 Mark Word

Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、 线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。Mark Word对应的类型是 markOop 。源码位于 markOop.hpp (src/share/vm/oops/markOop.hpp)中。

image-20200827182802360

在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:4

image-20200827185646437

在32位虚拟机下,Mark Word是32bit大小的,其存储结构如下:

image-20200827185827028

3.4 klass pointer

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的 实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。 如果应用的对 象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内 存。为了节约内存可以使用选项-XX:+UseCompressedOops 开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:

  1. 每个Class的属性指针(即静态变量)
  2. 每个对象的属性指针(即对象变量)
  3. 普通对象数组的每个元素指针

当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

对象头 = Mark Word + 类型指针(未开启指针压缩的情况下)

在32位系统中,Mark Word = 4 bytes,类型指针 = 4bytes,对象头 = 8 bytes = 64 bits;

在64位系统中,Mark Word = 8 bytes,类型指针 = 8bytes,对象头 = 16 bytes = 128bits; (类型指针默认开启压缩的为4bytes)

3.5 实例数据

就是类中定义的成员变量。

3.6 对齐填充

对齐填充并不是必然存在的,也没有什么特别的意义,他仅仅起着占位符的作用,由于HotSpot VM的 自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的 整数倍。而对象头正好是8字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。(方便操作系统来寻址)

3.7 查看Java对象布局

1
2
3
4
5
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>

image-20200827191547369

对象使用情况和预期一样,就是说好的31位的hashcode呢…. 这个hashcode不是一来就有,我们需要使用才会出现。

image-20200827192058914

对比一下,有点茫然,顺序颠倒了,这是因为Java使用的是大端寻址(计算机组成原理讲解过大端,小端寻址,我这就不讲了),而这个jol输出方式是小端输出。

3.8 小结

Java对象由3部分组成,对象头,实例数据,对齐数据
对象头分成两部分:Mark World + Klass pointer

4. 偏向锁

4.1 目标

学习偏向锁的原理和好处

4.2 什么是偏向锁

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

偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及 ThreadID即可。

image-20200827185646437

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.openjdk.jol.info.ClassLayout;

public class Demo01 {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();

}
}

class MyThread extends Thread {
static Object obj = new Object();

@Override
public void run() {
for (int i = 0; i < 5; i++) {
synchronized (obj) {
// ...
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
}
}

最后三个二进制位101即表示偏向锁(因为大端模式所以看前面8位)

image-20200827203336727

不过一旦出现多个线程竞争时必须撤销偏向锁,所以撤销偏向锁消耗的性能必须小于之前节省下来的 CAS原子操作的性能消耗,不然就得不偿失了。

4.2 偏向锁原理

当线程第一次访问同步块并获取锁时,偏向锁处理流程如下:

  1. 虚拟机将会把对象头中的标志位设为“01”,即偏向模式。
  2. 同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中 ,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。

4.3 偏向锁的撤销

  1. 偏向锁的撤销动作必须等待全局安全点 (在这个时间点上没有正在执行的字节码)
  2. 暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
  3. 撤销偏向锁,恢复到无锁(标志位为 01)或轻量级锁(标志位为 00)的状态

偏向锁在Java 6之后是默认启用的,但在应用程序启动几秒钟之后才激活,可以使用XX:BiasedLockingStartupDelay=0 参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争 状态,可以通过 XX:-UseBiasedLocking=false 参数关闭偏向锁。

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

4.4 偏向锁好处

偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。

它同样是一个带有效益权衡性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问比如线程池,那偏向模式就是多余的。

在JDK5中偏向锁默认是关闭的,而到了JDK6中偏向锁已经默认开启。但在应用程序启动几秒钟之后才 激活,可以使用-XX:BiasedLockingStartupDelay=0 参数关闭延迟,如果确定应用程序中所有锁通常 情况下处于竞争状态,可以通过 XX:-UseBiasedLocking=false 参数关闭偏向锁。

4.5 小结

偏向锁的原理是什么?

当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中 ,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作,偏向锁的效率高。

偏向锁的好处是什么?

偏向锁是在只有一个线程执行同步块时进一步提高性能,适用于一个线程反复获得同一锁的情况。偏向锁可以 提高带有同步但无竞争的程序性能

5. 轻量级锁

5.1 目标

学习轻量级锁的原理和好处

5.2 什么是轻量级锁

轻量级锁是JDK 6之中加入的新型锁机制,它名字中的“轻量级”是相对于使用monitor的传统锁而言的, 因此传统的锁机制就称为“重量级”锁。首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的。

引入轻量级锁的目的:在多线程交替执行同步块的情况下,尽量避免重量级锁引起的性能消耗,但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁,所以轻量级锁的出现并非是要替代重量级锁。

5.3 轻量级锁原理

当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下: 获取锁

  1. 判断当前对象是否处于无锁状态(hashcode、0、01),如果是,则JVM首先将在当前线程的栈帧 中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方 把这份拷贝加了一个Displaced前缀,即Displaced Mark Word),将对象的Mark Word复制到栈 帧中的Lock Record中,将Lock Reocrd中的owner指向当前对象。
  2. JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00,执行同步操作。
  3. 如果失败则判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态

image-20200827204118538

image-20200827204127687

image-20200827220807101

5.4 轻量级锁的释放

轻量级锁的释放也是通过CAS操作来进行的,主要步骤如下:

  1. 取出在获取轻量级锁保存在Displaced Mark Word中的数据。

  2. 用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功。

  3. 如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要将轻量级锁需要膨胀升级为重量级锁。

对于轻量级锁,其性能提升的依据是“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁 比重量级锁更慢。

image-20200827222258380

5.5 轻量级锁好处

在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。

5.6 小结

轻量级锁的原理是什么?

将对象的Mark Word复制到栈帧中的Lock Recod中。Mark Word更新为指向Lock Record的指针。

轻量级锁好处是什么?

在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。

6. 自旋锁

6.1 目标

学习自旋锁原理

6.2 自旋锁原理

1
2
3
4
synchronized (Demo01.class) {
...
System.out.println("aaa");
}

前面我们讨论monitor实现锁的时候,知道monitor会阻塞和唤醒线程,线程的阻塞和唤醒需要CPU从 用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,这些操作给系统的并发性能 带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很 短的一段时间,为了这段时间阻塞和唤醒线程并不值得。如果物理机器有一个以上的处理器,能让两个 或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一下”,但不放弃处理器的执行 时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自 旋) , 这项技术就是所谓的自旋锁。

自旋锁在JDK 1.4.2中就已经引入 ,只不过默认是关闭的,可以使用-XX:+UseSpinning参数来开启,在 JDK 6中 就已经改为默认开启了。自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本 身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等 待的效果就会非常好,反之,如果锁被占用的时间很长。那么自旋的线程只会白白消耗处理器资源,而 不会做任何有用的工作,反而会带来性 能上的浪费。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值 是10次,用户可以使用参数-XX : PreBlockSpin来更改。

6.3 适应性自旋锁

在JDK 6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上 的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持 有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持 续相对更长的时间,比如100次循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取 这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控 信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虛拟机就会变得越来越“聪明”了。

7. 锁消除

目标

学习如何进行锁消除

锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享 数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中, 堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们 是线程私有的,同步加锁自然就无须进行。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确 定,但是程序员自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下要求同步呢?实际上有 许多同步措施并不是程序员自己加入的,同步的代码在Java程序中的普遍程度也许超过了大部分读者的 想象。下面这段非常简单的代码仅仅是输出3个字符串相加的结果,无论是源码字面上还是程序语义上 都没有同步。

1
2
3
4
5
6
7
8
9
public class Demo01 {
public static void main(String[] args) {
contactString("aa", "bb", "cc");
}

public static String contactString(String s1, String s2, String s3) {
return new StringBuffer().append(s1).append(s2).append(s3).toString();
}
}

StringBuffer的append ( ) 是一个同步方法,锁就是this也就是(new StringBuilder())。虚拟机发现它的 动态作用域被限制在concatString( )方法内部。也就是说, new StringBuilder()对象的引用永远不会“逃 逸”到concatString ( )方法之外,其他线程无法访问到它,因此,虽然这里有锁,但是可以被安全地消除 掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。

8. 锁粗化

8.1 目标

学习锁粗化的原理

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,只在共享数据的实际作 用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线 程也能尽快拿到锁。大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对 象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操 作也会导致不必要的性能损耗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Demo01 {
public static void main(String[] args) {
// synchronized (Demo01.class) {
// System.out.println("aaa");
// }

StringBuffer sb = new StringBuffer();

for (int i = 0; i < 100; i++) {
sb.append("aa");
}

System.out.println(sb.toString());
}
}

8.2 小结

什么是锁粗化?JVM会探测到一连串细小的操作都使用同一个对象加锁,将同步代码块的范围放大,放 到这串操作的外面,这样只需要加一次锁即可。

9. 平时写代码如何对synchronized优化

9.1 减少synchronized的范围

同步代码块中尽量短,减少同步代码块中代码的执行时间,减少锁的竞争。

1
2
3
synchronized (Demo01.class) {
System.out.println("aaa");
}

9.2 降低synchronized锁的粒度

将一个锁拆分为多个锁提高并发度

1
2
3
Hashtable hs = new Hashtable(); 
hs.put("aa", "bb");
hs.put("xx", "yy");

image-20200827204836289

image-20200827204913213

LinkedBlockingQueue入队和出队使用不同的锁,相对于读写只有一个锁效率要高

image-20200827204923594

9.3 读写分离

读取时不加锁,写入和删除时加锁
ConcurrentHashMap,CopyOnWriteArrayList和ConyOnWriteSet