读写锁ReentrantReadWriteLock
之前介绍了两种锁:synchronized和显式锁ReentrantLock,对于同一受保护对象的访问,无论是读还是写,它们都要求获得相同的锁。但在一些场景中,多个线程的读操作完全可以并行。
在Java并发包中,接口ReadWriteLock表示读写锁,主要实现类是可重入读写锁ReentrantReadWriteLock。
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
通过一个ReadWirteLock产生两个锁:一个读锁,一个写锁。读操作使用读锁,写操作使用写锁。“读-读”操作是可以并行的,“读-写”,“写-写”操作不可以
只有一个线程可以进行写操作,在获取写锁时,只有没有任何线程持有任何锁才可以获取到,在持有写锁时,其他任何线程都获取不到任何锁。
在没有其他线程持有写锁的情况下,多个线程可以获取和持有读锁。
内部,它们使用同一个整数变量表示锁的状态,16位给读锁用,16位给写锁用,使用一个变量便于进行CAS操作,锁的等待队列其实也只有一个。
信号量Semaphore
之前介绍的锁都是限制只有一个线程可以同时访问一个资源。
有的情况下,单个资源可以即使可以被并发访问,但并发访问数多了可能影响性能,所以希望限制并发访问的线程数。
信号量Semaphore可以限制对资源的并发访问数
public Semaphore(int permits)
public Semplhore(int permits, boolean fair)
fair表示是否公平,permits表示许可数量
主要方法有两类:获取许可和释放许可
// 阻塞获取许可,可响应中断
public void acquire() throws InterruptedException;
// 阻塞获取许可,不响应中断
public void acquireUninterruptibly();
// 批量获取多个许可
public void acquire(int permits) throws InterruptedException;
public void acquireUninterruptibly(int permits);
// 尝试获取
public boolean tryAcquire();
// 限定等待时间获取
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException;
// 释放许可
public void release();
信号量的基本原理比较简单,基于AQS实现的,permits表示共享的锁个数,acquire方法就是检查锁个数是否大于0,大于则减一,获取成功,否则就等待,release就是将锁个数加一,唤醒第一个等待的线程。
倒计时门闩CountDownLatch
CountDownLatch相当于一个门闩,一开始是关闭的,所有希望通过该门的线程都需要等待,然后开始倒计时,倒计时变为0后,门闩打开,等待的所有线程都可以通过,它是一次性的,打开后就不能再关上了。
CountDownLatch的两种应用场景:
线程同时开始
public class RacerWithCountDownLatch {
static class Racer extends Thread {
CountDownLatch latch;
public Racer(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
try {
// 检查计数是否为0,如果大于0就等待,await可以被中断
this.latch.await();
System.out.println(getName() + " start Run ");
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) throws InterruptedException {
int num = 10;
CountDownLatch countDownLatch = new CountDownLatch(1);
Thread[] racers = new Thread[num];
for (int i = 0; i < racers.length; i++) {
racers[i] = new Racer(countDownLatch);
racers[i].start();
}
Thread.sleep(2000);
// 检查计数,如果已经为0,直接返回;否则减少计数。 如果新的计数变为0,则唤醒所有等待的线程
countDownLatch.countDown();
}
}
主从协作
public class MasterWorkerDemo {
static class Worker extends Thread {
CountDownLatch latch;
public Worker(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
try {
Thread.sleep((long) (Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
this.latch.countDown();
}
}
}
public static void main(String[] args) throws InterruptedException {
int workerNum = 100;
CountDownLatch countDownLatch = new CountDownLatch(workerNum);
Worker[] worker = new Worker[workerNum];
for (int i = 0; i < worker.length; i++) {
worker[i] = new Worker(countDownLatch);
worker[i].start();
}
System.out.println("Main wait");
countDownLatch.await();
System.out.println("Main run");
}
}
循环栅栏CyclicBarrier
CyclicBarrier相当于是一个栅栏,所有线程在到达该栅栏后都需要等待其他线程,等所有线程都到达后再一起通过,它是循环的,可以用作重复的同步。
// 表示参与的线程个数
public CyclicBarrier(int parties)
// 接口一个Runnable参数,当所有线程到达栅栏后,在所有线程执行下一步动作前,运行参数中的动作,这个动作由最后一个到达栅栏的线程执行
public CyclicBarrier(int parties, Runnable barrierAction)
// 表示自己已经到达,如果自己是最后一个到达的,就执行可选的命令,执行后,唤醒所有等待的线程,最后重置内部的同步计数器。
public int await() throw InterruptedException, BrokenBarrierException;
// 可以限定最长等待时间
public int await() throw InterruptedException, BrokenBarrierException, TimeoutException;
在CyclicBarrier中,参与的线程是互相影响的,只要其他一个线程在调用await时被中断了,或者超时了,栅栏就会被破坏。如果栅栏动作抛出了异常,栅栏也会被破坏。被破坏后,所有在调用await的线程就会退出,抛出BrokenBarrierException
CyclicBarrier与CountDownLatch的区别:
1. CyclicBarrier可以重复利用,CountDownLatch是一次性的
2. CyclicBarrier的参与线程角色是一样的,用于同一角色线程间的协调一致,大家相互等待。CountDownLatch的参与线程是不同角色,有的负责倒计时,有的在等待。
3. CountDownLatch做减计数,而栅栏CyclicBarrier则是加计数
ThreadLocal
直接看用法吧
public class ThreadLocalDemo {
static ThreadLocal<Integer> local = new ThreadLocal<Integer>();
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(){
@Override
public void run() {
System.out.println("child thread init : " + local.get());
local.set(998);
System.out.println("child thread final : " + local.get());
}
};
local.set(10);
System.out.println("main thread:" + local.get());
thread.start();
thread.join();
System.out.println("main thread final :" + local.get());
}
}
local是一个静态变量,main方法创建了一个子线程,在主线程和子线程都访问了local。输出如下:
main thread:10
child thread init : null
child thread final : 998
main thread final :10
说明它们访问的虽然是同一个变量local,但每个线程都有自己独立的值,这就是ThreadLocal的作用。
除了get/set方法,还有两个常用方法
// 用于提供初始值,当调用get方法时,如果之前没有设置过,会调用该方法获取初始值,默认返回null
protected T initialValue();
// 删除当前线程对应的值
public void remove();
ThreadLocal原理
public ThreadLocal() {
}
public void set(T value) {
Thread t = Thread.currentThread();
// 获取ThreadLcalMap,ThreadLcalMap是一个内部类,与一般的map不同,它的键类型为WeakReference<ThreadLocal>,利于回收内存
ThreadLocalMap map = getMap(t);
if (map != null)
// 在线程自己的ThreadLcalMap中设置一个条目,键为当前的ThreadLocal对象,值是value
map.set(this, value);
else
// 创建ThreadLcalMap并存入值
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
public T get() {
Thread t = Thread.currentThread();
// 获取ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 调用setInitialValue
return setInitialValue();
}
private T setInitialValue() {
// 调用initialValue获取值
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}