synchronized
可以用于控制一个线程是否可以访问临界区资源,Object.wait()
和Object.notify()
方法可以实现线程等待和通知。这些工具都很简单可靠,但是想要实现更复杂和高级的功能,就要用到Java中的锁。
锁
1.重入锁(ReentrantLock)
-
lock.lock()
简单的上锁
重入锁完全可以替代synchronized
关键字,在Java早期版本中重入锁的性能远远优于synchronized
,而从JDK1.6开始,synchronized
进行了大量的优化,两者性能不相上下。
public class Main {
public static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args){
for (int i = 0; i < 10; i++) {
new Thread(() -> {
lock.lock();
lock.lock();
try {
System.out.println(LocalDateTime.now());
} finally {
lock.unlock();
lock.unlock();
}
}).start();
}
}
}
重入锁如它的名字一样,一个线程可以连续两次获得同一把锁,(否则线程会在第二次请求锁时和自己产生死锁),同样,多次获得锁之后也要多次释放锁,否则其他线程将无法获取锁。
-
lock.lockInterruptibly()
响应中断
public class Main {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args){
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
t2.interrupt();
}
static class MyThread extends Thread {
@Override
public void run() {
while (true) {
try {
lock.lockInterruptibly();
System.out.println(LocalDateTime.now());
} catch (InterruptedException e) {
System.out.println("break");
break;
} finally {
lock.unlock();
}
}
}
}
}
使用lock.lockInterruptibly()
可以使线程在等待锁时响应中断,此时线程会抛出一个InterruptedException
异常并放弃锁的竞争。
-
lock.tryLock()
超时结束
public boolean tryLock()
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
无参数的lock.tryLock()
方法会在调用后尝试获得锁,如果成功立刻返回true,失败立刻返回false。而有参数的方法可以持续请求一段时间后自动退出请求并返回false,同时有参数的lock.tryLock()
同样可以响应中断。
-
new ReentrantLock(true)
公平锁
使用synchronized
关键字进行锁控制,产生的锁就是非公平锁,即在分配锁时不会管线程请求锁的时间先后,所有线程都有可能分配到锁。而公平锁则按照请求锁的时间先后分配锁,这保证了不会出现饥饿现象,但公平锁需要维护一个有序的请求队列,因此开销更大,性能较低。
public ReentrantLock(boolean fair)
- 重入锁的实现
第一,原子状态。原子状态使用CAS操作来存储当前锁的状态,判断锁是否已经被其他线程持有。
第二,等待队列。所有没有请求到锁的线程,会进入等待队列中进行等待。待有线程释放锁之后,系统就能从等待队列中唤醒一个线程,继续工作。
第三,阻塞原语(park)和(unpark),用来挂起和恢复线程。没有得到锁的线程将被挂起。
2.Condition接口
Condition
的作用类似于Object.wait()
和Object.notify()
,不过Condition
是用于和ReentrantLock
合作。它有以下方法。
// await()方法会使当前线程等待,同时释放当前锁,当其他线程使用
// signal()或signalAll()方法时,线程会重新获得锁并继续执行。或者
// 当线程被中断时,也能跳出等待。功能上类似于Object.wait()方法。
void await() throws InterruptedException;
// 与await()方法类似,但是并不响应中断
void awaitUninterruptibly();
long awaitNanos(long nanosTimeout) throws InterruptedException;
boolean await(long time, TimeUnit unit) throws InterruptedException;
boolean awaitUntil(Date deadline) throws InterruptedException;
// 用于唤醒一个在等待中的线程,类似于Object.notify()
void signal();
void signalAll();
类似于Object.wait()
和Object.notify()
,执行前必须用synchronized
获得Object对象的锁。Condition对象由锁的newCondition()
方法生成,使用await()
和signal()
方法前线程必须获得锁对象,而使用后要释放锁对象。
public Condition newCondition() { return sync.newCondition(); }
3.信号量(Semaphore)
信号量是对锁的拓展,可以允许多个线程同时访问临界区资源。
public Semaphore(int permits)
public Semaphore(int permits, boolean fair)
构造信号量时,必须要指定信号量的准入数,还可以指定是否公平分配信号量。
// 尝试获取信号量,获取时可以响应中断
public void acquire() throws InterruptedException
// 尝试获取信号量,获取时不响应中断
public void acquireUninterruptibly()
// 尝试获取信号量,立刻返回结果,成功为true,失败为false
public boolean tryAcquire()
// 尝试获取信号量一段时间,可以响应中断
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException
// 释放占有的信号量
public void release()
信号量的方法与重入锁基本类似,区别只有同时进入临界区的线程数量。
4. 读写锁(ReadWriteLock)
如果使用synchronized
关键字或者重入锁,则所有读与读之间、读与写之间、写与写之间都是串行操作,而读写锁允许多个线程同时读,写写和读写之间依然相互排斥。如果系统中的读远大于写,则读写锁可以很好的提升系统性能。
ReadWriteLock lock = new ReentrantReadWriteLock();
Lock readLock = lock.readLock();
Lock writeLock = lock.writeLock();
使用时,从ReadWriteLock
上分别获得读锁和写锁,读写操作时分别请求对应的锁。
5.倒计时器(CountDownLatch)
倒计时器可以让某个线程等待直到计数器归零。
public class Main {
private static CountDownLatch count = new CountDownLatch(10);
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
Thread.sleep(new Random().nextInt());
count.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
try {
count.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// todo
}
}
countDownLatch.await()
方法同样可以响应中断。
6.循环栅栏(CyclicBarrier)
循环栅栏类似于倒计时器但功能更多一些,首先循环栅栏可以重复触发,另外可以接受一个Runnable
对象,作为一次计数完成后系统会触发的动作。
public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)
public int await() throws InterruptedException, BrokenBarrierException
在使用时,每有一个线程执行CyclicBarrier.await()
,程序计数加一,到达指定数后就会触发barrierAction
。CyclicBarrier.await()
会返回线程到达的名次,最后一个到达的将会返回0。CyclicBarrier.await()
可以响应中断,而当线程已经不可能满足程序计数时(比如某个线程被中断),则其余线程会抛出BrokenBarrierException
异常。
7.线程阻塞工具类(LockSupport)
LockSupport
可以在线程中的任意位置让线程阻塞,与Thread.suspend()
方法相比,弥补了由于resume()
方法发生导致线程无法继续执行的情况;与Object.wait()
方法相比,不需要先获得某个对象的锁,也不会抛出InterruptedException
异常。
public static void park(Object blocker)
public static void unpark(Thread thread)
即使unpark()
方法发生在park()
方法之前,它也能使下一次的park()
方法立即返回,同时,处于park()
方法挂起状态的线程不会像Thread.suspend()
方法一样显示Runnable
状态,而是明确的Waiting
状态,并且还会标注是由park()
方法引起的。
8.限流(RateLimiter)
限流算法的思路:
- 最简单的限流算法就是给出一个单位时间,然后使用一个计数器count统计单位时间内收到的请求数量,当请求数量超过门限时,余下的请求丢弃或等待。
- 漏桶算法:利用一个缓冲区,无论请求的速率如何,都先进入缓冲区等待,然后以固定的流速离开缓冲区。
- 令牌桶算法:系统以一定的速率生成令牌并存入令牌桶中,桶中只能存放一定时限内的令牌。当有请求到来时,拿走桶中的一个令牌,如果桶中没有令牌,则等待或丢弃请求。
RateLimiter
就是是Google旗下一个库Guava中的一个工具,采用了令牌桶算法来控制流量。可以防止过量的请求创建的线程压垮服务器。