可重入锁
比如:synchronized,ReentrantLock
(同一个线程重复请求由自己持有的锁对象时,可以请求成功而不会发生死锁。)
其中一个场景:一个类里面有多个方法加锁,使用可重入锁可以在方法里面调用这个类加了锁的其他方法如下面的方法A可以调用方法B
public class Demo1 {
public synchronized void functionA(){
System.out.println("iAmFunctionA");
functionB();
}
public synchronized void functionB(){
System.out.println("iAmFunctionB");
}
}
Lock (java中的锁都是基于管程模型)
与 synchronized 不同,它支持中断,超时,非阻塞等方式获得锁【当然还要其他方式】
1.支持中断的方法
void lockInterruptibly() throws InterruptedException;
2.支持超时的方法
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
3.支持非阻塞获取锁的方法
boolean tryLock();
【lock的方法总是在try 里面,要注意在finally释放锁】
class X {
private final Lock rtl =
new ReentrantLock();
int value;
public void addOne() {
// 获取锁
rtl.lock();
try {
value+=1;
} finally {
// 保证锁能释放
rtl.unlock();
}
}
}
使用Lock(管程)实现阻塞队列
public class BlockedQueue<T>{
final Lock lock =
new ReentrantLock();
// 条件变量:队列不满
final Condition notFull =
lock.newCondition();
// 条件变量:队列不空
final Condition notEmpty =
lock.newCondition();
// 入队
void enq(T x) {
lock.lock();
try {
while (队列已满){
// 等待队列不满
notFull.await();
}
// 省略入队操作...
// 入队后, 通知可出队
notEmpty.signal();
}finally {
lock.unlock();
}
}
// 出队
void deq(){
lock.lock();
try {
while (队列已空){
// 等待队列不空
notEmpty.await();
}
// 省略出队操作...
// 出队后,通知可入队
notFull.signal();
}finally {
lock.unlock();
}
}
}
信号量
Java里面的Semaphore 可以提供类似信号量的功能
创建对象时初始化一个值,即信号量初值\
acquire方法获取信号量
release方法释放信号量
两个方法之间就是临界区
对比Lock锁的优势:可以控制多个线程进入临界区
【比如要弄什么线程池对象池连接池之类的资源,会用到信号量来限制进入线程的个数】
Semaphore semaphore = new Semaphore (2);
try {
semaphore.acquire ();
} catch (InterruptedException e) {
e.printStackTrace ();
}
// 临界区
semaphore.release ();
对象池(使用信号量实现限流器)
class ObjPool<T, R> {
final List<T> pool;
// 用信号量实现限流器
final Semaphore sem;
// 构造函数
ObjPool(int size, T t){
pool = new Vector<T>(){};
for(int i=0; i<size; i++){
pool.add(t);
}
sem = new Semaphore(size);
}
// 利用对象池的对象,调用 func
R exec(Function<T,R> func) {
T t = null;
sem.acquire();
try {
t = pool.remove(0);
return func.apply(t);
} finally {
pool.add(t);
sem.release();
}
}
}
// 创建对象池
ObjPool<Long, String> pool =
new ObjPool<Long, String>(10, 2);
// 通过对象池获取 t,之后执行
pool.exec(t -> {
System.out.println(t);
return t.toString();
});
信号量和管程区别:
和管程相比,信号量可以实现的独特功能就是同时允许多个线程进入临界区,但是信号量不能做的就是同时唤醒多个线程去争抢锁,只能唤醒一个阻塞中的线程,而且信号量模型是没有Condition的概念的,即阻塞线程被醒了直接就运行了而不会去检查此时临界条件是否已经不满足了,基于此考虑信号量模型才会设计出只能让一个线程被唤醒,否则就会出现因为缺少Condition检查而带来的线程安全问题。正因为缺失了Condition,所以用信号量来实现阻塞队列就很麻烦,因为要自己实现类似Condition的逻辑
读写锁(ReadWriteLock接口,ReentrantReadWriteLock实现类)
使用场景:读多写少【感觉锁的是this】
【不支持锁的升级,即不能获得读锁后获得写锁】
【支持锁的降级,即获得写锁之后获得读锁。因为读写锁互斥,本身获得了读锁就不能申请写锁,永远阻塞】
【支持公平模式和非公平模式】
【只有写锁支持条件变量,读锁是不支持条件变量的,读锁调用 newCondition() 会抛出 UnsupportedOperationException 异常】
用读写锁实现缓存
public class Cache<K,Y> {
private final Map<K, Y> map = new HashMap<> ();
private final ReadWriteLock lock = new ReentrantReadWriteLock ();
// 读锁
private final Lock r = lock.readLock ();
// 写锁
private final Lock w = lock.writeLock ();
public Y get( K key){
r.lock ();
try{
return map.get (key);
}finally {
r.unlock();
}
}
public void put(K key,Y value){
w.lock ();
try {
map.put (key,value);
}finally {
w.unlock ();
}
}
}
StampedLock
使用场景:读多写少【感觉锁的是this】
性能比读写锁更好,因为它有3种模式:写锁,悲观读锁,乐观读(获取的时候会放回stamp,解锁的时候要传入stamp)乐观读性能很好,和数据库的读写锁有点像,不是锁,就是相信在读的时候没有被修改过(可以通过validate方法查看),如果修改过,乐观读升级为悲观读锁
注意事项:
-
StampedLock 不支持重入
-
StampedLock 的悲观读锁、写锁都不支持条件变量
-
如果线程阻塞在 StampedLock 的 readLock() 或者 writeLock() 上时,此时调用该阻塞线程的 interrupt() 方法,会导致 CPU 飙升使用
-
StampedLock 一定不要调用中断操作,如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()。
示例
final StampedLock sl = new StampedLock(); // 获取 / 释放悲观读锁示意代码 long stamp = sl.readLock(); try { // 省略业务相关代码 } finally { sl.unlockRead(stamp); } // 获取 / 释放写锁示意代码 long stamp = sl.writeLock(); try { // 省略业务相关代码 } finally { sl.unlockWrite(stamp); } class Point { private int x, y; final StampedLock sl = new StampedLock(); // 计算到原点的距离 int distanceFromOrigin() { // 乐观读 long stamp = sl.tryOptimisticRead(); // 读入局部变量, // 读的过程数据可能被修改 int curX = x, curY = y; // 判断执行读操作期间, // 是否存在写操作,如果存在, // 则 sl.validate 返回 false if (!sl.validate(stamp)){ // 升级为悲观读锁 stamp = sl.readLock(); try { curX = x; curY = y; } finally { // 释放悲观读锁 sl.unlockRead(stamp); } } return Math.sqrt( curX * curX + curY * curY); } }
线程同步工具
CountDownLatch 一个线程等待多个线程执行完成CyclicBarrier 一组线程互相等待,会自动重置,即一直循环**【check方法是回调函数,这个函数是在主线程的,最好是新建一个单线程的线程池来异步执行任务,不然就不是异步了】**
并发容器
Java 在 1.5 版本之前所谓的线程安全的容器,主要指的就是同步容器。不过同步容器有个最大的问题,那就是性能差,所有方法都用 synchronized 来保证互斥,串行度太高了。因此 Java 在 1.5 及之后版本提供了性能更高的容器,我们一般称为并发容器。
并发容器虽然数量非常多,但依然是前面我们提到的四大类:List、Map、Set 和 Queue
注:Java7中的HashMap在执行put操作时会涉及到扩容,由于扩容时链表并发操作会造成链表成环,所以可能导致cpu飙升100%。
1.CopyOnWriteArrayList
里面有一个array,读操作基于这个array。但遍历的时候有写操作,会复制一份array,在新的array上修改,执行完之后指向新array,遍历的期间,迭代器指向的是旧array
坑:
- CopyOnWriteArrayList 仅适用于写操作非常少的场景,而且能够容忍读写的短暂不一致。例如上面的例子中,写入的新元素并不能立刻被遍历到。
- CopyOnWriteArrayList 迭代器是只读的,不支持增删改。因为迭代器遍历的仅仅是一个快照,而对快照进行增删改是没有意义的。
2.ConcurrentHashMap 和 ConcurrentSkipListMap
前者key无序,后者key有序。两者的key和value都不能为null
后者内部采用跳表,跳表插入、删除、查询操作平均的时间复杂度是 O(log n),理论上和并发线程数没有关系,所以在并发度非常高的情况下,若你对 ConcurrentHashMap 的性能还不满意,可以尝试一下 ConcurrentSkipListMap
3.CopyOnWriteArraySet 和 ConcurrentSkipListSet
使用场景可以参考前面讲述的 CopyOnWriteArrayList 和 ConcurrentSkipListMap,它们的原理都是一样的
4.队列
Java 并发包里面 Queue 这类并发容器是最复杂的,你可以从以下两个维度来分类。一个维度是阻塞与非阻塞,所谓阻塞指的是当队列已满时,入队操作阻塞;当队列已空时,出队操作阻塞。另一个维度是单端与双端,单端指的是只能队尾入队,队首出队;而双端指的是队首队尾皆可入队出队。Java 并发包里阻塞队列都用 Blocking 关键字标识,单端队列使用 Queue 标识,双端队列使用 Deque 标识。
这两个维度组合后,可以将 Queue 细分为四大类,分别是:
- 单端阻塞队列:其实现有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、LinkedTransferQueue、PriorityBlockingQueue 和 DelayQueue。内部一般会持有一个队列,这个队列可以是数组(其实现是 ArrayBlockingQueue)也可以是链表(其实现是 LinkedBlockingQueue);甚至还可以不持有队列(其实现是 SynchronousQueue),此时生产者线程的入队操作必须等待消费者线程的出队操作。而 LinkedTransferQueue 融合 LinkedBlockingQueue 和 SynchronousQueue 的功能,性能比 LinkedBlockingQueue 更好;PriorityBlockingQueue 支持按照优先级出队;DelayQueue 支持延时出队。
- 双端阻塞队列:其实现是 LinkedBlockingDeque。
- 单端非阻塞队列:其实现是 ConcurrentLinkedQueue。
- 双端非阻塞队列:其实现是 ConcurrentLinkedDeque。
另外,使用队列时,需要格外注意队列是否支持有界(所谓有界指的是内部的队列是否有容量限制)。实际工作中,一般都不建议使用无界的队列,因为数据量大了之后很容易导致 OOM。上面我们提到的这些 Queue 中,只有 ArrayBlockingQueue 和 LinkedBlockingQueue 是支持有界的,所以在使用其他无界队列时,一定要充分考虑是否存在导致 OOM 的隐患。