在多线程或高并发情境中,经常会为了保证数据一致性,而引入锁机制,本文将为各位带来有关锁的基本概念讲解。关注我的公众号「Java面典」了解更多 Java 相关知识点。
根据锁的各种特性,可将锁分为以下几类:
乐观锁与悲观锁并不是特指某两种类型的锁,是人们定义出来的概念或思想,主要是指看待并发同步的角度。
前提:认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁;
实现:在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。
应用:在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式CAS(Compare and Swap 【比较并交换】)实现的。CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
前提:认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改;
实现: 总是假设最坏的情况,以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会阻塞直到拿到锁;
应用:Java中的 Synchronized 就是悲观锁,AQS 框架下的锁则是先尝试 CAS 乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。
悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升;
悲观锁在 Java 中的使用,就是利用各种锁;
乐观锁在 Java 中的使用,是无锁编程,常常采用的是 CAS 算法,典型的例子就是原子类,通过 CAS 自旋实现原子操作的更新。
定义: 独享锁是指该锁一次只能被一个线程所持有;
特点:独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。
应用:ReentrantLock 就是以独占方式实现的互斥锁。
定义:共享锁是指该锁可同时被多个线程所持有,并发访问、共享资源;
特点:共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源;
应用:
定义:可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。
应用:在 JAVA 环境下 ReentrantLock 和 synchronized 都是可重入锁。
加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先得。
加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自动到队尾等待。
分段锁也并非一种实际的锁,而是一种思想 ConcurrentHashMap 是学习分段锁的最好实践。
这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让他申请的线程进入阻塞,性能降低。
在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
如果锁的竞争激烈,或者占用锁时间长短的代码块,不适合使用自旋锁。
**同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 CPU 的线程又不能获取到 CPU,造成 CPU 的浪费。所以这种情况下我们要关闭自旋锁。在 JDK1.5 及之前自旋时间是固定的,从 JDK1.6 开始,引入了适应性自旋锁。
在Java中,需要谨慎使用锁。如无必要,不用最好;必须要用的话,也需要尽可能优化锁的使用,以此来提高程序的吞吐量。关于锁的优化,主要分为应用方面的优化与 JVM 方面的优化,JVM方面的优化,一般不需要开发人员操心,开发人员更应该提升自身代码素质,关注应用方面的优化。
Java多线程并发03——什么是线程上下文,线程是如何调度的
Java多线程并发02——线程的生命周期与常用方法,你都掌握了吗
原文:https://www.cnblogs.com/weechang/p/12539361.html