java中都有哪些锁?

      相信大家在学习java的过程中, 一定听到过很多, 关于java中的锁 , 但这些锁并不完全都是指一把真正的锁, java中的锁有的是指锁的实现, 而有的指的是锁的特性, 下面来介绍

目录

乐观锁与悲观锁

死锁

可重入锁

读写锁

 分段锁

自旋锁

共享锁与独占锁

AQS

公平锁与非公平锁

偏向锁, 轻量级锁与重量级锁

java对象头

Synchronized

ReetrantLock


乐观锁与悲观锁

乐观锁与悲观锁其实指的是看待并发问题的角度,并不是锁实现

乐观锁 : 从名字上我们就可以看出, 乐观锁对于并发问题看待的角度是乐观的 , 它认为我们不显式加锁的操作是没有事情的, 对同一数据的并发操作是不影响的, 在更新数据时, 大多会采用自旋的方式更新数据(不断尝试更新:CAS)

悲观锁 : 顾名思义, 悲观锁认为我们对同一数据的并发操作肯定会有问题, 所以悲观锁就是采用显式加锁的方式来解决问题, 悲观的认为不加锁的并发操作肯定会有事

       在大多并发操作中 , 一般可以分为读操作和写操作 , 读操作并不会影响数据的安全性, 只有写操作会影响 , 所以, 如果在读操作很多的情况下, 我们可以采用乐观锁的思想, 同样的, 如果写操作很多的情况下, 我们就采用悲观锁的思想

        我们知道, 加锁虽然保证了安全问题, 但是同样带来了效率变低的问题 , 所以灵活运用乐观锁的思想可以提高效率, 例如CAS机制, 它采用自旋不断尝试更新的操作, 大大提升了效率, 而悲观锁的方式就是显式加各种锁(synchronized等)

死锁

     死锁指的是不同的线程相互占用对方所需的共享资源不释放, 导致两个线程卡住, 无法运行, 两个线程都在等待对方释放自己需要的资源,  出现死锁时, 不会有异常错误等, 而是导致这些线程阻塞,无法运行, 在程序设计时应该尽量避免死锁问题

       导致死锁出现的问题主要是对共享资源的抢占, 在程序时 ,我们应尽量减少锁的循环嵌套等, 通过死锁产生的一些必要条件入手, 避免死锁, 例如资源一次性分配等, 破坏请求条件, 这样可以有效避免死锁的发生

可重入锁

        可重入锁又叫递归锁, 它指的是同一个线程如果在外层获得锁时, 那么在进入内层方法时也会自动获得锁, java中的ReentrantLook和Synchronized都是可重入锁

          上面说的是什么意思呢? 我们来解释如下:

         如图 , 这里有两个同步方法A和B , 在A中调用B , A方法先拿到锁, 调用B方法 , B方法也为同步方法 , 因为Synchronized是可重入锁, 所以B方法此时也自动获得锁, 可以获得执行权. 

         那么如果不是可重入锁会怎样呢? 不是可重入锁, A调用B , 到了B执行的时候, 此时A方法没有执行完 ,不会释放锁, 但是B方法又需要这把锁, 一直等待A释放, A不释放, B也一直拿不到, 这样就形成了死锁, 造成线程卡死无法继续执行

读写锁

           跟上面的都不同, 读写锁(ReadWriterLock)指的是一种锁实现 , 它的特点是 1. 多个读者可以同时读  2. 写者必须互斥(读写也互斥)  3 . 写者优先于读者(一旦有写者, 则写者优先,后续读者等待)

        java中的 ReaderWriteLock是一个接口 

其中 , ReentrantReadWriteLock 类 实现了这个接口, 它是一个可重入读写锁, 使用ReetrantReadWriteLock实现读写锁如下 : 

public class ReentrantReadWriteDemo {
    private int data;
    private ReadWriteLock rwl = new ReentrantReadWriteLock();

    //写操作
    public void set(int data) {
        rwl.writeLock().lock(); //拿到写锁
        try {
            System.out.println(Thread.currentThread().getName() + "准备写入数据");
            this.data = data;
            System.out.println(Thread.currentThread().getName() + "写入" + this.data);
        } finally {
            rwl.writeLock().unlock(); //释放写锁
        }
    }

    //读操作
    public void get() {
        rwl.readLock().lock(); //拿到读锁
        try {
            System.out.println(Thread.currentThread().getName() + "准备读数据");
            System.out.println(Thread.currentThread().getName() + "读取" + this.data);
        } finally {
            rwl.readLock().unlock(); //释放读锁
        }
    }
}

 分段锁

        分段锁也也是一种锁的思想 , 指的是将数据分段, 进一步细化锁 ,在每个分段上单独加锁, 用以提高并发效率

      java中的ConcurrentHashMap 在jdk 1.8之前采用的就是分段锁的机制, 在1.8之后采用了锁分段的机制替换分段锁(关于此点在我前几篇文章: JUC常用类中提到)

那么怎样理解分段锁的好处呢 ? 如下图

      如果我们的共享数据是一个hashMap , 它底层是一个hash表, 每个hash表存储一个结点 , 采用分段锁的思想 , 在每个结点上单独加锁 , 如果此时有两个线程对hashMap进行操作, 第一个线程去操作A结点, 那么此时第二个线程是不是可以去操作A结点后续的B,C等结点 . 如果不采用分段锁的思想, 当第一个线程在hashMap中操作时, 那么其他线程只能在外等待. 这两种方式造成的效率对比可想而知

      当然分段锁的做法也有不足, 不然也不会在ConcurrentHashMap中 1.8之后弃用这种做法, 在每个分段上加锁, 可能导致浪费内存空间, 并且hashMap中竞争同一把锁概率也较小, 有可能反而导致效率低下, 但是这种做法的优点也很明显

自旋锁

       自旋锁其实并不是一种锁的特性, 自旋锁其实指的是一种思想 , 自旋就是不断循环重试, 例如 : CAS机制(关于CAS可以去看我之前的文章:并发编程核心问题),  不断循环尝试获得锁, 如果循环多次还没有拿到锁 , 那么就使该线程阻塞

       可想而知, 不断自旋会极大消耗CPU ,在低并发 , 加锁时间较短的情况下, 我们就可以采用自旋的思想, 不阻塞线程 ,提高效率 , 在jdk 1.8之后ConcurrentHashMap中就使用了自旋锁的思想 ,采用了CAS机制来添加数据

共享锁与独占锁

共享锁与独占锁同样指锁的特性 

   共享锁 : 锁可以被多个线程共享, 可以并发访问共享资源
 

   独占锁 : 也叫互斥锁, 锁只能被一个线程占用, 与其他线程互斥

         java中的Synchronied和ReentrantLook 都是独占锁, 但是在ReadWritLock接口下的实现类ReentrantReadWriteLock(读写锁)而言 , 它的读锁是共享锁, 写锁是独占锁 , 它底层的独占和共享是由 AQS 实现的, 通过不同的方法实现独占或共享

AQS

         AbstractQueuedSynchronizer(抽象队列同步器)这个类在 java.util.concurrent.locks 包, 上面说的ReentrantReadWriteLock底层就有此类的对象 , 包括ReentrantLock底层也是它

         AQS的核心思想是 , 如果被请求的共享资源空闲 , 那么则将当前请求的线程设置成有效的工作线程 , 并将共享资源设置成锁定状态 , 此时共享资源被占用 ,就需要一套线程阻塞等待以及被线程被唤醒后锁分配的机制 , 这个机制是使用CLH队列锁(CLH是三位大佬发明的, 它们名字的简称, 它是一个FIFO,先进先出的双向同步队列 ,AQS依赖它完成同步) , 就是将暂时获得不到锁的线程加到等待队列中

      在AQS中 , 存在一个采用volatile修饰的共享变量state , 它代表加锁的状态 , 这个值默认为0 , 如果有线程已经加锁了 , 那么这个值会变成 1 , 这时其他线程想拿到锁, 会通过CAS机制尝试将0 改为1 , 但此时state已经为 1 了, 所以它更改失败 ,进入等待队列

公平锁与非公平锁

 公平锁与非公平锁指的也是锁的特性

 公平锁 : 指当锁被释放后, 检查是否有线程在排队等待, 优先将锁释放给等待时间较长的线程

非公平锁 : 锁被释放后 , 不考虑排队的情况, 线程直接去尝试获得锁

       java中的Synchronized是非公平锁, ReentrantLook默认是非公平锁, 但也可以通过参数设置使它变成一个公平锁 ,底层使用AQS实现

 fair 默认为false ,设置为true则是公平锁

偏向锁, 轻量级锁与重量级锁

        它们指的是锁的状态, 是 jvm 为了提高锁的效率做的优化 ,而且它们是专门针对于synchronized而言的

偏向锁 : 当一段同步代码一直被一个线程所访问, 那么此线程下次到来将会自动获得锁 , 降低此线程获得锁的代价

轻量级锁 : 当锁是偏向锁时, 此时来了另外一个线程, 这时这把锁升级为轻量级锁, 后续的所有线程不进行阻塞 ,通过自旋的方式尝试获得锁

重量级锁 : 当锁是轻量级锁时 , 此时另一个线程自旋了很多次还是没有拿到锁 , 那么这个线程此时会阻塞, 或者就是访问的线程过多, 那么此时锁升级为重量级锁 , 重量级锁将后面访问的线程都进行阻塞

这几种锁状态都在 java 的对象头中标注

java对象头

         在HotSpot虚拟机中 , 对象可分为3部分区域 : 对象头、实例数据和对齐填充  ,  对象头是实现Synchronize锁对象的基础 , Java对象头一般占有两个机器码(在 32 位虚拟机中,1 个机器码等于 4 字节,也就是32bit),对象头中包含了很多信息, 如下 : 

      可见, 锁的状态在对象头中是有标注的, 对于锁是轻量级锁还是重量级锁等

Synchronized

          java提供的一种原子性内置锁(关键字), 每个对象都可以使用它当作同步监视器, 当线程进入使用Synchronized修饰的代码块中会自动获取内部锁 , 此时其他线程想要访问此同步代码时只能被阻塞, 等待锁的释放(前一个线程执行完, 或者出现异常,调用了wait()方法等), 在进入 synchronized 会从 主内存把变量读取到自己工作内存,在退出的时候会把工作内存的值写入到主内存,保证了原子性。

       synchronized的实现是通过字节码指令完成的 , 我们可以通过反编译去看具体的底层实现

      代码块的同步是利用 monitorenter 和 monitorexit 这两个字节码指令。在虚拟机执行到monitorenter 指令时,首先要尝试获取对象的锁。 当前线程拥有了这个对象的锁,把锁的计数器+1;当执行 monitorexit 指令时将模计数器-1;当计数器为 0 时,锁就被释放了.

      另外synchronized 通过在对象头设置标记,达到了获取锁和释放锁的目的

ReetrantLock

      前面提到, ReetrantLock是一个类 , 使用 CAS + AQS实现, 它可以是公平锁, 也可以是非公平锁, 它用于显示的加锁和释放锁

          通过名字也可以看出 , 它是一个可重入锁 , 调用 lock() 方法加锁的过程如下 : 

    如果有线程通过CAS自旋拿到了锁, 那么将 底层的state状态改为1 , 其他线程通过CAS自旋发现更改不了 state ,就会进入到一个等待队列中

unlock() : 释放锁

通过头结点的状态判断唤醒下个与头结点关联的线程 

  • 15
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
Java,常见的单例模式实现方式有以下几种: 1. 饿汉式(线程安全,类加载时就初始化): ``` public class Singleton { private static Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } } ``` 2. 懒汉式(线程不安全,需要时才初始化): ``` public class Singleton { private static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } ``` 3. 双重校验式(线程安全,延迟初始化,使用双重检查机制): ``` public class Singleton { private volatile static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } } ``` 4. 静态内部类式(线程安全,延迟初始化,使用静态内部类实现懒加载): ``` public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton() {} public static Singleton getInstance() { return SingletonHolder.INSTANCE; } } ``` 5. 枚举式(线程安全,防止反序列化生成新的实例,且可以防止反射攻击): ``` public enum Singleton { INSTANCE; public void doSomething() { // ... } } ``` 以上是常见的几种单例模式实现方式。
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值