Java多线程5:synchronized和Lock区别

前言:这是Java面试中常常会被问到的问题,特此总结一下。在分布式开发中,锁是线程控制的重要途径,Java为此也提供了两种锁机制,synchronized和Lock。既然有了synchronized,为啥还要提供Lock接口呢?也许你会说Lock接口比synchronized性能高。在jdk1.5之前确实如此,但是在jdk1.6版本synchronized优化之后,两者性能差不多了。


1、synchronized关键字

synchronized是Java中的关键字,是一种同步锁。synchronized可以保证方法或代码块在运行时,同一时刻只有一个线程可以进入到临界区(互斥性),同时它还保证了共享变量的内存可见性。

synchronized关键字会让没有得到锁资源的线程进入Blocked状态,而后在争夺到锁资源后恢复为Runnable状态,这个过程中涉及到操作系统【用户模式】和【内核模式】的切换,代价比较高。

 尽管java1.6为Synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。

synchronized修饰的对象有以下几种: 

①修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;  

②修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象; 

③修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象; 

④修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

Java中的每个对象都可以作为锁:

  • 普通同步方法,锁是当前实例对象。

  • 静态同步方法,锁是当前类的class对象。

  • 同步代码块,锁是括号中的对象。


2、Lock接口和ReentrantLock实现类

既然有了synchronized,为啥还要提供Lock接口呢?也许你会说Lock接口比synchronized性能高。在jdk1.5之前确实如此,但是在jdk1.6版本synchronized优化之后,两者性能差不多了。

直接来看Lock接口的定义,看看比synchronized多了哪些功能:

public interface Lock {

    // 加锁
    void lock();
    // 能够响应中断
    void lockInterruptibly() throws InterruptedException;
    // 非阻塞获取锁
    boolean tryLock();
    // 非阻塞超时获取锁
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    // 解锁
    void unlock();
    // 定义阻塞条件
    Condition newCondition();
}

可以看到Lock接口相比synchronized多了很多特性,详细解释一下方法

lock():用来获取锁,如果锁被其他线程获得则进行等待,需要和unlock方法配合主动释放锁。发生异常时,不会主动释放锁,所以释放锁的操作放在finally块中

lockInterruptibly():当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程

tryLock():用来尝试获取锁,如果获取成功,则返回true。如果获取失败则返回false。也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待

tryLock(long time, TimeUnit unit):和tryLock()类似。只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true

unlock():解锁

newCondition():定义条件


3、synchronized与Lock的区别 

  • 来源:

       lock是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;

  • 异常是否释放锁:

     synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)

  • 是否响应中断

       lock等待锁过程中可以用interrupt来中断等待,而synchronized只能等待锁的释放,不能响应中断; 

  • 是否知道获取锁

        Lock可以通过trylock来知道有没有获取锁,而synchronized不能;

  • Lock可以提高多个线程进行读操作的效率。(可以通过ReadWriteLock实现读写分离)

  • 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

  • synchronized使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度,

//Condition定义了等待/通知两种类型的方法
Lock lock=new ReentrantLock();
Condition condition=lock.newCondition();
condition.await();
condition.signal();
condition.signalAll();

3.1、synchronized和lock的用法区别

synchronized:在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。

lock:一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。

3.2、synchronized和lock性能区别

synchronized是托管给JVM执行的,而Lock是Java写的控制锁的代码。

在Java1.5中,synchronize是性能低效的。因为这是一个重量级操作,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。相比之下使用Java提供的Lock对象,性能更高一些。

但是到了Java1.6,发生了变化。synchronized在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronized的性能并不比Lock差。官方也表示,他们也更支持synchronized,在未来的版本中还有优化余地。

2种机制的具体区别:

synchronized原始采用的是CPU悲观锁机制,即线程获得的是独占锁。独占锁意味着其他线程只能依靠阻塞来等待线程释放锁。而在CPU转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起CPU频繁的上下文切换导致效率很低。

而Lock用的是乐观锁方式。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁实现的机制就是CAS操作(Compare and Swap)。我们可以进一步研究ReentrantLock的源代码,会发现其中比较重要的获得锁的一个方法是compareAndSetState。这里其实就是调用的CPU提供的特殊指令。

现代的CPU提供了指令,可以自动更新共享数据,而且能够检测到其他线程的干扰,而 compareAndSet() 就用这些代替了锁定。这个算法称作非阻塞算法,意思是一个线程的失败或者挂起不应该影响其他线程的失败或挂起的算法。

3.3、synchronized和lock用途区别

synchronized原语和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用ReentrantLock,特别是遇到下面3种需求的时候。

a.某个线程在等待一个锁的控制权的这段时间需要中断

b.需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程

c.具有公平锁功能,每个到来的线程都将排队等候

下面细细道来……

先说第一种情况,ReentrantLock的lock机制有2种,忽略中断锁和响应中断锁,这给我们带来了很大的灵活性。比如:如果A、B 2个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock就提供了2种机制:可中断/可不中断

第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此);

第二,B线程中断自己(或者别的线程中断它),ReentrantLock处理了这个中断,并且不再等待这个锁的到来,完全放弃。


 4、synchronized和ReentrantLock的异同

ReentrantLock支持非阻塞的方式获取锁,能够响应中断,而synchronized不行

ReentrantLock必须手动获取和释放锁,而synchronized不需要

ReentrantLock可以是公平锁或者非公平锁,而synchronized只能是非公平锁

synchronized在发生异常的时候,会自动释放线程占有的锁,而ReentrantLock在发生异常时,如果没有通过unlock去释放锁,很有可能造成死锁,因此需要在finally块中释放锁

synchronized和ReentrantLock都是可重入锁


参考链接:

synchronized和Lock的区别是什么?


版权声明:本文为CSDN2497242041原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。