一、Synchronized同步锁
使用Synchronized关键字将一段代码用锁"锁起来",只有持有当前锁的线程才能访问。在同一时间,只能有一个线程持有锁,这样,才能保证线程的安全性。
二、Synchronized关键字的使用方法
1、Synchronized关键字使用在方法体上。(作用范围:整个方法)
public synchronized void dosth1() {
}
2、Synchronized关键字代码块(作用范围:被Synchronized代码块包围的代码)
referencr-to-lock:Java对象(任何Java对象都可以当做锁)
synchronized (reference-to-lock) {
(不具有原子性的代码)
}
三、Synchronized关键字中对象的使用
1、Synchronized修饰在实例方法上,使用的锁对象为this当前对象。
必须使用同一对象,这样才能保证不同线程获取的是同一把锁。
public class Demo03 {
public static void main(String[] args) {
//使用同一对象调用dosth1()
Demo demo = new Demo();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
demo.dosth1();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
demo.dosth1();
}
});
//启动线程
t1.start();
t2.start();
}
}
class Demo{
//synchronized关键字既可以加在方法体上,也可以使用synchronized代码块
//区别:作用的范围不一样
public synchronized void dosth1() {
//使用this当前对象当做锁对象
}
public void dosth2() {
synchronized (this) {
//使用this当前对象当做锁对象
}
}
}
2、Synchronized修饰在静态方法上,使用的锁对象为当前对象的Class对象。
可以使用不同的对象,但是不同对象的Class对象必须一样。
public class Demo03 {
public static void main(String[] args) {
//使用不同对象调用dosth1(),但必须是同一个类的不同实例
Demo demo1 = new Demo();
Demo demo2 = new Demo();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
demo1.dosth1();
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
demo2.dosth1();
}
});
//启动线程
t1.start();
t2.start();
}
}
class Demo{
public synchronized void dosth1() {
//使用当前对象的Class对象
}
public void dosth2() {
synchronized (Demo.class) {
//使用当前对象的Class对象
}
}
}
3、Synchronized修饰代码块,使用自定义的对象当做锁。
可以是任何类型的Java对象。
自定义的锁对象只能用在Synchronized代码块中。
class Demo{
//公共的锁对象
public static final Object lock = new Object();
public void dosth2() {
synchronized (lock) {
//表示用Object类型当做锁
}
}
}
四、Synchronized实现原理
Synchronized底层使用了monitorenter指令和monitorexit指令,通过对象内部叫做监视器(monitor)来实现的,分别代表尝试获取锁和释放锁。
1、监视器(monitor)
每个对象内部有一个监视器(monitor),线程通过执行monitorenter指令尝试获取monitor所有权。当monitor被占用时就会处于锁定状态。monitor内部有三个重要的部分,一是Owner:指向持有monitor所有权(持有锁)的线程,二是EntryList:存放阻塞线程,三是WaitSet:存放等待当前资源的线程。
获取monitor的过程:
如果monitor的进入数为0,当前线程进入monitor,将进入数改为1,表示该线程持有锁。
如果线程已经占有monitor,重新再次进入时,monitor的进入数+1。
如果有其他的线程已经占有了monitor,则当前线程进入阻塞状态,直到monitor的进入数变为0,当前线程再尝试获取monitor的所有权。
2、锁升级
在Java 6之前,Synchronized的实现依靠操作系统内部的互斥锁,非常消耗系统的资源。所以,在Java 6之后,提供了三种不同的锁:偏向锁,轻量级锁,重量级锁。锁的升级、降级,就是根据不同线程对锁的竞争情况进行这三种锁之间的升级、降级。
3、偏向锁
假设只有一个线程访问资源,使用偏向锁,如果发现多一个的线程竞争资源,则将锁升级为轻量级锁。
当没有并发线程时,默认使用偏向锁。因为只有一个线程访问资源,所以在偏向锁的对象头中的Mark Word里只设置线程ID,表示这个对象偏向于当前线程。使用偏向锁是为了降低无资源竞争。
当有另外一个线程来竞争当前资源时,就要升级锁为轻量级锁。
4、轻量级锁
当有线程串行竞争资源时,使用轻量级锁,轻量级锁要比偏向锁复杂一点,它涉及加锁和解锁的过程,轻量级锁的加锁过程:
线程在进行串行竞争资源时,如果对象锁状态为无锁状态,首先在当前线程的栈帧中建立一个名为锁记录(Lock Recode)的空间,存储拷贝锁对象头中的Mark Word。拷贝成功后,尝试将锁对象的
Mark Word中的ptr_to_record指向到线程对象的Mark Word,并将Lock record里的owner指向锁对象的Mark Word。如果这个更新成功了,那么这个线程就拥有了该锁,此线程就可以执行后续代码了。如果这个更新失败了,就检查是否有另一个线程也在对该对象的锁进行加锁,如果有,就说明有多个线程在竞争该对象的锁。此时,就要升级锁为重量级锁。
5、重量级锁
多个线程竞争同一个锁对象,如果其中一个线程竞争到当前锁对象,则其余线程进入阻塞状态。重量级锁在释放锁的同时,要通知其他线程重新参与锁的竞争。
依赖于操作系统互斥锁所实现的锁。操作系统的互斥锁实现线程之间的切换,需要从用户态(用于运行用户程序)转换到核心态(用于运行操作系统程序),切换成本高,效率低。依赖于操作系统互斥锁所实现的锁,称为"重量级锁"。