应用方式

首先要知道synchronized实现同步的基础为Java中每一个对象都可以作为锁,synchronized一般的使用方法有以下三个。

  1. 在普通方法上加synchronized,为方法的对象加锁。
  2. 在静态方法上加synchronized,为该方法的类模板加锁。
  3. 同步方法块,为给定对象加锁。

每一个线程执行到同步块或者是同步方法(对共享内存进行访问的程序片段称为临界区),就会去申请获得锁。当同步的操作执行完或者是抛出异常时就会把该锁给释放掉。

简单应用

之前的volatile文章有提过,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
package com.yw;

import java.util.concurrent.CountDownLatch;

public class Test{
public static void main(String[] args) throws InterruptedException {
Data data = new Data();
// 用来辅助确保二十个线程全部执行完毕
CountDownLatch countDownLatch = new CountDownLatch(20);
for (int i = 0; i < 20; i++) {
new Thread(() -> {
// 设置次数要多,太少结果不明显
for (int j = 0; j < 1000; j++) {
data.addData();
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println("当前资源类的值为----->" + data.datanum);
}
}

class Data {
int datanum = 0;
// 为该方法加锁,锁的是对象
public synchronized void addData() {
datanum++;
}
// 与上面等价,锁的是this对象
public void addData1() {
synchronized(this) {
datanum++;
}
}
}

由此可以看见测试了很多次,答案都是20000,从而用synchronized可以保证了这个例子的原子性。下面我用图来简单讲解一下为什么synchronized能够保持这个例子的原子性。

Monitor

介绍synchronized的原理之前先了解一个重要的概念,Monitor。

Monitor又称监视器,是一种同步机制,synchronized就是通过这个机制来实现方法或者代码块的同步的。

实际上任何一个Java对象都可以作为Monitor机制的monitor object,每一个Object的类都在底层实现了ObjectMonitor模式(这是底层用C来实现的),我们一般看不到,只要知道每一个Object类的对象都可以作为monitor对象

同步代码块会在代码块开始位置插入一条monitorenter在代码块结尾插入一条monitorexit,用这两条指令来实现Monitor机制。

同步方法是通过一个ACC_SYNCHRONIZED标志来确定,执行方法时判断有无该标志位,然后会隐式调用上面两条指令。

起初在没有线程执行到临界区时,monitor的大概情况是这样子的。

当有一个线程进入临界区,Monitor中的Owner就会指向该线程。

此时要是有其他线程也执行到了临界区的范围,就会进入Enter Set,即将线程的状态转变为阻塞状态,故Enter Set也可以称为阻塞队列。

当Owner的线程1离开了临界区就会出现三种情况。

  1. 线程1因某些原因发生了wait等待,Owner恢复为null,并进入Wait set,然后阻塞队列中的线程开始竞争Owner的位置。(这里假设线程2竞争到了)

  2. 线程1正常离开临界区,Owner恢复为null,线程1之后不再进入临界区,阻塞队列中的线程就会开始竞争Owner的位置。(这里假设线程2竞争到了)

  3. 线程1正常离开临界区,Owner恢复为null,线程1之后还会进入临界区,那线程1就会与阻塞队列中的线程竞争Owner的位置。(这里假设线程2竞争到了)

Monitor是synchronized实现线程同步的原理(当初只有重量级锁,锁的优化靠其他的机制)。
从图中也可以看出为什么wait这些方法要放在synchronized中使用,因为都需要借助到Monitor的机制来进行实现。

Java对象头

Java对象头是存储对象的一些基础信息的集合。

长度 内容 说明
32bit Mark Word 存储对象的hashCode或锁信息等
32bit Class Metadata Address 存储到对象类型数据的指针
32bit Arraylength 数组的长度(要是对象不是数组则没有该项)

这里就不详细探讨后面两个了,这里的重点是Mark Word,现在我们来看看这里面具体存储了什么。

锁状态 25bit 4bit 1bit 2bit
23bit 2bit 是否是偏向锁 锁的标志位
无锁 对象的hashCode 分代年龄 0 01
偏向锁 线程ID Epoch 对象分代年龄 1 01
轻量级锁 指向栈中锁记录的指针 00
重量级锁 指向互斥量(重量级锁)的指针 10
GC标志 11

在JDK1.6之前,synchronized只是一个重量级的锁,当JDK1.6进行更新之后,引入了偏向锁和轻量级锁的概念,所以锁的状态变成了以上的四种。

偏向锁

偏向锁是JDK1.6之后引进的,HotSpot作者发现大多数情况锁不仅存在多线程竞争,而且总是由同一线程多次获得。如果是之前,该线程每次都要获得锁,这样代价也未免太大了一点,所以为了优化这种情况,就出现了偏向锁。

偏向锁,顾名思义,偏爱某个线程的锁。当一个线程访问同步块并获取锁,会在对象头和栈帧中的锁记录里存储偏向的线程ID。以后该线程再次来获取锁,就只需测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁就好了,不必再次进行CAS来操作加锁和解锁。

  1. 偏向锁在Java6中是默认开启的,但是要在应用程序启动几秒之后才会激活,也就是说是有延迟的,有必要也可以通过设置参数来设置延迟
    -XX:BiasedLockingStartupDelay=0
  2. 当不希望使用偏向锁,也可以通过设置参数来关闭,默认进入轻量级锁
    -XX:-UseBiasedLocking=false

参考资料