临界区
一段代码内如果存在对共享资源的多线程读写操作,称这段代码为临界区
竞态条件:多个线程在临界区内执行,由于代码执行序列不同导致结果无法预测,称之为竞态条件
synchronized解决方案
实际上是用对象锁保证了临界区内代码的原子性,临界区内的代码对外界是不可分割的,不会被线程切换打断
方法上的synchronized:
public方法上,相对于锁对象是this(当前对象)
static方法上,相当于锁当前类对象this.class
方法的访问修饰符是有意义的,private,final防止子类重写,可以一定程度避免线程安全问题
常见的线程安全类
String
Integer
StringBuffer
Random
Vector
Hashtable
JUC包下的类
线程安全是指,多个线程调用它们同一个实例的某个方法时,是线程安全的
不可变类线程的安全性:String、Integer等都是不可变类,内部的状态不可改变
外星方法:抽象方法(实现可能是并发不安全的)
String为什么设置为final,因为String是线程安全的,但是如果我有子类,子类可能重写方法等,破坏我的安全性
Monitor概念
对象头(32位为例)
普通对象:Object Header(64字节):Mark Word(32位)+ Klass Word(32位)
| 状态 | Mark Word 内容 | 锁标志位 | ||||
|---|---|---|---|---|---|---|
| 无锁(Normal) | `hashcode:25 | age:4 | biased_lock:0 | 01` | 01 | |
| 偏向锁(Biased) | `thread:23 | epoch:2 | age:4 | biased_lock:1 | 01` | 01 |
| 轻量级锁(Lightweight) | ptr_to_lock_record:30 |
00 | ||||
| 重量级锁(Heavyweight) | ptr_to_monitor:30 |
10 | ||||
| GC标记(Marked for GC) | ... |
11 |
Monitor:监视器、管程
每个java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向monitor对象的指针
Monitor对象包含:owner(获取锁对象)、EntryList(阻塞队列)、WaitSet(与wait/notify相关)
synchronized
轻量级锁
一个对象虽然有多线程访问,但多线程访问的时间是错开的,那么可以使用轻量级锁来优化
过程:
一个栈帧可以有多个 Lock Record,用来支持多个锁和可重入,本质是一个"栈式结构"
- 创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结果,内部可以存储锁定对象的Mark Word
- 让锁记录中Object reference 指向锁对象,并尝试用CAS替换Mark Word,将Mark Word的值存入锁记录
- 如果CAS替换成功,对象头中存储了锁记录地址和状态00,表示该线程给对象加锁
- 加锁失败
- 如果是其他线程已经持有了该Object的轻量级锁,这是表示有竞争,进入锁膨胀过程
- 如果是自己执行了synchronized锁重入,那么再添加一条LockRecord作为重入计数
- 当退出synchronized代码块,如果锁记录值为null,表示重入,重置锁记录,表示重入计数减一
- 当退出synchronized代码块时锁记录不为null,这时使用CAS将mark word的值恢复给对象头
- 成功:解锁成功
- 失败,进入重量级锁解锁流程
锁膨胀
如果在尝试加锁的过程中,CAS操作失败,这时一种情况就是有其他线程对此线程加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁
- 当thread1进行轻量级加锁时,thread0已经对该对象加了轻量级锁
- 这时thread1加轻量级锁失败,进入锁膨胀流程
- 即为object对象申请monitor锁,让object指向重量级锁的地址
- 然后自己进入monitor的entrylist(Blocked状态)
- 当thread0退出同步块解锁时,使用CAS将mark word的值恢复给对象头,失败。这时会进入重量级锁解锁流程,按照monitor地址找到monitor对象,设置owner为null,唤醒entrylist中的blocked线程
自旋优化
自旋只会发生在"轻量级锁竞争失败"阶段,并不是所有锁竞争失败都会自旋。
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁进程已经推出了同步块,释放了锁),这时当前线程就可以避免阻塞
偏向锁
轻量级锁没有竞争时,每次重入仍然需要执行CAS操作
java6引入偏向锁来进一步优化,只有第一次使用CAS将线程ID设置到对象的mark word头,之后发现这个线程id是自己的就表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归线程所有
偏向状态
一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,markword值为ox05即最后3位为101,这时它的thread/epoch/age都为0
- 偏向锁是默认延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加vm参数 -XX:BiasedLockingStartpDelay=0来禁止延迟
- 如果没有开启偏向锁,那么对象创建后,markword值为0x01即后三位为001,这时它的hashcode/age都为0,第一次用到hashcode时才会赋值
偏向锁撤销
- 调用对象hashcode
- 其他线程使用锁对象
- 调用wait/notify(重量级锁才有)
批量重偏向
如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程t1的对象仍然有可能重新偏向t2,重偏向会重置对象的thredID
当撤销偏向锁阈值超过20次后,jvm会觉得,我是不是偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程
批量重偏向(Bulk Rebias)是"针对同一类(Class)的多个对象"进行的,而不是单个对象。
批量撤销
当撤销偏向锁阈值超过40次后,jvm会觉得,自己确实偏向错了,根本就不应该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的
锁消除
JIT发现加不加锁一样,直接消除锁(比如用局部变量作为锁对象)
wait/notify
- owner线程发现条件不满足,调用wait方法,即可进入waitset变为waiting状态
- blocked和waiting到的线程都处于阻塞状态,不占用CPU时间片
- blocked线程会在owner线程释放锁时唤醒
- waiting线程会在owner线程调用notify或notifyall时唤醒,但唤醒后并不意味着立即获得锁,仍需进入entrylist重新竞争
相关API
- wait():让进入object监视器的线程到waitset等待
- notify():在object上正在waitset等待的线程中挑一个唤醒
- notifyAll():让object上正在waitset等待的线程全部唤醒
- wait(long timeout):设置最长等待时间
都是线程之间进行协作的手段,属于object对象的方法,必须后的此对象的锁,才能调用
sleep和wait的区别
- sleep是Thread方法,而wait是object的方法
- sleep不需要强制和synchronized配合使用,但wait需要
- sleep在睡眠的同时,不会释放对象锁,而wait会释放对象锁
模式之保护性暂停
即guarded suspension,用在一个线程等待另一个线程的执行结果
要点:
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个guardedobject
- 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(生产者-消费者)
- JDK中,join的实现、future的实现,采用的就是此模式
- 因为要等待另一方的结果,因此归类到同步模式
解耦等待和生产
通过中间类解耦
异步模式之生产者/消费者
- 与前面的保护性暂停中的guardobject不同,不需要产生结果和消费结果的线程一一对应
- 消费队列可以用来平衡生产和消费线程资源
- 生产者仅负责生产结果数据,不关心数据该如何处理,而消费者专心处理结果数据
- 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据
- JDK中各种阻塞队列,采用的就是这种模式
park/unpark
与wait相比
- wait必须配合object monitor一起使用
- park是以线程为单位来阻塞和唤醒线程,而notify只能随机唤醒一个等待线程,notifyall是唤醒所有等待线程,就不那么精确
- park,可以先unpark
每个线程都会关联一个parker对象,由_counter,_cond,_mutex组成
多把锁
给互不相关的共享资源加不同锁
活跃性
死锁
一个线程需要同时获取多把锁,这时就容易发生死锁
定位:可以使用jconsole,或者使用jsp定位线程,然后用jstack
哲学家就餐问题
活锁
两个线程互相改变对方的结束条件,最后谁也无法结束
饥饿
教程定义:一个线程优先级太低,始终得不到CPU调度执行,也不能够结束
ReentrantLock
相比synchronized具备如下特点
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
都支持可重入
基本语法
对象级别
通过try-finally保证释放锁
获取锁:lock()
释放锁:unlock()
可重入
同一个线程如果首次获得这把锁,那么他就是这把锁的拥有者,因此有权利再次获取这把锁。
如果是不可重入锁,那么第二次获得锁时,自己也会被挡住
可打断
其他线程可以使用interrupt打断阻塞
lock.lockInterruptibly()
其实就是加入一种机制,可以停止无限制等待
锁超时
tryLock():获得锁返回true,获取锁失败返回false,直接尝试获取,不等待
tryLock(timeout,TimeUnit):在指定时间内尝试获取锁
tryLock可以被打断
锁超时解决银行家算法
通过tryLock(),获取一把锁后,没有获得另一把锁,我会释放第一把锁,重新尝试
公平锁
reentrantlock(boolean):传参数true,改为公平锁
公平锁一般没有必要,会降低并发度,通过tryLock就可以
条件变量
synchronized中也有条件变量,就是waitset休息室,当条件不满足进入waitset等待
reentrantlock的条件变量比synchronized强大在于,他是支持多条件变量
- synchronized是那些不满足条件的线程都在一间休息室等消息
- reentrantlock支持多休息室,有专门等各种资源的休息室,唤醒也可以按休息室来唤醒
- await前需要获得锁
- await执行后,会释放锁,进入conditionObject等待
- await的线程被唤醒(或打断,或超时)需重新竞争锁
- 竞争lock锁成功,从await后继续执行
创建条件变量(休息室):对象.newCondition();
线程进入条件变量:condition.await();
唤醒条件变量中线程:condition.signal()
同步模式之顺序控制
交替打印(见上一篇文章)