浅谈java中的各种锁

概述

顾名思义,锁就是将一种资源锁住,防止其他线程同步修改。锁机制是保证获取数据有序性的重要手段,为此java提供了大量的锁。无论是在面试或学习中,会遇到各种锁,如对象锁与类锁、轻量级与重量级锁、悲观锁与乐观锁等等。

1.对象锁与类锁

相信大家对synchronized关键字并不陌生,synchronized在jdk1.6之前是一个重量级锁,而1.6以后做了了很大的优化,synchronized分为了四种锁状态,分别是无锁、偏向锁、轻量级锁、重量级锁,极大提高了synchronized的性能。

对象锁与类锁是通过synchronized实现的,那么具体是如何实现呢?又有何区别?

通过名称可以看出对象锁是作用于对象上的,那么多个对象实例各自拥有属于自己的锁,多个线程访问多个对象实例时,互不影响。而类锁是作用于类上的,属于这个类的多个对象实例共同使用同一把锁,多个线程访问时,只有一个线程可以获得锁
在这里插入图片描述

代码实现
类锁
/**
 * @author yz
 * @version 1.0
 * @date 2021/1/7 15:17
 */
public class ClazzLock {


	/**
	 * 通过synchronized修饰静态方法
	 */
    public synchronized static void method(){

    }

	/**
	 * 通过xxx.class充当锁
	 */
	public void method2(){
		synchronized (ClazzLock.class){

		}
	}

	public static Object obj=new Object();

	/**
	 * 通过静态变量充当锁,因为静态变量属于类变量,在jvm中只有一份
	 */
	public void method3(){
    	synchronized (obj){
		}
	}

}
对象锁
/**
 * @author yz
 * @version 1.0
 * @date 2021/1/7 15:29
 */
public class ObjectLock {


    /**
     *通过synchronized修饰普通方法
     */
    public synchronized void method(){

    }

    /**
     * 通过当前实例充当锁
     */
    public void method2(){
        synchronized (this){

        }
    }

    public Object obj =new Object();

    /**
     * 通过普通对象充当锁
     */
    public void method3(){
        synchronized (obj){

        }
    }
}

通过代码可以发现,实现类锁的代码中,无论是静态方法、静态变量或类信息,在JVM中只有一份的,和对象实例并没有关系。而对象锁的代码中,无论是普通方法、变量或自身实例,都是属于对象实例的。

synchronized是如何实现锁机制的呢?

JVM是基于Monitor对象来实现synchronized同步的,通过javap -v 对类锁代码反编译,可以发现method方法中有一个标识ACC_SYNCHRONIZED,JVM通过此标识判断一个方法是否为同步方法。而method2方法中多了monitorenter和monitorexit两个指令,说明同步代码块是通过这两个指令来实现的,线程通过monitorenter获取锁和monitorexit释放锁。
在这里插入图片描述

2.乐观锁与悲观锁

乐观锁与悲观锁只是一种概念,用来指导具体的实现。

乐观锁

对数据的修改是乐观的,每次操作数据时不会被其他线程修改。不会进行锁定,只有在对数据进行修改时判断数据是否已经被修改过。乐观锁适用于读多写少的应用场景,对于高并发的应用较为友好。
在这里插入图片描述
通过上图可以看出,乐观锁其实并没有进行加锁操作,乐观锁是无锁的,只是在修改数据时进行和原值进行判断。如java中的CAS算法和数据库中通过version实现乐观锁。

CAS算法

CAS算法只涉及到三个值
进行更新的值(更新值)
需要比较的值(比较值)
新值

当更新值==比较值时,将更新值设置为新值,如果更新值!=比较值时,则什么都不做。CAS十分简单和高效,但是也会存在一些问题,如ABA问题等,ABA问题可以采用version解决,这里暂不做详细介绍。

悲观锁

对数据的修改是悲观的,每次操作数据时都会被其他线程修改。每次操作都会进行锁定,将其他线程进行堵塞。乐观锁适用于写多读少的应用场景。对高并发的应用不友好
在这里插入图片描述
通过上图可以看出,只有一个线程可以获取锁,其他线程会进入堵塞状态。当线程过多时,必然会出现问题,所以在高并发应用中很难看到悲观锁的应用。

3.可重入锁与不可重入锁

可重入锁

锁可以重复获取,当一个线程获取锁后,将计数器进行+1,线程执行过程中再次获取锁时,会判断该锁是否已经被获取,如果获取该锁的是当前线程,那么计数器会再次+1,当计数器归零时,该锁被释放

不可重入锁

锁不可以被重复获取,当一个线程获取锁后,无论是其他线程还是当前持有该锁的线程,都无法获取或再次获取该锁,只能进入堵塞状态。不可重入锁十分笨重,容易造成死锁

4.独享锁与共享锁

独享锁

独享锁又称互斥锁,是指一个锁只能被一个线程持有。主要用来写数据,列如ReentrantLock的写锁。

共享锁

一个锁可以被多个线程持有。主要用来读数据,列如ReentrantLock的读锁。

4.自旋锁

当一个线程获取锁时,该锁已经被其他线程持有,当前线程会进行自旋等待该锁被释放,而不是进入睡眠状态。自旋锁适用于执行时间短的操作,效率高于互斥锁。但是自旋锁可能会导致死锁。
优点:在执行时间短的操作中,不需要切换线程上下文, 效率高
缺点:在执行时间长的操作中,锁短时间内没有被释放,线程会一直自旋,造成性能浪费

5.分段锁

顾名思义,将数据分段加锁,不会将整个数据锁住,适用于线程较多的场景。ConcurrentHashMap、LongAdder采用了分段锁的实现机制。

6.公平锁与非公平锁

公平锁

每个线程获取锁的机会都是公平的,当一个线程获取锁时,该锁已经被其他线程持有,就会进入等待队列,等待过程中如果还有其他线程获取锁,则会进入等待队列进行排队。公平锁的获取是有序的,缺点就是性能不如非公平锁。

非公平锁

每个线程获取锁的机会是不公平的,当一个线程获取锁时,该锁已经被其他线程持有,就会进入等待队列,等待过程中如果还有其他线程获取锁,在获取锁时,锁刚刚被释放,则会被该线程获取,不会对等待队列中的线程进行唤醒。非公平锁的获取是无序的,可以进行插队,就像常说的来的早不如来的巧,对于等待中的线程是不公平的。优点是在吞吐量高的应用中比公平锁性能高。

7.分布式锁

在微服务大行其道的时代,分布式锁是保证数据一致性的重要手段。一般会采用数据库、redis、zookeeper三种方式进行实现,而redis是最常用的实现方案,其中redission基于redis实现了分布式锁,在实际开发中经常通过redission使用分布式锁。分布式锁主要是通过将锁放在数据源中,多个服务共同竞争同一个锁,从而达到数据一致性的目的。

8.锁消除与锁粗化

锁消除与锁粗化都是对锁的一种优化

锁消除

当JVM在编译代码时,通过逃逸分析,会检测加锁代码是否存在竞争,如果不存在竞争问题,那么加锁毫无意义,只会消耗性能,JVM在编译时会对不存在竞争的锁进行消除。

锁粗化

锁的作用范围越小越好,但是如果在操作中反复进行加锁和解锁,会增加性能消耗。在不影响数据安全的前提下,可以将锁的作用范围扩大,降低性能消耗。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

回家放羊吧

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值