如何在并发性能和数据安全性之间做好平衡,在很多地方都有类似的设计,比如 cpu的三级缓存、mysql 的 buffer_pool、Synchronized 的锁升级等等。
作用:
可见性:一个线程修改共享变量后,其他线程立刻可以看到修改后的只。
造成可见性问题的原因:
2.1 CPU 层面,针对 MESI 协议的更进一步优化去提升 CPU 的利用率,引入了StoreBuffer 机制,而这一种优化机制会导致 CPU 的乱序执行。当然为了避免这样的问题,CPU 提供了内存屏障指令,上层应用可以在合适的地方插入内存屏障来避免 CPU 指令重排序问题。
2.2 编译器的优化,编译器在编译的过程中,在不改变单线程语义和程序正确性的前提下,对指令进行合理的重排序优化来提升性能。 所以,如果对共享变量增加了 volatile 关键字,那么在编译器层面,就不会去触发编译器优化,同时再 JVM 里面,会插入内存屏障指令来避免重排序问题。
除了 volatile 以外,从 JDK5 开始,JMM 就使用了一种 Happens-Before 模型去描述多线程之间的内存可见性问题。如果两个操作之间具备 Happens-Before 关系,那么意味着这两个操作具备可见性关系,不需要再额外去考虑增加 volatile 关键字来提供可见性保障。
实现 wait/notify 机制的条件:
调用 wait 线程和 notify 线程必须拥有相同对象锁。
wait() 方法和 notify()/notifyAll() 方法必须在 Synchronized 方法或代码块中。
在 ThreadLocal 中,除了空间换时间的设计思想以外,还有一些比较好的设计思想,比如线性探索解决 hash 冲突,数据预清理机制、弱引用 key 设计尽可能避免内存泄漏等。
线程池本质上是一种池化技术,是一种资源复用的思想。比如常见的连接池、内存池、对象池。
Executors
最大线程数是 Integer.MaxValue,线程存活时间是 60 秒,阻塞队列用的是 SynchronousQueue,这是一种不存才任何元素的阻塞队列,也就是每提交一个任务给到线程池,都会分配一个工作线程来处理,由于最大线程数没有限制。所以它可以处理大量的任务,另外每个工作线程又可以存活 60s,使得这些工作线程可以缓存起来应对更多任务的处理。
核心线程和最大线程数量都是一个固定的值,如果任务比较多工作线程处理不过来,就会加入到阻塞队列里面等待。,
线程数量无法动态更改,因此可以保证所有的任务都按照 FIFO 的方式顺序执行。
newScheduledThreadPool: 具有延迟执行功能的线程池, 可以用它来实现定时调度
newWorkStealingPool: Java8 里面新加入的一个线程池它内部会构建一个 ForkJoinPool,利用工作窃取的算法并行处理请求。
这些线程都是通过工具类 Executors 来构建的,线程池的最终实现类是 ThreadPoolExecutor。
当我们提交一个任务到线程池的时候,它的工作原理分为四步。
所以,如果希望这个任务不进入队列,那么只需要去影响第二步的执行逻辑就行了。Java 中线程池提供的构造方法里面,有一个参数可以修改阻塞队列的类型。其中,就有一个阻塞队列叫 SynchronousQueue, 这个队列不能存储任何元素。它的特性是,每生产一个任务,就必须要指派一个消费者来处理,否则就会阻塞生产者。
基于这个特性,只要把线程池的阻塞队列替换成 SynchronousQueue。就能够避免任务进入到阻塞队列,而是直接启动最大线程数去处理这个任务。以上就是我对这个问题的理解。
java中的四种引用:
项 | synchronized | Lock |
---|---|---|
特性 | JAVA关键字,在JVM层面 | J.U.C包中的接口 |
获取 | A获得锁,B等待,A阻塞,B一直等待 | 可尝试获得锁,线程可以不用一直等待 |
释放 | 执行完同步代码或发生异常,被动释放 | 在finally中国释放,避免死锁 |
状态 | 无法判断 | 可以判断 |
类型 | 可重入,不可中断,非共偶 | 可重入,可中断,可公平,可非公平 |
性能 | 少量同步 | 大量同步 |