JUC笔记(二):悲观锁
JUC笔记(二):悲观锁
共享变量带来的问题
举一个简单的例子:
- 线程一和线程二都需要去改变一个静态变量
i = 0
- 线程一将
i++
- 线程二将
i--
由于i++
和i-
-都并非原子操作,在编译成字节码以后会拆分成多个人字节码指令:
i++
1 | getstatic i //获取静态变量i |
i--
1 | getstatic i //获取静态变量i |
由于线程一和线程二并发执行,CPU会切换不同的线程,这导致了,CPU运行时的真实情况并不一定是先执行完线程一的i++的所有指令再执行线程二的i–,而是可能会出现很多种情况,如下。最终 i 的结果为 -1,与预期的不同,出现了并发问题
相关概念
临界区:一段代码块内如果存在对共享资源的多线程读写操作,称这块代码块为临界区。
竞态条件:多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。
synchronized
synchronized【对象锁】,采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】。其他线程再想获得这个【对象锁】时就会阻塞住。
同步代码块
语法
1 | synchronized(对象) { |
例子
1 | static int counter = 0; |
加在方法上
synchronized加在方法上,本质上是锁住this
语法
1 | class Test { |
例子
1 | class Room { |
Monitor工作原理
Monitor 被翻译为监视器或管理。
每个java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁后,该对象头的Mark Word就被设置为指向 Monitor对象的指针
Monitor 结构如下
刚开始Monitor中的Owner为null
当Thread-2执行synchronized(obj) 以后就会将Monitor的所有者设置为Thread-2,Monitor中只有一个Owner
在Thread-2上锁的过程中,如果Thread-3,Thread-4,Thread5也来执行synchronized(obj),就会进入EntryList中
Thread-2执行完同步代码块内容,然后唤醒EntryList中的等待的线程来竞争锁,竞争锁时是非公平的
其中Mark Word的结构为:
wait notify
使用
API
- obj.wait() 让进入 object 监视器的线程到 waitSet 的等待
- obj.notify() 在 object 上正在 waitSet等待的线程中挑一个唤醒
- obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
他们都是线程之间协作的手段,都属于Object对象的方法。必须获得此对象的锁,才能调用这些方法
例子
1 | static final Object object = new Object(); |
原理
- Owner线程调用wait方法,即可进入WaitSet变为WAITING状态。
- BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
- BLOCKED 线程会在 Owner 线程释放时唤醒
- WAITING 线程会在 Owner 线程调用notify 或 notify All时唤醒,但唤醒后并不意味着立即获得锁,仍需进入 EntryList重新竞争
sleep(long n)和wait(long n)的区别
sleep时Thread方法,而wait是Object方法
sleep不需要强制和synchronized配合使用,但wait需要和synchronized配合使用
sleep在睡眠的同时,不会释放对象锁,但wait在等待的时候会释放对象锁
相同点:他们的状态都是Time_Waiting,有时限的等待
park unpark
- park停止线程,unpark开启
使用
先通过park停止线程,然后通过unpark恢复线程使用
1 | //暂停当前线程 |
例:
1 | Thread t1 = new Thread(() -> { |
注意:unpark()可以在park()之前执行来解除park()
ReentrantLock
- 可中断
- 可以设置超时时间,如果一段时间无法获取到锁,就放弃竞争锁
- 可以设置为公平锁
- 支持多个条件变量
- 与synchronized一样都是可重入的
使用
基本语法
1 | // 获取锁 |
可打断
reentrantLock.lock()
是无法被打断的,reentrantLock.lockInterruptibly()
可以被打断z
1 | private static ReentrantLock lock = new ReentrantLock(); |
锁超时
通过reentrantLock.tryLock()
来尝试获取锁。也可以传递一个时间,通过reentrantLock.tryLock(1, TimeUnit.SEOCNDS)
来等待一定时间,如果在这段时间内都没有获取到锁,则返回false,获得锁返回true。可避免死锁
1 | private static ReentrantLock lock = new ReentrantLock(); |
公平锁
非公平锁:对于synchronized
,如果一个线程持有锁,其他线程就会进入阻塞队列。等到该线程释放锁,在阻塞队列的线程就会一拥而上去争夺锁,谁先抢到谁就拥有锁。并不会按照阻塞队列的顺序获取锁。
公平锁:按照阻塞队列,谁先进去的,谁就先获得锁。
ReentrantLock
默认是非公平锁,通过RenntrantLock lock = new RentrantLock(true)
设置为公平锁。实际上,公平锁一般没有必要,会降低并发度。
条件变量
和synchronized
中也有条件变量,就是waitSet
休息室,当条件不满足的时候,进入waitSet
等待。
通过Condition condition = lock.newCondition()
来创建条件变量,如果条件不满足则调用condition.await()
来停止线程,如果条件满足了调用condition.signal()
来唤醒一个线程,通过condition.signalAll()
唤醒所有线程
1 | private static ReentrantLock lock = new ReentrantLock(); |