并发

1 minute read

  很多开发同学可能只是大概了解这些用法,但如果深入研究下,会有很大的收获,也能更好地使用他们。
并发编程带来的问题
  Java内存模型规定,所有的变量都放在主内存,当需要使用时,会把主内存里面的变量复制到自己的工作内存,线程读写变量操作的都是自己工作内存的变量。 Java内存模型是一个抽象的概念,在实际实现中,每个CPU核都有自己的一级缓存,被集成到CPU内部,在某些架构中还有一个所有CPU都共享的二级缓存。Java内存模型里的工作内存对应着这里的L1或L2缓存或CPU的寄存器。
synchronized
  synchronized块是Java提供的一种原子性内置锁,Java中的每个对象都可以当做一个同步锁来使用。线程的执行代码进入synchronized代码块前会自动获取内部锁,这时候其它线程访问该同步代码块会被阻塞挂起。 拿到内部锁的线程在正常退出代码块或者抛出异常后或者在同步块内调用了该内置锁资源的wait系列方法时释放该内置锁。内置锁是排它锁,也就是当一个线程获取锁后,其它线程必须等待该线程释放锁后才能获取该锁。
  由于Java中的线程是与操作系统的原生线程一一对应的,当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,会导致上下文切换,这是一个很耗时的操作。
volatile
  当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或缓存中,而是会把值刷新回主内存。当其它线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程工作内存的值。 可以这样理解,线程写入volatile变量值时等价于线程退出synchronized同步块,此时会把写入工作内存的变量值同步到主内存,读取volatile变量时相当于进入同步块,此时会清空本地内存变量值,再从主内存获取最新值。
  volatile还有防止指令重排序的功能。
  写volatile变量时,可以确保volatile写之前的操作不会被编译器重排序到volatile写之后。
  读volatile变量时,可以确保volatile读之后的操作不会被编译器重排序到volatile读之前。
  volatile关键字能保持共享变量的可见性,但不能保证复合操作的原子性。一般什么时候适合使用volatile变量?
  写入变量值不依赖变量的当前值。
CAS
  CAS,即Compare and Swap,jdk提供的非阻塞原子操作,通过硬件保证了比较-更新操作的原子性。
  boolean compare(Object obj, long valueOffset, long expect, long update)
  参数分别为对象内存位置、对象中变量的偏移量、变量预期值、新值,含义为如果对象obj中内存偏移量为valueOffset的变量值为expect,则使用新的值update替换旧的值expect。这个方法是处理器提供的原子性指令。
  CAS可能会带来的ABA问题,JDK中AtomicStampedReference类给每个变量的状态值都配备了一个时间戳,避免了ABA问题的产生。
CPU Cache

  Cache一般是集成到CPU内部,Cache内部按行存储,其中每一行称为一个Cache行。Cache行是Cache与主内存进行数据交互的单位,Cache行的大小一般为2的幂次数字节。
  当CPU访问某个变量时,首先会从CPU Cache中是否有该变量,若有直接获取,若没有则从内存中获取该变量,根据局部性原理,然后把该变量所在内存区域的一个Cache行大小的内存复制到Cache中。 由于存到Cache行的是内存块而不是单个变量,所以一个Cache行会有多个变量。当多个线程同时修改一个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,相比于将每个变量放到一个缓存行,性能会有所下降,这就是伪共享。   多线程下并发修改一个缓存行中的多个变量时,会出现缓存行频繁地从内存load到缓存,然后失效,然后再load,从而降低程序的性能。
  JDK8之前一般是通过字节填充避免该问题,也就是创建一个变量时使用填充字段填充该变量所在的缓存行,避免将多个变量存放在同一个缓存行。
  JDK8提供sun.misc.Contented注解,可用来修饰类,也可以修饰变量。
乐观锁与悲观锁
  乐观锁与悲观锁一开始是数据库中引入的名词,悲观锁一般是使用数据库提供的锁机制,乐观锁一般是在表中添加version字段实现。
公平锁与非公平锁
  公平锁一般是指线程获取锁的顺序是按照线程请求锁的时间早晚来决定的。在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销。
独占锁与共享锁
  独占锁保证任何时候都只有一个线程能得到锁,ReentrantLock是独占锁。共享锁可以同时由多个线程持有,ReadWriteLock读写锁。
可重入锁
  可重入意味着同一个线程可以再次获得同一个锁,synchronized属于可重入锁,可重入锁原理是在锁内部维护一个线程标识,标识该锁目前被哪个线程占用,然后关联一个计数器,一开始计数器值为0,说明该锁没有被 任何线程占用。当一个线程获取该锁时,计数器值会变为1,这时其它线程再来回去该锁时会发现锁的所有者不是自己而被阻塞挂起。
自旋锁
  自旋是指当前线程获取锁时,如果发现锁已经被其它线程占用,它不会马上阻塞自己,在不放弃CPU使用权的情况下,多次尝试获取(默认次数是10),很有可能在后面几次尝试中其它线程已经释放锁。如果尝试指定的次数后仍没有 获取到锁则当前线程会被阻塞挂起。
  由此看来自旋锁是使用CPU时间换取线程阻塞和调度的开销,但也很有可能这些CPU时间被白白浪费。

Updated:

Leave a comment