5.3 synchronized
1 底层原理
synchronized底层原理 = java对象头markword + 操作系统对象monitor
对象头的Mark word:
Monitor结构:
//部分属性
ObjectMonitor() {
_count = 0; //锁计数器 进入数
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
}
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
1 synchronized无论是加在同步代码块还是方法上,效果都是加在对象上,其原理都是对一个对象上锁
2 如何给这个obj上锁呢?当一个线程Thread-1要执行临界区的代码时,首先会通过obj对象的markword指向一个monitor锁对象
3 当Thread-1线程持有monitor对象后,就会把monitor中的owner变量设置为当前线程Thread-1,同时计数器count+1表示当前对象锁被一个线程获取。
4 当另一个线程Thread-2想要执行临界区的代码时,要判断monitor对象的属性Owner是否为null,如果为null,Thread-2线程就持有了对象锁可以执行临界区的代码,如果不为null,Thread-2线程就会放入monitor的EntryList阻塞队列中,处于阻塞状态Blocked。
5 当Thread-0将临界区的代码执行完毕,将释放monitor(锁)并将owner变量置为null,同时计算器count-1,并通知EntryList阻塞队列中的线程,唤醒里面的线程
1》 synchronized作用在代码块时:
它的底层是通过monitorenter、monitorexit指令来实现的
monitorenter:
- 每个对象都是一个监视器锁(monitor),当对象被占用时就会是锁定状态
- **monitor进入数(锁计数器)**为0时代表无线程占用,当有线程进入时,进入数设置为1,且该线程就是monitor的拥有者owner
- 当进入线程已经拥有了该monitor,则monitor进入数+1
- 如果该monitor已被其他线程占用,则该线程进入monitor的阻塞队列中,等待monitor进入数为0
monitorexit:
- 执行monitorexit的线程必须是objectref所对应的monitor持有者
- 执行monitorexit后,monitor进入数减1,如果进入数减为0,则该线程释放monitor
2》synchronized作用在方法时:
- 相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的
- 当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor
- 在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
2 三种实现方式
作用于实例方法
public class Test8 implements Runnable {
//静态变量 临界区
static int count = 0;
//synchronized修饰实例方法
public synchronized void add() {
count++;
}
@Override
public void run() {
//线程体
for (int i = 0; i < 1000; i++) {
add();
}
}
public static void main(String[] args) throws InterruptedException {
Test8 test8 = new Test8();
//多个线程操作一个实例对象
Thread thread1 = new Thread(test8);
Thread thread2 = new Thread(test8);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count); //2000
}
}
上述代码模拟了两个线程操作一个共享变量count,分别对count进行自加1000,最终结果是2000。
因为count++
不是一个原子操作,分为先读值在加1两步操作,所以在并发执行时,如果不使用synchronized修饰实例方法,那么最终结果很大可能是小于2000的
问题:一个实例对象只有一把synchronized锁,如果有多个实例对象操作一个共享变量时,synchronized锁并不能保证线程的安全,如将上述代码的main方法修改为:
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Test8());
Thread thread2 = new Thread(new Test8());
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count); //最终结果会小于2000
}
解决这种问题的方式是将synchronized作用于静态的add方法,这样的话,对象锁就当前类对象,无论创建多少个实例对象,类只有一个,所有在这样的情况下对象锁就是唯一的
作用于静态方法
当synchronized作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态成员的并发操作
public class Test8 implements Runnable {
//静态变量 临界区
static int count = 0;
//synchronized修饰静态方法
public static synchronized void add() {
count++;
}
@Override
public void run() {
//线程体
for (int i = 0; i < 1000; i++) {
add();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Test8());
Thread thread2 = new Thread(new Test8());
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count); //最终结果还是2000
}
}
作用于同步代码块
当一个方法很大时,直接锁住整个方法会很费时;可以用同步代码快用于锁住一个方法中的小部分代码
synchronized不能修饰静态代码块
注:使用synchronized锁住同步代码块时,多个线程的实例对象也必须是同一个,不能作用于多个实例对象
public class Test8 implements Runnable {
//全局静态实例
static Test8 test8=new Test8();
//静态变量 临界区
static int count = 0;
//synchronized修饰实例方法
public void add() {
//可以直接锁指定实例synchronized (test8)
//也可以通过锁定传入的this实例
synchronized (this) {
count++;
}
}
@Override
public void run() {
//线程体
for (int i = 0; i < 1000; i++) {
add();
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(test8);
Thread thread2 = new Thread(test8);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(count); //2000
}
}
3 synchronized升级机制
JDK 1.6之前,synchronized 还是一个重量级锁。但JDK1.6之后引入了偏向锁和轻量级锁 ,从此以后锁的状态就有了四种:无锁、偏向锁、轻量级锁、重量级锁
锁升级是不可逆的
1》 无锁
没有对资源进行锁定,所有线程都能访问并修改同一个资源,但只有一个线程能修改成功,其他线程会不断循环,直到修改成功
2》偏向锁
初次执行到synchronized代码块的时候,锁对象变成偏向锁。并且执行完同步代码后不会释放锁
只依赖一次CAS原子指令,第二次进入退出同步区时检测Mark Word 里是否存储着指向当前线程的偏向锁。
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁
3》轻量级锁
当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能
轻量级锁的获取主要由两种情况:
- 当关闭偏向锁功能时
- 由于多个线程(两个及以上)竞争偏向锁导致偏向锁升级为轻量级锁
4》重量级锁
**忙等:**一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting),轻量级锁自旋就是忙等
如果自旋次数超过10次,则会升级锁重量级锁。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待将来被唤醒。