JAVA锁相关术语及同步关键字synchronized详解

JAVA锁相关术语及同步关键字synchronized详解

JAVA中锁的概念

自旋锁:为了不放弃CPU执行事件,循环中使用CAS技术对数据尝试进行更新,直到成功。(理解上类似乐观锁)

悲观锁:假定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁。

乐观锁:假定没有冲突,在修改数据时如果发现数据和之前获取的不一致,则读最新数据,修改后重试修改。(有比较过程)

独享锁(写):给资源加上写锁,线程可以修改资源,其他线程不能再加锁。(单写)

共享锁(读):给资源加上锁后只能读不能改,其他线程也只能加读锁,不能加写锁。(多读)(缓存cache中有用到单写多读)

可重入锁、不可重入锁:线程拿到一把锁之后,可以自由进入同一把锁所同步的其他代码。

公平锁、非公平锁:争抢锁的顺序,如果是按先来后到,则为公平。

几种重要的锁的实现方式:synchronized(同步关键字)、ReentrantLock(可重入锁)、ReentrantReadWriteLock(可重入读写锁),这三种都是可重入锁,而自旋锁就属于不可重入锁

同步关键字synchronized

属于最基本的线程通信原则,基于对象监视器(monitor)实现的。

Java中的每个对象都与一个监视器相关联,一个线程可以锁定或解锁。

一次只有一个线程可以锁定监视器。

试图锁定该监视器的任何其他线程都会被阻塞,直到它们可以获得该监视器上的锁定为止。

特性:可重入、独享、悲观锁

锁的范围:类锁、对象锁(可理解为实例)、锁消除、锁粗化

提示:同步关键字,不仅是实现同步,根据JMM规定还能保证可见性(读取最新主内存数据,结束后写入主内存)

demo

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
package com.study.lock.sync;

// 锁 方法(静态/非静态),代码块(对象/类)
public class ObjectSyncDemo1 {

static Object temp = new Object();

public void test1() { // 方法上面:锁的对象 是 类的一个实例
synchronized (this) { // 类锁(class对象,静态方法),实例锁(this,普通方法)
try {
System.out.println(Thread.currentThread() + " 我开始执行");
Thread.sleep(3000L);
System.out.println(Thread.currentThread() + " 我执行结束");
} catch (InterruptedException e) {
}
}
}

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
new ObjectSyncDemo1().test1();
}).start();

Thread.sleep(1000L); // 等1秒钟,让前一个线程启动起来
new Thread(() -> {
new ObjectSyncDemo1().test1();
}).start();
}
}

输出结果

1
2
3
4
我开始执行
我执行结束
我开始执行
我执行结束

若synchronized关键字加在public void test1()上面

1
2
3
4
5
6
7
8
9
10
public synchronized void test1(
...
)

or

public void test1() {
synchronized (this) {
}
}

输出结果

1
2
3
4
我开始执行
我开始执行
我执行结束
我执行结束

则不能起到锁的作用,原因是方法是类的一个实例,类的实例每次new的时候都会新建一个,所以起不到锁的效果,这种称为对象锁(实例锁),而

1
2
3
4
5
6
7
8
public void test1() { 
synchronized (ObjectSyncDemo1.class) {
}
}
or
public static synchronized void test1(
...
)

这种关键字加载类上,类是独一份的,则可以起到锁的效果,称为类锁。

Monitorenter(加锁)/ Monitorexit(退出锁)

看下上面demo的字节码 javap -v ObjectSyncDemo.class,截取某段

锁粗化————范围扩大
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 锁粗化(运行时 jit 编译优化)
// jit 编译后的汇编内容, jitwatch可视化工具进行查看
public class ObjectSyncDemo3 {
int i;

public void test1(Object arg) {
synchronized (this) {
i++;
}
// 两个锁之间啥事没做
synchronized (this) {
i++;
}
}

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10000000; i++) {
new ObjectSyncDemo3().test1("a");
}
}
}

jit会把代码优化成(从jdk1.6开始)

1
2
3
4
5
6
 synchronized (this) {
i++;
// }
// synchronized (this) {
i++;
}
锁消除
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
package com.wyj.jvm.sync;

// 锁消除(jit)
public class ObjectSyncDemo4 {
public void test3(Object arg) {
StringBuilder builder = new StringBuilder();
builder.append("a");
builder.append(arg);
builder.append("c");
System.out.println(arg.toString());
}

public void test2(Object arg) {
String a = "a";
String c = "c";
System.out.println(a + arg + c);
}

public void test1(Object arg) {
// jit 优化, 消除了锁 (Buffer 实例 局部变量)
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("a");
stringBuffer.append(arg);
stringBuffer.append("c");
// System.out.println(stringBuffer.toString());
}

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000000; i++) {
new ObjectSyncDemo4().test1("123");
}
}
}

test1方法是不存在竞争,StringBuffer之所以是线程安全的,是因为他的方法上都加上了synchronized同步关键字,JIT优化会发现这个方法没有竞态条件,是线程安全的,所以对于这种锁就不会再锁,这就是锁消除。

同步关键字加锁原理

对象中Mark Word即为偏向锁->轻量锁->重量级锁过程图(下图)中标橙色的部分,正因为这个关键字有00(无锁),01(轻量级锁),10(重量级锁),11(等待被GC回收)这几种状态,才有了锁的变化。

JVM锁经历:偏向锁->轻量锁->重量级锁,一种三个过程,如下图:

![](JAVA锁相关术语及同步关键字synchronized详解/图片 2.png)

偏向锁

偏向锁到轻量锁的过程

epoch代表偏向锁的线程ID

一个锁默认是开启偏向锁,若发现某对象存在大量争抢,就会在创建的时候撤销偏向锁。

偏向标记第一次有用,出现过争用后就没用了。-XX:-UseBiasedLocking禁用使用偏置锁定,

偏向锁,本质就是无锁,如果没有发生过任何多线程争抢锁的情况,JVM认为就是单线程,无需做同步(jvm为了少干活:同步在JVM底层是有很多操作来实现的,如果是没有争用,就不需要去做同步操作)

为什么会有偏向锁的概念?

当对象刚创建的时候,JVM并不知道对象会被锁多次,据统计,很多代码虽然用到锁,但是并发情况较少,也就是开发者事前会考虑到多线程情况,实际情况下不代表一点会有,所以偏向锁(纳秒微妙级别)出现了,是为了减少锁对象的创建。若JVM已经发现有竞争,不会再偏向。退出。

只有锁升级,没有降级

同步关键字加锁原理——轻量级锁

使用CAS修改mark world完毕,加锁成功。则mark world中的tag进入00(轻量级锁)状态。

解锁的过程,则是一个逆向恢复mark world的过程

重量级锁——监视器(monitor)

修改mark worldrug1失败,会自旋CAS一定次数,该次数可以通过参数配置:

超过次数,仍未抢到锁,则锁升级为重量级锁,进入阻塞。

minitor也叫做管程,计算机操作系统原理中有提及类似概念。一个对象会有一个对应的monitor。

![](JAVA锁相关术语及同步关键字synchronized详解/图片 1.png)

总结

偏向锁(减少在无竞争情况,JVM资源消耗)——— > 出现两个及以上的线程争取升级为轻量级锁(CAS修改状态)
——— > 线程CAS自旋一定次数之后,升级为重量级锁(对象的mark word 内部会保存一个监视器锁的一个地址)

对象mark word里面 包含四种状态tag( 00 01 10 11 )
01 无锁

00 轻量锁

10 重量锁

11 GC废弃

LockSupport

park:挂起,阻塞线程

Unpark:释放,解除阻塞

LockSupport不能被实例化,LockSupport 的核心方法都是使用的 sun.misc.Unsafe 类中的 park 和 unpark 实现的。不同于wait/notify针对object,park/unpark是针对Thread的

扩展

实现一个Lock(流程如上图)

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
package com.wyj.jvm.lock;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;

// 优化--C++,运行时,只能推论
// sync对象监视器的原理 owner
public class DemoLock {
// 锁的拥有者
AtomicReference<Thread> owner = new AtomicReference<>();
// 需要锁池
LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();

public void lock() {
// CAS -- 此处直接CAS,是一种非公平的实现
while (!owner.compareAndSet(null, Thread.currentThread())) {
// 没拿到锁,等待
waiters.add(Thread.currentThread());
LockSupport.park(); // 挂起,等待被唤醒...
}
}

public void unlock() {
if (owner.compareAndSet(Thread.currentThread(), null)) {
// 释放锁之后,要唤醒一个线程
Thread next = waiters.poll();
LockSupport.unpark(next);
}
}
}

test

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
package com.wyj.jvm.lock;

import java.io.IOException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

// 两个线程,对 i 变量进行递增操作
public class LockDemo1 {
volatile int i = 0;

// 自己写 --

DemoLock lock = new DemoLock(); // 基于sync关键字的原理来手写

public void add() { // 方法栈帧~ 局部变量
// TODO xx00
lock.lock(); // 如果一个线程拿到锁,其他线程会等待
try {
i++; // 三次操作,字节码太难懂
} finally {
lock.unlock();
}
}

public static void main(String[] args) throws IOException {
LockDemo1 ld = new LockDemo1();

for (int i = 0; i < 2; i++) { // 2w相加,20000
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
ld.add();
}
}).start();
}
System.in.read(); // 输入任意键退出
System.out.println(ld.i);
}
}

运行结果

1
10678

可见没有达到预期的20000,原因就是thread-1执行完毕之后,进入unlock方法后,第27行从等待队列中获取一个新线程thread-2,若拿到的是空的,则unpark不会生效,thread-2就会被永久挂起(park状态),少了一个线程运行,所以会出现运行结果< = 20000的情况。

优化版

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
public class DemoLock implements Lock {
// 锁的拥有者
AtomicReference<Thread> owner = new AtomicReference<>(); // 独享锁 -- 资源只能被一个线程占有
// 需要锁池
LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue<>();

public void lock() { // 没拿到锁的线程运行这个方法
// TODO 拿到锁,等待
waiters.add(Thread.currentThread());
// CAS -- 此处直接CAS,是一种非公平的实现
while (!owner.compareAndSet(null, Thread.currentThread())) {
LockSupport.park(); // 挂起,等待被唤醒...
}
waiters.remove(Thread.currentThread());
}

public void unlock() { // 拿到锁的线程运行这个方法
if (owner.compareAndSet(Thread.currentThread(), null)) {
// 释放锁之后,要唤醒线程(所有 -- 惊群效应)
// for (Thread waiter : waiters) {
// LockSupport.unpark(waiter);
// }
// poll() : 获取并移除此队列的头元素,若队列为空,则返回 null
// peek() :获取但不移除此队列的头;若队列为空,则返回 null
Thread next = waiters.peek();
LockSupport.unpark(next);
}
}

运行结果

1
20000

Condition

用于替代wait/notify

Object中的wait(),notify(),notify()方法适合synchronized关键字配合使用的,可以唤醒一个或者全部(单个等待集);Condition是需要与Lock配合使用的,提供多个等待集合,更精确的控制(底层是park/unpark机制);

如何选择?

当Lock锁使用公平模式的时候,可以使用Condition的signal(),线程会按照FIFO的顺序冲await()中唤醒。当每个锁上有多个等待条件时,可以优先使用Condition,这样可以具体一个Condition控制一个条件等待。

上图场景的代码实现如下

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
package com.wyj.jvm.condition;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

// condition 实现队列线程安全。
public class QueueDemo {
final Lock lock = new ReentrantLock();
// 指定条件的等待 - 等待有空位
final Condition notFull = lock.newCondition();
// 指定条件的等待 - 等待不为空
final Condition notEmpty = lock.newCondition();

// 定义数组存储数据
final Object[] items = new Object[100];
int putptr, takeptr, count;

// 写入数据的线程,写入进来
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length) // 数据写满了
notFull.await(); // 写入数据的线程,进入阻塞
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal(); // 唤醒指定的读取线程
} finally {
lock.unlock();
}
}
// 读取数据的线程,调用take
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await(); // 线程阻塞在这里,等待被唤醒
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal(); // 通知写入数据的线程,告诉他们取走了数据,继续写入
return x;
} finally {
lock.unlock();
}
}
}

一张图理解Thread.sleep、Object.wait、LockSupport.park