本章内容:
- 总结下并发有关问题(以某公司并发编程讲座为参考资料)
1、java的并发模型和操作系统有关么?
操作系统的不同会影响java中并发模型的实现方式。
Java在JVM层面抽象了一套自己的线程机制(里面的规则和操作系统层面的有一定相似性),用以映射不同的操作系统的任务调度。
譬如早期不支持线程的操作系统,jvm在操作系统上面是一个进程,当这个进程被操作系统调度到后,jvm内部实现的线程库再调度java线程。
之后,当操作系统开始支持多线程(如windows从一开始就支持线程),这样java线程就和操作系统线程一一对应或多多对应了,这个时候,如果是一一对应,那么线程的调度完全交给了操作系统内核,当然jvm还保留一些策略足以影响到其内部的线程调度。
比如线程模型中很多的实现都是native方法,native关键字的函数都是操作系统实现的, java只能调用。
如果一个机器上只有一个逻辑CPU,此时系统调度器通过轮换时间片的方式来调度任务,所以不存在真正的并行,只是并发;只有多个逻辑CPU的情形下,才会出现并行。
http://blog.csdn.net/gatieme/article/details/51481863
2、并发相关特性和解决办法
Visibility:通过并发线程修改变量值, 必须将线程变量同步回主存后, 其他线程才能访问到。volatile
Ordering:通过java提供的同步机制或volatile关键字(也可以是实现Visibility), 来保证内存的访问顺序。
Cache coherency :它是一种管理多处理器系统的高速缓存区结构,其可以保证数据在高速缓存区到内存的传输中不会丢失或重复。
Happens-before :synchronized,volatile,final,java.util.concurrent.lock,atomic
Java存储模型有一个happens-before原则,就是如果动作B要看到动作A的执行结果(无论A/B是否在同一个线程里面执行),那么A/B就需要满足happens-before关系。
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
- 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;
- volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作;
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
开始接触happens-before规则和指令重排序的时候,着实乱了以阵子,尤其是happens-before规则的第一条,明明说了同一个线程中每个动作都happens-before其后一个动作,而指令重排序又会对没有依赖的两个操作进行重排序,这不是相互矛盾的么?
经过网上翻阅了一些资料以后,我的理解是,happens-before规则是用来判断一个动作对另一个动作是否可见的法则,他只是用来判断可见性的,而不是决定执行顺序的,就是说动作A和动作B 的执行顺序是可以通过指令重排发生变化的,而如果你要保证A和B的可见性关系,就必须采用其他控制手段来保证AB的执行顺序不被打乱,这样就能用happens-before规则来判断AB两个动作的可见性。
3、分离锁
分离锁负面作用:对容器加锁,进行独占访问更加困难,并且更加昂贵了。
内存使用的问题:sina就曾经因为在Action层使用ConcurrentHashMap而导致内存使用过大,修改array后竟然单台服务器节省2G。
https://www.ibm.com/developerworks/cn/java/j-lo-lock/(如何更聪明地使用锁)
4、volatile关键字:
volatile关键字:
1:简化实现或者同步策略验证的时候来使用它;
2:确保引用对象的可见性;
3:标示重要的生命周期的事件,例如:开始或者关闭。
脆弱的volatile的使用条件:
1:写入变量不依赖变量的当前值,或者能够保证只有单一的线程修改变量的值;
2:变量不需要和其他变量共同参与不变约束;
3:访问变量时不需要其他原因需要加锁。
Volatile 变量是一种非常简单但同时又非常脆弱的同步机制,它在某些情况下将提供优于锁的性能和伸缩性。如果严格遵循 volatile 的使用条件即变量真正独立于其他变量和自己以前的值 ,在某些情况下可以使用 volatile 代替 synchronized 来简化代码。
http://blog.csdn.net/lazyer_dog/article/details/52345914
5、几个建议
对共享可变数据同步访问;
避免过多的同步;
永远不要在循环外面调用wait;
不要依赖于线程调度器;
线程安全的文档化;
避免使用线程组。
6、JUC包
Executors
Executor
ExecutorService
ScheduledExecutorService
Callable
Future
ScheduledFuture
Delayed
CompletionService
ThreadPoolExecutor
ScheduledThreadPoolExecutor
AbstractExecutorService
Executors
FutureTask
ExecutorCompletionService
Queues
BlockingQueue
ConcurrentLinkedQueue
LinkedBlockingQueue
ArrayBlockingQueue
SynchronousQueue
PriorityBlockingQueue
DelayQueue
Concurrent Collections
ConcurrentMap
ConcurrentHashMap
CopyOnWriteArray{List,Set}
Synchronizers
CountDownLatch
Semaphore
Exchanger
CyclicBarrier
Timing
TimeUnit
Locks
Lock
Condition
ReadWriteLock
AbstractQueuedSynchronizer
LockSupport
ReentrantLock
ReentrantReadWriteLock
Atomics
Atomic[Type], Atomic[Type]Array
Atomic[Type]FieldUpdater
Atomic{Markable,Stampable}Reference
7、线程池
1:线程池的大小最好是设定好,因为JDK的管理内存毕竟是有限的;
2:使用结束,需要关闭线程池;
3: Runtime.getRuntime().addShutdownHook(hook); 对不能正常关闭的
线程做好相关的记录。
8、任务池
任务池: ScheduledExecutorService
9、阻塞队列:BlockingQueue
生产者消费者模型中,会用到阻塞队列。
各种阻塞队列:
ArrayBlockingQueue:一个由数组支持的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。
Delayed 元素的一个无界阻塞队列,只有在延迟期满时才能从中提取元素.
LinkedBlockingDeque一个基于已链接节点的、任选范围的阻塞双端队列。
LinkedBlockingQueue一个基于已链接节点的、范围任意的 blocking queue。此队列按 FIFO(先进先出)排序元素
PriorityBlockingQueue一个无界阻塞队列,它使用与类 PriorityQueue 相同的顺序规则,并且提供了阻塞获取操作。
SynchronousQueue一种阻塞队列,其中每个插入操作必须等待另一个线程的对应移除操作 ,反之亦然。
10、ConcurrentHashMap
ConcurrentHashMap主要的知识点:锁分离,hash算法
11、CopyOnWriteArray{List,Set}
当读操作远远大于写操作的时候,考虑用这个并发集合。例如:维护监听器的集合。注意:其频繁写的效率可能低的惊人。
12、四大同步器
同步器是一些使线程能够等待另一个线程的对象,允许它们协调动作。最常用的同步器是CountDownLatch和Semaphore,不常用的是Barrier 和Exchanger
13、互斥锁: ReentrantLock
Lock 更加灵活,性能更好
interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException;
void unlock();
Condition newCondition();
}
支持多个Condition
可以不以代码块的方式上锁
可以使用tryLock,并指定等待上锁的超时时间
调试时可以看到内部的owner thread,方便排除死锁
RenntrantLock支持fair和unfair两种模式
14、读写锁: ReadWriteLock
ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。
15、AbstractQueuedSynchronizer
- 为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架;
- 基于JDK底层来操控线程调度,LockSupport.park和LockSupport.unpark;
- 此类支持默认的独占 模式和共享 模式之一,或者二者都支持;
- 如果想玩玩它,请看CountDownLatch的源码。
- 当然,也可以去FutureTask这个类看看。
- -
16、CAS
乐观锁
处理器指令,全称Compare and swap,原子化的读-改-写指令
处理竞争策略:单个线程胜出,其他线程失败,但不会挂起
目前的多处理器系统基本都支持原子指令,典型模式:首先从V中读取值B,由A生成新值B,然后使用CAS原子化地把V的值由A修改成B,并且期间不能有其他线程修改V的值,CAS能够发现来自其他线程的干扰,所以即使不使用锁,也能解决原子化地读-写-改的问题
非阻塞算法
- 一个线程的失败或挂起不影响其他线程
- J.U.C的非阻塞算法依赖Atomic
- 算法里一般采用回退机制处理Atomic的CAS竞争
- 对死锁免疫的同步机制
- 目标:相对阻塞算法,减少线程切换开销、减少锁的竞争等。
- 也是Lock-free,即无锁编程
编程实践
使用更优的锁
缩小锁的范围
- 减少线程把持锁的时间
- 避免在临界区进行耗时计算
- 常见做法1:缩小同步快
- 常见做法2:把一个同步块分拆成多个
- 需要保证业务逻辑正确为前提
避免热点域
- 热点域:每次操作,都需要访问修改的fields
- eg. ConcurrentHashMap的size计算问题
使用不变和Thread Local 的数据
- 不变数据,即Immutable类型数据、在其生命周期中始终保持不变,可以安全地在每个线程中复制一份以便快速读取。
- ThreadLocal数据,只被线程本身使用,因此不存在不同线程之间的共享数据的问题。
使用高并发容器
高速缓存计算结果
安全发布
利用成熟的框架
ThreadLocal
http://blog.csdn.net/youyou1543724847/article/details/52460852