参考:Java技术驿站、尚硅谷
1. happens-before
定义:一个操作的执行结果对另一个操作可见
规则:
- 程序次序规则(单线程情况下)
- volatile:如果一个线程对volatile变量写,一个线程读,那么写操作肯定happens-before读操作
- 传递规则:A happens-before B, B happens-before C,那么A happens-before C
- 线程启动规则:假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行后确保对线程B可见。
- 线程终结规则:假定线程A在执行的过程中,通过制定ThreadB.join()等待线程B终止,那么线程B在终止之前对共享变量的修改在线程A等待返回后可见。
2. JMM
JMM中主内存中的共享变量线程不可以操作,只能拷贝到自己的工作内存中进行操作,操作完成后写回主内存。
2.1 可见性 要让线程的操作让其他线程及时看到。
2.2 原子性
2.3 有序性
3. volatile
- 定义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新到主内存中。 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
- 底层实现:插入内存屏障 编译时会添加lock信号,会引起处理器缓存回写到内存。缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到内存会导致其他处理器的缓存无效。
- 特点:
(1)保证可见性 (及时刷主内存) 验证:
class A{
int num=0;
addto2{
num=2;
}
}
mian方法{
A a = new A();
thread1{
a.addto2;
}
while(num == 0) {}
System.out.println("任务完成");
}
没有可见性,则一直死循环,没有人通知main线程num更改,volatile之后就能显示任务完成,num=2
(2)不保证原子性 解决 :1.加synchronized,2.使用atomic类
验证:
class A{
int n=0;
add1{
n++;//包括三个步骤,1.从主内存获取tmp=n,工作内存tmp+1,主内存n=tmp
} // 可能出现一个线程在刷的时候被挂起了,导致小的数会
//把原来大数覆盖了,丢失数据
}
main{
A a = new A();
for(i:0-20){
new Thread{
for(j:0-1000){
a.add1(); //最后a.n怎么跑都是小于20000的
} //原因其实就是n++在字节码中是三条指令,在写回时由于线程
} //调度造成写覆盖
}
}
(3)禁止重排序:
- 如果第一个操作为volatile读,则不管第二个操作是什么,都不能重排序。这个操作确保volatile读之后的操作不会被编译器重排序到volatile读之前;
- 当第二个操作为volatile写,则不管第一个操作是什么,都不能重排序。这个操作确保volatile写之前的操作不会被编译器重排序到volatile写之后;
- 当第一个操作volatile写,第二操作为volatile读时,不能重排序。
4. AQS(AbstarctQueuedSynchronizer)
优点:实现了AQS的组件能保证只在同一时刻发生阻塞,从而降低了上下文切换的开销。
实现原理:使用int型state变量记录锁状态,state>0代表获取了锁,state=0表示释放了锁,另外AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作。
使用方法:子类通过继承同步器并实现它的抽象方法来管理同步状态。
5. CAS(Compare And Swap)
在CAS中有三个参数:内存值V、旧的预期值A、要更新的值B,当且仅当内存值V的值等于旧的预期值A时才会将内存值V的值修改为B,否则什么都不干,(循环比较直到可以,注意此时竞争线程并没有阻塞,而是原地空转)。
例子:JUC下的atomic类都是通过CAS来实现的、concurrenthashmap-jdk1.8
实现原理:
1.CAS是原语,原语的执行是连续的
2.Unsafe: CAS的核心类,方法都是native的,像C语言指针一样直接操作内存,它提供了硬件级别的原子操作。
为什么CAS能保证线程安全?
计算机底层保证的:一个线程T在CAS操作时,其他线程无法访问V指向的内存地址,并且一旦T更新了V指向内存中的值,其他所有线程的V指向内存都变得无效。两种方式:总线发出#Lock信号;缓存一致性协议。
尚硅谷的具体讲解:
输出
缺陷:
- 循环时间长
- 只能保证一个共享变量的原子操作
- ABA问题(某一个线程更快,开始和结束一样,但是过程中改变了,A -> B -> A实际是发生了改变的,慢的线程以为没有改变)
ABA问题的解决: Java提供了AtomicStampedReference来解决。AtomicStampedReference通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题
6. synchronized
使用场景
图片来源:www.jianshu.com/p/d53bf830f…
实现原理:
同一时刻只有一个线程能够获取到monitor
执行同步代码块后首先要先执行monitorenter指令,退出的时候monitorexit指令
执行静态同步方法的时候就只有一条monitorexit指令,并没有monitorenter获取锁的指令。这就是锁的重入性
缺点:并发性不好,需要加锁
注意:synchronized所保证的原子性是 块与块的有序性(保证同一时间只有单线程执行) volatile保证的是指令之间的有序性(插入内存屏障禁止重排序)
synchorized 锁的是方法调用者(对象/class模板)
- wait会释放锁, sleep抱着锁睡
7. CountDownLatch
public enum StudentEnum {
ONE(1,"老大"),TWO(2,"老二"),THREE(3,"老三"),
FOURE(4,"老四"),FIVE(5,"老五");
private Integer retCode;
private String retName;
StudentEnum(Integer retCode, String retName) {
this.retCode = retCode;
this.retName = retName;
}
public Integer getRetCode() {
return retCode;
}
public String getRetName() {
return retName;
}
public static StudentEnum forEach_StudentEnum(int index) {
StudentEnum[] studentArray = StudentEnum.values();
for(StudentEnum element:studentArray) {
if(index == element.getRetCode()) {
return element;
}
}
return null;
}
}
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
int n = 5;
CountDownLatch countDownLatch = new CountDownLatch(n);
for(int i=0; i<n; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 考试结束,离开考场");
countDownLatch.countDown();
},StudentEnum.forEach_StudentEnum(i+1).getRetName()).start();
}
countDownLatch.await();
//阻塞到count=0:
System.out.println(Thread.currentThread().getName() + " 监考老师收卷");
}
}
*****************运行结果
老二 考试结束,离开考场
老四 考试结束,离开考场
老五 考试结束,离开考场
老大 考试结束,离开考场
老三 考试结束,离开考场
main 监考老师收卷