一、简介
同步代码块一般使用 Java
的 synchronized
关键字来实现
两种加锁方式:
- 在方法签名处加
synchronized
关键字 - 使用
synchronized(对象 或 类)
进行同步
synchronized
锁特性由 JVM
负责实现
JVM
底层是通过 监视锁 来实现 synchronized
同步的
监视锁(monitor
): 是每个对象与生俱来的一个隐藏字段
使用步骤:
- 使用
synchronized
时,JVM
会根据synchronized
的当前环境, 找到对应对象的monitor
Tips: class 也是一个对象
- 再根据
monitor
的状态进行加、解锁的判断
**注意: ** synchronized
同步块执行完成或者遇到异常, 锁会自动释放
相反:
Lock
必须调用unlock()
方法释放锁
二、synchronized
提供三种锁
(1) 偏向锁
偏向锁: JVM
利用 CAS
在对象头上设置线程 ID, 表示这个对象偏向与当前线程
目的: 为了在资源没有被多线程竞争的情况下尽量减少锁带来的性能开销
使用过程:
在锁对象的对象头中有一个
ThreadId
字段
- 当第一个线程访问锁时, 如果该锁没有被其他线程访问过, 即
ThreadId
字段为空, 那么JVM
让其持有偏向锁, 并将ThreadId
字段的值设置为该线程的 ID - 当下一次获取锁时, 会判断当前线程的 ID 是否与锁对象的
ThreadId
一致
如果一致, 那么该线程不会重复获取锁, 从而提高了程序的运行效率
锁升级:
- 如果出现锁的竞争情况, 那么 偏向锁 会被撤销并升级为 轻量级锁
- 如果资源的竞争非常激烈, 会升级为 重量级锁
(2) 轻量级锁
轻量级锁 能提升程序同步性能的依据是 “对于绝大部分的锁, 在整个同步周期内都是不存在竞争的”
这是一个经验数据.
如果没有竞争, 轻量级锁使用CAS
操作避免了使用互斥量的开销
如果有两条以上的线程争用同一个锁, 那轻量级锁就不再有效, 要膨胀为重量级锁, 锁标志的状态值变为 “10”, Mark Word
中存储的就是指向重量级锁(互斥量) 的指针, 后面等待锁的线程也要进入阻塞状态
(3) 重量级锁
三、使用
public class SynchronizedTest {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
final Object lock = new Object();
Thread[] ths = new Thread[10000];
for(int i=0;i<ths.length;i++) {
Thread thread = new Thread(new Runnable() {
public void run() {
for(int i=0;i<10000;i++) {
synchronized (lock) {
count ++;
}
}
}
});
ths[i] = thread;
}
long begin = System.currentTimeMillis();
for(int i = 0; i < ths.length; i++) {
ths[i].start();
ths[i].join();
}
System.out.println(count);// 100000000
System.out.println(System.currentTimeMillis() - begin); //4073
}
}
四、源码分析
synchronized
同步方法 与 同步代码块的字节码不同:
public class SynchronizedTest {
public synchronized void test1(){
}
public void test2(){
synchronized (this){
}
}
}
// test1();
public synchronized void test1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/donaldy/lock/cas/SynchronizedTest;
synchronized
注在方法上,在字节码flags: ACC_SYNCHRONIZED
会标识
// test2();
public void test2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_1
5: monitorexit
6: goto 14
9: astore_2
10: aload_1
11: monitorexit
12: aload_2
13: athrow
14: return
Exception table:
from to target type
4 6 9 any
9 12 9 any
如图:
有一个monitorenter
,二个monitorexit
为什么会有两个monitorexit
?
因为synchronized
中发生异常,会自动释放锁。
所以最后一个monitorexit
是在异常情况下时候使用的。
总结下,原理图如下:
五、实际运用
- 同步方法
六、参考资料
- <<码出高效>>