并发编程之道(2)

528 阅读6分钟

[TOC]

1.Lock和Condition

1.1 如何保证可见性

顺序性规则:对于线程 T1,value+=1 Happens-Before 释放锁的操作 unlock(); volatile 变量规则:由于 state = 1 会先读取 state,所以线程 T1 的 unlock() 操作 Happens-Before 线程 T2 的 lock() 操作; 传递性规则:线程 T1 的 value+=1 Happens-Before 线程 T2 的 lock() 操作

class SampleLock {
  volatile int state;
  // 加锁
  lock() {
    // 省略代码无数
    state = 1;
  }
  // 解锁
  unlock() {
    // 省略代码无数
    state = 0;
  }
}

1.2 可重入锁

所谓可重入锁,顾名思义,指的是线程可以重复获取同一把锁。

class X {
  private final Lock rtl =
  new ReentrantLock();
  int value;
  public int get() {
    // 获取锁
    rtl.lock();         ②
    try {
      return value;
    } finally {
      // 保证锁能释放
      rtl.unlock();
    }
  }
  public void addOne() {
    // 获取锁
    rtl.lock();  
    try {
      value = 1 + get(); ①
    } finally {
      // 保证锁能释放
      rtl.unlock();
    }
  }
}

1.3 公平锁与非公平锁

我们介绍过入口等待队列,锁都对应着一个等待队列,如果一个线程没有获得锁,就会进入等待队列,当有线程释放锁的时候,就需要从等待队列中唤醒一个等待的线程。如果是公平锁,唤醒的策略就是谁等待的时间长,就唤醒谁,很公平;如果是非公平锁,则不提供这个公平保证,有可能等待时间短的线程反而先被唤醒。


//无参构造函数:默认非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}
//根据公平策略参数创建锁
public ReentrantLock(boolean fair){
    sync = fair ? new FairSync() 
                : new NonfairSync();
}

1.4 用锁的最佳实践

永远只在更新对象的成员变量时加锁 永远只在访问可变的成员变量时加锁 永远不在调用其他对象的方法时加锁 减少锁的持有时间、减小锁的粒度等业界广为人知的规则 下面的例子,有可能活锁,A,B两账户相互转账,各自持有自己lock的锁,都一直在尝试获取对方的锁,形成了活锁。这个例子可以稍微改下,成功转账后应该跳出循环。加个随机重试时间避免活锁。

class Account {
  private int balance;
  private final Lock lock
          = new ReentrantLock();
  // 转账
  void transfer(Account tar, int amt){
    while (true) {
      if(this.lock.tryLock()) {
        try {
          if (tar.lock.tryLock()) {
            try {
              this.balance -= amt;
              tar.balance += amt;
            } finally {
              tar.lock.unlock();
            }
          }//if
        } finally {
          this.lock.unlock();
        }
      }//if
    }//while
  }//transfer
}

1.5 利用两个条件变量快速实现阻塞队列

public class BlockedQueue<T>{
  final Lock lock =
    new ReentrantLock();
  // 条件变量:队列不满  
  final Condition notFull =
    lock.newCondition();
  // 条件变量:队列不空  
  final Condition notEmpty =
    lock.newCondition();

  // 入队
  void enq(T x) {
    lock.lock();
    try {
      while (队列已满){
        // 等待队列不满
        notFull.await();
      }  
      // 省略入队操作...
      //入队后,通知可出队
      notEmpty.signal();
    }finally {
      lock.unlock();
    }
  }
  // 出队
  void deq(){
    lock.lock();
    try {
      while (队列已空){
        // 等待队列不空
        notEmpty.await();
      }  
      // 省略出队操作...
      //出队后,通知可入队
      notFull.signal();
    }finally {
      lock.unlock();
    }  
  }
}

1.6 同步和异步

通俗点来讲就是调用方是否需要等待结果,如果需要等待结果,就是同步;如果不需要等待结果,就是异步。 以下是dubbo的异步简化调用

private final Lock lock = new ReentrantLock();
private final Condition done = lock.newCondition();

boolean isDone() {
	return response != null;
}

Object get(int timeout) {
	long start = System.nanoTime();
	lock.lock();
	try {
		while(!isDone()) {
			done.await(timeout);
			long cur = System.nanoTime();
			if (isDone() || cur -start > timeout) {
				break;
			}
		}
	} finally {
		lock.unlock();
	}	
	if (!isDone()) {
		throw new TimeoutException();
	}
	return returnFromResponse();
}

private void doReceived(Response res) {
	lock.lock();
	try {
		reponse = res;
		if (done != null) {
			done.signal();
		}
	} finally {
		lock.unlock();
	}
}

2. 限流器Semaphore

2.1 信号量

信号量模型还是很简单的,可以简单概括为:一个计数器,一个等待队列,三个方法。在信号量模型里,计数器和等待队列对外是透明的,所以只能通过信号量模型提供的三个方法来访问它们,这三个方法分别是:init()、down() 和 up()。 init():设置计数器的初始值。 down():计数器的值减1;如果此时计数器的值小于0,则当前线程将被阻塞,否则当前线程可以继续执行。 up():计数器的值加1;如果此时计数器的值小于或者等于0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。

class Semaphore {
	// 计数器
	int count;
	// 等待队列
	Queue queue;
	// 初始化操作
	Semaphore(int c) {
		this.count = c;
	}
	// acquire
	void down() {
		this.count--;
		if (this.count < 0) {
		  // 将当前线程插入等待队列
		  // 阻塞当前线程
		}
	}
	// release
	voud up() {
		this.count++;
		if (this.count <= 0) {
		  // 移除等待队列中的某个线程T
		  // 唤醒线程T
		}
	}
}

2.2 如何使用信号量

static int count;
static final Semaphore s = new Semaphore(1);

// 用信号量保证互斥
static void addOne() {
	s.acquire();
	try {
		count += 1;
	} finally {
		s.release();
	}
}

3.快速实现一个完备的缓存ReadWriteLock

3.1 读写锁

读多写少的场景。读写锁,并不是Java语言特有的,而是一个广为使用的通用技术,所有的读写锁都遵守以下三条基本原则:允许多个线程同时读共享变量;只允许一个线程写共享变量;如果一个写线程正在执行写操作,此时禁止读线程读共享变量。读写锁与互斥锁的一个重要区别就是读写锁允许多个线程同时读共享变量,而互斥锁是不允许的,这是读写锁在读多写少场景下性能优于互斥锁的关键。但读写锁的写操作是互斥的,当一个线程在写共享变量的时候,是不允许其他线程执行写操作和读操作。如果你曾经使用过缓存的话,你应该知道使用缓存首先要解决缓存数据的初始化问题。缓存数据的初始化,可以采用一次性加载的方式,也可以使用按需加载的方式。 不支持读锁升级成写锁。

class Cache<K,V> {
	final Map<K, V> m = new HashMap<>();
	final ReadWriteLock rwl = new ReentrantReadWriteLock();
	// 读锁 ReadWriteLock 是一个接口, ReentrantReadWriteLock来实现
	final Lock r = rwl.readLock();
	// 写锁
	final Lock w = rwl.writeLock();
	// 读缓存
	V get(K key) {
		r.lock();
		try {
			return m.get(key);
		} finally {
			r.unlock();
		}
	}
	// 写缓存
	V put(K key, V value) {
		w.lock();
		try {
			return m.put(key, value);
		} finally 
			w.unlock();
		}
	}
	
}

3.2 按需加载数据

class Cache<K,V> {
	final Map<K,V> m = new HashMap<>();
	final ReadWriteLock rwl = new ReentrantReadWriteLock();
	final Lock r = rwl.readLock();
	final Lock w = rwl.writeLock();
	
	V get(K key) {
		v v = null;
		r.lock();
		try {
			v = m.get(key);
		} finally {
			r.unlock();
		}
		if (v != null) {
			return v;
		}
		w.lock();
		try {
			// 再次验证
			v = m.get(key);
			if (v == null) {
			  // 查询数据库
			  m.put(key, v);
			}
		} finally {
			w.unlock();
		}
		return v;		
	}	
}

3.3 写锁降级


class CachedData {
  Object data;
  volatile boolean cacheValid;
  final ReadWriteLock rwl =
    new ReentrantReadWriteLock();
  // 读锁  
  final Lock r = rwl.readLock();
  //写锁
  final Lock w = rwl.writeLock();
  
  void processCachedData() {
    // 获取读锁
    r.lock();
    if (!cacheValid) {
      // 释放读锁,因为不允许读锁的升级
      r.unlock();
      // 获取写锁
      w.lock();
      try {
        // 再次检查状态  
        if (!cacheValid) {
          data = ...
          cacheValid = true;
        }
        // 释放写锁前,降级为读锁
        // 降级是可以的
        r.lock(); ①
      } finally {
        // 释放写锁
        w.unlock(); 
      }
    }
    // 此处仍然持有读锁
    try {use(data);} 
    finally {r.unlock();}
  }
}

4.比读写锁更快的锁StampedLock

ReadWriteLock支持两种模式:一种是读锁,一种是写锁。而StampedLock支持三种模式,分别是:写锁、悲观读锁和乐观读。其中,写锁、悲观读锁的语义和ReadWriteLock的写锁、读锁的语义非常类似,允许多个线程同时获取悲观读锁,但是只允许一个线程获取写锁,写锁和悲观读锁是互斥的。不同的是:StampedLock里的写锁和悲观读锁加锁成功之后,都会返回一个stamp;然后解锁的时候,需要传入这个stamp。相关的示例代码如下。

final StampedLock sl = 
  new StampedLock();
  
// 获取/释放悲观读锁示意代码
long stamp = sl.readLock();
try {
  //省略业务相关代码
} finally {
  sl.unlockRead(stamp);
}

// 获取/释放写锁示意代码
long stamp = sl.writeLock();
try {
  //省略业务相关代码
} finally {
  sl.unlockWrite(stamp);
}

4.1 乐观读升级为悲观读锁

class Point {
	private int x,y;
	final StampedLock sl = new StampedLock();
	
	int distanceFromOrigin() {
		// 乐观读
		long stamp = sl.tryOptimisticRead();
		// 读入局部变量,读的过程中,数据可能被修改了
		itn curX = x, curY = y;
		// 判断执行读操作期间,是否存在写操作,如果存在,则sl.validate返回false
		if (!sl.validate(stamp)) {
			stamp = sl.readLock();
			try {
				curX = x;
				curY = y;
			} finally {
				sl.unlockRead(stamp);
			}
		}
		return Math.sqrt(curX * curX + curY * curY);
	}	
}

5.多线程步调一致CountDownLatch和CyclicBarrier

5.1 CountDownLatch实现线程同步

Executor executor = Executors..newFixedThreadPool(2);
while(存在未对账订单) {
  // 计数器初始化为2
  CountDownLatch latch = new CountDownLatch(2);
  // 查询未对账订单
  executor.execut(() -> {pos = getPOrders();latch.countDown();});
  // 查询派送订单
  executor.execut(() -> {dos =getDOrders();latch.countDown();});
  // 等待两个查询操作结束
  latch.await();
  // 执行对账操作
  diff = check(pos, dos);
  // 差异写入差异库
  save(diff);
}

5.2 Cyclicbarrier实现线程同步

// 订单队列
Vector<P> pos;
// 派送单队列
Vector<P> dos;
// 执行回调的线程池
Executor executor = Executors.newFixedThreadPool(1);
final CyclicBarrier barrier = new CyclicBarrier(2, () -> {
	executor.execute(() -> check());
});

void check() {
	P p = pos.remove(0);
	D d = dos.remove(0);
	diff = check(p, d);
	save(diff);
}

void checkAll() {
	// 循环查询订单库
	Thread T1 = new Thread(() -> {
		while(存在未对账订单) {
			pos.add(getPOrders());
			barrier.await();
		}
	});
	T1.start();
	Thread T2 = new Thread(() -> {
		while(存在未对账订单) {
			dos.add(getDOrders());
			barrier.await();
		}
	});
	T2.start();
}

6.原子类

无锁方案。JavaSDK并发包将这种无锁方案封装提炼之后,实现了一系列的原子类。主要执行CAS操作。

public class Test {
  AtomicLong count = 
    new AtomicLong(0);
  void add10K() {
    int idx = 0;
    while(idx++ < 10000) {
      count.getAndIncrement();
    }
  }
}

6.1 基本数据类型

有AtomicBoolean、AtomicInteger 和 AtomicLong,操作方法有

getAndIncrement() //原子化i++
getAndDecrement() //原子化的i--
incrementAndGet() //原子化的++i
decrementAndGet() //原子化的--i
//当前值+=delta,返回+=前的值
getAndAdd(delta) 
//当前值+=delta,返回+=后的值
addAndGet(delta)
//CAS操作,返回是否成功
compareAndSet(expect, update)
//以下四个方法
//新值可以通过传入func函数来计算
getAndUpdate(func)
updateAndGet(func)
getAndAccumulate(x,func)
accumulateAndGet(x,func)

6.2 原子化的对象引用类型

相关实现有 AtomicReference、AtomicStampedReference 和AtomicMarkableReference,利用它们可以实现对象引用的原子化更新。AtomicReference提供的方法和原子化的基本数据类型差不多,这里不再赘述。不过需要注意的是,对象引用的更新需要重点关注ABA问题,AtomicStampedReference 和 AtomicMarkableReference 这两个原子类可以解决 ABA 问题。

6.3 原子化数组

AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray

6.4 原子化对象属性更新器

相关实现有 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater,利用它们可以原子化地更新对象的属性。

6.5 原子化的累加器

DoubleAccumulator、DoubleAdder、LongAccumulator 和 LongAdder,这四个类仅仅用来执行累加操作,相比原子化的基本数据类型,速度更快,但是不支持 compareAndSet()方法。如果你仅仅需要累加操作,使用原子化的累加器性能会更好

7.线程池

7.1 线程池,实际上是一种生产者-消费者模式。代码如下所示:

class MyThreadPool {
	
	BlockingQueue<Runnable> workQueue;
	List<WorkerThread> threads = new ArrayList<>();
	
	MyThreadPool(int poolSize, BlockingQueue<Runanble> workQueue) {
		this.workQueue = workQueue;
		for (int idx = 0; idx < poolSize; idx++) {
			WorkerThread work = new WorkerThread();
			work.start();
			threads.add(work);
		}
	}
	
	void execute(Runnable cmd) {
		workQueue.put(cmd);
	}

	class WorkerThread extends Thread {
		public void run() {
			while(true) {
				Runnable task = workQueue.take();
				taks.run();
			}
		}		
	}
}

BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(2);
MyThreadPool pool = new MyThreadPool(10, workQueue);
pool.execute(()->{System.out.pringln("hello")});

7.2 Java线程池参数


ThreadPoolExecutor(
  int corePoolSize,
  int maximumPoolSize,
  long keepAliveTime,
  TimeUnit unit,
  BlockingQueue<Runnable> workQueue,
  ThreadFactory threadFactory,
  RejectedExecutionHandler handler) 
  
threadFactory:通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。handler:通过这个参数你可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略,你可以通过 handler 这个参数来指定。ThreadPoolExecutor 已经提供了以下 4种策略。CallerRunsPolicy:提交任务的线程自己去执行该任务。AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException。DiscardPolicy:直接丢弃任务,没有任何异常抛出。DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。

考虑到 ThreadPoolExecutor 的构造函数实在是有些复杂,所以 Java 并发包里提供了一个线程池的静态工厂类 Executors,利用 Executors 你可以快速创建线程池。不过目前大厂的编码规范中基本上都不建议使用 Executors 了,所以这里我就不再花篇幅介绍了。不建议使用 Executors 的最重要的原因是:Executors 提供的很多方法默认使用的都是无界的 LinkedBlockingQueue,高负载情境下,无界队列很容易导致 OOM,而 OOM 会导致所有请求都无法处理,这是致命问题。所以强烈建议使用有界队列。

8.线程池future功能

8.1 Future

// 提交Runnable任务,类似Thread.join();
Future<?> submit(Runnable task);
// 提交Callable任务
<T> Future<T> submit(Callable<T> task);
// 提交Runnable任务及结果引用
<T> Future<T> submit(Runable task, T result);

演示result返回数据

ExecutorService executor = Executors.newFixedThreadPool(1);
// 创建Result对象r
Result r = new Result();
r.setAAA(a);
// 提交任务
Future<Result> future = executor.submit(new Task(r), r);  
Result fr = future.get();
// 下面等式成立
fr === r;
fr.getAAA() === a;
fr.getXXX() === x

class Task implements Runnable{
  Result r;
  //通过构造函数传入result
  Task(Result r){
    this.r = r;
  }
  void run() {
    //可以操作result
    a = r.getAAA();
    r.setXXX(x);
  }
}

8.2 Future有5个方法

// 取消任务
boolean cancel(
  boolean mayInterruptIfRunning);
// 判断任务是否已取消  
boolean isCancelled();
// 判断任务是否已结束
boolean isDone();
// 获得任务执行结果
get();
// 获得任务执行结果,支持超时
get(long timeout, TimeUnit unit);

8.3 FutureTask

线程池执行

FutureTask<Integer> futureTask = new FutureTask<>(()-> 1+2);
ExecutorService es = Executor.newCachedThreadPool();
es.submit(futureTask);
Integer result = futureTask.get();

线程执行

FutureTask<Integer> futuretask = new FutureTask<>(()-> 1+2);
Thread T1 = new Thread(futureTask);
T1.start();
Integer result = futuretask.get();

8.4 多任务执行举例

// T2Task需要执行的任务
class T2Task implements Callable<String> {

	@Override
	String call() throws Exception {
		System.out.pringln("T2:洗茶壶...");
		TimeUnit.SECONDS.sleep(1);
		
		System.out.pringln("T2:洗茶杯...");
		TimeUnit.SECONDS.sleep(2);
		
		System.out.pringln("T2:拿茶叶...");
		TimeUnit.SECONDS.sleep(1);
		return "龙井";
		
	}

}

// T1Task 洗水壶、烧开水、泡茶
class T1Task implements Callable<String> {
	
	FutureTask<String> ft2;
	
	T1Task(FutureTask<String> ft2) {
		this.ft2 = ft2;
	}
	
	@Override
	String call() throws Exception {
		System.out.pringln("T1:洗水壶...");
		TimeUnit.SECONDS.sleep(1);
		
		System.out.pringln("T1:烧开水...");
		TimeUnit.SECONDS.sleep(15);
		
		// 获取T2线程的茶叶
		String tf = ft2.get();
		
		System.out.println("T1:拿到茶叶:" + tf);
		
		System.out.pringln("T1:泡茶...");
		return "上茶:" + tf;		
	}
}

FutureTask<String> ft2 = new FutureTask<>(new T2Task());
FutureTask<String> ft1 = new FutureTask<>(new T1Task());

Thread T1 = new Thread(ft1);
Thread T2 = new Thread(ft2);

T1.start();
T2.start();
System.out.pringln(ft1.get());