粗谈synchronize和Lock锁的区别及使用场景

说起这两个单词,大家肯定都是熟悉,我所说的熟悉是指大家都耳濡目染了,但是究竟有什么区别呢,这里我主要从底层原理和使用场景上进行浅谈:

区别

  • Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个接口,通过这个接口的实现类可以实现同步访问;
  • Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后或者执行同步代码异常,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象
  • synchronize可以锁对象、类、代码块,而lock锁住的是代码块
  • synchronize是非公平锁,而lock可以是公平锁,也可以是非公平锁
  • synchronize等待不中断,而lock等待可中断
  • synchronize不知道线程有没有获取到锁,而lock能知道
  • synchronize是隐式锁。Lock是显示锁
  • synchronize是悲观锁的一种实现。Lock的实现类ReentrantLock主要用到unsafe的CAS和park两个功能实现锁(CAS + park )乐观锁的一种实现
  • 性能比较:在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
    ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。
    粗谈synchronize和Lock锁的区别及使用场景_第1张图片

关于synchronize

由于synchronize是由JVM实现的,因此当加锁代码出现异常时,对象锁可以由JVM释放,包含以下三种情况:
1、 占有锁的线程执行完了代码块,然后释放对锁的占有;
2、 占有锁的线程发生了异常,此时JVM会让线程自动释放锁;
3、 占有锁的线程调用了wait()方法,从而进入了WAITING状态需要释放锁。
4、synchronize中途不能被打断只能等代码块执行完或者出现异常由jvm主动释放,才能被其他线程执行,如果执行完可以执行notifyAll或者notify等object对象的api来进行唤醒其他阻塞线程立马执行

关于Lock

粗谈synchronize和Lock锁的区别及使用场景_第2张图片

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解决了在实现同步容器时设计的大量细节问题

粗谈synchronize和Lock锁的区别及使用场景_第3张图片
AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus

ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:

非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;

公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。

Synchronized进过编译,会在同步块的前后分别形成monitorentermonitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止

PS:Sync和ReentrantLock虽然底层实现方式不一样 但是都能保证获取锁这个动作是原子操作的 换言之并发下 只能一个线程能够获取锁

你可能感兴趣的:(线程并发+锁机制,多线程,java,并发编程,锁)