
JUC-01_线程拓展和关键字总结
前置知识:线程基础,JMM内存结构,happens before准则,并发顺序一致性保证(阻塞非阻塞,无同步)
内存结构知识看jvm最后一章笔记。
线程基础看javase多线程笔记。
本章主要整理juc包下的使用以及sync,volatile展开介绍
线程中通信
对于之前的基础内容的一些补充
线程之间是独立运行的,互不影响,如果不通信,就失去了多线程交互协作的意义。
上下文切换
cpu调度,在多个线程间时间片调度,a执行执行着,保存a的程序计数器,跳到b去交替执行,速度快看起来像多个程序同时在运行。
程序计数器保证cpu上下文切换时能够知道执行到了哪里。
在多线程情况下由于cpu上下文切换,线程间指令是交替执行,所以无法保证指令之间有序。故无法保证并发原子性。
临界区
线程共享的地方叫做临界区,也可理解为线程不安全的地方,如代码块,全局变量这种地方。
interrupt
线程中断,可理解为线程的一个标识,表示一个线程在运行期间是否被其他线程进行了中断操作。
-
其他线程可调用Thread.interrupt()方法来中断线程。
-
Thread.interrupted()复位该标识
-
isInterrupted()来判断该线程是否被中断过
-
注意,若抛出了InterruptedException,会被复位,isInterrupted()此时为false。
等待-通知
Object类中的wait(),notify(),notifyAll()方法,利用他们来实现线程之间的通信。
| 方法名 | 描述 |
|---|---|
| notify() | 通知一个在对象上等待的线程,使其从wait()方法上返回,而返回的前提是该线程获取到了对象的锁 |
| notifyAll() | 通知所有在该对象上等待的线程 |
| wait() | 调用该方法使对象进入WAITING状态,只有等待另一个线程的通知或被中断才会返回,返回后会释放锁 |
| wait(long) | 单位毫秒,超时等待一端时间,超时了没有通知就返回 |
| wait(long,int) | 超时等待,纳秒 |
-
需要对调用的对象加锁,才能调用它的wait()|notify()方法
-
当调用wait()时,线程由running变为waiting,进入等待队列等待唤醒
-
其他线程调用该对象的notify()方法时,该线程释放了锁之后,才有机会唤醒原先wait()的线程
-
notify()是把等待队列的一个等待线程移到同步队列中,而notifyAll()是挪全部
-
从wait()方法返回的前提是要获取到对象的锁
更优雅的停止线程
boolean标识符配合isInterrupt()方法来中断线程,不会释放监视器,同时停止线程可控,更加灵活。
volatile
作用
-
变量在并发间的立即可见性
-
禁止重排序优化(开辟空间,初始化对象,对象引用指向内存空间)
先行发生原则:对于一个volatile变量的读操作永远在它的下次写操作之前
**原理:**内存屏障:标识这段代码不允许重排序,保证一次读写操作的原子性
内存屏障说明
StoreStore 屏障 禁止上面的普通写和下面的 volatile 写重排序。
StoreLoad 屏障 防止上面的 volatile 写与下面可能有的 volatile 读/写重排序。
LoadLoad 屏障 禁止下面所有的普通读操作和上面的 volatile 读重排序。
LoadStore 屏障 禁止下面所有的普通写操作和上面的 volatile 读重排序
使用场景:
-
状态标志:保证原子性-》布尔状态标志,标识重要操作,例如初始化或停机
-
一次性安全发布:保证可见性-》不同步情况下发布对象会导致多线程之间的工作内存中有旧值也有新址,导致无法正确发布一个对象
-
独立观察:需要一个全局变量让其他线程随时能够读取到新值
-
volatile bean:一个javabean属性全部是volatile,且getset不能带其他逻辑,不能使用到数组
-
开销较低的读-写锁策略:配合内部锁实现volatile变量的同步自增,使它成为安全的计数器,减少开销
-
双重检查,单例懒汉模式,两次if,第一次减少同步开销,第二次避免重复创建对象,volatile禁止重排保证创建正确初始化的对象
synchronized
作用
-
jvm层级,块级别的同步锁
-
锁对象
-
锁方法
-
锁代码块
-
静态类,方法,代码块锁的是类层级
注意
-
一个锁只能被一个对象获取,其他没获取到锁的线程只能等待
-
每个实例都有单独的锁(this),对于static类和static代码块,所有对象共用一把锁
-
synchronized修饰的方法,无论执行成功还是抛出异常都会释放锁
Monitor
字节码层面,
-
synchronized代码块会在进入代码块钱插入一条monitorenter指令,monitor计数器+1
-
在程序执行完毕或者产生异常上插入monitorexit指令,monitor计数器-1
-
当monitor计数器为0时,锁被释放
可重入锁:当一个线程已经获取了对象的锁时,再次进入该对象时就不用再次获取锁。
- 也就是一个对象内部有多个sync,我获取到了你的锁,就只会让monitor计数器+1而不是再去获取一个额外的锁,当sync执行结束,计数器-1直至0时释放锁。
java对象头

其中mark word中定义了对象在不同状态时对应的标记,例如hashcode,分代回收年龄age,01锁标识,无锁,偏向,轻量,重量锁等
Monitor
监视器,管程
每个java对象都可以关联一个monitor对象
若给该对象上锁synchronized,该对象的对象头的mark word就会指向一个monitor对象的指针
Monitor结构

-
一开始monitor的owner为null
-
当一个线程执行synchionzed(obj)就会把monitor的owner对象设置为当前线程,同时只能有一个owner
-
其他线程此时尝试进入synchronized中代码时会先检查monitor对象的owner是不是自己,若否则放入entry list 进入阻塞
-
之前获得过锁,但是不满足条件的线程,进入了waitset中等待唤醒(调wait()方法等待被notify())
-
当前线程执行结束后会释放锁(monitorexit),此时唤醒entrylist中等待的线程争夺锁,成为下一个owner
-
synchronized必需是同一个对象监视器才可争夺
-
不加synchronized的对象不关联监视器,不会阻塞同步
-
锁升级
1.6以后对synchronized加入了优化和锁升级机制。
-
锁粗化:减少不必要的连续的lock和unlock(多个连续的lo unlock合并)
-
锁消除:jit运行时编译器进行逃逸分析,直接设置局部变量,消除锁(调用工具类方法时可能已经保证同步而不自知,优化去锁)
-
轻量级锁:无锁竞争时cas指令获取锁和释放锁,有锁竞争,cas失败时进入阻塞
-
偏向锁:在无锁竞争下减少不必要的cas指令(本地延迟)
-
自适应自旋:当线程获取轻量级锁cas失败时,在进入monitor重量级锁之前,会进入忙等待自己重新尝试几次,仍不成功则进入monitor重量级锁
-
锁膨胀方向(单向不可逆):无锁 -》偏向锁 -》轻量锁 -》重量锁
-
自旋锁
占用cpu时间片,线程获取不到锁时自循环尝试获取锁,jdk中默认10次,超过10次转为传统的阻塞
-
自适应自旋
若自旋刚结束就释放了锁,得不偿失,灵活度不高
自适应自旋:自旋时间由前一次在锁上自旋的时间和拥有者的状态决定,更加灵活聪明的自旋方案
-
若同一个锁对象上,自旋等待刚刚成功获取过锁,则代表自旋容易获取锁,会增加自旋次数
-
反之,省略自旋流程,减少不必要的cpu调度消耗
-
-
偏向锁
引入背景:很多环境下,锁总是由同一个线程多次获取,此时再进行各种上锁释放锁带来不必要的性能消耗和上下文切换
定义:偏向于单个多次重复获取的线程,在进入和退出线程时不必再通过CAS加锁解锁,只简单判断对象头的mark word里是否存着当前线程的偏向锁,若成功代表已经获取了锁。
作用:偏向某一个线程,舍去cas步骤,减少加锁解锁的消耗,判断对象头markword是否存在偏向锁标记。

**偏向锁的撤销:**偏向锁使用了一种等待竞争出现时才会释放锁的机制(偏向锁适用于不存在锁竞争的情况,所以当存在了锁竞争就要升级了)
偏向锁撤销需要等到全局安全点:当前线程没有正在执行的字节码指令时
-
暂停当前持有偏向锁的线程
-
检查持有偏向锁的线程是否活着
-
非活跃--直接设置为无锁状态
-
活着--偏离栈帧锁记录,锁记录和对象头要么偏向其他锁,要么退为无锁状态,或标记该对象适合为偏向锁
-
轻量锁
-
在线程执行同步块前,会把锁对象的对象头的markword用cas拷贝一份到当前线程的栈帧中的锁记录去
-
被锁原对象的对象头的锁标记字段cas改成当前线程的锁记录的引用
-
更新完毕后把原对象的markword的锁标志01改成00,表示处于轻量级锁状态
-
其他线程执行前同样进行cas操作,若该对象已经被其他线程先一步使用了,此时的cas操作肯定是会失败的。
- 若有两条以上的线程争夺同一个锁,直接升级为重量级锁。
-
**轻量锁的撤销:**通过cas将拷贝的markword替换回原先的对象头中,若成功代表没有竞争关系,若失败,升级为重量级锁。
-
重量锁
monitor级别上锁,其他任何线程阻塞等待锁释放
final
-
加类:绝后,不允许被继承
-
加变量:初始化一次后不允许被修改(引用类型为不可修改引用)
-
加方法:无法被重写,可以被重载(private方法隐式的final)
-
编译期常量为编译期确定的常量,非编译期常量如 final i = Random().nextInt();
-
可声明不赋值,但是使用前必须赋值
-
与static配合,类级别间共享一个值定义时必需赋值否则报错
-
基本类型重排序
-
final域写:禁止final域写与构造方法重排序,即禁止final域写重排序到构造方法之外,从而保证该对象对所有线程可见时,该对象的final域全部已经初始化过。
-
final域读:禁止初次读对象的引用与读该对象包含的final域的重排序。
-
-
引用类型重排序
-
额外增加约束:禁止在构造函数对一个final修饰的对象的成员域的写入与随后将这个被构造的对象的引用赋值给引用变量 重排序-
禁止构造函数对final引用类型写入
-
把它赋值给对象引用
-
-
原理:内存屏障,final写后StoreStore屏障,读前loadload屏障