在多线程编程中,我们在写代码中不可避免地涉及到锁的使用,那么什么是锁呢?
所谓的锁,在我通俗化的理解是这样的,就是线程在执行一段需要同步的代码(也就是会有线程安全问题)时,为了避免其他线程也执行这段代码,导致线程不安全,就需要给这个线程加锁,其他代码访问时,如果没有持有锁,就不能执行,当执行完成后,就需要释放锁。让其他线程继续去争夺锁。
锁的持有线程在其获得锁之后和释放锁之前这段时间内所执行的代码被称为临界区(Critical Section)。因此,共享数据只允许在临界区内进行访问,临界区一次只能被一个线程执行。
锁具有排他性,即一个锁一次只能被一个线程持有。因此这种锁称为排他锁或者互斥锁。
Java虚拟机对锁的实现方式划分,Java平台中的锁包括内部锁(Intrinsic Lock) 和显式锁(Explicit Lock)。
内部锁是通过synchronized关键字实现的;
显式锁是通过 java.concurrent.locks.Lock接口的实现类如java.concurrent.locks.ReentrantLock类实现。锁的作用:
锁能够保护共享数据来实现线程安全,其作用就是保障原子性,可见性和有序性。
对原子性的保障
对原子性的保障由于锁具有互斥性,就是说,一次只能够被一个线程持有,一个线程持有时,其他线程无法持有,因此能够保障原子性。
可见性的保障
而可见性的保障是通过写线程冲刷处理器缓存和读线程刷新处理器缓存来实现的,在java中,锁的获得隐含着刷新处理器这个动作,能够使得读线程在执行临界区代码之前(锁获得之后)可以将写线程对共享变量所作的更新同步到该线程执行处理器的高速缓存中,而锁的释放隐含着冲刷处理器缓存的动作,使得写线程对共享变量所做的更新能够被推送到执行处理器的高速缓存中,从而对线程可同步。
锁的互斥性和保障可见性合在一起,能够保障临界区内代码能够读取到共享数据的最新值。因为同一个锁保护共享数据一次只能被一个线程访问,因此线程在临界区访问到是相对新值也是最新值,因为我们说是最新值。对于引用型变量也可以保障读取到最新值。
有序性如何保障
有序性是指代码执行的顺序跟代码看上去的顺序是一致的,因为这里说的保障有序性是指,对于对读线程来说,当写线程对共享变量操作并加锁时,由于锁具有原子性,加锁后,临界区的代码执行是一个原子性操作,对于读线程来说,由于具备可见性,因此可以说,对读线程来说,执行下来说有序的,保障有序性。但是在临界区内的代码执行中,不保障有序性,指令不保证不会重排序。
锁在java中的性质
1.可重入性
一个线程在持有一个锁的时候能否再次申请该锁。具备可重入性的叫做可重入锁,比如ReEntrantLock,Synchronized 都是可重入锁。伪代码演示
void meTheadA() {
acquireLock(lock); //II申请锁lock
//II省略其他代码
methodB();
releaseLock(lock); //II释放锁lock
}
void metheadB () {
acquireLock(lock); //申请锁 lock
//省略其他代码
releaseLock(lock}; //释放锁 lock
}
可重入锁实现的原理
可重入锁可以被理解为一个对象,该对象包含一个计数器属性。计数器属性的初始值为 o, 表示相应的锁还没有被任何线程持有。 每次线程获得一个可重入锁的时候, 该锁的 计数器值会被增加1。 每次一个线程释放锁的时候, 该锁的计数器属性值就会被减I。 一 个可重入锁的持有线程初次荻得该锁时相应的开销相对大,这是因为该锁的持有线程必须与其他线程 ”竞争” 以获得锁。 可重入锁的持有线程继续荻得相应锁所产生的开销要小得 多,这是因为此时Java虚拟机只需要将相应锁的计数器属性值增加1即可以实现锁的获得。
2.互斥性
一次只能被一个线程持有。
3.锁的争用与调度
Java平台中锁的调度策略也包括公平策略和非公平策略,相应的锁就被称为公平锁和非公平锁。 内部锁属于非公平锁, 而显式锁则既支持公平锁又支持非公平锁。主要就是根据线程启动的先后顺序去争用
4.锁的粒度
就是指锁保护的共享数据的数量的大小,或者说代码的长度。
java中的锁的区别
内部锁
内部锁是通过synchronized关键字实现的。synchronized关键字可以用来修饰方法以及代码块(花括号 "{}"包裹的代码)。synchronized关键字修饰的方法就被称为同步方法 (SynchronizedMethod) 。 synchronized修饰的静态方法就被称为同步静态方法, synchronized修饰的实例方法就被称为同步实例方法。 同步方法的整个方法体就是一个临界区.
作为锁句柄的变量通常采用final修饰。 这是因为锁句柄变量的值一旦改变, 会导致执行同一个同步块的多个线程实际上使用不同的锁,从而导致竞态。有鉴于此, 通常我们会使用private修饰作为锁句柄的变量。
作为锁句柄的变量通常采用private final修饰,如:
private final Object lock = new Object();
内部锁的使用并不会导致锁泄漏。这是因为 Java 编译器 (javac) 在将同步块代码编译为字节码的时候,对临界区中可能抛出的而程序代码中又未捕获的异常进行了特殊(代为)处理,这使得临界区的代码即使抛出异常也不会妨碍内部锁的释放。
关于synchronied后面我会补上一篇详细原理的介绍:
这里分享一篇我觉得写的很好的博客:
https://blog.csdn.net/javazejian/article/details/72828483
内部锁的调度
由于 Java 虚拟机对内部锁的调度仅支持非公平调度,被唤醒的等待线程占用处理器运行时可能还有其他新的活跃线程 (处于 RUNNABLE 状态,且未进 入过入口集)与该线程抢占这个被释放锁,因此被唤醒的线程不一定就能成为该锁的持有 线程。另外,Java 虚拟机如何从一个锁的入口集中选择一个等待线程,作为下一个可以参与再次申请相应锁的线程,这个细节与 Java 虚拟机的具体实现有关:这个被选中的线程有可能是入口集中等待时间最长的线程,也可能是等待时间最短的线程,或者完全是随机的一个线程。因此, 我们不能依赖这个具体的选择算法。
显示锁
显式锁是自 JDK 1.5 开始引入的排他锁。是java.util.concurrent.lcoks.Lock 接口的实例。该接口对显式锁进行了抽象,类 java.util.concurrent.lcoks.ReentrantLock 是Lock 接口的默认实现类。ReentrantLock 是一个可重入锁。在访问共享数据前申请相应的显式锁。这一步,我们直接调用相应Lock.lock()即可。在临界区中访问共享数据。 Lock.lock()调用与Lock.unlock()调用之间的代码区域为临界区。 不过, 一般我们视上述的 try 代码块为临界区。 因此, 对共享数据的 访问都仅放在该代码块中。
共享数据访问结束后释放锁。虽然释放锁的操作通过调用Lock.unlock()即可实现, 但是为了避免锁泄涌, 我们必须将这个调用放在 finally 块中执行。 这样, 无论是临界区代码执行正常结束还是由于其抛出异常而提前退出,相应锁的 unlock 方法总是可以被执行,从而避免了锁泄漏。 可见, 显式锁不像内部锁那样可以由编译器代为规避锁泄漏问题。
显式锁的调度
ReentrantLock 既支持非公平锁也支持公平锁。ReentrantLock 的一个构造器的签名如下ReentrantLock(boolean fair)
该构造器使得我们在创建显式锁实例的时候可以指定相应的锁是否是公平锁 (fair参数值 true 表示是公平锁)。
公平锁保障锁调度的公平性往往是以增加了线程的暂停和唤醒的可能性,即增加了上下文切换为代价的。因此,公平锁适合于锁被持有的时间相对长或者线程申请锁的平均间隔时间相对长的情形。总的来说使用公平锁的开销比使用非公平锁的开销要大,
因此显式锁默认使用的是非公平调度策略。
显式锁与内部锁的比较
1.灵活性
内部锁是基于代码块的锁,因此其使用基本无灵活性可言:要么使用它,要么不使用它,除此之外别无他选。而显式锁是基千对象的锁,其使用可以充分发挥面向对象编程的灵活性。而内部锁从代码角度看仅仅是一个关键字,它无法充分发挥面向对象编程的灵活性。比如,内部锁的申请与释放只能是在一个方法内进行(因为代码块无法跨方法), 而显式锁支持在一个方法内申请锁,却在另外一个方法里释放锁。
2.内部锁基于代码块的这个特征也使其具有一个优势:简单易用,且不会导致锁泄漏。
3.显式锁支持了一些内部锁所不支持的特性
如果一个内部锁的持有线程一直不释放这个锁(这通常是由于代码错误导致的),那么同步在该锁之上的所有线程就会一直被暂停而使其任务无法进展。而显式锁则可以轻松地避免这样的问题。Lock 接口定义了一个 tryLock 方法。 该方法的作用是尝试申请相应Lock 实例锁表示的锁。如果相应的锁未被其他任何线程待有,那么该方法会返回 true, 表示其获得了相应的锁;否则,该方法并不会导致其执行线程被暂停而是直接返回 false, 表示其未获得相应的锁。
4.在锁的调度方面,内部锁仅支持非公平锁;而显式锁既支持非公平锁,又支持公平锁。
显式锁与内部锁在性能方面的差异主要包括:
Java 1.6/1. 7对内部锁做了一些优化, 这些优化在特定情况下可以减少锁的开销。 这些优化包括锁消除(LockElimination)、 锁粗化(LockCoarsening)、偏向锁 (Biased Lock)和适配性锁(AdaptiveLock) ,
在Java 1.6/1.7中并没有运用到显式锁上。不过, 这并不排除Java的后续版本会在显式锁中引入这些优化(可能只是部分) 80
在Java 1.5中,在高争用的情况下,内部锁的性能急剧下降, 而显式锁的性能下降则少得多。 换而言之,Java 1.5中显式锁的可伸缩性(Scalability)比内部锁的可伸缩性要好。 到了Java 1.6, 随着JDK对内部锁所做的一些改进,显式锁和内部锁之间的可伸缩性差异已经变得非常小了。
关于锁的选用:
内部锁的优点是简单易用,显式锁的优点是功能强大,这两种锁各自都存在一些弱势。
一般来说, 新开发的代码中我们可以选用显式锁。但是选用显式锁的时候需要注意: 显式锁的不正确使用会导致锁泄漏这样严重的问题;线程转储可能无法包含显式锁的相关信息, 从而导致问题定位的困难。
另外, 我们也可以使用相对保守的策略—默认情况下选用内部锁, 仅在需要显式锁所提供的特性的时候才选用显式锁。比如,在多数线程持有一个锁的时间相对长或者线程 申请锁的平均时间间隔相对长的情况下使用非公平锁是不太恰当的,因此我们可考虑使用公平锁(显式锁)。