J.U.C包简介

来源:互联网 发布:莽荒纪神兵进阶数据 编辑:程序博客网 时间:2024/06/15 08:30

  本文主要是在拜读了《java并发编程的艺术》之后的一个总结,对相关重点进行结构性的梳理。这本书写的还是相当赞的,还是比较符合个人的思维方式。《java并发编程实战》阅读起来还是相对晦涩些,建议读者先看《java并发编程的艺术》,再啃《java并发编程实战》这本书,并没有变低或者抬高谁的意思。

some words

  juc包是jdk1.5之后引入的,并且是以api的方式,是一个叫Doug Le的大神写的。那问题来了,既然是一个人写的,那为什么不是我写的,既然是一个人写的 ,为什么早不写晚不写恰恰在jdk1.5时引入。这就涉及到一个背景,从jdk1.5开始,java使用新的JSR-133内存模型,而JSR-133增强了volatile关键字的内存语义,这只是一个开始。由下至上的总结下:

volatile、CAS、final

  像指令重排序、内存屏障这些词汇可以查找相关文献 理解下到底是什么含义。volatile它的作用就是保证数据的可见性,volatile变量的内存语义:当写一个volatile变量时,jmm会把该线程对应的本地内存中的共享变量值刷新到主内存中;volatile变量的内存语义:当读一个volatile变量时,jmm会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量(普通变量 线程都会在自己本地内存拷贝一份,写的时候并不会刷主存,读的时候直接拿来用,并发时作为共享变量当然会有问题了)。那volatile怎么做到的呢?其实在volatile写操作之前会插入一个SS屏障,写操作之后插入一个SL屏障。volatile读操作之后插入一个LL屏障和一个LS屏障。屏障对应到底层就是一个特殊的指令了,反正目的就是避免volatile变量和其他变量的重排序,并保证可见性。像java语言内置的Synchronized关键字编译之后底层也是插入了相关的特殊指令。
  CAS(compare and swap)是一个组合操作(读-改-写),但是它会保证操作的原子性,它基于处理器提供的高效机器级别的原子指令,依赖底层硬件支持。更深入的介绍可以查看相关资料。
  volatile和CAS是juc包的基石。顺带介绍下final关键字,它的作用就是final的引用不能从构造函数中“溢出”,什么意思?就是在构造一个对象时,它里面的final域会先初始化。这是在final关键字修辞成员变量时的作用,在修辞局部变量或者参数时,仅仅是保证final引用不可变而已,编译之后相关信息会被擦除。

AQS

  AbstractQueuedSynchronizer(同步器)是用来构建锁或者其他同步组件的基础框架。它是一个模板类,子类可重写的五个方法有:tryAcquire(),tryRelease(),tryAcquireShared(),tryReleaseShare()及isHeldExclusively()。第一二个方法独占锁使用,三四个方法共享锁使用。第五个方法表示是否被当前线程所独占。它里面有一个volatile int state 变量,用于记录状态,还有一个FIFO的CLH队列,这个队列持有被阻塞线程的引用。具体细节可以查看相关资料

锁及同步组件

  这里介绍几个比较典型的类。其实他们大多数都是实现了Lock接口然后定义一个内部类继承于AQS(为啥用内部类,可自行google),或者组合相关类。
  ReentrantLock 它是一个可重入的排他锁,这点和Synchronized的语义类似。
  ReadWriteLock 读写锁 。好奇的是AQS只有一个state变量,它是怎么做到同时记录读、写两个状态的。其实 它把state变量分为了两部分,高16位用于记录读状态,低16位用于记录写状态。使用场景当然是用于读多写少的 场景了。
  CountDownLatch 底层实现很简单,内部类直接实现了AQS。
  ConcurrentHashMap 里面segment继承了ReentrantLock 。分segment锁,锁的粒度小,就可以并发访问了。写操作是加锁的,读不加锁。解决hash冲突的方式和hashmap一样,放在链表头,为什么要放在链表头,因为next属性是final的。
  这里说到了锁Lock,锁当然会阻塞线程了。它是怎么阻塞线程的呢?调用LockSupport类的park()方法,唤醒线程用unpark()方法。park()和unpark()方法是如何阻塞和唤醒线程的呢?它其实是调用了Unsafe类提供的方法,Unsafe是如何阻塞和唤醒线程呢?其实就是一个本地系统调用了,声明为native的方法。unsafe是个后门类,有很多超级方法。我们不能直接使用Unsafe类,它进行了安全认证,但我们可以通过反射来使用。具体细节可以google

Executor框架

  我们知道线程池处理任务的时候。处理流程是 :核心线程(CorePoreSize)池->queue->最大线程(maximumPoolSize),其中核心线程数和最大线程数这两步会获取全局锁,这会是一个严重的伸缩瓶颈(通常都会实现预热Executor使线程数大于或等于CorePoreSize)。线程池里的线程之所以不停的执行任务,是因为线程的run方法是一个for()无限循环,在里面不停的执行我们提交的Runnable任务。
  我们知道了增长的顺序,那如何线程怎么回收呢。默认情况下如果普通线程获取不到任务的这个线程将会被回收,获取任务的方法就是poll(),获取不到任务就会退出上面说的for循环。它有个时间参数,这个时间就是我们初始化线程池时设置的timeOut参数。逻辑就是如果线程空闲时间到了timeOut这个线程会被回收。这个是最大线程减少逻辑。那核心线程什么时候被回收呢?正常情况下核心线程是不会被回收的,因为核心线程获取任务的方法是take(),如果获取不到任务它将被一直阻塞,所以 如果我们初始化了一个线程池但是没有shutDown(),将会导致整个jvm进程不会退出,因为核心线程一直没有被回收呢。那不正常情况是啥,我们设置allowCoreThreadTimeOut为true就行了(默认为false),这样核心线程获取任务的方法变为poll(),超时获取不到任务就会被回收了。
  本文只是简单的介绍了下juc包,具体细节还请查阅相关书籍。系统的组织看到的东西便于记忆,不然时间久来容易忘记。纯文字记录阅读起来相对枯燥。

参考资料:

Java并发编程的艺术(方腾飞、魏鹏、程晓明)
http://www.cnblogs.com/nullzx/p/5175574.html
http://extremej.itscoder.com/threadpoolexecutor_source/

原创粉丝点击