Java并发笔记

2017-05-27  本文已影响0人  奔跑的小栋栋

volatile

volatile是轻量级的synchronized,它在多处理中开中保证了共享变量的“可见性”,即:一个线程的写操作的结果可以被另一个线程读到。
volatile不会引起线程上下文的切换和调度,比synchronized使用和执行成本更低。

写操作

jvm会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存,将其他CPU里缓存了该内存地址的数据置为无效。

读操作

发现读到的是无效数据,重新从系统内存中读到处理器缓存中。
JMM把线程本地内存置为无效,从主内存中读取共享变量

synchronized

  1. 对于普通同步方法,锁是当前实例对象
  2. 对于静态同不方法,锁是当前类的Class对象
  3. 对于同步方法块,锁是Synchronized括号里配置的对象

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,monitorenter插入同步代码块开始位置,monitorexit插入方法结束处和异常处。
任何对象都有一个monitor与之关联。

synchronized用的锁是存在Java对象头中的。

Java对象头:hashcode 分代年龄 是否是偏向所 锁标志位

锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态

原子操作的实现原理

1. 总线锁

使用处理器提供一个LOCK#信号,其他处理器请求被阻塞,那么该处理器就可以独占共享内存

2. 缓存锁

修改内部的内存地址,并允许它的缓存一致性机制
两种情况不使用:
1.不能缓存在CPU
2.CPU不支持缓存锁

3. Java实现原子操作

通过锁和循环CAS实现

Java内存模型

  1. 抽象定义

    1. 共享变量:实例域 静态域 数组元素
    2. JMM定义了线程和主内存之间的抽象关系:
      1. 线程之间的共享变量存储在主内存中
      2. 每个线程都有一个私有的本地内存
      3. 本地内存中存储了该线程以读/写共享变量的副本
Java内存模型的抽象结构示意图.png
  1. 重排序

    1. 编译器优化重排序
    2. 指令级并行重排序
    3. 内存系统重排序

    可能会导致多线程程序出现内存可见性问题,如何解决:

  2. JMM的编译器重排序规则会禁止特定类型的编译器重排序

  3. JMM的处理器重排序规则会插入内存屏障,通过内存屏障来禁止特定类型的处理器重排序。

重排序目的:再不改变程序执行结果的前提下,尽可能提高并行度。

happens-before:前一个操作的结果对后一个操作可见

concurrent包

concurrent包的实现示意图.png

线程

Java线程状态变迁.png

wait/notify

  1. wait() notify() notifyAll() 需要先对调用对象加锁
  2. 调用wait()方法后,线程状态由RUNNING变为WAITING,并把当前线程放置到对象的等待队列
  3. notify()notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()notifAll()的线程释放锁之后,等待线程才有机会从wait()返回。
  4. notify将线程从等待队列移到同步队列,线程状态由WAITING变为BLOCKED

同步队列

同步队列的基本结构.png 节点加入同步队列.png 首节点的设置.png

共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。以文件的读写为例,如果一个程序在对文件进行读操作,那么这一时刻对于该文件的写操作均被阻塞,而读操作能够同时进行。写操作要求对资源的独占式访问,而读操作可以是共享式访问,两种不同的访问模式在同一时刻对文件或资源的访问情况,如下图:

共享式与独占式访问资源的对比.png

ReentrantLock

重入锁:顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。

重进入:

  1. 线程再次获取锁:通过识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
  2. 锁的最终释放:获取多少次锁,最终就需要释放多少次锁,才能成功释放锁。

公平锁和非公平锁:

  1. 公平锁的获取需要判断同步队列中当前节点是否有前驱节点。
  2. 非公平锁的获取只需要CAS设置同步状态成功即可。
  3. 公平锁能够减少“饥饿”发生的概率,保证FIFO,非公平锁效率高。

ReadWriteLock读写锁

  1. 读写锁,维护一对锁:一个读锁和一个写锁,通过分离读写锁,提升了更好的 并发性和吞吐量。
  2. 同一时刻,多个读线程可同时访问读写锁。
  3. 写线程访问时,所有读线程和其他写线程均被阻塞。
  4. Java并发包实现:ReentrantReadWriteLock

LockSupport

LockSupport工具类定义了一组公共静态方法,提供了对线程阻塞和唤醒的操作。
part()阻塞 unpark()唤醒

Condition

Condition定义了等待/通知两种类型的方法,Condition依赖Lock对象,由Lock.newCondition()获取

ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个队列(以下称为等待队列),该队列是Condition对象实现等待/通知功能的关键。
并发包中的Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列。

同步队列和等待队列.jpg

Condition的await():将同步队列中的首节点通过addConditionWaiter()方法把当前线程构造成一个新节点并将其加入等待队列。如下图:

当前线程加入等待队列.png

Condition的signal():唤醒等待队列中等待时间最长的节点(首节点)。如下图。

  1. 检查当前线程是否获取了锁:isHeldExclusively()
  2. 将等待队列的首节点移动到同步队列
  3. 使用LockSupport唤醒节点中的线程
节点从等待队列移动到同步队列.png

ConcurrentHashMap

  1. get操作:不加锁,get方法中使用到的共享变量都被定义为volatile类型,除非读到的值时空才会加锁重读。

  2. put操作:

    1. 1.6采用锁住Segment实现并发操作
    2. 1.8采用CAS+synchronized实现并发操作

阻塞队列

JDK 7提供了7个阻塞队列,如下。
ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

实现原理:通过Condition的等待/通知实现生产者和消费者的通信。

Fork/Join框架

设计:

  1. 分割任务
  2. 执行任务并合并结果

实现:

  1. 创建ForkJoinTask,继承ForkJoinTask的子类
    1. RecursiveAction:无返回结果
    2. RecursiveTask:有返回结果
  2. 调用ForkJoinPool执行ForkJoinTask:forkJoinPool.submit(task);

原子操作类

  1. 原子更新基本类型类:AtomicBoolean AtomicInteger AtomicLong
  2. 原子更新数组:AtomicBooleanArray AtomicIntegerArray AtomicLongArray AtomicReferenceArray
  3. 原子更新引用类型:AtomicReference AtomicReferenceFieldUpdater AtomicMarkableReference
  4. 原子更新字段类:AtomicIntegerFieldUpdater AtomicLongFieldUpdater AtomicStampedReference

原理
使用CAS操作:Unsafe类提供了3种CAS方法:compareAndSwapObject compareAndSwapInt compareAndSwapLong

并发工具类

CountDownLatch 递减的计数器

countDown() getCount() await()
等待N个线程执行完成,功能类似Join,但是提供了await方法,可以用于指定时间后不再阻塞当前线程。count是同步队列中的数量。

CyclicBarrier 同步屏障

让一组线程到达一个屏障时被阻塞,知道最后一个线程到达屏障时,所有线程才会继续运行。
可用于多线程计算数据后合并计算结果。

CountDownLatch和CyclicBarrier区别

countDownLatch是一次性的,CyclicBarrier可以reset

CountDownLatch : 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行。
CyclicBarrier : N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。
这样应该就清楚一点了,对于CountDownLatch来说,重点是那个“一个线程”, 是它在等待, 而另外那N的线程在把“某个事情”做完之后可以继续等待,可以终止。而对于CyclicBarrier来说,重点是那N个线程,他们之间任何一个没有完成,所有的线程都必须等待。

总结:
CountDownLatch 是计数器, 线程完成一个就记一个, 就像 报数一样, 只不过是递减的。

而CyclicBarrier更像一个水闸, 线程执行就想水流, 在水闸处都会堵住, 等到水满(线程到齐)了, 才开始泄流。

Semaphore 信号量

acquire()获取许可证 release()释放许可证 tryAcquire()尝试获取许可证
控制只有指定数量的线程获取到资源。

Exchanger 交换者

exchange(T t) 交换数据
交换两个线程的数据。一个线程先执行exchange()等待另一个线程,另一个线程执行exchange()后,交换两个线程的数据。

线程池

处理流程

execute流程.png

新任务提交到线程池 -->

  1. 核心线程池 是否满了?
    1. 未满,创建线程执行任务
    2. 已满,下个流程
  2. 队列 是否满了?
    1. 未满,将任务存储在队列中
    2. 已满,下个流程
  3. 线程池 是否满了?
    1. 未满,创建线程执行任务
    2. 已满,按照策略处理无法执行的任务

线程池创建线程时,会将线程封装成工作线程Worker,Worker执行完任务后,还会循环从工作队列中获取任务来执行。

合理配置线程池

根据以下几个角度分析:

  1. 任务的性质:CPU密集型任务、IO密集型任务和混合型任务
  2. 任务的优先级:高、中、低
  3. 任务的执行时间:长、中、短
  4. 任务的依赖性:是否依赖其它系统资源,如数据库连接

性质
CPU密集型任务应配置尽可能小的线程,如:N(cpu)+1
IO密集型任务应配置尽可能多的线程,如:2*N(cpu)
混合型任务首先考虑拆分为前两个,如果拆分后两个任务的执行时间相差太大,则没有拆分的必要。

优先级
一般考虑使用优先级队列,注意低优先级任务可能会没机会执行

执行时间
给执行时间不同的任务分配不同的线程池,或者使用优先级队列让时间短的任务先执行

依赖性
等待时间越长,线程数越多,反之越少

建议使用有界队列

监控线程池

largestPoolSize 线程池中曾经创建过的最大线程数量
getActiveCount 获取活动的线程数

通过继承线程池自定义线程池,重写任务执行前后和线程池关闭前执行的方法进行监控。例如:监控任务的最大、最小和平均执行时间

Executor框架

任务的两级调度模型

任务的两级调度模型.png

Executor框架使用示意图

Executor框架使用示意图.png
上一篇下一篇

猜你喜欢

热点阅读