另外还有两个术语:临界区和竞态条件
1.临界区,一段代码块如果存在对共享资源的读写操作就称为临界区
2.竞态条件,多线程在临界区执行,由于代码执行序列不同导致结果无法预期,称之为发生了竞态条件
语法:
synchronized(对象){
//临界区
}
示例代码:
public class Synchronized1 {
public static void main(String[] args) throws InterruptedException {
Member m = new Member();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
m.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
m.decrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(m.getCounter());
}
}
class Member {
static int counter;
synchronized void increment() {
counter++;
}
//上下等价
void increment1() {
synchronized (this) {
counter++;
}
}
synchronized void decrement() {
counter--;
}
synchronized static void decrement1() {
counter--;
}
//上下等价
static void decrement2() {
synchronized (Member.class) {
counter--;
}
}
synchronized int getCounter() {
return counter;
}
}
注意:非静态方法的synchronized锁this对象,静态方法的synchronized锁类对象。线程获取到锁之后,在无外界干扰(wait方法等)的情况下,将临界区代码执行完才会释放锁,即使没有获得CPU时间片。
门票超卖问题
public class SellTicket {
static Random random = new Random();
public static void main(String[] args) throws InterruptedException {
TicketWindow ticketWindow = new TicketWindow(1000);
Thread[] threads = new Thread[2000];
List<Integer> list = new Vector<>();
for (int i = 0; i < 2000; i++) {
Thread thread = new Thread(() -> {
list.add(ticketWindow.sell(random.nextInt(5) + 1));
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
threads[i] = thread;
}
for (Thread thread : threads) {
thread.join();
}
System.out.println(list.stream().mapToInt(Integer::intValue).sum());
System.out.println(ticketWindow.getCount());
}
}
class TicketWindow {
private int count;
public TicketWindow(int count) {
this.count = count;
}
//获取剩余票数量
public int getCount() {
return count;
}
public synchronized int sell(int amount) {
if (this.count >= amount) {
this.count -= amount;
return amount;
}
return 0;
}
}
多共享变量问题
public class Transfer {
public static void main(String[] args) throws InterruptedException {
Acount a = new Acount(1000);
Acount b = new Acount(1000);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
a.transfer(b, 3);
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
b.transfer(a, 6);
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(a.getMoney());
System.out.println(b.getMoney());
}
}
class Acount {
private int money;
public Acount(int money) {
this.money = money;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
//方法修饰符添加synchronized无效,只能锁当前对象
public void transfer(Acount target, int amount) {
synchronized (Acount.class) {
if (this.getMoney() >= amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
}
synchronized锁住的是对象,那么多个线程抢锁,如何知道线程获得了锁呢?线程不会记录自己获得的锁避免遍历线程做判断,锁的状态是记录在对象上面的。线程如果能成功修改对象上锁的状态,就表明该对象获得了锁,可以执行临界区代码。执行完之后再将锁的状态还原,其他线程就可以再次竞争锁。
对象在内存中的布局分为对象头、实例数据和对齐填充。对象上锁的状态就是记录在对象存储空间的对象头中
对象头又分为两部分,第一部分存储对象自身运行时数据,如锁标记、哈希码、GC分代年龄等,称为Mark Word。另一部分用于存储指向对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分存储数组长度,这部分称为Class Word。以32为操作系统为例
普通对象:
数组对象:
其中Mark Word结构为:
如图,锁状态包括Noramal无锁、Biased偏向锁、Lightweight Locked轻量级锁、Heavyweight Locked 重量级锁、Markd for GC表示被GC标记即将被清除,也是一种无锁状态。
Monitor是OS定义的,翻译为监视器或者管程。每个java对象都可以关联一个Monitor对象,当对象被synchronized(obj)加锁(重量级)之后,对象头中的Mark Word会记录指向Monitor对象的指针,即上图中的ptr_to_heavyweight_monitor。synchronized在jdk1.6之后经过优化并不会一开始就使用重量级锁,只有发生竞争升级到重量级锁才会使用Monitor。Monitor结构如下:
注意:synchronized必须加在同一个对象上,不加synchronized的对象不会关联监视器,不遵从上述效果。
public class Synchronized {
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
}
上面这段代码,编译成字节码,再反汇编就得到字节码,Windows系统cmd进入命令行
javac Synchronized.java
javap -c Synchronized.class
得到的字节码指令,加上了中文注释
public class synchronize.Synchronized {
static final java.lang.Object lock;
static int counter;
public synchronize.Synchronized();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // lock引用,synchronized开始
3: dup
4: astore_1 // 临时存储lock引用 ->slot1
5: monitorenter // lock对象头置为Monitor指针
6: getstatic #3 // 获得counter
9: iconst_1 // 准备常量 1
10: iadd // +1
11: putstatic #3 // 写回counter
14: aload_1 // 加载lock引用 <-slot1
15: monitorexit // 重置lock MarkWord,唤醒EntryList
16: goto 24 // 正常结束返回
19: astore_2 // 发生异常,e->slot2
20: aload_1 // 加载lock引用 <-slot1
21: monitorexit // 重置lock MarkWord,唤醒EntryList
22: aload_2 // 加载e引用 <-slot2
23: athrow // 抛出异常
24: return
Exception table:
from to target type
6 16 19 any // 6-16行发生异常进入19行
19 22 19 any // 19-22行发生异常再进入19行,确保释放锁
static {};
Code:
0: new #4 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."":()V
7: putstatic #2 // Field lock:Ljava/lang/Object;
10: iconst_0
11: putstatic #3 // Field counter:I
14: return
}
执行synchronized(obj) 不会马上使用Monitor实现的重量级锁,在没有发生锁竞争时都是使用轻量级锁。那么什么是轻量级锁呢,轻量级锁是相对于原来使用操作系统互斥量来实现的传统锁而言的,所以才有轻重的区别。下面看下轻量级锁的实现过程
1.在线程栈帧中创建一个锁记录结构,JVM中称之为Lock Record,内部可以存储锁对象头的Mark Word。Lock Record中有两块区域,一块指向锁对象地址,称之为Object reference。另一块存储Lock Record的地址以及锁标记(00就代表轻量级锁)
2.尝试用CAS操作替换锁对象中的Mark Word,将MarkWord存放到线程栈帧。
3.如果CAS替换成功,代表加锁成功,锁对象的对象头将指向lock record,标记为变为00。如下图所示
4.如果CAS替换失败,有两种情况
5.退出synchronized代码块时释放锁,遍历所有的Lock Record如果锁记录为null,说明是重入锁,删除Lock Record,重入计数减一。直到最后一个锁记录不为null的Lock Record,使用CAS将对象头Mark Word和锁记录交换回来。
锁膨胀或者叫锁升级,指的是在CAS操作尝试加轻量级锁时失败,说明已经有其他线程加上了轻量级锁(发生了竞争),将轻量级锁变为重量级。下面看下膨胀过程
1.thread1加锁时发现,thread0已经对Objet加了轻量级锁
2.thread1加锁失败,进入锁膨胀
3.thread0解锁时,使用CAS替换对象头Mark Word失败,进入重量级锁解锁流程。将Monitor的Owner置为空,唤醒Entrylist的Blocked线程进行锁竞争。
自旋优化的时机是线程获取轻量级锁失败,在获取重量级锁过程中不会立即阻塞(进入Entrylist),先自旋尝试获取锁。到达临界值后,再阻塞该线程,直到被唤醒。
自旋成功情况
自旋成功的好处是,自旋过程中线程不放弃CPU时间片,不会发生线程切换,达到提升效率的目的。但是在单核情况下,不放弃CPU其他线程无法执行,也不会释放锁,自旋永远失败。
自旋失败情况
自旋次数过多就会浪费CPU资源,java6之后自旋次数是自适应的,JVM会分析自旋成功的可能性来决定自旋的次数。
偏向锁是指在轻量级锁发生重入的时候(未发生竞争,竞争就变重量级了),对轻量级锁的一种优化。具体过程是:只有第一次获取锁时使用CAS将线程ID设置到对象头的Mark Word,多次重入时只要线程ID未改变就表示没有竞争,不用进行CAS。之后只要没有竞争,这个锁对象就归该线程所有。
示例代码:
public class Biased {
static Object obj = new Object();
public static void main(String[] args) {
Biased biased = new Biased();
biased.m1();
}
private void m1() {
synchronized (obj) {
m2();
}
}
private void m2() {
synchronized (obj) {
m3();
}
}
private void m3() {
synchronized (obj) {
}
}
}
加锁过程:
看下64为系统对象头的偏向状态,线程ID为54
偏向锁在一些情况下会撤销
撤销代表锁膨胀了,如果膨胀成轻量级锁,那么还有机会再次优化为偏向锁。