《实战Java高并发程序设计》JDK并发包/JUC–第三章
synchronized的扩展
重入锁:java.util.concurrent.locks.ReentrantLock
public static ReentrantLock lock=new ReentrantLock();
@Overriede
public void run(){
lock.lock();
try{
....
}finally{
lock.unlock();
}
}
由于这种锁一个线程可以反复的进入,所以叫重入锁
/**
一个线程可以连续两次获得同一把锁
*/
lock.lock();
lock.lock();
try{
i++;
}finally{
lock.unlock();
lock.unlock();
}
重入锁和synchronize的区别
①重入锁对逻辑控制的灵活性要比synchronize好
②重入锁可以提供中断处理能力
中断响应
如果一个线程正在等待锁,那么它依然可以获得一个通知,被告知无需再等待,可以停止工作,这种情况对死锁有帮助。
例如你和朋友约好一起去打球,如果你等了半个小时朋友还没到,突然一个电话过来说有突发情况不能去了,那么等待这个事就被中断了
锁申请等待限时
通常我们无法判断一个线程为什么迟迟拿不到锁,但如果给定一个等待时间,让线程自动放弃,那么对系统来说是有意义的。我们可以使用tryLock()方法进行一次限时等待。
公平锁
公平锁会按照时间的先后顺序,保证先到者先得原则,这样做不会产生饥饿现象。
public static ReentrantLock fairlock=new ReentrantLock(true);
CAS(compare and swap)原则(比较交换原则)
CAS原理就是函数中三个参数,一个是当前内存值A,一个是期望值B,一个是即将更新的值C,当且仅当预期值B和内存值A相等时,则将A的值修改为C的值并返回true,否则什么都不做,并返回false.
ABA问题
如果一个变量V初次读取的时候是A,并在准备赋值的时候检查到它仍然是A,并不是说A就一定没有被改变过,因为有可能在这一段时间内这个值曾经被修改为B,然后又修改回了A,那么CAS操作就会被误认为没有改变过,这个问题就是ABA问题。解决这个问题的核心思想就是加上时间戳来标识不同阶段的数值。比如JUC里面提供的一个原子引用类”AtomicStamedReference",还可以使用传统的互斥同步也就是利用锁的原理。
ReentranLock的几个重要方法整理
Lock() | 获得锁,如果锁已经被占用,则等待 |
---|---|
lockInterruptibly() | 获得锁,但优先响应中断 |
tryLock(long time,TimeUnit unit) | 在给定时间内尝试获得锁,如果成功则返回true,如果失败,则返回 |
tryLock() | 尝试获得锁,如果成功,返回true,失败返回false,该方法不等待,立即返回 |
unlock() | 释放资源 |
重入锁的三个要素
①原子状态。原子装填使用CAS操作来存储当前锁的状态,判断锁是否已经被别的线程持有.
②等待队列。所有没有请求到锁的线程会进入等待队列(BlockingQueue)进行等待,待有线程释放锁后,系统就能从等待队列中唤醒一个线程,继续工作。
③阻塞原语park()和unpark()。用来挂起和恢复线程。没有得到锁的线程将会被挂起。
配合重入锁的方法:Condition条件
在synchronized同步中配合使用的是wait和notify(),在重入锁Lock中和这两个方法有相同作用的是await()和signal和signalAll()
await() | 将当前线程进入等待状态,同时释放当前锁,当其他线程调用signal()时线程重新获得锁继续执行 |
---|---|
awaitUninterrupibly() | 该方法和await()方法基本相同,但是它不会在等待过程中响应中断 |
signal() | 用来唤醒一个等待中的线程 |
public static ReentrantLock lock=new ReentrantLock();
public static Condition condition=new lock.newCondition();
信号量
信号量可以指定多个线程,同时访问某一资源,信号量主要提供以下构造函数
public Semaphore(int permits,boolean fair);
ReadWriteLock读写锁
线程A1,A2进行读操作,线程B1,B2进行写操作,如果重入锁和内部锁理论上说读读、读写、写写之间都是串行的,由于读操作并不对数据的完整性进行破坏,所以我们可以在读读之间利用读写锁,则可以进行并行操作。
读写锁的访问约束
读-读不互斥,可以并行。
读-写,写-写之间都互斥,只能串行。
倒计时器:CountDownLatch
用来控制线程等待,它可以让某一个线程等待直到倒计时结束再开始执行。
线程池
为什么使用线程池
①降低资源消耗。通过重复利用已经创建的线程降低线程创建和销毁造成的消耗
②提高响应速度。当任务到达时,任务可以不需要等待线程创建就可以直接执行
③提高线程的可管理性。线程是稀缺资源。如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优以及监控。·
JUC线程池体系
ThreadPoolExecutor类实现了Executor和ExecutorService这两个接口,并有Executors类扮演线程池工厂的角色
ThreadPoolExecutor(int corePoolSize,int maximumPoolsize,long keepAliveTime,TimeUnit unit,BlockingQueue workQueue,RejectedExecutionHandler handler)
corePoolSize | 线程池维护线程的最少数量,即常用线程数量 |
---|---|
maximumPoolsize: | 线程池维护线程池的最大线程数量 |
keepAliveTime | 当线程池线程数量超过corePoolSize时,多余的空闲线程的存活时间 |
unit | keepAliveTime的单位 |
BlockingQueue | 任务队列,任务被提交但尚未被执行,即等待队列 |
handler | 线程池对拒绝任务所使用的策略 |
线程池的调度策略(增长策略)
当一个任务需要调用线程池中线程进行处理时:
1)如果此时线程池中的数量小于等于corePoolSize,则线程池会创建新的线程来处理被添加的任务
2)如果此时线程池中的线程数量大于corePoolSize,但是等待队列BlockingQueue未满,那么这个任务就会被添加到等待队列当中
3)如果此时线程池中的数量大于corePoolSize,但是等待队列BlockingQueue已满,但是线程池中的线程数量小于maximumPoolSize,则建立新的线程来处理被 添加的任务
4)如果此时的线程数量大于corePoolSize,等待队列已满,并且大于maximumPoolSize,那么就会通过handler来调用拒绝策略来处理
等待队列BlockingQueue 4种
**1)**直接提交队列:SynchronousQueue队列,它比价特殊,因为它没有容量,每执行一个插入操作就会阻塞,需要再执行一个删除操作才会被唤醒,反之每一 个删除操作也都要等待对应的插入操作
**2)**有界任务队列:ArrayBockingQueue.若有新的任务需要执行时,线程池会创建新的线程,直到创建的线程数量达到corePoolSize时,那么会将新的任务加入 到等待队列当中,如果等待对列已经满了,线程数量没有达到线程池最大线程容量的情况下,则继续创建新的线程,若达到了最大线程数量,则执行拒绝策略
**3)**无界任务队列:LinkedBlockingQueue.该队列可以无限制的添加新的任务,线程池创建的常驻线程数量就是corePoolSize,而等待队列可以无限制的增加,也 就是当线程数量达到常驻线程数量之后,就会添加到无界任务队列当中。使用这种队列,一定要注意任务提交和处理之间的协调和控制,不然会出现资源耗 尽
4)优先任务队列:PriorityBlockingQueue.它其实是一个特殊的无界任务队列,只是执行的时候采用的是优先级的顺序执行的。
拒绝策略4种
1)abortPolicy:直接抛异常
2)callerRunsPolicy策略:直接执行当前任务
3)DiscardOledestPolicy策略:丢弃最老的任务(即即将执行的任务),提交当前的任务到队列当中
4)DiscardPolicy策略:直接丢弃当前任务。这种策略最合理
Executors里面主要有以下几种线程池方法
(1)newFixedThreadPool:固定大小线程池,corePoolSize和maximumPoolSize都为用户指定的线程数量,阻塞队列采用LinkedBlockingQueue
(2)newcachedThreadPool:可以无限扩大的线程池,比较适合处理执行时间比较小的任务,采用synchronousQueue装等待任务
(3)newsingleThreadExecotor:单一线程池,它只会创建一条工作线程处理任务,采用LinkedBlockingQueue
(4)ScheduledThreadPool:可调度的线程池,实现周期性调度,比较常用
JDK的并发容器
(1)ConcurrentHashMap,这是一个高效的并发线程安全的hashMap
(2)CopyOnWriteArrayList:这和ArrayList一样,但这是利用了读写分离锁,在读多写少方面性能非常好
(3)ConcurrentLinkedQueue:线程安全的LinkedList
(4)BlockingQueue,通过链表和数组实现的一个接口,表示阻塞队列,非常适合于作为数据共享的通道
(5)ConcurrentSkipListMap:跳表,使用跳表的数据结构进行快速查找
读写分离锁,在读多写少方面性能非常好
(3)ConcurrentLinkedQueue:线程安全的LinkedList
(4)BlockingQueue,通过链表和数组实现的一个接口,表示阻塞队列,非常适合于作为数据共享的通道
(5)ConcurrentSkipListMap:跳表,使用跳表的数据结构进行快速查找
跳表内的所有数据都是有序的,查找时,可以从顶级链表开始找,一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续查找,这也就是说这种查找方式是跳跃式的。