也可以在我的个人博客中看这篇文章?:Java高并发与多线程
代码在我的GitHub上,代码中也会有详细的解释:JUC
为啥叫JUC?因为Java中的并发包名称是java.util.concurrent
代码在 c_000 部分。
synchronized(Object)
线程同步
synchronized优化:代码 c_013 部分
synchronized底层实现:
早期JDK中,synchronized是重量级的,即需要调用操作系统(OS)来申请锁。
后来改进了,有了锁的升级:
只有锁的升级,没有锁的降级。
彻底搞懂synchronized(从偏向锁到重量级锁)
synchronized代码在c_001 至 c_011 部分。
保证线程间可见
禁止指令重排
可参考内容Java内存模型
volatile代码在 c_012 部分
单例模式–双检锁代码
原子操作
Compare And Set (CAS)
操作原理
//V:代表要改的那个值;E:代表期望值;N:代表新值
//只有目前的变量值和所期望的值相等的时候,才会去赋值新值;否则再次尝试或失败
cas(V,E,N){
if(V == E){
V = N;
}
//otherwise try again or fail
}
即期望值一开始是A,由于别人操作改成了B,之后又变成了A。
解决方式:加version(版本)
如果是基础类型(int…),无所谓;如果是引用类型,比如你和你的前女友复合,你不知道她中间经历了多少男人或女人,你不难受吗。
Java的CAS操作,AtomicXXX类,都依靠了Unsafe类。这个类很牛逼,可以像C和C++一样操作内存,但是该类在JDK1.9之后不让用了。
代码在c_015部分。
并发的核心:CAS 是什么?Java8是如何优化 CAS 的?
代码在c_016部分
重量级锁、自旋锁、轻量级锁、偏向锁、悲观、乐观锁等各种锁
countDown也不是说只能在一个线程里countDown一下,也可以在一个线程里countDown N多下,只要到0了,就继续执行剩下代码。
代码c_017部分
代码c_018部分
代码c_019部分
共享锁,读锁就是共享锁,在读的时候大大提高效率。即只要是读操作,就不会阻塞等待锁释放,大家可以一起读。
排他锁,互斥锁,写锁就是排他锁
代码c_020部分
限流,最多的时候我允许你有多少个线程同时运行。
acquire,得到,这是一个阻塞方法,当来一个线程时,调用acquire,信号量减1,信号量为0时,别的线程就得等着。
就是用来控制同时运行的线程,比如你有100个线程,但是信号量定为2,表示你有100个线程,但是同时运行的只有两个线程。
release,线程业务处理完毕后,一定要调用该方法,将个数还回去,否则影响别的线程的运行,会导致别的线程一直处于阻塞
代码c_021部分
只能是两个线程之间,exchange方法是阻塞的,一个A线程exchange了,另一个B线程没有exchange,那么A线程就等着,阻塞。
三个线程之间没有意义。
代码c_022部分
LockSupport翻译过来是锁支持。
代码c_023
实现一个容器,提供两个方法,add和size;写两个线程,线程1添加10个元素至容器中,线程2实现监控元素个数,当容器内个数达到5个的时候,线程2给出提示并结束。
写一个固定容量同步容器,拥有put和get方法,以及getCount方法,能够支持2个生产者线程以及10个消费者线程的阻塞调用。
AQS的核心就是使用CAS的操作,操作双向链表的head和tail,替代了synchronized操作。
参考这篇文章,包含AQS讲解和ReentrantLock源码的分析(jDK1.8):从源码角度彻底理解ReentrantLock(重入锁)
还可以看这篇JAVA并发编程: CAS和AQS
代码c_025_AQS
代码c_026_VarHandle
代码c_027_ThreadLocal
用途:spring的声明式事务,保证同一个connection
参考文章:Java并发编程:深入剖析ThreadLocal
代码:c_029_RefType
我们平常典型编码 Object obj = new Object();
中的obj就是强引用。通过关键字new创建的对象所关联的引用就是强引用。当JVM内存空间不足,JVM宁愿抛出OOM运行时错误,使程序异常终止,也不会随意回收具有强引用的“存活”对象来解决内存不足的问题。对于一个普通对象,如果没有其他引用关系,只要超过了引用的作用域或者显示地将相应(强)引用赋值为null,就是可以被垃圾收集了,具体回收时机要看垃圾收集策略。
软引用通过SoftReference类实现。软引用的生命周期比强引用短一些。只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象:即JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。后续,我们可以调用ReferenceQueue的poll()方法来检查是否有它所关心的对象被回收。如果队列为空,将返回一个null,否则该方法返回队列中前面的一个Reference对象。
应用场景:软引用通常用来实现内存敏感的缓存。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
弱引用通过WeakReference类实现。弱引用的生命周期比软引用短。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。由于垃圾回收器是一个优先级很低的线程,因此不一定会很快回收弱引用的对象。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
对应的ThreadLocal中的ThreadLocalMap的Entry继承的就是弱引用。
加了个图,但是在GitHub上显示不全不知道为啥,可以直接访问链接查看:弱引用ThreadLocal中的Entry
还有WeakHashMap,可以参考这篇文章或看源码:WeakHashMap的详细理解
虚引用也叫幻象引用,通过PhantomReference类来实现。无法通过虚引用访问对象的任何属性或函数。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
ReferenceQueue queue = new ReferenceQueue ();
PhantomReference pr = new PhantomReference (object, queue);
程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取一些程序行动。
应用场景:可用来跟踪对象被垃圾回收器回收的活动,当一个虚引用关联的对象被垃圾收集器回收之前会收到一条系统通知。
Java中容器分两大类:Collection和Map,其中Coll8ection中又分List、Set、Queue。
Queue是后加的,在很大程度上是为了高并发准备的(线程池中经常用),其中阻塞队列(BlockingQueue)很重要。(这也是List和Queue的区别)。
Vector和HashTable在JDK1.0就有了,在当初设计的时候有点问题,在其内部的所有方法上都加了锁,自带锁,现在基本不用。
代码:c_030_01_FromHashTable2CHM
Map的进化历程:HashTable -> HashMap -> SynchronizedHashMap -> ConcurrentHashMap
跳表参考:跳表(SkipList)及ConcurrentSkipListMap源码解析
T01_ConcurrentHashMap
代码:c_030_02_FromVector2Queue
进化历程:Vector -> List -> SynchronizedList -> Queue
使用早期的同步容器以及Collections.snchronizedXXX
方法的不足之处,参考:
java集合框架【3】 java1.5新特性 ConcurrentHashMap、Collections.synchronizedMap、Hashtable讨论
使用新的并发容器:jdk1.5新特性 ConcurrentHashMap
代码:T02_CopyOnWriteList
ConcurrentQueue都是线程安全的操作。T03_ConcurrentQueue
LinkedBlockingQueue无界的,最大是Integer.MAX_VALUE;ArrayBlockingQueue有界的。
T04_LinkedBlockingQueue
T05_ArrayBlockingQueue
BlockingQueue在Queue的基础上多了put
和take
方法(这两个方法才体现了Blocking,offer是不会阻塞的)。
put元素时如果慢了,则线程会阻塞住;take取元素时,没有元素了会阻塞住。
BlockingQueue是天生的友好的生产者消费者模型
T06_DelayQueue
T08_SynchronousQueue
transfer
方法,该方法装元素,装完等着,有人来取才会继续。put
方法装完就不管了,装完就继续执行了,不等待。T09_TransferQueue
PriorityQueue是有排序的,内部是一课堆排序的树结构。
T07_PriorityQueue
题:使用两个线程,线程1打印 A B C …;线程2打印 1 2 3…最后打印出的结果要是A1B2C3这种交替打印。
在调用wait()和notify()之前,必须使用synchronized语义绑定住被wait/notify对象,否则会报错。
提供了多种实现方式,难易程度:LockSupport cas BlockingQueue wait-notify lock-condition (仅供参考)。
代码:c_028_interview
ThreadPoolExecutor
线程池忙(包括指定的最大线程数),而且任务队列满,这时候会进行拒绝策略(拒绝策略可以自定义,但是JDK提供了四种)。
四种拒绝策略:(实战上都不用,一般都自定义)
有两种执行线程池的方法execute和submit,这两个方法都可以启动线程,但是submit有返回值,可以返回一个future对象
ForkJoinPool
简单使用及参数说明,这个要背下来:T06_HelloThreadPool
Executors返回的线程池对象的弊端如下:
- FixedThreadPool和SingleThreadExecutor:允许的请求队列长度为
Integer.MAX_VALUE
,可能会堆积大量的请求,从而导致OOM。- CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为
Integer.MAX_VALUE
,可能会创建大量的线程,从而导致OOM。
Executors.newSingleThreadExecutor
只有一个线程的线程池,可以保证任务扔进去的顺序。
可以去看源码实现,都是使用的ThreadPoolExecutor类来指定的。
代码:T07_SingleThreadExecutor
这个线程池,来任务就起一个新的线程,不会放入队列中,因为任务队列为SynchronousQueue的容量为0.
代码:T08_CachedThreadPool
代码:T09_FixedThreadPool
代码:T10_ScheduleThreadPool
阿里都不用,自己估算,进行精确定义。
并行是并发的子集。
ThreadPoolExecutor源码解析
代码:T12_ForkJoinPool
代码:T11_WorkStealingPool
Disruptor