本文首发在我的个人博客:https://jlice.top/p/7t3gc/。欢迎大家前去参观,么么哒~
这两天快速看了下《实战Java高并发程序设计》这本书,对Java高并发程序有一个初步的认识。
这本书是在iPad上用MarginNotes 3看的,只是做了些笔记,还没进行代码实践,后续还需要细化。
并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的,而并行是真正意义上的“同时执行”。
临界区用来表示一种公共资源或者说共享数据,可以被多个线程使用。但是每一次, 只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源就必须等待。
原子性是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作旦开始,就不会被其他线程干扰。
指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。
关键字 volatile 并不能代替锁,它也无法保证一些复合操作的原子性。关键字 volatile 也能保证数据的可见性和有序性。
当一个Java应用内只有守护线程时,Java虚拟机就会自然退出。
关键字 synchronized 的作用是实现线程间的同步。它的工作是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全性。
重入锁 使用 java.util.concurrent.locks.ReentrantLock 类来实现
重入锁有着显示的操作过程。开发人员必须手动指定何时加锁,何时释放锁。
一个线程连续两次获得同一把锁是允许的
如果同一个线程多次获得锁,那么在释放锁的时候,也必须释放相同次数。
如果释放锁的次数多了,那么会得到一个 java.lang.IllegaMonitorStateException 异常
如果释放锁的次数少了,那么相当于线程还持有这个锁,因此,其他线程也无法进入临界区。
lockInterruptibly()是一个可以对中断进行响应的锁申请动作,即在等待锁的过程中,可以响应中断。
使用 tryLock() 方法进行一次限时的等待。
重入锁允许对其公平性进行设置,公平锁的实现成本比较高,性能却非常低下:
public ReentrantLock(boolean fair)
利用 Condition 对象, 可以让线程在合适的时间等待,或者在某一个特定的时刻得到通知,继续执行。
信号量(Semaphore):信号量可以指定多个线程,同时访问某一个资源
ReadwriteLock是读写分离锁。可以有效地帮助减少锁竞争提升系统性能。
倒计数器: CountdownLatch
循环栅栏: CyclicBarrier
CyclicBarrier 可以接收一个参数作为 barrierAction 就是当计数器一次计数完成后,系统会执行的动作。
LockSupport是一个非常方便实用的线程阻塞工具,它可以在线程内任意位置让线程阻塞。
更为一般化的限流算法有两种:漏桶算法和令牌桶算法。
在使用线程池后,创建线程变成了从线程池获得空闲线程,关闭线程变成了向线程池归还线程
Executor框架
核心线程池的内部实现:都只是 Threadpoolexecutor 类的封装
拒绝策略
自定义线程创建: ThreadFactory
ThreadPoolExecutor 是一个可以扩展的线程池。它提供了beforeExecute()、afterExecute()、terminaerd() 三个接口用来对线程池进行控制。
除JDK内置的线程池以外,Guava 对线程池也进行了一定的扩展
DirectExecutor线程池总是将任务在当前线程中直接执行
将普通线程池转为Daemon线程池的方法 MoreExecutors.getExitingExecutorService()
ConcurrentHashMap:这是一个高效的并发 Hashmap
CopyOnWriteArrayList: 在读多写少的场合,这个List的性能非常好,远远优于 Vector。读取是完全不用加锁的, 写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。
ConcurrentLinkedQueue:高效的并发队列
BlockingQueue 表示阻塞队列,非常适合作为数据共享的通道。BlockingQueue 让服务线程在队列为空时进行等待,当有新的消息进入队列后,自动将线程唤醒
ConcurrentSkipListMap:跳表的实现。
JMH (Java Microbenchmark Harness) 专门用于性能测试的框架,其精度可以到达毫秒级。
减少锁持有时间
减小锁粒度。所谓减小锁粒度,就是指缩小锁定对象的范围,从而降低锁冲突的可能性,进而提高系统的并发能力
用读写分离锁来替换独占锁。在读多写少的场合使用读写锁可以有效提升系统的并发能力
锁分离
锁粗化 虚拟机在遇到一连串连续地对同一个锁不断进行请求和释放的操作时,便会把所有的锁操作整合成对锁的一次请求,从而减少对锁的请求同步的次数,这个操作叫作锁的粗化。
锁偏向 锁偏向的核心思想是:如果一个线程获得了锁, 那么锁就进入偏向模式。当这个线程再次请求锁时,无须再做任何同步操作。 使用Java虚拟机参数-XX:+UseBiasedLocking
可以开启偏向锁。
轻量级锁 使用轻量级锁时,不需要申请互斥量,仅仅将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。
自旋锁 让当前线程做几个空循环(这也是自旋的含义), 如果还不能获得锁,才会真的将线程在操作系统层面挂起
锁消除 Java虚拟机在JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竟争的锁。
逃逸分析 观察某一个变量是否会逃出某一个作用域
Threadlocal
ThreadLocal的实例代表了一个线程局部的变量,每条线程都只能看到自己的值,并不会意识到其它的线程中也存在该变量。 它采用采用空间来换取时间的方式,解决多线程中相同变量的访问冲突问题。 为每一个线程分配不同的对象,需要在应用层面保证 ThreadLocal 只起到了简单的容器作用。ThreadLocalMap 是定义在 Thread 内部的成员,ThreadLocalMap的实现使用了弱引用。
无锁的策略使用一种叫作比较交换(CAS, Compare And Swap)的技术来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。
CAS算法的过程是:它包含三个参数CAS(VE,N),其中V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果值和E值不同, 说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。
JDK并发包中有一个atomic包,里面实现了一些直接使用CAS操作的线程安全的类型。
死锁 哲学家就餐问题
【单例模式】
public class StaticSingleton {
private StaticSingleton() {
System.out.println("StaticSingleton is create");
}
private static class SingletonHolder {
private static StaticSingleton instance = new StaticSingleton();
}
public static StaticSingleton getInstance() {
return SingletonHolder.instance;
}
}
不变模式
对属性的final定义确保所有数据只能在对象被构造时赋值1次。之后,就永远不发生改变。
对class的final确保了类不会有子类
【生产者-消费者模式】
生产者-消费者模式中的内存缓冲区的主要功能是数据在多线程间的共享,此外,通过该缓冲区,可以缓解生产者和消费者间的性能差。
BlockigQueue 充当了共享内存缓冲区,用于维护任务或数据队列
Disruptor 框架使用无锁的方式实现了一个环形队列,非常适合实现生产者-消费者模式
消费者监控冲区中的信息策略由 WaitStrategy 接口进行封装,
当两个变量存放在一个缓存行时,在多线程访问中,可能会影响彼此的性能。
Future模式
Future 模式的核心思想是异步调用
并行流水线
执行过程中有数据相关性的运算都是无法完美并行化的
并行搜索
并行排序
奇偶交换排序
希尔排序
矩阵乘法
将矩阵A进行水平分割,得到子矩阵A1和A2,矩阵B进行垂直分割,得到子矩阵B1和B2。再将结果进行拼接就能得到原始矩阵A和B的乘积。
网络NIO
AIO
在Java8中,使用 default关键字可以在接口内定义实例方法
函数式接口就是只定义了单一抽象方法的接口
方法引用使用 :: 定义, :: 的前半部分表示类名或者实例名,后半部分表示方法名称。如果是构造函数,则使用new表示。
parallel() 方法得到一个并行流
集合对象并行化可以使用 parallelStream() 函数
Arrays.parallelSort() 方法直接使用并行排序
新版本的 ConcurrentHashMap 增加了一些foreach操作、reduce操作、computeIfAbsent()、search操作
newKeySet() 方法返回一个线程安全的Set
在反应式编程中,核心的两个组件是Publisher和Subscriber。Publisher将数据发布到流中, Subsciber则负责处理这些数据