从本课开始学习并发编程的内容。主要介绍并发编程的基础知识、锁、内存模型、线程池、各种并发容器的使用。
并发编程
并发基础
锁
AQS
Synchronized
Lock
这小节咱们来学习并发编程中锁的知识。主要包括关键字synchronized
、各种Lock
、AQS
的原理、以及各自的应用。
synchronized
可以修饰方法或者代码块
表示多个线程访问该方法或者代码块时要进行排队,串行的执行该方法或者代码块
执行效率低,但是它是并发编程容器的基础
分类 | 具体分类 | 被锁的对象 | 示例代码 | 说明 |
---|---|---|---|---|
方法 | 实例方法 | 类的实例对象 | synchronized void methodA() {};<br />void methodB() {};<br />synchronized void methodC() {}; | 线程调用了同步方法,<br />别的线程可以调用非同步方法,<br />对于其他同步方法,必须该方法在执行完成后才能调用<br />不影响静态方法的调用(包括同步,非同步) |
静态方法 | 类对象 | static synchronized void methodA() {};<br />static void methodB() {};<br />static synchronized void methodC() {}; | 线程调用了同步方法,<br />别的线程可以调用非同步方法,<br />对于其他同步方法,必须该方法在执行完成后才能调用<br />不影响对象方法的调用(包括同步,非同步) | |
代码块 | 实例对象 | 类的实例对象 | synchronized(this) {} | 同上 |
class对象 | 类对象 | synchronized(SynchronizedTest.class) {} | 同上 | |
任意实例对象 | 实例对象Object | Object lock = new Object();<br />synchronized(lock) {} | 只影响锁住的对象,而不影响类和类的实例对象 |
synchronized
的实现机制JAVA对象头和Monitor是实现synchronized
的基础。
JAVA对象头,对于Hotspot虚拟机的对象头主要包含两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)
Monitor,是一种同步机制,即同一时刻只允许一个线程进入Monitor的临界区,从而达到互斥的效果。synchronized
的对象锁,其指针指向的是一个Monitor对象的起始地址。每个对象实例都有一个monitor。由C++实现,其数据结构如下。
```C++
ObjectMonitor() {
_count = 0;
_owner = NULL;
_waitSet = NULL;
_waitSetLock = 0;
_EntryList = NULL;
}
其中,**_owner**指向持有ObjectMonitor对象的线程。当多个线程同时访问一段同步代码时,会把线程存放在锁的对象的**_EntryList**中。当某个线程获得对象的Monitor时,就会把*_owner*的值设置为当前线程,同时*_count*加1。如果线程调用**wait()**方法,就会释放当前持有的Monitor,*_owner*置为null,*_count*减1,并将该线程放入**_waitSet**中。当然,如果持有monitor的线程正常执行完毕,也会释放monitor,*_owner*置为null,*_count*减1。
对于加在代码块上的synchronized
,其字节码是:一次monitorenter
、两次monitorexit
(含有一次编译器自动生成的异常处理的monitorexit
);
对于加在方法上的synchronized
,其字节码是:标识方法为ACC_SYNCHRONIZED
synchronized
是一个重量级锁,相较Lock,比较笨重,不高效。在JDK1.6中,其实现过程引入了大量的优化,如自旋锁、自适应自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁的开销。
自旋锁,是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断判断锁是否能够成功获取,直到获取到锁才退出循环。
优点:自旋锁不会使线程发生状态切换,而是一直处于活动状态,不会进入阻塞状态,减少了不必要的上下文切换,执行速度快
缺点:如果某个线程持有锁的时间过长,就会导致其他等待获取锁的线程进入循环等待,消耗CPU。如果使用不当会导致CPU使用率极高;不公平的锁会导致“线程饥饿”问题
自适应自旋锁,JDK1.6引入的更聪明的自旋锁。就是说自旋的次数不是固定的,它是由前一次在同一个锁上的自旋时间以及锁的持有者的状态决定的。JVM自适应的调整自旋次数,使能更有效的获取到锁,避免浪费资源
锁消除,当JVM检测到不可能存在共享数据竞争,此时会对这些锁进行锁消除
锁粗化,在使用锁时,需要让同步代码块的作用范围尽可能小。所谓锁粗化,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁
偏向锁,是指同一段代码一直被同一个线程访问,那么该线程会自动获取到锁,从而降低获取锁的代价
轻量级锁,当锁是偏向锁的时候,此时被另一个线程访问,偏向锁会升级为轻量级锁。其他线程会通过自旋的形式尝试获取锁,不会阻塞
synchronized
的实现依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态
Lock是一个接口,有以下方法。
public interface Lock {
void lock();
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
void lockInterruptibly();
Condition newCondition();
}
这里说下方法void lockInterruptibly()
,一个线程获得锁之后是不可以被interrupt()
方法中断的,是不能中断正在执行中的线程的,只能中断阻塞过程中的线程,lockInterruptibly
方法允许当线程等待获取锁的过程中由其他线程来中断等待。
区别:
相同点:
可重入锁,是Lock接口的唯一实现类。
可重入锁:是指如果一个线程获得了一个对象的锁,那么它不需要再获取该对象的锁而可以直接执行方法。也就是锁的分配机制是基于线程来分配的,而不是基于方法调用的分配。
可中断锁:可以响应中断的锁。只有lockInterruptibly()
方法的锁是可中断锁,lock()
还是不可中断的
公平锁:指尽量以请求锁的顺序来获取锁。比如有多个线程在等待一个锁,当锁被其他线程释放时,最先请求锁的线程会获得该锁
非公平锁:无法保证锁的获取是按照请求锁的顺序进行的。可能导致某个或者一些线程永远获取不到锁
对于ReentrantLock
,默认是非公平锁,但可指定为公平锁。
ReentrantLock lock = new ReentrantLock(true)
在ReentrantLock
中定义了两个内部类,一个是NotFairSync
,一个是FairSync
。当构造器参数为true时,表示创建公平锁,参数为false或者无参时,表示非公平锁。
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
一个用来获取读锁,一个用来获取写锁。
可重入读写锁,实现了ReadWriteLock
接口。
多个线程同时进行读操作时,会使多个线程交替进行,从而提高读操作的效率。但是,如果有线程占用读锁,此时其他要获取写锁的话,就必须等待读锁释放后才可执行;如果有线程占用写锁,此时其他线程不管是要获取读锁或者写锁的话,都必须等待写锁释放。
ReentrantLock
的FairSync
和NotFairSync
都继承了AbstractQueuedSynchronizer
,并且真正lock()
和unlock()
的实现过程都是在AQS中。
首先,AQS的数据结构是:一个表示锁状态的变量volatile int state
,取值范围是 0 无锁、1 有锁;一个用于存储等待获取锁的线程的双向链表transient volatile Node head
和transient volatile Node tail
。
其次,加锁流程NotFairSync.lock()
是:
通过CAS去尝试获取锁:判断当前state是0的话表示无锁,然后把当前线程设置为独占执行线程,再修改state为1表示有锁
acquire(1)
(这是AQS的方法)主要是三个方法:
tryAcquire
,再次尝试通过CAS获取一次锁(子类NotFairSync
的方法)
addWaiter
,把当前线程放入等待队列的双向链表Node中(通过无限循环-自旋,找到链表尾,接到尾部)acquireQueued
,通过自旋,判断当前线程是否到达链表头部,当到达头部时,就尝试获取锁。如果获取到锁就把其从头部移除
shouldParkAfterFailedAcquire
判断当前线程的状态是Cancelled
、Signal
等,从而在parkAndCheckInterrupt
中对线程进行剔除(Cancelled)、阻塞(Signal)操作
下面借两张图(<https://yq.aliyun.com/articles/640868>)
cdn.com/71e8b71038243dfaf21ebcf6f9fcc5fbaa659b08.png">
获取锁的流程:
然后,解锁的流程是调用NotFairSync.release()
,主要是对重入数量的调整。每次释放锁都只会对数量减1,直到state为0时表示锁释放完成。
根据CAS的特性,建议在低锁冲突的情况下使用Lock
在JDK1.6后,官方对synchronized做了大量的优化(偏向锁、自旋、轻量级锁等),因此在非必要的情况下,建议都使用synchronized做同步操作
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。