概述
关于synchronized和ReentrantLock的区别如下:
关于两种锁使用方式的伪代码如下:
// **************************Synchronized的使用方式**************************
// 1.用于代码块
synchronized (this) {}
// 2.用于对象
synchronized (object) {}
// 3.用于方法
public synchronized void test () {}
// 4.可重入
for (int i = 0; i < 100; i++) {
synchronized (this) {}
}
// **************************ReentrantLock的使用方式**************************
public void test () throw Exception {
// 1.初始化选择公平锁、非公平锁
ReentrantLock lock = new ReentrantLock(true);
// 2.可用于代码块
lock.lock();
try {
try {
// 3.支持多种加锁方式,比较灵活; 具有可重入特性
if(lock.tryLock(100, TimeUnit.MILLISECONDS)){ }
} finally {
// 4.手动释放锁
lock.unlock()
}
} finally {
lock.unlock();
}
}
synchronized
synchronized的底层实现
synchronized是 Java内建的同步机制,所以也被称为 Intrinsic Locking,它提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取锁的线程时只能等待或者阻塞在那里。Java5之前,synchronized是唯一的同步方式。synchronized关键字既可以用来修饰方法,也可以使用在特定的代码块上,本质上 synchronized方法相当于把方法全部语句用 synchronized 代码包起来。
synchronized是基于一对 monitorenter/monitorexit 指令实现的,Monitor对象是同步的基本实现单元,无论是显示同步,还是隐式同步都是如此。区别是同步代码块是通过明确的 monitorenter 和 monitorexit 指令实现,而同步方法通过ACC_SYNCHRONIZED 标志来隐式实现。
同步代码块实现:
public class Test1 {
public void fun1(){
synchronized (this){
System.out.println("fun111111111111");
}
}
}
将.java文件使用javac命令编译为.class文件,然后将class文件反编译出来。
反编译的字节码文件截取:
通过反编译后的内容查看可以发现,synchronized编译后,同步块的前后有monitorenter/monitorexit两个 字节码指令。
在Java虚拟机规范中有描述两条指令的作用:
翻译一下如下:
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权
monitorexit:
- 执行monitorexit的线程必须是objectref所对应的monitor的所有者。
- 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
问题:synchronized 代码块内出现异常会释放锁吗?
A:会自动释放锁,查看字节码指令可以知道,monitorexit插入在方法结束处和异常处。从Exception table异常表中也可以看出。
同步方法代码演示:
public class Test1 {
//锁当前对象(this)
public synchronized void fun2(){
System.out.println("fun2222222222222222222222");
}
//静态synchronized修饰:使用的锁对象是当前类的class对象
public synchronized static void fun3(){
System.out.println("fun33333333333333");
}
}
编译之后反编译截图:
从反编译的结果来看,同步方法表面上不是通过monitorenter/monitorexit指令来完成,但是与普通方法相比,常量池中多出来了ACC_SYNCHRONIZED标识符。java虚拟机就是根据ACC_SYNCHRONIZED标识符来实现方法的同步,当调用方法时,调用指令先检查方法是否有 ACC_SYNCHRONIZED访问标志,如果存在,执行线程将先获取monitor,获取成功之后才执行方法体,执行完后再释放monitor。在方法执行期间,其他线程都无法再获取到同一个monitor对象。 虽然编译后的结果看起来不一样,但实际上没有本质的区别,只是方法的同步是通过隐式的方式来实现,无需通过字节码来完成。
ACC_SYNCHRONIZED的访问标志,其实就是代表:当线程执行到方法后,如果检测到有该访问标志就会隐式的去调用monitorenter/monitorexit两个命令来将方法锁住。
小结
synchronized 同步代码块的实现是通过 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor的持有权(monitor对象存在于每个Java对象的对象头中, synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)。
其内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0 ,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止
synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
synchronized 的特性
synchronized 作为悲观锁,具有两个特性,一个是 可重入锁,一个是不可中断性。
可重入锁
ReentrantLock和synchronized都是可重入锁
定义
指的是 同一个线程的 可以多次获得 同一把锁(一个线程可以多次执行synchronized,重复获取同一把锁)。
/* 可重入特性 指的是 同一个线程获得锁之后,可以再次获取该锁。*/
public class Demo01 {
public static void main(String[] args) {
Runnable sellTicket = new Runnable() {
@Override
public void run() {
synchronized (Demo01.class) {
System.out.println("我是run");
test01();
}
}
public void test01() {
synchronized (Demo01.class) {
System.out.println("我是test01");
}
}
};
new Thread(sellTicket).start();
new Thread(sellTicket).start();
}
}
原理
synchronized 的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁,每重入一次,计数器就 + 1,在执行完一个同步代码块时,计数器数量就会减1,直到计数器的数量为0才释放这个锁。
优点
可以一定程度上避免死锁(如果不能重入,那就不能再次进入这个同步代码块,导致死锁);
更好地封装代码(可以把同步代码块写入到一个方法中,然后在另一个同步代码块中直接调用该方法实现可重入);
不可中断
定义
线程A获得锁后,线程B要想获得锁,必须处于阻塞或等待状态。如果线程A不释放锁,那线程B会一直阻塞或等待,阻塞等待过程中,线程B不可被中断。
synchronized 是不可中断的,处于阻塞状态的线程会一直等待锁。
public class Demo02_Uninterruptible {
private static Object obj = new Object(); // 定义锁对象
public static void main(String[] args) {
// 1. 定义一个Runnable
Runnable run = () -> {
// 2. 在Runnable定义同步代码块;同步代码块需要一个锁对象;
synchronized (obj) {
// 打印是哪一个线程进入的同步代码块
String name = Thread.currentThread().getName();
System.out.println(name + "进入同步代码块");
Thread.sleep(888888);
}
};
// 3. 先开启一个线程来执行同步代码块
Thread t1 = new Thread(run);
t1.start();
// 保证第一个线程先去执行同步代码块
Thread.sleep(1000);
/**
4. 后开启一个线程来执行同步代码块(阻塞状态)到时候第二个线程去执行同步代码块的时候,
锁已经被t1线程锁获取得到了;所以线程t2是无法获取得到Object obj对象锁的;
那么也就将在同步代码块外处于阻塞状态。*/
Thread t2 = new Thread(run);
t2.start();
/** 5. 停止第二个线程;观察此线程t2能否被中断;*/
System.out.println("停止线程前");
t2.interrupt(); // 通过interrupt()方法给t2线程进行强行中断
System.out.println("停止线程后");
// 最后得到两个线程的执行状态
System.out.println(t1.getState()); // TIMED_WAITING
System.out.println(t2.getState()); // BLOCKED
}
}
// 运行结果:
Thread-0
进入同步代码块
停止线程前
停止线程后
TIMED_WAITING
BLOCKED // t2的状态依然为BLOCKED,说明synchronized是不可被中断的
结果分析:
通过interrupt()方法让 t2 线程强行中断,最后打印t2的状态,依然为BLOCKED,即线程不可中断。
锁升级过程及monitor
详情请看 Java中的锁
ReentrantLock
ReentrantLock的实现依赖于AQS,详情请看 AQS详解
公平锁和非公平锁
非公平锁
// java.util.concurrent.locks.ReentrantLock
static final class NonfairSync extends Sync {
...
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
...
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer
public final void acquire(int arg) {
if (!tryAcquire(arg) && //如果该方法返回了True,则说明当前线程获取锁成功,就不用往后执行了
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//如果获取失败,就需要加入到等待队列中。
selfInterrupt();
}
// java.util.concurrent.locks.AbstractQueuedSynchronizer
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
可以看出,这里只是AQS的简单实现,具体获取锁的实现方法是由各自的公平锁和非公平锁单独实现的(以ReentrantLock为例)。如果该方法返回了True,则说明当前线程获取锁成功,就不用往后执行了;如果获取失败,就需要加入到等待队列中。
原理
ReentrantLock对AQS的独占方式实现为:ReentrantLock中的state初始值为0表示无锁状态。在线程执行 tryAcquire()获取该锁后ReentrantLock中的state+1,这时该线程独占ReentrantLock锁,其他线程在通过tryAcquire() 获取锁时均会失败,直到该线程释放锁后state再次为0,其他线程才有机会获取该锁。该线程在释放锁之前可以重复获取此锁,每获取一次便会执行一次state+1, 因此ReentrantLock也属于可重入锁。 但获取多少次锁就要释放多少次锁,这样才能保证state最终为0。如果获取锁的次数多于释放锁的次数,则会出现该线程一直持有该锁的情况;如果获取锁的次数少于释放锁的次数,则运行中的程序会报锁异常。
可/不可中断性
ReentrantLock 的lock方法是不可中断的,tryLock方法是可中断的。
演示不可中断:
public class Demo03_Interruptible {
// 创建一个Lock对象
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
test01();
}
// 演示 Lock 不可中断
public static void test01() {
Runnable run = () -> {
String name = Thread.currentThread().getName();
try {
lock.lock(); // lock() 无返回值
System.out.println(name + "获得锁,进入锁执行");
Thread.sleep(88888);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // unlock也是没有返回值的
System.out.println(name + "释放锁");
}
};
Thread t1 = new Thread(run);
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(run);
t2.start();
System.out.println("停止t2线程前");
t2.interrupt();
System.out.println("停止t2线程后");
Thread.sleep(1000);
System.out.println(t1.getState());
System.out.println(t2.getState());
}
}
----------------------------------------------
运行效果:
Thread-0获得锁,
进入锁执行
停止t2线程前
停止t2线程后
TIMED_WAITING // t1线程在临界区睡88888ms,有时限的等待
WAITING // t2线程处于等待状态,WAITING
演示可中断:
public class Demo03_Interruptible {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
test02();
}
// 演示 Lock 可中断
public static void test02() throws InterruptedException {
Runnable run = () -> {
String name = Thread.currentThread().getName();
boolean b = false;
try {
b = lock.tryLock(3, TimeUnit.SECONDS);
//说明尝试获取得到了锁;则进入if块当中
if (b) {
System.out.println(name + "获得锁,进入锁执行");
Thread.sleep(888888);
} else {
// 没有获取得到锁执行else,证明了Lock.tryLock()是可中断的;
System.out.println(name + "在指定时间内没有获取得到锁则做其他操作");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (b) { //得到了锁才释放锁
lock.unlock();
System.out.println(name + "释放锁");
}
}
};
Thread t1 = new Thread(run);
t1.start();
Thread t2 = new Thread(run);
t2.start();
}
}
----------------------------------------------------
代码执行效果:
Thread-0获得锁,进入锁执行
Thread-1在指定时间没有得到锁做其他操作
synchronized和ReentrantLock的区别
- 使用synchronized关键字实现同步,线程执行完同步代码块会自动释放锁,而ReentrantLock需要手动释放锁。
- synchronized是非公平锁,ReentrantLock可以设置为公平锁。
- ReentrantLock上等待获取锁的线程是可中断的,线程可以放弃等待锁。而synchonized会无限期等待下去。
- ReentrantLock 可以设置超时获取锁。在指定的截止时间之前获取锁,如果截止时间到了还没有获取到锁,则返回。
- ReentrantLock 的 tryLock() 方法可以尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false