重入锁
java.util.concurrent.locks.ReentrantLock
重入锁的作用和synchronized关键字一样,为代码块加锁。但与synchronized关键字原理不一样,synchronized关键字是根据对象头的锁标志判断当前线程是否可以获得锁,而重入锁是基于AbstractQueuedSynchronizer,底层是CAS,是一种乐观锁(无锁)。
重入锁的基本使用如下:
public static class HasLock implements Runnable {
private ReentrantLock lock = new ReentrantLock();
private int i = 0;
@Override
public void run() {
try {
Thread.sleep(5);
lock.lock();
i++;
lock.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public int getI() {
return i;
}
}
在成员变量声明一个ReentrantLock对象,在需要同步的代码块中调用lock();unlock();方法。
在声明重入锁的时候,构造方法有个boolean 参数 fair可以传,如果fair为true,表示是一个公平锁,先申请锁的线程,先执行。为false,则为非公平锁,不能保证线程获得锁的顺序。
正常来说,一个线程在等待synchronized锁时,是没法取消的,只能等目标线程释放锁之后,代码继续执行,如果此时需要中断线程,只能等线程获取锁之后,中断操作才会进行。重入锁提供了lockInterruptibly()方法,使线程在等待锁的过程中,也可以中断,抛出InterruptedException。见下例子:
public static class TwoLock implements Runnable {
private Integer i;
private ReentrantLock lock1;
private ReentrantLock lock2;
public TwoLock(Integer i, ReentrantLock lock1, ReentrantLock lock2) {
this.i = i;
this.lock1 = lock1;
this.lock2 = lock2;
}
@Override
public void run() {
try {
if (i == 1) {
//如果用lock,线程没法中断
// lock1.lock();
//使用lockInterruptibly,可以随时中断线程
lock1.lockInterruptibly();
System.out.println("i=1, 1上锁");
Thread.sleep(500);
lock2.lockInterruptibly();
System.out.println("i=1, 2上锁");
} else {
lock2.lockInterruptibly();
System.out.println("i=2, 2上锁");
Thread.sleep(500);
lock1.lockInterruptibly();
System.out.println("i=2, 1上锁");
}
} catch (InterruptedException e) {
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()) {
lock2.unlock();
}
System.out.println(Thread.currentThread().getName() + "中断了");
}
}
}
private static void interrupt() throws Exception {
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();
TwoLock l1 = new TwoLock(1, lock1, lock2);
TwoLock l2 = new TwoLock(2, lock1, lock2);
Thread t1 = new Thread(l1);
Thread t2 = new Thread(l2);
t1.start();
t2.start();
Thread.sleep(5000);
System.out.println("hello world");
t1.interrupt();
}
t1线程先获取锁1,500ms后获取锁2,与此同时,t2线程获取锁2,500ms后获取锁1。
由于500ms后,由于两把锁分别被两个线程占用,导致死锁。但是上锁使用的是lockInterruptibly()方法,在5000ms后,发现代码还没执行完毕,中断线程1,并在exception中释放锁1,这样线程2就可以拿到锁1了,线程2顺利执行。如果使用的lock()和synchronized锁,那么这两个线程永远无法停止
重入锁还提供了tryAcquire()方法,用于判断当前线程是否获得锁,返回的是个boolean值,有点像我们状态机里的redis锁,如果获取锁失败,执行xxx逻辑,tryAcquire()如果无参,立即返回结果,如果有参,则等待指定时间后,返回结果。
重入锁有个搭档,Condition,作用相当于Object里面的wait(),notify()方法,用法如下:
private static void condition() throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
ExecutorService es = Executors.newFixedThreadPool(3);
for (int i = 0; i < 3; i++) {
es.execute(() -> {
System.out.println(Thread.currentThread().getName() + "开始执行");
try {
lock.lock();
condition.await();
System.out.println(Thread.currentThread().getName() + "继续执行");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
}
Thread.sleep(5000);
lock.lock();
condition.signalAll();
lock.unlock();
}
当调用await()方法时,当前线程阻塞,直到有其他线程调用signal()方法或者signalAll()方法,线程继续执行。
读写锁
java.util.concurrent.locks.ReadWriteLock
相当于数据库的读写锁,同一把锁一分为二,分为读锁和写锁,读读不互斥,读写互斥,写写互斥,用法如下:
public static void main(String[] args) {
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
new Thread(() -> {
readWriteLock.readLock().lock();
try {
System.out.println("读线程1获取读 ");
try {
Thread.sleep(2000);
} catch (Exception ex) {
}
System.out.println("线程1结束");
} finally {
readWriteLock.readLock().unlock();
}
}).start();
new Thread(() -> {
readWriteLock.readLock().lock();
try {
System.out.println("读线程2获取读 ");
try {
Thread.sleep(2000);
} catch (Exception ex) {
}
System.out.println("线程2结束");
} finally {
readWriteLock.readLock().unlock();
}
}).start();
new Thread(() -> {
Lock lock = readWriteLock.writeLock();
lock.lock();
try {
System.out.println("写线程3获取写 ");
try {
Thread.sleep(2000);
} catch (Exception ex) {
ex.printStackTrace();
}
System.out.println("线程3结束");
} finally {
lock.unlock();
}
}).start();
}
运行结果
线程1和线程2同时进行,线程3需要等待线程1和线程2完成,且释放读锁后才会执行
信号量
java.util.concurrent.Semaphore
一般来说,需要同步的代码块,一次只能一个线程执行,读写锁的话,只能多个读线程同时执行。使用信号量,可以让指定线程数量,同时访问同一个资源。比方说,5个售票窗,100个人买票,那么同一时间,最多也就只能卖出5张票,信号量就是售票窗,如果只有一个售票窗,那么可以理解为这个售票窗就是锁,只有前面的人买完了,后面的人才可以买票。信号量的使用如下:
public static void main(String[] args) {
Runnable rb = new Runnable() {
private Semaphore semaphore = new Semaphore(5);
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "开始");
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "结束");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
ExecutorService es = Executors.newFixedThreadPool(5);
for (int i = 0; i < 20; i++) {
es.submit(rb);
}
ThreadPoolUtils.stopPool(es);
}
以上代码,一次最多5个线程访问run()里面的内容
倒计时器
java.util.concurrent.CountDownLatch
一个任务,分成3个线程执行,所有线程执行完了之后,才执行下一步,正常写代码我们会使用join()在主线程阻塞3个线程,当3个线程都完成后,主线程继续执行,CountDownLatch可以帮我们完成这件事情,使用如下:
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(10);
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 15; i++) {
final Integer j = i;
executorService.execute(() -> {
try {
countDownLatch.countDown();
System.out.println(j + ":准备就绪");
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
System.out.println("等待所有线程就绪");
countDownLatch.await();
System.out.println("十个线程已经就绪");
ThreadPoolUtils.stopPool(executorService);
}
调用countDown()方法,通知计时器-1,await()方法等待所有线程执行完毕,当计时器为0时,await()之后的代码继续执行。相当于3个任务全部完成。
循环栅栏
java.util.concurrent.CyclicBarrier
功能与倒计时器类似,不过它可以重复统计,如果CountDownLatch是一辆车,人满车开,那么CyclicBarrier是一批车,前一辆车开走后,当等待乘车的人满后,会有新的车来将这些人接走,如果人没满,那么等待的人一直等待,直到车来为止。而如果是CountDownLatch,车只会开一次,后面不管有多少人在等车,都不在会有车到来。用法如下:
public static void main(String[] args) {
CyclicBarrier cb = new CyclicBarrier(2);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
Thread.sleep(1000);
cb.await();
System.out.println("每2个线程执行完就会打印这句话");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
以上语句只会打印4次,第五个线程会一直等待第六个线程的出现,因为CyclicBarrier设置的值为2,每2个线程执行完,才会打印。