对同一个线程,能否在获取到锁以后继续获取同一个锁?
答案是肯定可以获取同一个锁。因为JVM 允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。
一、synchronized同步锁
在 Java中synchronized 同步锁是一种可重入的锁。
什么是Synchronized同步锁?
Synchronized 同步锁,简单来说,使用 Synchronized 关键字将一段代码逻辑,用一把锁给锁起来,只有获得了这把锁的线程才访问。并且同一时刻,只有一个线程能持有这把锁,这样就保证了同一时刻只有一个线程能执行被锁住的代码,从而确保代码的线程安全。
1. 语法
(1)synchronized代码块:自定义对象,作为锁。
(2)synchronized修饰普通方法:使用this关键字获取当前对象,作为锁。
(3)synchronized修饰静态方法:使用当前类的Class对象,作为锁。
2. synchronized实现原理
(1)synchronized关键字通过monitor enter/monitor exit两个指令实现。
(2)通过Monitor监视器机制实现线程的同步:
Owner 线程拥有者
EntryList 线程阻塞区
WaitSet 线程等待区
3. synchronized锁升级(锁膨胀)
在JDK1.6之前,synchronized性能开销较大;在JDK1.6之后,对synchronized进行了优化,它会自动根据程序的执行情况,自动进行锁的升级:偏向锁->轻量级锁->重量级锁。
偏向锁(偏斜锁):只有一个线程访问时,使用偏向锁,通过Owner记录线程ID(ThreadID)实现。
轻量级锁:出现多个线程访问时(没有并发),使用轻量级锁,通过CAS实现。
重量级锁:出现多个线程并发访问时,使用重量级锁。由于重量级锁,使用操作系统的互斥锁实现。(使用互斥锁,从“用户态”切换至“内核态”,带来性能开销,所以性能相对较差)
4. synchronized线程安全的案例
可变字符串的线程安全
(1)StringBuffer : 线程安全(在改变字符串内容的方法上使用synchronized同步锁),性能较差
(2)StringBuilder:线程不安全,性能较好
集合类的线程安全(使用synchronized关键字实现线程安全)
(1)List接口的线程安全实现类:Vector、Stack
(2)Map接口的线程安全实现类:Hashtable
5. synchronized 关键字的补充
当一个线程访问对象的一个 synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非 synchronized(this)同步代码块。
在没有加锁的情况下,所有的线程都可以自由地访问对象中的代码,而synchronized关键字只是限制了线程对于已经加锁的同步代码块的访问,并不会对其他代码做限制。所以,同步代码块应该越短小越好。
父类中 synchronized 修饰的方法,如果子类没有重写,则该方法仍然是线程安全性;如果子类重写,并且没有使用 synchronized 修饰,则该方法不是线程安全的。
在定义接口方法时,不能使用 synchronized 关键字。
构造方法不能使用 synchronized 关键字,但可以使用 synchronized 代码块来进行同步
离开 synchronized 代码块后,该线程所持有的锁,会自动释放。
二、ReentrantLock锁
synchronized 关键字虽然已经实现可重入锁,但由于获取时必须一直等待,没有额外的尝试机制。所以,在 java.util.concurrent.locks 包提供的 ReentrantLock用于替代 synchronized。顾名思义, ReentrantLock 也是可重入锁,它和 synchronized 一样,一个线程可以多次获取同一个锁。
ReentrantLock是核心类库提供的锁实现类,实现了Lock接口,通过lock()方法加锁,unlock()方法释放锁。
支持公平锁和非公平锁,内部通过AQS机制实现。
通过trylock()方法支持获取锁的尝试机制。使用 ReentrantLock比直接使用 synchronized 更安全,线程在 tryLock()失败的时候不会导致死锁。
ReentrantLock总共有三个内部类:Sync、NonfairSync、FairSync。
NonfairSync 类继承了 Sync 类,表示采用非公平策略获取锁:每一次都尝试获取锁,不会按照公平等待的原则进行等待,不会让等待时间最久的线程获得锁。
FairSync 类也继承了 Sync 类,表示采用公平策略获取锁:当资源空闲时,它总是会先判断sync 队列是否有等待时间更长的线程,如果存在,则将当前线程加入到等待队列的尾部,实现了公平获取原则。
ReentrantLock 构造函数:默认是采用的非公平策略获取锁。
ReentrantLock实现线程安全的案例:CopyOnWriteArrayList、ArrayBlockingQueue
public class Main {
public static void main(String[] args) {
// 公共的锁对象
final ReentrantLock lock = new ReentrantLock();
// 通过锁对象,创建用于线程之间通信的Condition对象
final Condition condition = lock.newCondition();
Thread t1 = new Thread(new Number(lock, condition));
Thread t2 = new Thread(new Character(lock, condition));
t1.start();
t2.start();
}
}
class Number implements Runnable {
private ReentrantLock lock;
private Condition condition;
public Number(ReentrantLock lock, Condition condition) {
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
for (int i = 1; i < 53; i++) {
if (i % 2 == 1) {
System.out.print(" ");
}
System.out.print(i);
if (i % 2 == 0) {
// 唤醒
condition.signal();
try {
// 等待
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} finally {
// 释放锁
lock.unlock();
}
}
}
class Character implements Runnable {
private ReentrantLock lock;
private Condition condition;
public Character(ReentrantLock lock, Condition condition) {
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
final ReentrantLock lock = this.lock;
// 加锁
lock.lock();
try {
for (char i = 'A'; i <= 'Z'; i++) {
System.out.print(i);
if (i < 'Z') {
// 唤醒数字线程
condition.signal();
try {
// 等待
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} finally {
// 释放锁
lock.unlock();
}
}
}