文章目录
- 1.synchronized关键字
- 1.1.synchronized的作用
- 1.2.synchronized的使用方式
- 1.2.1.synchronized代码示例
- 1.2.2.锁粒度的区别
- 1.3.小结
- 2.synchronized实现原理
- 2.1.同步块和同步方法在指令码上的差别
- 2.2.如何保证多个线程互斥
- 2.2.1.monitor概念和特点
- 2.2.2.Java对象如何关联monitor对象
- 2.2.3.monitor的关键属性
- 2.2.4.线程抢锁流程
- 2.3.小结
1.synchronized关键字
下面讲的synchronized都默认为重量级锁,偏向锁和轻量级锁及锁的升级过程在下一篇笔记中《(五)锁优化及锁升级过程》。
1.1.synchronized的作用
synchronized是Java中的一个关键字,用来保证互斥同步的最基本的手段。
互斥同步指的是在并发条件下,同一时间synchronized代码段只能有一个线程执行。
1.2.synchronized的使用方式
主要有三种方式:修饰实例方法,修饰静态方法,修饰代码块
- 修饰实例方法:在实例方法中加入synchronized修饰,锁对象是当前调用实例方法的对象this。
- 修饰静态方法:在静态方法中加入synchronized修饰,锁对象是当前静态方法所在类的Class对象。
- 修饰代码块:可以指定任意一个对象作为代码块的锁对象,并且可以灵活的指定锁的粒度。
1.2.1.synchronized代码示例
下面用一个Demo来描述4种用法:
public class SynchronizedUsageDemo {
private volatile static int value = 0;
public synchronized void incre1() {
value++;
}
public synchronized static void incre2() {
value++;
}
public void incre3() {
synchronized (this) {
value++;
}
}
public void incre4() {
synchronized (SynchronizedUsageDemo.class) {
value++;
}
}
}
incre1的用法叫做对象锁,使用的是this对象,如果调用对象不同的话,锁就会失效。
例如:
public static void main(String[] args) {
SynchronizedUsageDemo demo1 = new SynchronizedUsageDemo();
SynchronizedUsageDemo demo2 = new SynchronizedUsageDemo();
for (int i = 0; i < 10000; i++) {
new Thread(demo1::incre1).start();
new Thread(demo2::incre1).start();
}
// 检查是否只有主线程存活,如果不是则让出CPU时间片
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(value);
}
此时打印出的结果是19997与期望值20000不符,不同对象调用的时候同步实例方法并不能解决线程安全问题。
与它同样的结果还有incre3(),synchronized(this)也不能解决上述的问题。
将上面的main方法做一点小修改,尝试使用同步静态方法查看是否可以正确的锁住。
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
new Thread(SynchronizedUsageDemo::incre2).start();
new Thread(SynchronizedUsageDemo::incre2).start();
}
// 检查是否只有主线程存活,如果不是则让出CPU时间片
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(value);
}
无论打印多少次,打印结果都是20000,同样使用synchronized (SynchronizedUsageDemo.class)也是一样的效果。
1.2.2.锁粒度的区别
上述几种使用方式的区别,主要锁粒度的区别:
- 修饰实例方法:锁的粒度为当前对象中使用synchronized修饰的所有实例方法。
- 修饰静态方法:锁的粒度为当前类中使用synchronized修饰的所有静态方法。
- 同步代码块:锁的粒度为使用相同锁对象的所有同步代码块。
1.3.小结
- 无论使用上面哪一种方式,锁住的都是对象,不管是实例对象还是Class对象,都是对象。
- 只要保证同一个synchronized块(或同步方法)使用的锁对象是同一个对象,就能保证锁的互斥性。
2.synchronized实现原理
2.1.同步块和同步方法在指令码上的差别
使用javap -c -v SynchronizedUsageDemo.class反编译出指令码,下面截取重要部分:
public synchronized void incre1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
public static synchronized void incre2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
public void incre3();
……
3: monitorenter
4: getstatic #2 // Field value:I
7: iconst_1
8: iadd
9: putstatic #2 // Field value:I
12: aload_1
13: monitorexit
……
public void incre4();
……
4: monitorenter
5: getstatic #2 // Field value:I
8: iconst_1
9: iadd
10: putstatic #2 // Field value:I
13: aload_1
14: monitorexit
……
可以看到的是,synchronized修饰方法是在方法中加入了ACC_SYNCHRONIZED标识,而同步代码块使用的是monitorenter和monitorexit对中间的指令码做了包裹。
使用ACC_SYNCHRONIZED标识表示这个方法是同步方法,同一时间的只能有一个线程获取锁并进入方法,而使用monitorenter和monitorexit只会对这两者之间的指令码做同步处理。
这种同一时间只能由一个线程运行的区域叫做临界区。
2.2.如何保证多个线程互斥
保证多个线程互斥的前置条件肯定是有一个可以由多个线程共享的资源,这个资源可以记录当前是否被获取,被哪一个线程获取了。
当一个线程尝试获取锁时,如果发现锁已经被其它线程获取了,在锁释放之前,无论如何都无法获取这个锁,这样才能达到互斥的目的。
在Java中,为每个对象都分配了这么一个共享的资源,就是monitor对象。
2.2.1.monitor概念和特点
monitor在操作系统中是一个监视器的概念,不同的语言对monitor有不同的实现,在JVM中是底层通过C++实现的,为每个Java对象都分配了一个monitor对象,所以Java中的每一个对象都可以用作锁对象。
其特点就是同一时间只有一个线程可以进入临界区实现互斥的效果,未抢占到monitor的线程会进入到等待队列中阻塞,直到进入临界区的线程释放锁后唤醒等待队列中的线程。除此之外,还提供了在合适的条件下主动阻塞和唤醒的API,wait()/notify(),使用wait()主动阻塞的线程会被放到另外一个等待队列中。
2.2.2.Java对象如何关联monitor对象
既然每个Java对象都关联了一个monitor对象,那Java对象和monitor对象是如何关联起来的呢?
通过Java对象头,下面是一张从Mark Word状态切换表。
可以看到,在重量级锁的情况下,指向互斥量的指针,这里的互斥对象就是指的monitor。
2.2.3.monitor的关键属性
monitor中主要包括以下的几个关键属性:
- owner:指向持有当前monitor对象的线程。
- entryList:阻塞队列,未抢占到锁的线程会放入这个队列中阻塞,直到持有锁的线程退出时按照进入队列的顺序进行唤醒。
- waitSet:等待队列,调用wait()方法时会进入这个队列,直到notify()/notifyAll()调用时唤醒。
- recursions:锁重入次数,初始值为0,一个线程抢占锁时+1。
2.2.4.线程抢锁流程
线程尝试抢占锁的简要流程如下:
上图中描述了一个线程抢占到锁后就会进入临界区执行代码,执行完毕后等释放锁。如果没有抢占到锁,则加入阻塞队列中阻塞,如果抢占到了锁,并且在临界区执行了wait()方法,就会进入等待队列等待,并且此时会释放锁,让其他线程可以去竞争锁。
调用notify()时会按照先入先出的顺序将第一个进入队列的线程所在节点放入到阻塞队列中,排队等待唤醒。
调用notifyAll()会直接唤醒等待队列中的所有线程,直接进行抢锁而不进入阻塞队列。
此外,线程抢锁成功后,会将owner指向当前线程,并且会将重入次数+1,当前线程重入这个锁时重入次数再次加+1,在对象头中会将锁标志位改为10。
释放锁时,每释放一次重入锁会将重入次数-1,重入次数减到0时会将owner置为null(释放锁),如果是调用wait(),则重入次数不会依次递减1,而是直接置为0,并释放锁。
这里的notifyAll()
就是依次唤醒waitSet
挂起的线程,并加入到entryList
中。
注:什么是可重入呢?
一个线程如果抢占某个锁成功,在释放锁之前再次进入相同锁对象的同步块时不需要再次进入抢占锁的过程而是直接进入临界区。
可重入的设计是为了避免造成死锁,在Java中的锁都是可重入的。
2.3.小结
synchronized的主要作用是保证临界区同一时间只有一个线程可以访问,所以就需要实现互斥,而互斥是通过monitor监视器来实现的。
monitor对象通过owner和两个队列来实现互斥锁。