多线程的团队协作: 同步控制
同步控制方法
- synchronized关键zi
- wait(),notify()方法
- 重入锁
1.关键字 synchronized 的功能扩展: 重入锁
- 重入锁可以替代synchronized (synchronized也是可重入的)
- 重入锁使用 java.uti1.concurrent.Locks.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<10000000;j++){
lock.lock();
lock.lock();
try{
i++;
}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);
}
}
代码解析
- 重入锁必须显示加锁和释放锁,灵活性比synchronized高
- 重入表示锁在一个线程中是可以重复进入的
- 获取锁和释放锁的次数必须相匹配
获取>释放:仍持有锁,没有释放
获取<释放:抛出java.lang.IllegalMonitorStateException异常
public void lock()//获取锁
- 如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1
- 如果当前线程已经保持该锁,则将保持计数加 1(用于释放锁),并且该方法立即返回
- 如果该锁被另一个线程保持,线程调度会禁用当前线程,并且在获得锁之前,该线程将处于休眠状态
可重入锁的功能
1. 响应中断
public void lockInterruptibly() throws InterruptedException
- 类似wait/sleep,若lockInterruptibly执行前或阻塞时,线程被设置了中断状态,则会抛出InterruptedException,之后清空中断
lockInterruptibly会优先处理中断,而不是获取锁
public class IntLock implements Runnable {
public static ReentrantLock lock1 = new ReentrantLock();
public static ReentrantLock lock2 = new ReentrantLock();
int lock;
public IntLock(int lock) {
this.lock = lock;
}
@Override
public void run() {
try {
if (lock == 1) {
lock1.lockInterruptibly();
try{
Thread.sleep(500);
}catch(InterruptedException e){}
lock2.lockInterruptibly();
} else {
lock2.lockInterruptibly();
try{
Thread.sleep(500);
}catch(InterruptedException e){}
lock1.lockInterruptibly();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock1.isHeldByCurrentThread())
lock1.unlock();
if (lock2.isHeldByCurrentThread())
lock2.unlock();
System.out.println(Thread.currentThread().getId()+":线程退出");
}
}
public static void main(String[] args) throws InterruptedException {
IntLock r1 = new IntLock(1);
IntLock r2 = new IntLock(2);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();t2.start();
Thread.sleep(1000);
//中断其中一个线程
t2.interrupt();
}
}
代码解析
- t1与t2在资源竞争时,形成死锁
- 通过线程外部调用t2.interrupt(),使t2抛出异常,释放锁资源,打破死锁
2. 锁申请限时
public boolean tryLock() //不响应中断
public boolean tryLock(long timeout,TimeUnit unit) throws InterruptedException
- tryLock()相当于tryLock(0,TimeUnit.SECONDS)
- lockInterruptibly()相当于tryLock(inf)
- 获取锁失败会返回false,而不是线程休眠
3. 公平锁
非公平锁:系统从锁的等待队列中随机取出一个线程
公平锁:保证锁的等待线程按时间顺序获取(不会产生饥饿)
实现公平锁需要维护一个有序队列,实现成本高,性能很低.
public ReentrantLock(boolean fair)
true为公平锁,false为非公平锁
ReentrantLock方法与要素
常用方法
1. lock(): 获得锁, 如果锁已经被占用, 则线程休眠。
2. lockInterruptibIy(): 获得锁, 但优先响应中断。
3. tryLock(): 尝试获得锁,成功则返回true;该方法不等待, 立即返回。
4. tryLock(Iong time,TimeUnit unit): 在给定时间内尝试获得锁。
5. unlock(); 释放锁int
6. getHoldCount()查询当前线程对此锁的暂停数量。
7. final boolean hasQueuedThread(Thread thread) thread是否在等待锁
8. Thread getOwner() 获取锁的持有线程(protected)
重入锁的三个要素
- 原子状态:用CAS操作存取锁的状态
- 等待队列:未获得锁的线程会进入等待队列
- 阻塞原语:park()与unpark(),分别用来挂起和恢复线程
2.Condition
Condition的创建
Condition cond=lock.newCondition();//将condition与lock关联
Condition方法
boolean await(long time, TimeUnit unit)
使当前线程等待直到发出信号或中断,或指定的等待时间过去。
long awaitNanos(long nanosTimeout) throws InterruptedException;
等待一定时间,返回值为[nanosTimeout-已等待时间]
void signal()/signalAll()
唤醒等待线程。
代码演示
public class ReenterLockCondition implements Runnable{
public static ReentrantLock lock=new ReentrantLock();
public static Condition condition = lock.newCondition();
@Override
public void run() {
try {
lock.lock();
condition.await();
System.out.println("Thread is going on");
} catch (InterruptedException e) {
e.printStackTrace();
}finally{
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReenterLockCondition tl=new ReenterLockCondition();
Thread t1=new Thread(tl);
t1.start();
Thread.sleep(2000);
//通知线程t1继续执行
lock.lock();
condition.signal();
lock.unlock();
}
}
代码分析(Object与Condition)
- lock+unlock=synchronized{}
- cond.await()=obj.wait()
- cond.signal()=obj.notify()
- await/signal要求获得与cond相关联的锁
wait/notify要求在相同对象的监视器中
Lock+Condition的应用
ArrayBlockingQueue的put()和take()
3.允许多个线程同时访问: 信号量
- synchronized和可重入锁只允许一个线程访问一个资源
- Semaphore却可以指定多个线程, 同时访问某一个资源
构造函数
public Semaphore(int permits)//指定资源数
public Semaphore(int permits,boolean fair) //指定是否公平
信号量方法
public void acquire(int permits=1)//响应中断
public void acquireUninterruptlbly()//不响应中断
public boolean tryAcquire(long timeoutf,TimeUnit unit)
public void release(int permits=1)
public int availablePermits()//返回此信号量中当前可用的许可数。
代码演示
public class SemapDemo implements Runnable {
final Semaphore semp = new Semaphore(5);
@Override
public void run() {
try {
semp.acquire();
Thread.sleep(2000);
System.out.println(Thread.currentThread().getId() + ":done!");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semp.release();
}
}
public static void main(String[] args) {
ExecutorService exec = Executors.newFixedThreadPool(20);
final SemapDemo demo = new SemapDemo();
for (int i = 0; i < 20; i++) {
exec.submit(demo);
}
}
}
最多同时可以有5个线程访问资源
线程池保持固有线程数,执行安排的任务
4.ReadWriteLock 读写锁
读操作时不修改数据内容,可以通过并发读提高性能.
读写锁使用
ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();
Lock readLock = readWriteLock.readLock();
Lock writeLock = readWriteLock.writeLock();
- ReadLock与WriteLock都是以ReentrantReadWriteLock的静态内部类
- ReadLock与WriteLock都是单例模式,并在ReentrantReadWriteLock构造函数中初始化
- ReadLock与WriteLock的使用方法与Lock相同.xxxxlock.lock()搭配xxxxlock.unlock()
- readLock.lock()之间不会阻塞
5.倒计数器: CountDownLatch
CountDownLatch在倒计时结束前会阻塞,倒计时结束后继续执行.
构造函数
public CountDownLatch(int count)//设置倒计时起始数值>=0
主要函数
public void countDown() // 计数减1
public long getCount()//返回当前计数
public boolean await(long timeout, TimeUnit unit)throws InterruptedException
//等待,当计数减到0时,所有线程并行执行;或超时执行
代码演示
public class CountDownLatchDemo implements Runnable {
static final CountDownLatch end = new CountDownLatch(5);
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 exec = Executors.newFixedThreadPool(10);
for(int i=0;i<5;i++){
exec.submit(demo);
}
//等待检查
end.await();
//发射火箭
System.out.println("Fire!");
exec.shutdown();
}
}
check complete
check complete
check complete
check complete
check complete
Fire!
代码解析
- 主线程中因end.await()阻塞
- 在其他线程中使用end.countDown()使计数器减1
- 当计数器为0时,end.await()不再阻塞,继续执行后续代码
使用场景:某些事件必须等待先行事件完成才能进行
6.循环栅栏: CyclicBarrier
- CountDownLatch只能完成一次倒计时
- CyclicBarrier能循环倒计时,每次倒计时完成执行barrierAction
构造函数
CyclicBarrier(int parties, Runnable barrierAction)
主要函数
int await(long timeout, TimeUnit unit)
等待所有 parties已经在此屏障上调用 await ,或指定的等待时间过去。
int getNumberWaiting()
返回目前正在等待障碍的各方的数量
int getParties()
返回旅行这个障碍所需的聚会数量
boolean isBroken()
查询这个障碍是否处于破坏状态
void reset()
将屏障重置为初始状态
代码演示
public class CyclicBarrierDemo2 {
public static class Soldier implements Runnable {
private String soldier;
private final CyclicBarrier cyclic;
Soldier(CyclicBarrier cyclic, String soldierName) {
this.cyclic = cyclic;
this.soldier = soldierName;
}
public void run() {
try {
//等待所有士兵到齐
cyclic.await();
doWork();
//等待所有士兵完成工作
cyclic.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
void doWork() {
try {
Thread.sleep(Math.abs(new Random().nextInt()%10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(soldier + ":劳动完成");
}
}
public static class BarrierRun implements Runnable {
boolean flag;
int N;
public BarrierRun(boolean flag, int N) {
this.flag = flag;
this.N = N;
}
public void run() {
if (flag) {
System.out.println("司令:[士兵" + N + "个,任务完成!]");
} else {
System.out.println("司令:[士兵" + N + "个,集合完毕!]");
flag = true;
}
}
}
public static void main(String args[]) throws InterruptedException {
final int N = 10;
Thread[] allSoldier=new Thread[N];
boolean flag = false;
CyclicBarrier cyclic = new CyclicBarrier(N, new BarrierRun(flag, N));
//设置屏障点,主要是为了执行这个方法
System.out.println("集合队伍!");
for (int i = 0; i < N; ++i) {
System.out.println("士兵 "+i+" 报道!");
allSoldier[i]=new Thread(new Soldier(cyclic, "士兵 " + i));
allSoldier[i].start();
//if(i==5)
// allSoldier[0].interrupt();
}
}
}
代码分析
- 当cyclic.await()的线程数等于parties,越过barrier,执行barrierAction
- CyclicBarrier.await()可能会抛出两种异常
1)InterruptedException–中断异常
2)BrokenBarrierException–CyclicBarrier特有的异常,表示无法再越过barrier- 若将注释取消,将获得1个nterruptedException和9个BrokenBarrierException(1个中断导致其他无法越过barrier)
7.线程阻塞工具类: LockSupport
LockSupport的线程阻塞特点
- 可以在线程内任意位置阻塞线程,且unpark可在park前
- 阻塞线程不需要获取锁,也不会引发InterruptedException
主要函数
static void park(Object blocker);
禁用当前线程,并设置阻塞对象,该对象会出现在线程Dump中
static void parkNanos(long nanos)
禁用当前线程进行线程调度,直到指定的等待时间,除非许可证可用。
static void unpark(Thread thread)
为给定的线程提供许可证(如果尚未提供)。
代码演示
public class LockSupportDemo {
static ChangeObjectThread t1 = new ChangeObjectThread("t1");
static ChangeObjectThread t2 = new ChangeObjectThread("t2");
public static class ChangeObjectThread extends Thread {
public ChangeObjectThread(String name){
super.setName(name);
}
@Override
public void run() {
System.out.println("in "+getName());
LockSupport.park(this);
System.out.println("in "+getName()+"i");
LockSupport.park(this);
System.out.println("in "+getName()+"o");
}
}
public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(100);
t2.start();
LockSupport.unpark(t1);
Thread.sleep(1000);
System.out.println("ok");
LockSupport.unpark(t1);
LockSupport.unpark(t2);
t1.join();
t2.join();
}
}
in t1
in t1i
ok
in t1o
in t2
in t2i
代码分析
- LockSupport是基于互斥量机制,park消费,unpark生产(start之后)
- LockSupport会为每个线程维护一个互斥量
- LockSupport是不可重入锁,多个park()需要多次获取互斥量
8. Guava 和 RateLimiter 限流
最简单的限流算法
限制单位时间的请求数目,当请求超过门限时, 余下的请求丢弃或者等待。
边界请求问题:本单位时间后半部分与下一个单位时间前半部分请求集中,超过了门限,但是在各自单位时间未超过门限.
两种常见限流算法
漏桶算法
利用一个缓存区, 当有请求进入系统时, 先在缓存区内保存, 然后以固定的流速流出缓存K 进行处理
两个重要参数
- 漏桶容积
- 流出速率
令牌桶算法
桶中存放的不再是请求, 而是令牌。 处理程序只有拿到令牌后, 才能对请求进行处理。
为了限制流速, 该算法在每个单位时间产生一定量的令牌存入桶中。
两个主要参数
- 令牌产生速率
- 令牌桶容量
RateLimiter(令牌桶算法)
构造方法
public static RateLimiter create(double permitsPerSecond)
静态工厂方法,每隔一段时间产生一个令牌(令牌的寿命默认1s)
主要函数
public double acquire(int permits)
阻塞线程,直到获取令牌,并返回获取令牌所消耗的时间
public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
阻塞线程,直到获得令牌,超时或被中断
public final void setRate(double permitsPerSecond)
设置令牌产生速率
代码演示
import com.google.common.util.concurrent.RateLimiter;
public class RateLimiterDemo {
static RateLimiter limiter = RateLimiter.create(2);
public static class Task implements Runnable {
@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
}
public static void main(String args[]) throws InterruptedException {
for (int i = 0; i < 50; i++) {
limiter.acquire();
new Thread(new Task()).start();
}
}
}
代码分析
- 使用RateLimiter.create(2)设置每2秒产生一个令牌
- limiter.acquire()未获得令牌前会阻塞,获得令牌后,继续执行