Java同步锁-synchronized与lock[通俗易懂]

Java (167) 2023-04-09 21:12

Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说Java同步锁-synchronized与lock[通俗易懂],希望能够帮助你!!!。

一、synchronized关键字

1、synchronized简介

synchronized实现同步的基础:java中每个对象都可以作为锁。当线程试图访问同步代码时,必须先获得对象锁,退出或抛出异常时必须释放锁。

表现形式为:代码块同步方法同步

2、synchronized的使用场景

  • 方法同步

public synchronized void method1(){}

锁住的是该对象的一个实例,当不同线程调用该实例对象中该同步方法,线程只有一个得到锁,其余被阻塞。但如果不同线程同时对该类的不同实例对象执行该同步方法,则不会阻塞,因为他们使用不同的锁。

  • 代码块同步

synchronized(this)( //ToDo}synchronized(普通变量){    }

同上

  • 静态方法同步

public synchronized static void method3(){}

锁住的是该类,当不同线程调用该类的该static同步方法时,就只能有一个线程获得锁其余线程被阻塞

  • 静态代码块

synchronized(Test.class){ //ToDo}synchronized(静态变量){ //ToDo}

同上

3、synchronized锁升级

锁一共有4种状态,级别从低到高依次是:无锁状态偏向锁状态轻量级锁重量级锁

锁可以升级但不能降级,意味着偏向锁升级为轻量级锁后不能降级为偏向锁。

  • 偏向锁

大所述情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得所的代价更低而引入偏向锁。

当一个线程访问同步代码并获取锁时,会对对象头和栈帧中的锁记录里保存锁偏向的线程ID,以后该线程再进入和退出同步锁时,不需要进行CAS操作来加锁和解锁,而只是简单地测试一下对象头的Mark Word里是否存储执行当前线程的偏向锁。

偏向锁作用是:在没有别的线程竞争的时候,一直偏向当前线程,当前线程可以一直执行。

  • 轻量级锁(自旋锁)

轻量级锁,由偏向锁升级而来。

偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。

  • 重量级锁

轻量级锁膨胀之后,就升级为重量级锁。

重量级锁时依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被称为互斥锁(synchronized就是重量级锁)。

偏向锁

优点:加锁和解锁不需要额外的消耗

缺点:线程存在竞争,会带来额外的锁撤销的消耗

场景:单一线程访问同步块场景

轻量级锁

优点:竞争的线程不会阻塞,提高了程序的响应速度。

缺点:线程自旋时不释放CPU

场景:追求响应时间,同步块执行速度非常快。

重量级锁

优点:线程竞争不使用自旋,释放CPU

缺点:线程阻塞,响应时间缓慢。

场景:追求吞吐量,同步块执行速度较长。

二、Lock接口

1、Lock接口

Lock,锁对象。在Lock接口出现之前,Java程序时靠synchronized关键字实现锁功能。而在Java SE5.0之后并发包中新增了Lock接口来实现锁的功能。

它能提供synchronized关键字类似的同步功能,但需要显示获得锁和释放锁,至于二者区别后文补充。

Lock接口的主要方法:

  • void lock()

执行该方法时,若锁处于空闲状态,当前线程将获得锁。相反,如果锁已经被其他线程持有,则禁止当前线程获得锁。

  • boolean tryLock()

若锁可用,则获得锁,并立即返回true,否则返回false。

tryLock()和Lock()的区别在于:

tryLock()只是尝试获得锁,若锁不可用不会导致当前线程被禁止,当前线程仍然继续往下执行代码。

Lock()则是一定要获得锁,如果锁不可用,就一直等待,在未获得锁之前,当前线程并不继续向下自行。

  • void unlock()

执行该方法时,当前线程将释放持有锁,锁只能由持有者释放,若线程没有持有锁,则执行该方法,可能导致异常方式。

  • Condition newCondition()

条件对象,获得等待通知组件。该组件和当前的锁绑定,当前线程只有获得锁,才会调用该组件的await()方法,调用后,当前线程将释放锁。

2、Reentrantlock的使用

Reentrantlock的使用很简单,只需要显示调用,获得同步锁,释放同步锁即可。

ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁
.....................

try {
    lock.lock(); //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果
    //操作
}catch(Exception e){
   //异常处理
} finally {
    lock.unlock();  //释放锁
}

reentrantlock锁,在高并发的条件下使用的性能远远高于synchronized关键字。

并且reentratnlock公平和非公平锁的队列都是基于锁内部维护的一个双向链表,表结点Node的值就是每一个请求当前锁的线程。

3、ReadWriteLock接口

ReadWriteLock接口中主要方法如下:

public interface ReadWriteLock {
    /** * Returns the lock used for reading. * * @return the lock used for reading */
    Lock readLock();

    /** * Returns the lock used for writing. * * @return the lock used for writing */
    Lock writeLock();
}

ReadWriteLock管理一组锁,一个是只读锁,一个是写锁。

Java并发库中ReentrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性。

读锁同一时刻允许有多个线程在访问,但是写进程访问时,所有的读线程其他写进程被阻塞

读写锁维护一对锁,一个读锁和一个写锁,通过读写锁分离,使得并发性相比一般的排它锁有很大的提升。

ReentrantReadWriteLock读写锁的几个特性

  • 公平选择性
  • 重入性
  • 锁降级

读写锁例子(程序来源于网上:blog.csdn.net/canot/artic…):

public class Cache{
  static Map<String,Object> map = new HashMap<String,Object>();
  static ReentrantReadWriteLock  rwl = new ReentrantReadWriteLock();
  static Lock rLock = rwl.readLock();
  static Lock wLock = rwl.writeLock();

  //获取一个key对应的value
  public static final Object get(String key){
     r.lock();
     try{
         return map.get(key);
     }finally{
         r.unlock();
     }
  }

  //设置key对应的value并返回旧的value
  public static fianl Object put(String key,Object value){
     w.lock();
     try{
        return map.put(key,value);
     }final{
        w.unlock();
     }
  }

  //清空缓存
  public static fianl void clear(){
     w.lock();
     try{
        map.clear();
     } finally{
        w.unlock();
     }
  }
}

  • 读写锁的锁降级

锁降级是指写锁降级成为读锁。如果当前线程持有写锁,然后将其释放再获取读锁的过程不能称为锁降级。锁降级指的在持有写锁的时候再获取读锁,获取到读锁后释放之前写锁的过程称为锁释放。

(程序来源于:blog.csdn.net/qq_38737992…

public void work() {
        reentrantReadWriteLock.readLock().lock();
 
        if (!update) {
            reentrantReadWriteLock.readLock().unlock();
 
            // 锁降级开始
            reentrantReadWriteLock.writeLock().lock();
            try {
                if (!update) {
                    // 准备数据
                    ++index;
                    try {
                        TimeUnit.MILLISECONDS.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    update = true;
                }
                reentrantReadWriteLock.readLock().lock();
            } finally {
                reentrantReadWriteLock.writeLock().unlock();
                // 锁降级结束,降级为读锁
            }
        }
        try {
            // 使用数据
            for (int i=0; i<5; i++) {
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":" + index); } } finally { reentrantReadWriteLock.readLock().unlock(); } } 

三、synchronized和Lock区别

1、synchronized和Lock区别

synchronized是关键字,而Lock是接口

synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁

synchronized自动释放锁(a线程执行完同步代码会自动释放锁,b线程执行过程中发生异常会释放锁)

lock需在finally中手动释放锁(unlock()方法释放锁),否则容易造成线程死锁。

synchronized关键字的两个线程1和线程2,若当前线程1获得锁,线程2等待,如果线程1阻塞,线程2会一直等待下去。

而lock锁不一定会等待下去,如果尝试获得不到锁,线程可以不用一直等待就结束了。

synchronized的锁可重入、不可中断、非公平

lock锁可重入,可中断、可公平

lock锁适合大量同步的代码的同步问题synchronized锁适合代码少量的同步问题

不可重入锁:自旋锁、wait()、notify()、notifyAll()

不可重入锁,即不可递归调用,递归调用会发生死锁

2、reentrantlock和synchronized区别

reentrantLock拥有synchronized相同的并发性和内存语义,此外还多列锁投票、定时锁等候和中断所等候

使用synchronized锁,A不释放,B将一直等待下去

使用reentrantlock锁,A不释放,B等待一段时间就会中断等待,而干别的事情。

synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且代码执行时出现异常,JVM会走到哪个释放锁定。但是Lock不行

在资源竞争不是很激烈的情况下,synchronized的性能优于reentrantlock锁,而竞争激烈的情况下,synchronized的性能下降几十倍,而reentrantlock的性能维持常态。

  • 性能分析

synchronized多次自旋,以获得锁,在这个过程中等待的线程不会被挂起,因而节省了挂起和唤醒的上下文切换的开销

reentrantlock不会自旋,而是直接挂起

因而在线程并发量不大的情况下,synchronized因为拥有自旋锁、偏向锁和轻量级锁的原因,不用将等待线程挂起,偏向锁甚至不用自旋,所以在这种情况下要比reenttrantlock高效。

默认情况下synchronized非公平锁;reentrantlock默认是非公平锁。

  • 绑定多个条件

一个Reentrantlock对象可以同时绑定多个Condition对象,

而在synchronized中,锁对象的wait()notify()notifyAll()方法可以实现一个隐含的条件。

如果要和多余一个添加关联的时候,synchronized就不得不额外地添加一个锁,而Reentrantlock则无须这么做只需要多次调用new Condition()方法即可。

发表回复