提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
CountDownLatch
类似于一个跑步比赛,当比赛开始的时候,这个比赛啥时候结束,当最后一个选手撞线就结束
JUc提供了一个类来解决上述问题
使用的时候先设置一下有几个选手
每个选手撞线了,就调用一下countDown方法(计数器–,线程阻塞)
当撞线的次数达到了选手的个数,就任务比赛结束了(await被唤醒)
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
Thread t = new Thread(() -> {
System.out.println("选手出发! " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("选手到达! " + Thread.currentThread().getName());
//撞线
countDownLatch.countDown();
});
t.start();
}
//await是进行阻塞等到,会等到所有选手都撞线之后,才解除阻塞
countDownLatch.await();
System.out.println("比赛结束");
}
写时拷贝:copyonwriteArrayList一写多读不加锁保证线程安全
如何避免读到修改一数据:
旧方法修改不一定是原子的,可能读到修改了一半的数据,解决方法拷贝一份,在新的修改,引用指向新的顺序表,释放旧的内存
重点难点:
一、谈谈HashMap HashTable ConcurrentHashMap的区别
1.线程安全角度:
Hashmap不安全
Hashtable, CoucurrentHashtable安全
2.多线程下的优化:(CoucurrentHashMap好处)
1.锁粒度的控制:
HashTable 直接在方法上加synchroinzed,相当于是对this加锁,相当于是针对哈希表对象来加锁,一个哈希表,只有一个锁多个线程,无论这些线程都是如何操作的这个哈希表,都会产生锁冲突
CoucurrentHashMap每个哈希桶都有自己的锁,大大降低了锁冲突的概率,性能也就大大提高了
2.CoucurrentHashMap做了一个激进的操作:
只是个写操作加锁,读操作不加锁了
如果一个线程读,一个线程修改,也没有锁冲突
是否担心读的是修改一半的数据呢
不必担心,CoucurrentHashMap设计的时候保证读的是整个数据(要么是旧版本,要么是新版本,不会说读了修改一半的数据),另外广泛使用volatile保证内存可见性,读到数据是及时的
3.充分的利用到了cas特性
比如像维护个数,都是通过cas来实现,而不是加锁
包括还有些地方使用cas实现的轻量级锁来实现
总之,concurrentHashMap思路是能不加锁,就不加锁,尽一切可能来减低锁冲突的概率
4.ConcurrentHashMap对于扩容操作,进行了特殊的优化
化整为零(类似于拷贝)
HashTable的扩容方式是当put时候发现负载因子已经超过阈值,就触发扩容,申请一个更大数组,,然后把之前旧的数据给搬到新的数组上
很大的问题:
如果元素个数特别多,开销就很大,触发put这一操作可能会卡很久
CoucurrentHashMap在扩容的时候,就不再是直接一次性完成搬运了,而是搬运一点
扩容的时候,旧的和新的会同时存在一段时间,每次进行哈希表的操作,都会把旧的内存上的元素搬运到一部分到新的空间上,直到搬运完成
如果要查询元素旧的新的一起查
插入元素,直接往新的上插入
如果是删除元素,直接删了不用搬运了
分段锁:是旧版本的CouCurrentHashMap,是好几个链表公用同一个锁(java8开始就没了,锁冲突概率高,代码实现复杂)
5.key值
HashMap key允许为null
HashTable 和 CouCurrentHashMap key值不能为null
二、如何解决死锁
常见死锁场景
1.一个线程,一把锁,连续加锁俩次,如果不可重入,死锁
例子:
count++;
2.俩个线程,俩把锁
例子:1.钥匙锁车里,车钥匙锁家里
2.吃饺子:拿醋给酱油,拿酱油给醋
3.程序员修复一码通
死锁代码:
//演示死锁
public class demo34 {
public static void main(String[] args) {
Object locker1=new Object();
Object locker2=new Object();
Thread t1=new Thread(()->{
System.out.println("t1尝试获取locker1");
synchronized (locker1){
try {
Thread.sleep(500);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("t1尝试获取 locker2");
synchronized (locker2){
System.out.println("t1 获取俩把锁成功");
}
}
});
Thread t2=new Thread(()->{
System.out.println("t2 尝试获取locker1");
synchronized (locker2){
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("t2 尝试获取 locker2");
synchronized (locker1){
System.out.println("t2获取俩把锁成功");
}
}
});
t1.start();
t2.start();
}
}
产生死锁条件有双重循环,加锁顺序
3.多个线程多把锁:
哲学家进餐问题:
五个哲学家,五根筷子,
假设哲学家要么思考人生,啥事不做(阻塞)
要么吃面条,先拿左手筷子,再拿起右手筷子,吃一会就放下(线程执行)
由于线程调度是随机的,哲学家们,啥时候吃面,啥时候思考人生不确定
极端情况下:同时拿起左手筷子,就死锁了
解决方案:打破循环等待
约定:俩把锁必须先获取编号小端,后获取编号大的,不是先左后右,有效避免循环等待
银行家算法,统称分配