1.3.1 Lock接口及其实现
1、Lock:接口
方法说明:
lock:获取锁(不死不休)【最常用】
tryLock:浅尝辄止【试一下,没取到锁就返回false,否则返回true】
tryLock(time时间数字,时间单位):过时不候【带超时时间的锁】
lockInterruptibly:任人摆布【可中断的锁】(一般更昂贵,有的没有实现这个方法)
unlock:释放锁
Condition newCondition:相当于synchronized的WaitSet队列,死锁与wait/notify相似
ReadWriteLock:
概念:维护一对关联锁,一个只用于读操作,一个只用于写操作
读锁可以由多个读线程同时持有,写锁是排他的,同一时间,两把锁不能被不同线程持有
适用场景:
适合读取操作多于写入操作的场景(读取>写入),改进互斥锁的性能
比如:集合的并发线程安全性改造、缓存组件
锁降级:
指的是写锁降级成为读锁。持有写锁的同时,再获取读锁,随后释放写锁的过程。
写锁是线程独占,读锁是共享,所以写 -> 读是降级。(读 -> 写,是不能实现的)
ReentrantLock:可重入锁
ReentrantReadWriteLock
2、加锁lock、解锁unlock要成对出现,如果lock次数 > unlock次数,就死锁;
如果lock次数 < unlock次数,就抛异常
3、synchronized与Lock的区别
synchronized:
优点:
1、使用简单、语义清晰
2、由JVM提供,提供了多种优化方案(锁粗化、锁消除、偏向锁、轻量级锁)
3、锁的释放由虚拟机来完成,不用人工干预,也降低了死锁的可能性
缺点:
无法实现一些锁的高级功能如:公平锁、中断锁、超时锁、读写锁、共享锁
Lock:
优点:
1、所有synchronized的缺点
2、可以实现更多的功能,让synchronized缺点更多
缺点:
需手动释放锁unlock,新手使用不当可能造成死锁
结论:synchronized是卡片机,Lock是单反;都是互斥锁
1.3.2 AQS抽象队列同步器详解
1、AQS:是AbstractQueuedSynchronizer的简称
让线程阻塞、排队的公共业务逻辑,这些公共逻辑代码抽出来作为模板,就是AQS
2、JDK中AQS:
AQS中的主要方法:acquire获取、release释放、acquireShared 、releaseShared
tryAcquire tryRelease tryAcquireShared tryReleaseShared
AQS中的字段:
int state :实现ReadWriteLock时,前16位实现readCount,后16位实现writeCount
Thread Owner
Queue:
head、node、node、tail
3、ReentrantReadWriteLock:
写锁降级为读锁(避免其他线程很快拿到写锁,阻塞当前线程获取读锁)
4、读写锁的加锁过程:
写锁加锁:先判断readCount是否为0,
如果readCount不为0就进入等待队列;
如果readCount为0就判断writeCount是否为0,
如果writeCount不为0,就判断owner是否为自己,如果是自己,writeCount就加1;owner不是自己就进入等待队列;
如果writeCount为0,就将writeCount值改为0,owner赋值为当前线程
读锁加锁:先判断writeCount是否为0,
如果writeCount不为0,就判断owner是否为自己,如果是自己,readCount加1(锁降级);
如果owner不是自己,进入等待队列;
如果writeCount为0,就对readCount加1
1.3.3 并发容器类-1
1、查找算法:二分、分段、hash表
hash表:也叫散列表,它通过把关键码值key映射到表中一个位置来访问记录,以加快查找的速度。
这个映射函数叫做散列函数,存放记录的数组叫做散列表
2、JDK1.7的hashmap根据hash表算法查找数据(没有完全解决链表长度变长的问题)
1.存储方式:根据key计算hash值(hash值的取值范围int.min_value到int.max_value),
i=hash%length(hash值与map的size长度取);i就是key在map中的第几个链表,
每一个i里都对应着一个链表
2.每次扩容size扩大一倍(扩容后的size=原size2)
3.每次扩容需要将map中已经存储的数据重新计算i值,重新存储
4.当size >= size0.75 并且 table[i]不为null就扩容;(threshold=size0.75)
3、JDK1.8的hashmap:
如果链表的长度大于等于TREEIFY_THRESHOLD(默认值是8)时,就将链表转为红黑树
红黑树在插入时,性能比链表差
4、红黑树:相当于是一个索引,可以认为数据挂载到红黑树后,就被排序了
5、链表:插入比较快,当链表比较短时,查找不会太耗性能
6、算法复杂度:
O(1):最低的时空复杂度,耗时/耗空间与输入数据大小无关
O(n):比如时间复杂度为O(n),就代表数据量增大几倍,耗时也增大几倍【遍历算法】
O(n2):比如时间复杂度O(n2),就代表数据量增大n倍时,耗时增大nn倍【冒泡排序】
O(log n):比如,当数据增大256倍时,耗时只增大8倍;O(log 256)=8【二分查找】
O(n log n):就是n乘以log n,当数据增大256倍时,耗时增大256*8=2048倍【归并排序】
7、hashtable与hashmap的数据结构基本一致(数组加链表),扩容也和hashmap基本一致
只是比hashmap多了一个互斥锁(synchronized)
8、ConcurrentHashMap:
jdk1.7中:
concurrentHashMap就是一个分段锁
concurrentHashmap与hashmap完全不一样
每一个segments是一个特殊的hashtable(segments[i]每一个i位置都放的是一个hashtable)
segments的size不会扩容(不会改变),只会扩每个i对应的hashtable的size
用空间换时间,是一个比较大粒度的锁,性能较差
jdk1.8中:
没有分段锁,对链表加锁,粒度小(一般在10条以内),并发度就很高
依然使用数组、链表,数据结构与hashmap一致
使用CAS机制、synchronized保证线程安全
如果i位置为null(链表头部),就用cas操作将i位置赋值为new的node值,
如果cas失败,就自旋,用synchronized锁住链表头部
1.3.4 并发容器类-2
1、ConcurrentSkipListMap:跳表
所有的数据都按照key值进行排序
首先有一个HeadIndex,这个是进行所有的插入、查找的入口
有一个level(索引层级),node(数据节点)、right索引、down索引
HeadIndex指向最高level(层级)的索引
每一个node都有一个next,最后一个node的next为null
有一个算法,在put时随机的产生索引(索引的层级也是随机产生的),这个索引可以快速的查找到node
这个索引的node指向当前put的这个node,headIndex的right索引指向当前新产生的这个索引
2、ArrayList:
初始长度为0,调用add方法时初始化数组长度为10,
当调用add方法时,发现size不够时就扩容,每次扩容都是当前size*1.5
扩容时创建一个新的数组,将旧的数组中元素拷贝到新的数组中
3、ReadWriteLock的问题:
当读非常多时,写很难获取到锁,写就会饿死
当写获取到锁时,大量的读线程被阻塞,会很大的影响读的效率
用CopyOnWrite机制改善ReadWriteLock的问题
4、CopyOnWriteArrayList:
在执行add方法时,创建一个size为原size+1的数组,将原数组的值复制到新数组,
然后在新数组末尾赋以add的值,最后将CopyOnWriteArrayList的array引用指向新数组
写时复制的容器,和ArrayList比较,优点是并发安全
缺点:
1、多了内存占用:写数据是copy一份完整的数据,单独进行操作,占用双份内存
2、数据一致性:数据写完之后,其他线程不一定是马上读取到最新的内容
5、set和list的区别:
6、Queue
队列数据结构实现,分为阻塞队列、非阻塞队列。
阻塞队列特有方法:put、take
(循环数组)ArrayBlockingQueue:数组存储、有锁、put与take用的是同一把锁
LinkedBlockingQueue:链表存储、有锁、put用put锁,take用take锁(两把锁,互不影响)
ConcurrentLinkedQueue:链表存储、无锁(不阻塞,用CAS保证线程安全)
(同步队列)SynchronousQueue:
阻塞的:1、take会阻塞,直到取到元素
2、put时会阻塞,直到被take
非阻塞的:
3、若没有take方法阻塞等待,offer的元素会丢失
4、poll取不到元素,就返回null,如果正好有put被阻塞,可以取到
5、peek永远只能取到null,不能让put结束阻塞
(优先级队列)PriorityBlockingQueue:打破了先进先出规则,可以自定义优先级规则(排序规则)
public class Demo4_PriorityBlockingQueue3 {
public static void main(String args[]){
PriorityBlockingQueue<Student> queue = new PriorityBlockingQueue<>(5, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
int num1 = o1.age;
int num2 = o2.age;
if (num1 > num2)
return 1;
else if (num1 == num2)
return 0;
else
return -1;
}
});
queue.put(new Student(10, "enmily"));
queue.put(new Student(20, "Tony"));
queue.put(new Student(5, "baby"));
for (;queue.size() >0;){
try {
System.out.println(queue.take().name);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Student{
public int age;
public String name;
public Student(int age, String name){
this.age = age;
this.name = name;
}
}
1.3.5 Fork/Join框架详解
1、AQS的具体使用:
Semaphore:
是一个计数信号量,常用于限制可以访问某些资源(物理或逻辑)线程数目
简单说,是一种用来控制并发量的共享锁
CountDownLatch:
本质是一个共享锁(倒计数)
2、CyclicBarrier:
循环栅栏,可以循环利用的屏障
eg:排队上摩天轮时,每到齐4个人,就可以上同一个车厢
(用于多线程计算数据,最后合并计算结果的场景)
3、Callable:
call方法就相当于一个可以返回线程运行结果的线程run方法
用FutureTask将Callable对象包裹起来,好传入Thread对象中
一个FutureTask实例,只能使用一次
*************同时说明:这个任务,从头到尾只会被一个线程执行
4、fork/Join
是一个任务拆分、结果汇总的一个框架
只在单个进程中执行
ForkJoinPool:
是ExecutorService接口的实现,它专为可以递归分解成小块的工作而设计
fork/join框架将任务分配给线程池中的工作线程,充分利用多处理器的优势,提高程序性能
使用fork/join框架的第一步是编写执行一部分工作的代码
类似的伪代码如下:
if(当前工作部分足够小)
直接做这项工作
else
把当前工作分成两部分
调用这两个部分并等待结果
将此代码包装在ForkJoinTask子类中,通常是RecusiveTask(可以返回结果)、RecursiveAction
当数据量不是特别大的时候,我们没有必要使用ForkJoin。因为多线程会涉及到上下文的切换。所以数据量不大的时候使用串行比使用多线程快