synchronized 关键字
synchronized通过对象内部的一个叫做监视器锁的东西来实现的。
监视器锁本身依赖于操作系统底层的互斥锁Mutex Lock来实现的。
操作系统实现线程之间的切换需要从用户态转换到核心态,成本很高,状态转换要花费比较长的时间。
这就是synchronized效率低的原因。这种依赖于操作系统MutexLock实现的锁成为重量级锁。
synchronized v.s. volatile
Volatile:内存可见(不保证原子性,不保证互斥性);防止CPU指令重排(内存屏障)
扩展:
如何保证内存可见?
JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证。
什么是内存屏障?
MM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。
happens-before规则
happens-before表示的是前一个操作的结果对于后续操作是可见的,它是一种表达多个线程之间对于内存的可见性。所以我们可以认为在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作必须要存在happens-before关系。这两个操作可以是同一个线程,也可以是不同的线程。
Synchronized v.s. Lock
Synchronized 对象头的偏向锁标志位?
java中的锁
对象头偏向锁
锁的状态由低到高:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
只能升级,不能降级。
锁状态 | 存储内容 | 标志位 |
---|---|---|
无锁 | 对象的hashCode,对象分代年龄,是否是偏向锁(0) | 01 |
偏向锁 | 偏向线程ID,偏向时间戳,对象分代年龄,是否偏向锁(1) | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 |
重量级锁 | 指向互斥量的指针 | 11 |
锁的对比
锁 | 优点 | 缺点 | 场景 |
---|---|---|---|
偏向锁 | 加解锁不需要额外消耗,和执行非异步方法相比仅存在纳秒级差距。 | 若线程之间存在竞争,会代带来额外的锁撤销的消耗(撤销偏向锁会Stop The World) | 使用于只有一个线程访问同步代码场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了系统的响应速度 | 始终得不到锁的竞争线程,不断自旋会消耗CPU | 追求响应速度,同步代码块执行速度非常快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间慢 | 追求吞吐量,同步块执行速度较慢 |
锁如何升级
-
无锁
无锁是指线程通过无限循环来执行更新操作,如果执行成功就退出循环,如果执行失败(有其他线程更新了值),则继续执行,直到成功为止。CAS操作就属于无锁。如果从性能的角度来看,无锁状态的性能是非常高的。
-
偏向锁
偏向锁的前提假设是当一个线程获取锁,后面还有大概率该线程还会需要继续持有这把锁 。
当有另外一个线程区尝试获取这个锁的时候,偏向模式就宣告结束。
虚拟机启用偏向锁的参数
-XX:UseBiasedLocking
。如果当前偏向锁已启动,当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为01
,即偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁的同步块时,虚拟机都可以不用再进行同步操作了。 -
轻量级锁
当开始有锁的竞争了,那么偏向锁就会升级到轻量级锁;
当获取锁冲突多,时间越长的时候,线程肯定无法继续在这里死等了,所以只好先挂起,然后等前面获取锁的线程释放了锁之后,再开启下一轮的锁竞争,而这种形式就是我们的重量级锁。
在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为
01
状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。然后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后2bit)将转变为
00
,即表示此对象处于轻量级锁定状态。 轻量级锁的前提假设是对于绝大部分的锁,在整个同步周期内都是不存在竞争的,通过CAS操作来避免时候互斥锁的开销关于自旋锁:
自旋锁是一种通过让线程不释放当前的CPU执行一个忙循环,来尝试获取锁的方式。 自旋锁的前提假设是锁被其它线程占用的时间很短 。如果其它线程占用锁的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而带来性能上的浪费。自旋次数的默认值是
10
次,用户可以通过使用参数-XX:PreBlockSpin
来更改。 -
重量级锁
当有两个及以上的线程争用同一个锁,那么轻量级锁就不再有效,要膨胀为重量级锁。锁标志的状态值变为
10
,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。- 偏向锁只有设置了
-XX:UseBiasedLocking
参数才会存在 - 假设启用了偏向锁,对象头的锁标志位是
01
(和未锁定状态一样),但是存储的内容是偏向线程ID、偏向时间戳 - 当线程获取偏向锁是通过CAS操作将对象头中存储的偏向线程ID更新为当前线程的ID
- 对象是否被锁定是指对象头是否指向线程的锁记录(Lock Record)
- 只有是轻量级锁或者重量级锁时对象才会被锁定
- 偏向锁只有设置了
callable vs runnable
- callable可与Future结合拿到线程结束后的返回值
- callable的Callable的抽象方法有返回值。返回值类型就是泛型类型。
- Callable接口带泛型
- call()方法还能抛出异常。
线程池
为什么要用线程池?
为了减少在创建和销毁线程上所花的时间和对系统资源的消耗,解决资源不足的问题。
如果不适用线程池,有可能造成系统创建大量同类线程而消耗内存挥着”过度切换“问题。
四种线程池
- SingleThreadPool:单个线程线程池
- FixedThreadPool:指定大小的固定大小线程池
- CachedThreadPool:
- ScheduledThreadPool
问题
- FixedThreadPool和SingleThreadPool:允许的请求队列长度为Interger.MAX_VALUE,可能会堆积大量请求,造成OOM;
- CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量线程,造成OOM。
线程池参数?
- corePoolSize:核心线程数量,定义了最小可以同时运行的线程数量,没有任务也不会被回收。
- maximumPoolSize:当线程数量大于核心线程数,且队列中存放的任务达到队列容量的时候,继续创建线程。
- keepalivetime:超过corePoolSize之后”临时线程“的存活时间。
- Time_unit:单位
- workQueue:当新任务来的时候,保存未执行的任务。大于corePoolSize时进入阻塞队列等待。
- threadFactory:线程工厂,用来创建线程,默认就行
- RejectdExecutionHandler handler:拒绝策略,线程数量达到maximumPoolSize大小,且workQueue也塞满的情况下,线程池调用handler拒绝策略来处理请求。
拒绝策略:
-
AbortPolicy:默认拒绝策略,直接抛异常
-
DiscardPolicy:直接抛弃不处理。
-
DiscardOldestPolicy:丢弃队列中最老的任务。
-
CallerRunsPolicy:将任务分配给当前执行execute方法的线程来执行
自定义拒绝策略:实现
RejectedExecutionHandler
接口,可以实现友好的拒绝策略。包括但不限于:将数据存入数据库,用日志处理等。