小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
什么是synchronized
JDK官网给出的解释:synchronized关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读或者写都将通过同步的方式来进行,具体表现如下:
1.提供了一种锁的机制,能够确保共享变量的互斥访问,从而防止数据不一致的问题出现。
2.它包括monitor enter和monitor exit两个JVM指令,它能够保证在任何时候任何线程执行到monitor enter成功之前都必须从主内存中获取数据,而不是缓存中,在monitor exit运行成功之后,共享变量被更新后的值必须刷入主内存。
3.执行严格遵守java happens-before规则,一个monitor exit指令之前必定要有一个monitor enter。
实现原理
synchronized修饰同步代码块,javac在编译时,在synchronized同步块的进入的指令前和退出的指令后,会分别生成对应的monitorenter和monitorexit指令进行对应,代表尝试获取锁和释放锁。(为了保证抛异常的情况下也能释放锁,所以javac为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁。)
private final Object lock = new Object();
public void sync() {
synchronized(lock) {
//...
}
}
方法同步通过调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。
public synchronized void sync() {
//...
}
synchronized关键字用法
利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
synchronized关键字的基本语法
1.对于普通同步方法,锁的是当前实例对象
2.对于静态同步方法,锁的是当前类的Class对象
3.对于同步方法块,锁的是Synchonized括号里配置的对象。
不同的修饰类型,代表锁的控制粒度。synchronized关键字“给某个对象加锁”如下:
public Class Test {
public void synchronized method1() {
// ...
}
public static void synchronized method2(){
// ...
}
}
也可以写成下面这样:
public class Test {
public void method1() {
synchronized(this) {
// ...
}
}
public static void method2() {
synchronized(MyClass.class) {
// ...
}
}
}
实例方法的锁加在对象Test上;静态方法的锁加在Test.class上。
深入synchronized
synchronized关键字最重要的是互斥机制,即同一时刻,只能有一个线程访问同步资源。
public class Demo {
private final static Object lock = new Object();
public void accessResource() {
synchronized(lock) {
try {
TimeUnit.MINUTES.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final Demo sync = new Demo();
for(int i =0;i<4;i++) {
new Thread(){
public void run() {
sync.accessResource();
}
}.start();
}
}
}
上面定义一个方法accessResource,使用synchronized来对代码进行同步,再定义了4个线程调用accessResource方法,由于synchronized的互斥性,只能有一个线程获得lock的monitor锁,其他线程只能进入阻塞状态,等待获取lock的monitor锁。
这里就要提到两个重要指令:monitorenter和monitorexit。
monitorenter :
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:
执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,
如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
synchronized锁的问题
synchronized的重量级锁是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。
但是synchronized同步块对于同一条线程是可重入的,不会出现自己锁死自己的问题。其次,同步块在已进入的线程执行完以前,会阻塞后面其他线程的进入。大多数时候,共享数据的锁定状态一般只会持续很短的一段时间,为了这段时间去挂起和恢复线程其实并不值得。如果物理机上有多个处理器,可以让多个线程同时执行的话。可以让后面来的线程稍等一下,但是并不放弃处理器的执行时间,看看持有锁的线程会不会很快释放锁。这个稍等一下的过程又叫自旋。
总结
synchronized特点:保证内存可见性、操作原子性。 synchronized影响性能的原因:加锁解锁操作需要额外操作。互斥同步对性能最大的影响是阻塞的实现,因为阻塞涉及到的挂起线程和恢复线程的操作都需要转入内核态中完成(用户态与内核态的切换的性能代价是比较大的)。