在JDK中,已经给我们开发人员提供了并发包。如:java.util.concurrent。为了更好在项目中使用并发包下的相关类,这里对常用的类进行总结和阐述。一来对于我来说可以更好的巩固以前的知识点,二来契合知识就要分享的初衷。
先看该包的结构图:
其实主要包括两类:同步控制工具和并发集合。
ReentrantLock 可重入锁
简单的说,可重入锁就是能够在单线程内重复获取锁,释放锁的时候需要依次释放。相对于Synchrinized比较,前者具有很好的灵活性和更细的控制粒度。ReentrantLock具备有如下的性质:
示例:
public class ReenterLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static int i = 0;
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
lock.lock();
// 超时设置
// lock.tryLock(5, TimeUnit.SECONDS);
try {
i++;
} finally {
// 需要放在finally里释放, 如果上面lock了两次, 这边也要unlock两次
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
ReenterLock tl = new ReenterLock();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
}
Semaphore信号量
锁一般都是互斥排他的, 而信号量可以认为是一个共享锁,
允许N个线程同时进入临界区, 但是超出许可范围的只能等待.
如果N = 1, 则类似于lock.
具体API如下, 通过acquire获取信号量, 通过release释放,代码实例如下:
public class DeadLock implements Runnable{
// 设置5个许可
final Semaphore semp = new Semaphore(5);
@Override
public void run() {
try {
// 获取许可
semp.acquire();
// 模拟线程耗时操作
Thread.sleep(2000L);
System.out.println("Job done! " + Thread.currentThread().getId());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放许可
semp.release();
}
}
public static void main(String[] args){
// 创建20个线程的线程池
ExecutorService service = Executors.newFixedThreadPool(20);
final DeadLock demo = new DeadLock();
for (int i = 0; i < 20; i++) {
// 提交线程
service.submit(demo);
}
}
}
ReadWriteLock
读写分离锁, 可以大幅提升系统并行度.
读-读不互斥:读读之间不阻塞。
读-写互斥:读阻塞写,写也会阻塞读。
写-写互斥:写写阻塞。
示例
用方法与ReentrantLock类似, 只是读写锁分离.
private static ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();
private static Lock readLock = readWriteLock.readLock();
private static Lock writeLock = readWriteLock.writeLock();
CountDownLatch倒数计时器
一种典型的场景就是火箭发射。在火箭发射前,为了保证万无一失,往往还要进行各项设备、仪器的检查。
只有等所有检查完毕后,引擎才能点火。这种场景就非常适合使用CountDownLatch。它可以使得点火线程,
等待所有检查线程全部完工后,再执行.
public class CountDownLatchDemo implements Runnable{
static final CountDownLatch end = new CountDownLatch(10);
static final CountDownLatchDemo demo = new CountDownLatchDemo();
@Override
public void run() {
try {
Thread.sleep(new Random().nextInt(10) * 1000);
System.out.println("check complete!");
end.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
service.submit(demo);
}
// 等待检查
end.await();
// 所有线程检查完毕, 发射火箭.
System.out.println("fire");
service.shutdown();
}
}
Collections.synchronizedMap
其本质是在读写map操作上都加了锁, 因此不推荐在高并发场景使用.
ConcurrentHashMap
关于ConcurrentHashMap的内容已经在我的上篇文章做出了详细的说明,在这里不再赘述。
BlockingQueue
阻塞队列, 主要用于多线程之间共享数据.
当一个线程读取数据时, 如果队列是空的, 则当前线程会进入等待状态.
如果队列满了, 当一个线程尝试写入数据时, 同样会进入等待状态.
适用于生产消费者模型.
源码如下:
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 队列满了, 写进入等待
while (count == items.length)
notFull.await();
insert(e);
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 队列空的, 读进入等待
while (count == 0)
notEmpty.await();
return extract();
} finally {
lock.unlock();
}
}
注意:因为BlockingQueue在put take等操作有锁, 因此非高性能容器,
如果需要高并发支持的队列, 则可以使用ConcurrentLinkedQueue. 他内部也是运用了大量无锁操作.
CopyOnWriteArrayList
CopyOnWriteArrayList通过在新增元素时, 复制一份新的数组出来, 并在其中写入数据, 之后将原数组引用指向到新数组.
其Add操作是在内部通过ReentrantLock进行锁保护, 防止多线程场景复制多份数组.
而Read操作内部无锁, 直接返回数组引用, 并发下效率高, 因此适用于读多写少的场景.
其中add方法的源码如下:
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
其中get方法的源码如下:
public E get(int index) {
return get(getArray(), index);
}
可见:CopyOnWriteArrayList适合在读多写少的业务场景。