java并发笔记

148 阅读6分钟

重要提示:本文是笔记,原文是:

【Java并发专题】27篇文章详细总结Java并发基础知识

Java并发

线程状态

线程状态含义

线程状态转换

Java内存模型及happens-before规则

JMM抽象结构模型

指令重排序

计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排。 比如三条加法指令,第一条的两个参数未加载,第三条已加载完成,则优先执行第三条。前提是这三条指令没有数据依赖,比如a=b+c;d=a+b就有数据依赖,a作为d的参数在前一指令被写入了。

数据依赖性存在三种情况:1. 读后写;2.写后写;3. 写后读

as-if-serial

as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提供并行度),(单线程)程序的执行结果不能被改变。

happens-before

happens-before相当于多线程版的as-if-serial。

定义

1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。

2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。

并发的三大性质

  1. 原子性

原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败

  1. 可见性

可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改

  1. 有序性

即程序执行的顺序按照代码的先后顺序执行。

synchronized: 具有原子性,有序性和可见性; volatile:具有有序性和可见性

CAS

先了解乐观锁和悲观锁

  1. 悲观锁就是我们常说的锁。对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。

  2. 乐观锁又称为“无锁”,顾名思义,它是乐观派。乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。而一旦多个线程发生冲突,乐观锁通常是使用一种称为CAS的技术来保证线程执行的安全性。由于无锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说乐观锁天生免疫死锁。

  3. 乐观锁多用于“读多写少“的环境,避免频繁加锁影响性能;而悲观锁多用于”写多读少“的环境,避免频繁失败和重试影响性能。

cas概念

CAS的全称是:比较并交换(Compare And Swap)。CAS是一种原子操作,它是一种系统原语,是一条CPU的原子指令,从CPU层面保证它的原子性

在CAS中,有这样三个值: V:要更新的变量(var) E:预期值(expected) N:新值(new)

比较并交换的过程如下: 判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做。

CAS实现原子操作的三大问题

  • ABA问题

所谓ABA问题,就是一个值原来是A,变成了B,又变回了A。这个时候使用CAS是检查不出变化的,但实际上却被更新了两次。

ABA问题的解决思路是在变量前面追加上版本号或者时间戳。

  • CAS多与自旋结合。

如果自旋CAS长时间不成功,会占用大量的CPU资源。

解决思路是让JVM支持处理器提供的pause指令。 pause指令能让自旋失败时cpu睡眠一小段时间再继续自旋,从而使得读操作的频率低很多,为解决内存顺序冲突而导致的CPU流水线重排的代价也会小很多。

  • 只能保证一个共享变量的原子操作

有两种解决方案:

  1. 使用JDK 1.5开始就提供的AtomicReference类保证对象之间的原子性,把多个变量放到一个对象里面进行CAS操作;

  2. 使用锁。锁内的临界区代码可以保证只有当前线程能操作。

Synchronized

对象锁(monitor)机制

public class SynchronizedDemo {
    public static void main(String[] args) {
        synchronized (SynchronizedDemo.class) {
        }
        method();
    }

    private synchronized static void method() {
    }
}

如图,上面用黄色高亮的部分就是需要注意的部分了,这也是添Synchronized关键字之后独有的。

执行同步代码块后首先要先执行monitorenter指令,退出的时候monitorexit指令。

通过分析之后可以看出,使用Synchronized进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则就只能等待。

上面的demo中在执行完同步代码块之后紧接着再会去执行一个静态同步方法,而这个方法锁的对象依然就这个类对象,那么这个正在执行的线程还需要获取该锁吗?答案是不必的,从上图中就可以看出来,执行静态同步方法的时候就只有一条monitorexit指令,并没有monitorenter获取锁的指令。这就是锁的重入性,即在同一锁程中,线程不需要再次获取同一把锁。

Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。

经过试验,上面的代码结果,10个线程执行并不是每个线程都能执行完两个同步块,有些线程执行第一个同步块后被其他线程插队了。

我的理解是,可重入性并非指第二个同步块不需要通过锁争夺可直接进入,而是争夺到锁不需要上图的monitorenter。

volatile

作用

  • 保证变量的内存可见性
  • 禁止volatile变量与普通变量重排序

如何禁止指令重排序

通过内存屏障来实现,内存屏障有两个作用:

  1. 阻止屏障两侧的指令重排序;
  2. 强制把写缓冲区/高速缓存中的脏数据等写回主内存,或者让缓存中相应的数据失效。

关于volatile还需要回看文章加强巩固细节

线程池

Java中的线程池顶层接口是Executor接口,ThreadPoolExecutor是这个接口的实现类。

线程池参数及常用线程池

线程池原理