synchronized的相关性质

[TOC]

对象布局

image-20201108194840943

对象填充,是将一个对象大小不足 8 个字节的倍数时,使用 0 填充补齐,为了更高效效率的读取数据,64 java 虚拟机,一次读取是 64 bit(8 字节)。 // monitor也是class, 其实例会存储在堆中,MarkWord中保存的是它的指针

Mark Word:默认存储对象的 HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以 Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间 Mark Word 里存储的数据会随着锁标志位的变化而变化。

image-20201108195403335

Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

Array Length

数组长度只在数组类型的对象中存在。用于记录数组的长度。避免获取数组长度时,动态计算。以空间换时间

同步原理

反编译后,synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头).当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。

对象,对象监视器,同步队列和线程状态的关系

`synchronized关键字原理

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

用法

1.同步一个代码块

它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。

1
2
3
4
5
public void func() {
synchronized (this) {
// ...
}
}

2.同步一个方法

作用于同一个对象

1
2
3
public synchronized void func () {
// ...
}

3.同步一个类

作用于整个类

1
2
3
4
5
public void func() {
synchronized (SynchronizedExample.class) {
// ...
}
}

4.同步一个静态方法

整个类

1
2
3
public synchronized static void fun() {
// ...
}

和volatile的对比

锁优化

这里的锁优化主要是指 JVM 对 synchronized 的优化。

锁消除

锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。
锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成
私有数据对待,也就可以将它们的锁进行消除。

1
2
3
public static String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}

String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连
续 append() 操作:

1
2
3
4
5
6
7
public static String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
1
2
3
4
5
6
7
public static String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
1
2
3
4
5
6
@Override
public synchronized StringBuffer append(int i) {
toStringCache = null;
super.append(i);
return this;
}

锁粗化

如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部,这样只需要加锁一次就可以了。

重量级锁

当轻量级锁膨胀到重量级锁之后,意味着线程只能被挂起阻塞来等待唤醒了。每一个对象中都有一个Monitor监视器,而Monitor依赖操作系统的 MutexLock(互斥锁)来实现的, 线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。而且当一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。我们可以简单的理解为,在加重量级锁的时候会执行monitorenter指令,解锁时会执行monitorexit指令。

锁升级

若当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁(锁膨胀)。

另外,当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁(锁膨胀)。

自适应自旋

在 JDK1.7 开始,引入了自适应自旋锁,修改自旋锁次数的JVM参数被取消,虚拟机不再支持由用户配置自旋锁次数,而是由虚拟机自动调整。自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

自旋锁

轻量级锁在加锁过程中,用到了自旋锁。

互斥同步进入阻塞状态的开销都很大,应该尽量避免。自旋锁的思想是让一个线程在请求一个共享数据的锁时自旋一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。
自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行循环操作占用 CPU 时间,它只适用于共享数据的
锁定状态很短的场景。
在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数
及锁的拥有者的状态来决定。

轻量级锁

自旋锁的目标是降低线程切换的成本。

加锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

解锁

轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。下图是两个线程同时争夺锁,导致锁膨胀的流程图。

偏向锁

偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。

当锁对象第一次被线程获得的时候,进入偏向状态,同时使用 CAS 操作将线程 ID 记录到 Mark Word中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。
当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定
状态或者轻量级锁状态。

synchronized和volatile的比较

1.volatile本质是告诉JVM当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

2.volatile仅能用在变量级别,而synchronized可以使用在变量、方法、类级别。

3.volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。

4.volatile不会造成线程阻塞,synchronized可能会造成线程阻塞。

5.volatile标记的变量不会被编译器优化,synchronized标记的变量可以被编译器优化。

发生异常时自动释放锁

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
public class syntest {
int count=0;
synchronized void m()
{
System.out.println(Thread.currentThread().getName()+" start");
while (true)
{
count++;
System.out.println(Thread.currentThread().getName()+" count: "+count);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(count==5)
{
int i=1/0;
System.out.println(count);
}
}
}

public static void main(String[] args) {
syntest s=new syntest();
Runnable r=new Runnable() {
@Override
public void run() {
s.m();
}
};
new Thread(r,"t1").start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r,"t2").start();
}
}

如题, 发生异常的时候,synchronized锁释放,线程t2得以执行

但是要注意的是,对于显式锁, 如ReentrantLock,在发生异常的时候,必须要手动释放锁。

如果执行的代码段有可能发生异常,我们通常要这样处理, 需要在finally里面释放资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {


//可能发生异常的代码
...
} catch (Exception ex) {


} finally {
//释放锁
lock.unlock();
//释放IO资源
io.close();
}
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
public class reetest {
private static ReentrantLock lock = new ReentrantLock();
int count = 0;

void m() {
System.out.println(Thread.currentThread().getName() + " start");
lock.lock();
try {
while (true) {
count++;
System.out.println(Thread.currentThread().getName() + " count: " + count);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (count == 5) {
int i = 1 / 0;
System.out.println(count);
}

}
} finally {
lock.unlock();
}

}

public static void main(String[] args) {
reetest s=new reetest();
Runnable r=new Runnable() {
@Override
public void run() {
s.m();
}
};
new Thread(r,"t1").start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(r,"t2").start();
}
}

和Lock的区别

  1. 来源:
    lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;

  2. 异常是否释放锁:
    synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)

  3. 是否响应中断
    lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断;

  4. Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)

  5. 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

  6. synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度,

为什么要设计成可重入?

1. 介绍

可重入锁是指能够重复进入的锁。例如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁。场景:数据库事务操作,add操作将会获取锁,若一个事务当中多次add,就应该允许该线程多次进入该临界区。

synchronized关键字和ReentryLock都是可重入锁。

2. 可重入锁的作用:避免死锁

在以下程序中,子类改写了父类的synchronized 方法,然后调用父类中的方法,此时如果内置锁不是可重入的,那么这段代码将产生死锁。由于 Widget 和LoggingWidget中doSomething方法都是 synchronized 方法,因此每个doSomething方法在执行前都会获取 Widget 上的锁。然而如果内置锁不是可重入的,那么调用super.doSomething( )时无法获得 Widget 上的锁,因为这个锁已经被持有,从而线程将永远停顿下去,等待一个永远也无法获得的锁。重入则避免了这种死锁情况的发生。

image-20210412215915068

轻量级锁什么时候升级为重量级锁?

我们必须给线程空循环设置一个次数,当线程超过了这个次数,我们就认为,继续使用自旋锁就不适合了,此时锁会再次膨胀,升级为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

偏向锁升级为轻量级

轻量级锁由偏向锁升级而来,偏向锁运行在一个线程同步块时,第二个线程加入锁竞争的时候,偏向锁就会升级为轻量级锁。

sync 锁静态方法和非静态方法锁的分别是什么对象?

Synchronized修饰非静态方法

Synchronized修饰非静态方法,实际上是对调用该方法的对象加锁,俗称“对象锁”。

Synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”。

解释:因为对静态对象加锁实际上对类(.class)加锁,类对象只有一个,可以理解为任何时候都只有一个空间,里面有N个房间,一把锁,因此房间(同步方法)之间一定是互斥的。