说起这两个单词,大家肯定都是熟悉,我所说的熟悉是指大家都耳濡目染了,但是究竟有什么区别呢,这里我主要从底层原理和使用场景上进行浅谈:
区别
关于synchronize
由于synchronize是由JVM实现的,因此当加锁代码出现异常时,对象锁可以由JVM释放,包含以下三种情况:
1、 占有锁的线程执行完了代码块,然后释放对锁的占有;
2、 占有锁的线程发生了异常,此时JVM会让线程自动释放锁;
3、 占有锁的线程调用了wait()方法,从而进入了WAITING状态需要释放锁。
4、synchronize中途不能被打断只能等代码块执行完或者出现异常由jvm主动释放,才能被其他线程执行,如果执行完可以执行notifyAll或者notify等object对象的api来进行唤醒其他阻塞线程立马执行
关于Lock
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
通过代码我们知道,Lock是一个接口,里面定义了获取锁,释放锁等相关的抽象方法。下面来逐个讲述Lock接口中每个方法的使用:
lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的,他们有什么区别呢?
lock()
lock()方法是用来获取锁的,是我们平时使用最多的一个方法,如果锁已被其他线程获取,则进行等待。我们之前说过了,使用lock获取锁的时候,必须手动释放锁,并且在发生异常的时候,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。一般按照下面的方式使用:
Lock lock = ...; //声明锁
lock.lock(); //获得锁
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
tryLock()
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立 即返回。在拿不到锁时不会一直在那等待
tryLock(long time, TimeUnit unit)
tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。 如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
一般按照下面的方式使用:
Lock lock = ...;
if(lock.tryLock()) {
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
}else {
//如果不能获取锁,则直接做其他事情
}
lockInterruptibly()
lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通 过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。
lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。
什么是可重入锁
Lcok在Java中是一个接口,一般在面试问题中问到的可能是ReentrantLock与synchronized的区别。ReentrantLock是Lock的一个实现类,字面意思的话就是可重入锁,那么什么是可重入锁呢。
可重入锁是锁的一个相关概念,并不是特指我们的ReentrantLock,而是如果一个锁具备可重入性,那我们就说这是一个可重入锁。ReentrantLock和synchronized都是可重入锁。至于什么是可重入性,这里举个简单的例子,现在在一个类里我们有两个方法(代码如下),一个叫做去北京,一个叫做买票,那我们在去北京的方法里可以直接调用买票方法,假如两个方法全都用synchronized修饰的话,在执行去北京的方法,线程获取了对象的锁,接着执行买票方法,如果synchronized不具备可重入性,那么线程已经有这个对象的锁了,现在又来申请,就会导致线程永远等待无法获取到锁。而synchronized和ReentrantLock都是可重入锁,就不会出现上述的问题。
class Trip {
public synchronized void goToBeiJing() {
// 去北京
buyATicket();
}
public synchronized void buyATicket() {
// 买票
}
}
因此lockInterruptibly()一般的使用形式如下:
我们来分析ReentrantLock 调用lockInterruptibly方法,可以对线程interrupt方法做成响应
package com.reentranlock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* ReentrantLock还可以调用lockInterruptibly方法,可以对线程interrupt方法做成响应
*
* @author 小辉哥/小辉GE
*
* 2019年8月10日 下午15:30:00
*/
public class ReentrantLockInterrupt {
public static void main(String[] args) {
Lock lock = new ReentrantLock();
Thread thread1 = new Thread(() -> {
lock.lock();
try {
System.out.println("thread1 start......");
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
System.out.println("thread1" + "interrupt......");
} finally {
lock.unlock();
System.out.println("thread1 end......");
}
});
thread1.start();
Thread thread2 = new Thread(() -> {
boolean status = false;
try {
status = lock.tryLock();
System.out.println("thread2 start......");
// 可以对interrupt方法做出响应
if (!status)
lock.lockInterruptibly();
} catch (InterruptedException e) {
System.out.println("thread2" + "interrupt");
} finally {
if (status)
lock.unlock();
System.out.println("thread2 end");
}
});
thread2.start();
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread2.interrupt();
}
}
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似
[CAS]:Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现,无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。” Java并发包(java.util.concurrent)中大量使用了CAS操作,涉及到并发的地方都调用了sun.misc.Unsafe类方法进行CAS操作。
[AQS]:Abstract Queued Synchronizer(摘要排队同步器)简称AQS,是一个用于构建锁和同步容器的框架。事实上concurrent包内许多类都是基于AQS构建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解决了在实现同步容器时设计的大量细节问题
AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus
ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:
非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;
公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。
Synchronized进过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止
PS:Sync和ReentrantLock虽然底层实现方式不一样 但是都能保证获取锁这个动作是原子操作的 换言之并发下 只能一个线程能够获取锁