并发编程常见概念

本篇博文主要记录了各种并发概念,参考《JAVA并发编程的艺术》。

指令重排

在执行程序时,为了提高性能,处理器和编译器常常会对指令进行重新排序,排序时要遵循两个规则:在单线程的环境下不能改变程序运行的结果、存在数据依赖关系的不可以指令重排。这两个规则可以总结为不能通过happen-before原则推导出来,JMM允许任意指令重排。

as-if-serial:所有的操作都可以为了优化而重排序,但是必须保证重排序的执行的结果不能改变。as-if-serial只保证单线程,多线程环境下无效。

as-if-serial

A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。因此在最终执行的指令序列中,C不能被重排序到A和B前面。但是A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。

happens-before

由于我们就所有的场景来规定某个变量修改的变量何时对其他线程可见,我们可以指定某些规则,这些规则就是happens-before。在JMM中,如果一个操作的结果对另一个操作的结果可见,那么这两个操作之间必定存在happens-before关系。happens-before原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据。依靠这个原则,我们可以解决在并发环境下两操作之间是否可能存在冲突的所有问题。

happens-before原则的定义如下:

  1. 如果一个操作happens-before另一个操作,那么第一个操作的结果对于第二个操作结果可见,而且第一个操作的顺序排在第一个操作顺序之前。
  2. 两个操作之间存在happens-before关系,并不一定要按照happens-before原则指定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种排序结果不非法。

下面是happens-before的原则规则:

  1. 程序次序规则:一个线程内,按照代码的顺序,书写前面的操作先发生与书写在后面很的操作。
  2. 锁定规则:一个unlock操作先行发生与后面对同一个锁的unlock操作。
  3. volatile变量规则:对于变量的写操作先行发生于对这个变量的读操作。
  4. 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先发生于操作C、
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
  6. 线程中断规则:对线程的interrupt()方法的调用先行发生与被中断线程的代码检测到中断事件的发生。
  7. 线程终结规则:线程中的所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()返回值手段检测到线程已经终止执行。
  8. 对象终结规则:一个对象初始化完成先行发生于它的finalize()方法的开始。

synchronized和volatile比较

synchronized实现原理

Java中的每一个对象都可以作为锁,具体表现为以下三种形式。

  1. 对于普通的同步方法,锁是当前的同步对象。
  2. 对于静态同步方法,锁是当前类的class对象。
  3. 对于同步方法块,锁是synchronized括号里面配置对象。

JVM基于进入和退出Monitor对象来实现方法的同步和代码块的同步,但是两者的实现细节不一样。代码块的同步是使用monitorenter和monitorexit指令实现的,方法的实现是使用另一种方式实现的,但是可以同样使用这两个指令来实现。monitorenter指令是在编译后插入到同步代码块开始的位置,而monitorexit是插入到方法的结束处和异常处。任何对象都有一个monitorenter与之相关联,当一个monitor被持有后,它将处于锁定状态,线程执行到monitorenter指令时,将会尝试获取对象锁对应的monitor的所有权,即尝试获取对象的锁。

synchronized作用域

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号括起来的代码,作用的对象时调用这个代码块的对象。
  2. 修饰一个方法,被修饰的方法被称为同步方法,其作用范围是整个方法,作用的对象是调用这个方法的对象。
  3. 修饰一个静态的方法,作用的范围是整个静态的方法,作用的对象是这个类的所用对象。
  4. 修饰一个类,其作用的范围是synchronized后面括起来的部分,作用的对象是这个类的所用对象。

锁的四种状态

synchronized在JSK1.6之后为了减少获取锁和释放锁,引入了偏向锁和轻量级锁,使得synchronized变得不那么重,所以锁的状态一共有四种 ==》 无锁状态、偏向锁、轻量级锁、重量级锁。锁的状态只升级不能降级

无锁状态

无锁状态是指不对资源进行锁定,所有线程都可以访问,但只有一个线程可以修改成功。

偏向锁

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。

在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。

轻量级锁

CAS

重量级锁

升级为重量级锁之后等待的线程会阻塞

volatile 实现原理

  1. 使得变量更新变得具有可见性,只要被volatile关键字修饰的变量的赋值一旦发生变化就会通知到其他线程,其他线程会放弃副本拷贝的值,主动去主内存进行拷贝。
  2. 产生内存屏障,防止指令进行重排序。

volatile 和 synchronized的区别

  1. Volatile 的本质是告诉JVM当前变量是不确定的,需要从主存中获取。synchronized.synchronized 是锁定变量,只有当前线程可以访问。
  2. Volatile 仅能使用变量级别。synchrnized 则是锁定当前变量,方法。
  3. Volatile 不会造成线程的阻塞,synchronized会造成线程的阻塞。

怎么实现所有的线程在等待某个事件的发生才会去执行

  1. 读写锁:主线程先获取写锁,所有的子线程获取读锁,等待线程发生时主线程释放写锁。
  2. countDownLatch countDownLatch初始值设置为1,所有的子线程调用await方法等待,等事件发生时调用countDown方法计数减为0。

synchronized和lock有什么区别

  1. 首先synchronized是java内置关键字,在jvm层面,Lock是个java类。
  2. synchronied无法判断是否获取锁的状态,Lock可以判断是否获取到锁。
  3. synchronized会自动释放锁(a:线程执行完会自动释放锁 b:线程执行遇到异常时会自动释放锁),Lock需要在finally中手动释放锁,否则会造成线程死锁。
  4. 用synchronized关键字的两个线程1和线程2,如果线程1获得锁或者线程1阻塞,线程2则会一直等待下去。而lock锁不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了。
  5. synchronized锁可重入,不可中断,非公平。而lock锁可重入,可中断,可公平。
  6. Lock锁适合大量同步的代码同步问题,而synchronized锁适合代码少量同步问题。

CAS(无锁算法)

对于多线程编程问题,为了保证多个线程对同一对象进行访问时,我们需要加同步锁synchronized,保证对象在多线程环境下使用正确性,但是加锁会导致如下两个问题1. 加锁和释放锁会导致上下文切换,引起性能问题。2. 多线程可以导致死锁问题。

  • 独占锁(悲观锁):在整个处理过程当中将数据处于锁定状态,它指的是数据被外界修改保持悲观状态,synchronized就是一种独占锁,它会导致所有需要此锁的线程挂起,等待锁的释放。
  • 乐观锁:相对于悲观锁而言,乐观锁假设一般情况下不会造成冲突,在数据进行提交更新的情况下才会正式对数据的冲突与否进行检测,如果发现数据冲突则让用户决定如何去做。

CAS的机制就相当于这种(非阻塞算法),CAS由cpu内部硬件实现,执行速度较快,CAS有三个操作参数:内存地址,旧值,新值,操作时比较旧值有没有发生变化,如果没有发生变化就交换新值,没有发生变化则不交换。

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

  1. ABA问题: CAS操作值的时候,会检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成B,后又变成A,那么使用CAS进行检查时发现它的值没有发生变化,实际上却发生了变化,可以用版本号进行解决。
  2. 循环的时间长开销比较大。
  3. 只能保证一个共享变量的原子操作。

CountDownLatch和CyclicBarrier简介

CountDownLatch

CountDownLatch的构造函数接受一个int的参数作为构造器,如果想等待n个完成就需要传入参数为n。当调用countDownLatch的countDown方法时,n就会减1,countDownLatch的await方法会阻塞当前线程,直到n变为0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class AIMain {
public static void main(String[] args) throws InterruptedException {

CountDownLatch count = new CountDownLatch(2);
new Thread(() -> {
System.out.println(Thread.currentThread() + "正在执行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count.countDown();
System.out.println(Thread.currentThread() + "执行完毕");
}).start();

new Thread(() -> {
System.out.println(Thread.currentThread() + "正在执行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count.countDown();
System.out.println(Thread.currentThread() + "执行完毕");
}).start();

System.out.println("-----------");
System.out.println("waiting two thread excuted");
count.await();
System.out.println("all thread alread excuted");
}
}

底层分析:
CountDownLatch底层是基于AQS来实现的,使用一个status变量来判断是否阻塞线程,如果status不为0的情况下,调用await()方法会将当前线程阻塞,并将线程加入队列里面。当有线程调用countdown时,会将status变量减一,直到变为0。

CyclicBarrier

CyclicBarrier的字面意思是可循环使用的屏障。它要做的事情就是躺一组线程达到一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续执行。线程执行await()方法后线程将减1,并进行等待。直到计数器减为0,所有调用await()方法而在等待的线程才能继续执行。CyclicBarrier和CountDownLatch的区别是CyclicBarrier的计数器可以通过reset()方法循环使用,所以它才叫循环屏障。

底层分析:

与CountDownLatch的实现方式不同,并没有直接通过AQS来实现同步功能,而是通过ReentrantLock的基础上实现的。在线程调用await()方法时,会调用Condition的await方法进入等待状态,当最后一个线程调用await()方法时,调用Condition的signalAll方法唤醒所有线程。