synchronized
synchronized作用范围
- 对于普通同步方法,锁是当前实例对象。
- 对于静态同步方法,锁是当前类的Class对象。
- 对于同步方法块,锁是synchronized括号里面配置的对象。
对于上面的作用范围,该如何理解呢?可以看一下下面的例子。
对于普通同步方法,锁是当前实例对象
对于普通同步方法,锁是当前实例对象。说明同一时刻内的同一个实例只能有一个线程来拥有。
public class SynchronizedExample1 implements Runnable{
private static int i = 0;
public synchronized void increase() {
System.out.println(Thread.currentThread().getName() + " - current i ==> " + i);
i++;
}
@Override
public void run() {
for(int i=0; i<10; i++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedExample1 runnable = new SynchronizedExample1();
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
// 目的:为了T1和T2全部执行完成之后再打印i的值
thread1.join();
thread2.join();
System.out.println("final i = " +i);
}
}
每个线程输出结果可能不一样,但是最终结果一定会是20。
Thread-0 - current i ==> 0
Thread-1 - current i ==> 1
Thread-1 - current i ==> 2
Thread-1 - current i ==> 3
Thread-1 - current i ==> 4
Thread-1 - current i ==> 5
Thread-1 - current i ==> 6
Thread-1 - current i ==> 7
Thread-1 - current i ==> 8
Thread-1 - current i ==> 9
Thread-1 - current i ==> 10
Thread-0 - current i ==> 11
Thread-0 - current i ==> 12
Thread-0 - current i ==> 13
Thread-0 - current i ==> 14
Thread-0 - current i ==> 15
Thread-0 - current i ==> 16
Thread-0 - current i ==> 17
Thread-0 - current i ==> 18
Thread-0 - current i ==> 19
final i = 20
可以看出每个线程获得的 i 都是上一个线程执行完的结果。
你一定很奇怪,难道结果不是这样吗?或者说结果就应该是这样。为了凸显出 synchronized 的作用,我把 increase 的synchronized 关键词去掉,同时为了效果明显将循环次数提高到1000000并且增加竞争线程数量。
public class SynchronizedExample1 implements Runnable{
private static int i = 0;
public void increase() {
System.out.println(Thread.currentThread().getName() + " - current i ==> " + i);
i++;
}
@Override
public void run() {
for(int i=0; i<200000; i++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
SynchronizedExample1 runnable = new SynchronizedExample1();
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
Thread thread3 = new Thread(runnable);
Thread thread4 = new Thread(runnable);
Thread thread5 = new Thread(runnable);
Thread thread6 = new Thread(runnable);
Thread thread7 = new Thread(runnable);
Thread thread8 = new Thread(runnable);
Thread thread9 = new Thread(runnable);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
thread6.start();
thread7.start();
thread8.start();
thread9.start();
thread1.join();
thread2.join();
thread3.join();
thread4.join();
thread5.join();
thread6.join();
thread7.join();
thread8.join();
thread9.join();
System.out.println("final i = " +i);
}
}
此时最终结果就不一定是1800000。
这是为什么呢?
原因是 i++ 这个操作不是原子操作。
i++ 实际具体的操作可以分为三个步骤:
- 先将i从主内存中取出,放到线程的工作内存中。每个线程的工作内存都是不共享的。
- 在进行i+1操作,更新线程工作内存中的i值。
- 最后根据工作内存中的值,改变主内存中的i值。
i++ 并不是我们想想中的那样,一个线程改完,另一个线程能够直接获取最新的值。在没有锁的并发环境下,很可能出现的情况就是两个线程都获取了主内存中的 i 值,接着随后分别进行 increase 方法,最后可能的结果就是执行一次 increase 的结果。
对于静态同步方法,锁是当前类的Class对象。
普通同步方法,是针对于类的某一个实例对象。而静态同步方法可以理解为针对类的 所有 实例对象。
我们稍微改一下Example 1 中的代码。
public class SynchronizedExample2 implements Runnable{
private static int i = 0;
public synchronized void increase() {
System.out.println(Thread.currentThread().getName() + " - current i ==> " + i);
i++;
}
@Override
public void run() {
for(int i=0; i<1000000; i++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
// 这里的T1和T2用的是2个不同实例的线程。
Thread thread1 = new Thread(new SynchronizedExample2());
Thread thread2 = new Thread(new SynchronizedExample2());
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("final i = " +i);
}
}
多运行几次,你会发现结果都是不一样的。为了让方法在多实例下的并发结果一致,我们可以将其方法设置为 静态 。
public class SynchronizedExample2 implements Runnable{
private static int i = 0;
// 将 increase 方法改为静态方法
public synchronized static void increase() {
System.out.println(Thread.currentThread().getName() + " - current i ==> " + i);
i++;
}
@Override
public void run() {
for(int i=0; i<1000000; i++) {
increase();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new SynchronizedExample2());
Thread thread2 = new Thread(new SynchronizedExample2());
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("final i = " +i);
}
}
结果就是2000000。
对于同步方法块,锁是synchronized括号里面配置的对象
什么叫括号里面配置的对象呢?先看一下代码的形式。
// object 可以是 实例 也可以是class类
synchronized([object]) {
// code block
}
简单点可以理解为同步代码块可以实现锁某一个实例,可以锁一个类的所有实例。
让我们看看同步代码块的实现效果。
实现普通方法同步效果
public class SynchronizedExample3 implements Runnable{
private static int i = 0;
public void increase() {
System.out.println(Thread.currentThread().getName() + " - current i ==> " + i);
i++;
}
@Override
public void run() {
// 锁了当前实例
synchronized (this) {
for(int i=0; i<1000000; i++) {
increase();
}
}
}
public static void main(String[] args) throws InterruptedException {
// 用一个实例
SynchronizedExample3 runnable = new SynchronizedExample3();
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("final i = " +i);
}
}
结果为2000000。
实现静态同步方法效果
public class SynchronizedExample3 implements Runnable{
private static int i = 0;
public void increase() {
System.out.println(Thread.currentThread().getName() + " - current i ==> " + i);
i++;
}
@Override
public void run() {
// 锁了SynchronizedExample3所有的实例
synchronized (SynchronizedExample3.class) {
for(int i=0; i<1000000; i++) {
increase();
}
}
}
public static void main(String[] args) throws InterruptedException {
// 用不同的实例来启动线程
Thread thread1 = new Thread(new SynchronizedExample3());
Thread thread2 = new Thread(new SynchronizedExample3());
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("final i = " +i);
}
}
结果为2000000。
锁的优化与升级
在讲锁之前,先来了解一下 java 对象的结构。
- Java 对象在内存中储存的布局
- 对象头
- Mark Word
- HashCode
- GC分代年龄
- 锁状态标志
- 线程持有的锁
- 偏向线程ID
- 偏向线程时间戳
- 类型指针
- Mark Word
- 实例数据
- 对齐填充
- 对象头
由此可见 Mark Word 是对象头的一部分。
JDK6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。在 JDK6 中锁的状态一共有 4 种:“无锁状态”、“偏向锁”、“轻量级锁”和“重量级锁”。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。
4种锁状态下的标志位
Mark Word 里面存储的数据会随着锁标志位的变化而变化。
锁名称 | 锁标志位 | 偏向锁标志 |
---|---|---|
无锁 | 01 | 0 |
偏向锁 | 01 | 1 |
轻量级锁 | 00 | 0 |
重量级锁 | 10 | 0 |
偏向锁
特点:
-
减少 同一线程 重复获取对象锁所带来的性能消耗。
-
偏向锁被占用的时候,其他线程可以通过 CAS 来获取偏向锁。
-
偏向锁使用了一种等到竞争出现才释放锁的机制,即只有当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
偏向锁的获取
-
访问对象头中 Mark Word 里面的偏向锁标志是否为 1 ,锁标志位是否为 01 。 偏向锁的状态 — 偏向标志为 1 ,锁标志位为 01 。
-
如果为偏向状态,则 测试 线程 ID 是否指向当前线程。如果是,进入步骤5,否则,进入步骤3。
-
如果线程 ID 未指向当前线程,则通过 CAS 来竞争锁。如果竞争成功,则将Mark Word 中的偏向线程 ID 指向当前线程,进入步骤 5;否则,说明竞争失败,进入步骤 4。
-
如果 CAS 竞争失败,说明存在竞争。此时会执行 偏向锁撤销 动作。偏向锁会升级为 轻量级锁 ,然后被阻塞在安全点的线程继续往下执行同步代码。
-
执行同步代码。
偏向锁的释放
-
偏向锁是不会 主动 的释放,只有在遇到其他线程尝试竞争偏向锁的时候,持有偏向锁的线程才会释放偏向锁。
-
在安全点的时候暂停拥有偏向锁的线程,判断锁对象是否处于被锁定的状态,撤销偏向锁后,恢复到未锁定或轻量级锁的状态。
禁用偏向锁
在JDK6和JDK7中,偏向锁默认是开启的。可以使用JVM参数来关闭偏向锁:-XX:-UseBaisedLocking=false
,程序就会默认进入轻量级锁。
轻量级锁
获取轻量级锁
-
线程访问同步块。
-
在当前线程的工作内存中开辟一块内存空间用以储存 锁记录(Lock Record)。
-
将对象头中的 Mark Word (这部分的信息又称为Displaced Mark Word) 信息复制到锁记录中。
-
当前线程尝试CAS将对象头中的 Mark Word 内容替换成指向 锁记录(Lock Record) 的指针,并将同步对象的 Owner Thread 指向当前线程。若成功替换,执行步骤 6;否则执行步骤 5。
-
进行自旋获取锁。如果在一定时间内或者一定次数之后(这里只是我的猜测,但是一定会有一个条件来判断是否自旋获取锁失败),若仍然失败,锁将会膨胀为 重量级锁。
-
线程获取对象锁,执行同步代码块。
解锁轻量级锁
使用CAS操作将Displaced Mark Word替换掉对象头中的 Mark Word。如果成功,表示没有出现线程竞争;如果失败,表示当前存在线程竞争,锁就会膨胀成重量级锁。
偏向锁和轻量级锁之间的区别
- 轻量级锁每次退出 同步块 的时候,会释放锁。而偏向锁只有出现线程竞争情况下,才会解锁。
- 每次获取轻量级锁都会改变 对象头 中的 Mark Word。
- 争抢轻量级锁失败的线程会进入自旋尝试获取锁。
锁的优缺点对比
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要 额外 的消耗,和执行 非同步 方法相比仅存在纳秒级差别 | 如果出现线程竞争会带来额外的 锁撤销 消耗 | 使用只有 一个线程 访问同步块场景 |
轻量级锁 | 竞争的线程不会被阻塞,提高了程序的响应速度 | 如果线程始终得不到锁,使用自旋会消耗CPU | 适用于追求 响应时间,同步块执行速度非常块的场景 |
重量级锁 | 线程竞争不会自旋,不会额外消耗CPU | 线程阻塞,响应速度慢 | 适用于追求 吞吐量,同步块执行时间较长的场景 |
参考
- 《Java并发编程艺术 - 第二章》 & 作者原博客地址
- Synchronized下的三种锁:偏向锁 轻量锁 重量锁 理解
- 锁原理:偏向锁、轻量锁、重量锁