juc知识整理

222 阅读10分钟

synchronized和lock的区别

1、首先synchronized是一个关键字lock是一个类

2、synchronized无法获取锁的状态,lock可以判断处理是否获取到锁

3、synchronized会自动释放锁,当代码块执行完毕是,lock需要自己在finally 自己释放锁,调用unlock() 方法。

4、用synchroined 关键字加锁时A线程拿到锁,不释放B线程会处于一直等待状态,而lock锁就不会等待下去,如果尝试取不到锁,线程可以不用一直等待久结束了。

5、synchronized的锁时可入重锁,是非公平锁。而lock是可入重锁,公平锁和非公平锁

6、Lock锁适合大量同步代码的同步问题,synchronized锁的代码适合少量的同步问题。

7、synchronized不可中断除非抛出异常或者中断,lock可以设置超时方法 超时后中断运行。

8、lock锁可以绑定多个条件的condition 可以实现分组唤醒线程,可以精准唤醒,而不是想synchronized随机唤醒

可重入锁:当一个线程进入一个带锁的方法时,可以调用方法里面带锁的另一个方法。

锁现象

1,、被synchronized修饰的方法,锁的对象是调用者,如果两个方法用的是一个调用者,那么会先执行先被调用的方法,再去调用第二个方法

2、如果两个调用者调用不同加锁的两个方法相互不影响。

3、没有加锁的方法被调用,调用者不影响

4、用static synchronized 修饰的方法 锁的这个类的class模板 所以不管是几个调用者都是先要释放这个class别的线程才能调用

5、被synchronized和static修饰的方法,锁的对象是类的class对象。仅仅被synchronized修饰的方 法,锁的对象是方法的调用者。因为两个方法锁的对象不是同一个,所以两个方法用的不是同一个锁, 后调用的方法不需要等待先调用的方法。

集合类多线程下不安全

List 的安全类 CopyOnWriteArrayList

1、List list = new Vector<>();

2、List list = Collections.synchronizedList(new ArrayList<> ());

3、List list = new CopyOnWriteArrayList<>();

Set 的安全类 CopyOnWriteArraySet

1、Set set = new HashSet<>();

2、Set set = Collections.synchronizedSet(new HashSet<>());

3、Set set = new CopyOnWriteArraySet();

Map 的安全类 ConcurrentHashMap?

concurrentHashMap的实现原理是什么?

concurrentHash在jdk1.7和jdk1.8实现的方式不一样

jdk1.7 是通过segment数组和hashEntry数组组成的,即concurrentHashMap把哈希桶分成了几个segment组每个segment有n个hashentry数组

首先先将数据分成一组一组的数据,然后每一组数据都加上了锁,当一个线程访问这组数据的时候,会在这组数据加上一把锁,其他组的数据也能被其他的线程访问。 从而实现真正的并发访问。

segment继承的 类,所以segment是一个可入重锁,扮演锁的角色,segment默认长度16,也就是并发为16

volatile修饰了 hashentry 的数据value和下一个节点next,保证了多线程的可见性。

img

jdk1.8 concurrentHashMap和hashmap用的是相同的数据结构,node数组+链表+红黑树实现的,在锁的实现上放弃segment的分段是锁引用的是,CAS+synchronized 实现更加细粒度的锁。将锁的级别控制在更加细粒度的哈希数组级别,也就是说只需要锁住链表的表头或者是红黑树的根节点,就不影响其他哈希数组的读写,大大提高了并发度。

img

为什么concurrentHashMap会使用synchronized锁替换可入重锁ReentrantLock?

在jdk1.6的时候synchronized的加了很多的锁的优化,并且synchronized有多种锁的状态,会从无锁-->偏量锁-->轻量级锁-->重量级锁的转换

减少内存的开支,假设使用可入重锁获取同步支持,那么每一个节点都需要通过继承AQS获得同步的支持,但是并不是每一个节点都需要同步支持,获取同步支持的只有链表的表头和红黑树的根节点需要同步,这就会带来巨大的内存浪费。

concurrentHashMap的put方法的执行逻辑是什么?

jdk1.7 首先会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用scanAndLockForPut()自旋获取锁,

1、尝试自旋获取锁。2、如果重试次数达到了最大值,则改为阻塞锁获取,保证能获取成功

jdk1.8 : 1、根据key计算出hash值;

2、判断是否需要进行初始化

3、定位node,拿到首节点,判断首节点。如果为null 则通过CAS的方式尝试添加

如果首节点的hash为-1 说明其他线程在扩容,参与一起扩容

如果都不满足,synchronized就锁住该节点,判断是链表还是红黑树进行遍历插入

4、当链表长度达到8的时候,数组扩容或者将链表转换为红黑树。

concurrentHashMap的get放的执行逻辑?

jdk1.7 首先根据key值计算出hash值定位到具体的segment,在根据hash值定位到数组hashentry对象,并对该对象进行遍历查找

由于hashentry使用volatile修饰,volatile可以保证数据内存的可见性,没次获取的都是最新值。

jdk1.8 1、先根据hash值算出数组的位置判断是否为空

2、判断是否为首节点

3、如果结构是红黑树就从红黑树查询

4、如果是链表就从链表中遍历查询

concurrentHashMap的get方法需要加锁么,为什么

不需要加锁,因为node数组是用关键字volatile修饰的内存的可见性,在多线程环境下一个线程修改该值,其他线程是可见的。这也是它比其他并发集合比如hashtable、hashmap的效率高的原因之一

get方法不需要加锁于volatile修饰的哈希桶数组有关么?

没有关系,哈希桶数组table用volatile修饰主要保证数组的可见性

concurrentHashMap不支持key或者value为空的原因

1、value为什么不能为空,因为concurrentHashMap是用于多线程,get一个key的value

就无法判断是这个值为null还是没有找到key是空的,就有看二义性。然而单线程有一个containKey()去判断是否包含了这个null值。

至于key为什么不能为空,底层源码就是这样的写的,可能这个设计者不喜欢null,所以在设计之初就不支持null。

concurrentHashMap的并发度是什么

jdk1.7 根据segment的长度默认16

jdk1.8 中摒弃了segment的概念选择了node数组+链表+红黑树并发度依赖于数组的大小

ConcurrentHashMap 迭代器是强一致性还是弱一致性

与hashmap迭代器是强一性不同,concurrentHashMap迭代器是弱一致性

concurrentHashMap的迭代器创建以后会对哈希表中的数据进行遍历,在遍历过程中,内部元素可能会发生变化,如果发生改变的是已经遍历过的元素,迭代器不会反映出来,如果是没有遍历的元素,迭代器就会发现并反应出来,这就是弱一致性。

这样迭代器线程可以使用原来老的数据,而写线程也可以并发完成改变,这样就保证了多个线程并发执行的连续性和扩展性,是提升性能的关键

concurrentHashMap1.7和1.8的区别?

数据结构: jdk1.7 的结构是segment数组和hashentry数组构成的而jdk1,8是用的node数组和链表和红黑树组成的。

锁机制:1.7锁机制就分段锁机制实现线程安全的,其中segment继承roontrantlock

jdk1.8采用的CAS+synchronized保证线程安全

锁的粒度:jdk1.7是segment加锁,jdk1.8是随每个数组元素加锁 锁的粒度更小,效率更高

时间复杂度:jdk1.7的遍历时间复杂度O(n) jdk1.8 O(logn)

链表转化红黑树:定为节点的hash算法简化会带来弊端,hash冲突,印厂链表节点树两大于8,链表转化为红黑树进行存储

concurrentHashMap和hashtable那个效率更高?为什么

concurrentHashMap和效率高于hashtable应为锁的机制不通,hashtable是用的全表锁给整个hashtable加了一把锁,concurrentHashMap用的是CAS+synchronized锁细粒度更低

img

多线程下安全的操作 map还有其他方法吗?

还可以使用Collections.synchronizedMap方法,对方法进行加同步锁。

如果传入的是 HashMap 对象,其实也是对 HashMap 做的方法做了一层包装,里面使用对象锁来保证多线程场景下,线程安全,本质也是对 HashMap 进行全表锁。在竞争激烈的多线程环境下性能依然也非常差,不推荐使用!

写入复制思想:

写入复制思想是计算机程序设计领域中的一种优化策略,原理是如果多个调用者同时要求相同的资源,他们会共同回去相同的指针指向相同的资源,直接到某个调用者视图修改内容时,系统才会真正复制一份专用副本给该调用者,而其他调用者书所见的到的最初的字眼仍然保持不变。这过程对其他的调用者是透明的,取法主要的有点是日过调用者没有膝盖资源,就不会有副本奔创建。因此多个调用者只是读取操作是可以共享用一份资源。

读写分离,写入时复制一个新的数组,完成插入,修改或者移除操作后将新的数组赋值给原来的数组

CopyOnWriteArrayList为什么并发性能比vector好?

vector加了锁,用关键字synchronized修饰 保证同步,但是每一个方法都要获取锁执行的效率慢,性能会大大的下降,而CopyOnWriteArrayList只是咋增删改的时候加了锁,读的时候没有加锁,性能比vector好,CopyOnWriteArrayList 支持多读少写的情况。

实现多线程的三种方式

1、继承Thead 类

2、实现Runable接口

3、时间Callable接口

Runable和Callable的区别 Callable有返回参数

方法不一样,Callable是call,Runable是run

Callable会抛异常

常用的辅助类

CountDownLatch 计数器

  CountDownLatch  countDownLatch  = new CountDownLatch(6)
  countDownLatch.countDown()// 计数器会 -1
  // 当减为0是会停止
  

CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这些线程会阻塞 其他线程调用CountDown方法会将计数器减1(调用CountDown方法的线程不会阻塞) 当计数器变为0时,await 方法阻塞的线程会被唤醒,继续执行

CyclicBarrier 篱栅 作用:和上面的减法相反,这里是加法,好比集齐7个龙珠召唤神龙,或者人到齐了再开会!

Semaphore 信号量

原理:在信号量上我们定义两种操作:

  • acquire(获取)

    • 当一个线程调用acquire的操作是,他要么通过成功获取型号量

      要么就一直等下去,直到有线程释放型号量,或者超时

  • release(释放)

    • 实际上会将信号量的值+1,让步后唤醒等待的线程
  • 信号量主要用于两个目的:一个是用于多个共享资源的互斥使用,另外一个用于并发线程数的控制

    读写锁

独占锁(写锁):该锁一次只能被一个线程持有。对于Reentranlock和synchronized都是独占锁

共享锁(读锁):该锁被多个线程持有

对于reentrantReadWriteLock读锁时是共享锁,写锁是独占锁,读锁的共享锁课保证并发读是非常高效

阻塞队列

当列队是空的,从列队中获取元素的操作会阻塞

当列队是满的,上列队中插入数据是阻塞

阻塞队列的作用

在多线程领域:所谓阻塞,在某些情况下回挂起线程(即阻塞) 一但条件吗满足,被挂起的线程又会被唤起。

为什么需要BlockingQueue?

好处是我们不需要关心什么时候需要阻塞线程,什么时候唤醒线程,这一切BlockingQueue会自动帮我们实现