java各大框架学习笔记教程(Java基础多线程)
在计算机系统中,锁(lock)或互斥(mutex)是一种同步机制,用于在有许多执行线程的环境中强制对资源的访问限制。锁旨在强制实施互斥排他、并发控制策略。
多数情况下,锁都是硬件指令运行的。
锁相关的概念- 锁开销(lock overhead),锁占用内存空间、 cpu初始化和销毁锁、获取和释放锁的时间。
- 锁竞争(lock contention),一个进程或线程试图获取另一个进程或线程持有的锁,就会发生锁竞争。
- 死锁(deadlock),至少两个任务中的每一个都等待另一个任务释放其持有的锁的情况。
- 粒度(Granularity),衡量锁保护的数据量大小。粒度大的情况开销小但竞争大,粒度小的情况开销大但竞争小。
- 竞态条件:多线程的核心矛盾是“竞态条件”,即多个线程同时读写某个数据。
- 竞态资源:竞态条件下多线程争抢的是“竞态资源”。
线程间通信的四个方法,临界区,互斥量,信号量,事件。
- 临界区(Critical Section)
- 互斥体(Mutex/mutual exclusion)
- 信号量(Semaphore/binary semaphore)
- 事件(Event)
Java提供了种类丰富的锁,每种锁因其特性的不同,在各自适当的场景下能够展现出非常高的效率。
宏观的锁类型
- 悲观锁,在获取数据的时候会先加锁,确保数据不会被别的线程修改。synchronized关键字和Lock的实现类都是悲观锁。用于写操作多的场景。
- 乐观锁,使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。用于读操作多的场景。
JIT编译器(Just In Time编译器)可以在动态编译同步代码时,使用一种叫做逃逸分析的技术,可以判别程序中所使用的锁对象与线程的关系。
- 锁消除,如果只有一个线程使用锁对象,编译器在编译这个同步代码时就消除了锁的使用流程。减少不必要的加锁消耗。
- 锁粗化,若发现若发现前后相邻的同步块使用的是同一个锁对象,那么它就会把这几个同步块给合并为一个较大的同步块。减少频繁申请与释放锁。
对未获得锁的线程等待策略进行优化,来避免操作系统进程调度和线程切换带来的开销:
- 自旋锁,当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。
- 适应性自旋锁,自旋的时间(次数)不再固定,由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。前一次自旋等待成功了,则进入自旋等待,否则直接进入阻塞的状态。
计算机科学家们使用了各种方式来实现排队自旋锁,如TicketLock,MCSLock,CLHLock。
线程获取锁的策略为了减少唤起线程的开销,让代码整体的吞吐效率高,有一种优化策略是,让线程上来就直接尝试占有锁,如果尝试失败,就再去排队,这就是非公平锁。
普通的公平锁的策略是,每个线程在获取锁时会先查看此锁维护的等待队列,如果有等待,则会加入到等待队列中。此队列是FIFO的规则。
非公平锁策略因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。所以开销小,效率高。但是同时可能让已经在等待中的线程等待太久。
Java并发包中的ReentrantLock,默认情况下是非公平锁,如果使用new ReentrantLock(true),则是公平锁。synchronized 也是非公平锁。
锁的状态目前锁一共有4种状态,级别从低到高依次是:无锁、偏向锁、轻量级锁和重量级锁。锁状态只能升级不能降级。
synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。
四种锁状态对应的的Mark Word内容:
锁状态 |
存储内容 |
存储内容 |
无锁 |
对象的hashCode、对象分代年龄、是否是偏向锁(0) |
01 |
偏向锁 |
偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) |
01 |
轻量级锁 |
指向栈中锁记录的指针 |
00 |
重量级锁 |
指向互斥量(重量级锁)的指针 |
10 |
- 无锁,没有对资源进行锁定,但同时只有一个线程能修改,其他修改失败的线程会不断重试。
- 偏向锁,一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。其他线程尝试竞争偏向锁时,等到全局安全点后才撤销,可恢复到无锁或升级到轻量级锁。
- 轻量级锁,是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
- 重量级锁,当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。此时等待锁的线程都会进入阻塞状态。
同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁,不会因为之前已经获取过还没释放而阻塞,可一定程度避免死锁。这就是可重入锁又名递归锁。
相对来说,仍然需要获取而阻塞则是非可重入锁。
Java并发包中的ReentrantLock和synchronized都是可重入锁,NonReentrantLock是非可重入锁。
可重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,计数器为0时可调用并标识为1,其他线程将等待,退出时状态递减,状态为0时释放锁。同一个线程多数获取则状态递增。
对锁的占用方案并非所有场景都是只允许资源只能被一个线程独占。所以出现了可以被多个线程所共有的共享锁。
- 独享锁是指该锁一次只能被一个线程所持有。Java中实现此方案的叫互斥锁,也就是ReentrantLock。
- 共享锁是指该锁可被多个线程所持有。Java中实现此方案的叫读写锁,也就是ReadWriteLock。
在ReentrantReadWriteLock里面,读锁和写锁是分离的,读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
- AQS
抽象队列同步器(AbstractQueuedSynchronizer,简称AQS)是用来构建锁或者其他同步组件的基础框架,它使用一个整型的volatile变量(命名为state)来维护同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
Java对象附加的锁数据- 对象头
主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。
Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
Klass Point:对象指向它的类的元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
- Monitor
Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。
Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
,免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com