Java并发编程实战笔记 (四)锁

推荐查看:Java锁与线程的那些事 (youzan.com)

一、synchronized 同步锁

        主要解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

        原理:

        synchronized 同步语句块的情况:

        在java虚拟机中,锁获取的是对象监视器 monitor,在执行monitorenter时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。对象锁的的拥有者线程才可以执行 monitorexit 指令来释放锁。在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。

在 Java 虚拟机(HotSpot)中,Monitor 是基于 C++实现的,由ObjectMonitoropen in new window实现的。每个对象中都内置了一个 ObjectMonitor对象。

另外,wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因

        synchronized 修饰方法的的情况:

         方法上使用,ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

java 5.0 后的新功能:

二、ReentrantLock 显式锁

        Lock接口:提供了一种无条件的、可轮询的、定时的以及可中断的锁获取操作。

         ReentrantLock:实现lock接口、借由lock api 实现了与synchronized 相同的语义功能。 

        相比synchronizedReentrantLock增加了一些高级功能。主要来说主要有三点:

  • 等待可中断 : ReentrantLock提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly() 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  • 可实现公平锁 : ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
  • 可实现选择性通知(锁可以绑定多个条件): synchronized关键字与wait()notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。

四、ReadWriteLock 读/写锁 

       读/写锁:当一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行

读锁:共享锁 readLock 

写锁:**独占锁 writeLock

ReentrantReadWriteLock:

读可以共享,提升性能 同时多人读

写 只能由一个线程写入

缺点:

1.造成锁的饥饿,可能一直读没有写的操作

2.写的时候,自己线程可以读,读的时候,哪个线程都不可以写

读写锁的基本实现原理:

        从表面来看,ReadLock和WriteLock是两把锁,实际上它只是同一把锁的两个视图而已。什么叫两个视图呢?可以理解为是一把锁,线程分成两类:读线程和写线程。读线程和读线程之间不互斥(可以同时拿到这把锁),读线程和写线程互斥,写线程和写线程也互斥。

        从ReentrantReadWriteLock 的构造方法中可知,readerLock 和 writerLock 实际共用同一个 sync 对象。sync 对象同互斥锁一样,分为非公平和公平两种策略,并继承自AQS

    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

五、锁的类型

锁的类型

锁/类型公平/非公平锁可重入/不可重入锁共享/独享锁乐观/悲观锁
synchronized非公平锁可重入锁独享锁悲观锁
ReentrantLock都支持可重入锁独享锁悲观锁
ReentrantReadWriteLock都支持可重入锁读锁-共享,写锁-独享悲观锁

1、 公平锁和非公平锁

        公平锁表示线程获取锁顺序是按照线程加锁的顺序来分配的,等待最久的线程获取锁,即FIFO顺序 。而非公平锁就是一种获取锁的抢占机制,是随机获得锁的。有可能后申请的线程比先申请的线程优先获取锁,可能会造成优先级反转或者饥饿现象。

2、 可重入锁与不可重入锁

        当一个线程获取到当前 对象或类锁后,还能否重复获取到该锁。

        可重入锁是指:可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class)。

        不可重入锁是指:与重入锁相反,不可递归调用,递归调用就发生死锁

3、共享锁与独享锁

        独享锁:该锁每一次只能被一个线程所持有。

        共享锁:该锁可被多个线程所持有。典型的就是ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁确每次只能被独占。

4、乐观锁和悲观锁      

锁从宏观上分类,分为悲观锁与乐观锁。

悲观锁:

        总是假设最坏的情况,每次拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想要拿到它的数据就会被一直阻塞直到它拿到锁,传统的关系型数据库里面就用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。再比如java里面的synchronized关键字的实现也是悲观锁。

乐观锁:

        顾名思义,很乐观,每次拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会去判断一下别人有没有修改这个数据,可以使用版本号等机制。乐观锁适用于多读的应用场景,这样可以提高吞吐量,像数据库提供的类似于write_condition机制就是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

5、分段锁

        ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护者一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。
 

6、偏向锁/轻量级锁/重量级锁

 java 中的锁 -- 偏向锁、轻量级锁、自旋锁、重量级锁_朱清震的博客-CSDN博客_java 属性锁

重量级锁是悲观锁的一种,自旋锁,轻量级锁和偏向锁属于乐观锁。

锁的升级过程:Java锁升级_zycxnanwang的博客-CSDN博客_java锁升级

偏向锁:它会偏向第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁,它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM就会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。

     根据偏向锁对象头的存储结构:

  1. 首先检查对象头Mark Word中锁标记是否是偏向锁
  2. 检查对象头中记录的线程ID是否是当前线程的ID,如果是说明当前线程已经获得过锁,当前线程将再次获得锁,可以执行同步代码
  3. 如果对象头中的线程ID不是当前线程的ID,则通过CAS操作替换成当前线程的ID,如果替换成功意味着当前线程获得了锁,可以执行同步代码
  4. 如果步骤3的CAS操作失败,则意味着已经有别的线程获得了锁,针对这个锁出现了竞争,当已经获得了锁的线程到达全局安全点后(没有字节码执行)会被挂起,偏向锁膨胀为轻量级锁,被挂起的线程被唤醒,线程将按照轻量级锁的机制竞争锁

轻量级锁:轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;在存在锁竞争的情况下,不需要让线程在阻塞与唤醒状态间切换,它的对象头存储结构如下:除了对象头,轻量级锁还有一个相关的存储结构Monitor Record,它是JVM在栈中开辟出的一块空间,里面会保存获得锁的线程信息,而对象头中记录的锁指针就指向这个Monitor Record。

轻量级锁的获取

  1. JVM在执行同步代码块前,会在栈中开辟一块空间存储锁记录Monitor Record,并将对象头中的Mark Word复制到锁记录中,称为Displaced Mark Word。
  2. 线程通过CAS操作尝试将Mark Word指向锁记录,如果成功意味着线程获得了锁,Monitor Record中会有一个字段Owner记录获得锁的线程信息
  3. 如果步骤2中的CAS操作失败,则线程进入自旋等待(默认10次),如果自旋成功,则线程获得了锁可以执行同步代码,如果自旋失败,这个锁会膨胀成重量级锁
  4. 线程执行完成后,将通过CAS操作将Monitor Record中记录的Displaced Mark Word替换回对象头中的Mark Word,如果操作成功则锁被释放,如果操作失败,则意味着存在锁竞争,这个锁将膨胀成重量级锁
     

重量级锁:重量级锁在JVM中有一个监视器(Monitor),保持了两个队列:锁竞争队列和信号阻塞队列,一个实现线程互斥,另一个实现线程同步。重量级锁在底层是靠操作系统的Mutex Lock实现的,线程在阻塞和唤醒状态间切换需要操作系统将线程在用户态与核心态之间转换,成本很高,所以最早的synchronized效率不高。

查看锁的变化方式:

Java中的偏向锁,轻量级锁, 重量级锁解析_萧萧九宸的博客-CSDN博客_java 偏向锁 轻量级锁 重量级锁

7. 自旋锁 

自旋锁:自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值