第三章 JDK并发包https://www.cnblogs.com/sean-zeng/p/11957569.html

JAVA的线程是映射在操作系统的原生线程上的,所以使用synchronized对线程的操作是重量级的,在不必要的条件下应尽量避免使用。

JDK内部提供了大量实用的API和框架。本章主要介绍这些JDK内部功能,主要分为3大部分:

首先,介绍有关同步控制的工具,之前介绍的synchronized就是一种同步控制手段,将介绍更加丰富的多线程控制方法。

其次,将详细介绍JDK对线程池的支持,使用线程池,将很大程度提高线程调度的性能。

第三,介绍JDK的一些并发容器。这些容器专为并行访问所设计,绝对是高效、安全、稳定的实用工具。

其结构如下:

多线程团队协作:同步控制

之前提到的synchronized是最简单的同步控制的方法。本节中,首先介绍synchronized、Object.wait()、Object.notify()方法的替代品(或者说增强版)——重入锁

简而言之, 就是自由度更高的synchronized, 主要具备以下优点.

  • 可重入: 单线程可以重复进入,但要重复退出(锁两次释放两次)
  • 可中断: lock.lockInterruptibly()
  • 可限时: lock.tryLock()超时不能获得锁,就返回false,不会永久等待构成死锁
  • 公平锁: 先来先得, public ReentrantLock(boolean fair), 默认锁不公平的, 根据线程优先级竞争.
  • 带条件:可以绑定多个Condition对象,多次调用newCondition即可。

方法整理

  • lock():获得锁,如果锁已经被占用,则等待。
  • lockInterruptibly():获得锁,但优先响应中断。
  • tryLock():尝试获得锁,如果成功,返回true,失败返回false。不等待,立即返回。
  • tryLock(long time, TimeUnit unit):在给定时间内尝试获得锁。
  • unlock():释放锁。

重入锁(ReentrantLock):synchronized的功能扩展

jdk6.0之前,重入锁的性能远远好于synchronized,但是之后,两者差距并不大。

下面是一段重入锁使用案例:

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(); //1
try{
i++;
}finally{
lock.unlock(); //2
}
}
}
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);
}
}

1处加锁,2处释放锁。

重入锁是可以让线程反复进入的,这里的反复仅仅局限于一个线程。可以写成下面的形式:

lock.lock();
lock.lock();
try{
i++;
}finally{
lock.unlock();
lock.unlock();
}

这种情况下,一个线程连续两次获得同一把锁,这是允许的!同时,释放也必须释放两次,释放次数多了,抛出异常,次数少了,相当于线程还持有当前锁,其他线程无法进入临界区。

重入锁除了灵活,还提供了中断处理的能力:

中断响应

对于synchronized来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得这把锁继续执行,要么它保持等待。而重入锁提供了另一种可能,那就是线程可以被中断。也就是在等待锁的过程中,程序可以根据需要取消对锁的请求。有些时候,这么做很有必要。如果一个线程正在等待锁,那么它依然可以收到一个通知,被告知无需再等待,可以停止工作了。这种情况对于处理死锁是有一定帮助的。

下面代码产生了一个死锁,但得益于锁中断,我们可以轻松解决这个死锁:

public class IntLock implements Runnable {
//重入锁ReentrantLock
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() {
// TODO Auto-generated method stub
try {
if (lock == 1) {
lock1.lockInterruptibly(); //1
Thread.sleep(500);
lock2.lockInterruptibly();
System.out.println("lock1 is working....");
} else {
lock2.lockInterruptibly();
Thread.sleep(500);
lock1.lockInterruptibly();
System.out.println("lock2 is working....");
}
} catch (Exception 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(); //2
}
}

线程t1和t2启动后,t1先占用lock1,再占用lock2;t2先占用lock2,再请求lock1。这很容易照成t1、t2互相等待,形成死锁。这里,统一使用1处的lockInterruptibly()方法,这是一个可以对中断进行相应的锁申请动作,即在等待锁的过程中,可以响应中断。

在2处,t2线程被中断,放弃对lock1的锁申请,同时释放已获得的lock2。这时t1就能顺利执行完剩余程序

锁申请等待限时

除了外部通知之外,避免死锁还有另外一种方法,就是限时等待。我们可以使用tryLock()方法进行一次限时的等待。

public class TimeLock implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) { //2
Thread.sleep(6000); //1
} else {
System.out.println("get lock failed");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(lock.isHeldByCurrentThread())
lock.unlock();
}
}
public static void main(String[] args) {
TimeLock tl = new TimeLock();
Thread t1 = new Thread(tl);
Thread t2 = new Thread(tl);
t1.start();
t2.start();
}
}

在这里2处,tryLock()方法接收两个参数,一个表示等待时长,另外一个表示计时单位。没个进入临界区的线程需要占用6秒的锁(1处),而t2由于等待5秒没有等到想要的锁(2处),便返回false。若等待时间改为比5秒大,将返回true,并获得锁。

公平锁

在大多数情况下,锁的申请是非公平的。系统知识随机挑选一个,不保证其公平性。公平的锁,会按照时间的先后顺序,保证先到者先得,后到者后得。公平锁的一大特点是:不会产生饥饿现象。我们使用synchronized关键字得到的就是非公平锁,而重入锁可以对公平性设置。它有一个构造函数:

 public ReentrantLock(boolean fair) //为true时是公平锁

实现公平锁要维护一个有序队列,因此实现公平锁的成本较高,性能相对低下,因此,默认情况下,锁时非公平的。

public class FairLock implements Runnable{
//创建公平锁
private static ReentrantLock lock=new ReentrantLock(true); //1
public void run() {
while(true){
lock.lock();
try{
System.out.println(Thread.currentThread().getName()+"获得锁");
}finally{
lock.unlock();
}
}
}
public static void main(String[] args) {
FairLock lft=new FairLock();
Thread th1=new Thread(lft);
Thread th2=new Thread(lft);
th1.start();
th2.start();
}
}/**
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
Thread-0获得锁
Thread-1获得锁
*/

你运行上面的程序,会看到结果很有规律。

如果不使用公平锁,根据系统的调度,一个线程会倾向于再次获取已经持有的锁,这种分配方式是高效的。但是无公平性可言,将上面1中的true改成false即可。

对ReentrantLock的几个重要方法整理如下:

  • lock():获得锁,如果锁已经被占用,则等待。
  • lockInterruptibly():获得锁,但优先响应中断。
  • tryLock():尝试获得锁,如果成功,返回true,失败返回false。不等待,立即返回。
  • tryLock(long time, TimeUnit unit):在给定时间内尝试获得锁。
  • unlock():释放锁。

重入锁的实现

就重入锁的实现来看,它主要集中在java层面。主要包含三个要素:

  • 第一,是原子状态。原子状态使用CAS操作来存储当前锁的状态,判断锁是否已经被别的线程持有。
  • 第二,是等待队列。
  • 第三,是阻塞语句park()和unpark(),用来挂起和恢复线程。没有得到锁的线程将会被挂起。有关park()和unpark()的详细介绍,可以参考线程阻塞工具类:LockSupport。

重入锁的好搭档:Condition条件

Condition的作用和wait()和notify()方法的作用是大致相同的。不同的是wait()和notify()方法是和synchronized关键字合作使用的,而Condition是与重入锁合作的。通过Lock接口(重入锁实现了该接口)的newCondition()方法可以生成一个与当前重入锁绑定的Condition实例。

Condition接口提供的基本方法:

  • await():使当前线程等待,同时释放当前锁,当其他线程使用signal()或者signalAll()方法时,线程会重新获得锁并继续执行。当线程中断时,也能跳出等待,和Object.wait()非常相似。
  • awaitUninte00rruptibly():与await()基本相同,但是不会响应等待过程中的中断。
  • signal():唤醒一个等待中的线程,signalAll()会唤醒所有等待中的线程。

下面是Condition的演示:

public class ReenterLockCondition implements Runnable {
public static ReentrantLock lock = new ReentrantLock();
public static Condition condition = lock.newCondition(); @Override
public void run() {
try {
lock.lock();
System.out.println("Thread is start...");
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(); //1
condition.signal();
lock.unlock();
}
}

和Object.wait()和notify()方法一样,当线程使用Condition.await()时,要求线程持有相关的重入锁,在Condition.await()调用后,这个线程会释放这把锁。同理,在Condition.signal()方法调用时,也要求线程先获得相关的锁。在siganl()方法调用后,系统会从当前Condition对象的等待队列中,唤醒一个线程,一旦线程被唤醒,它会重新尝试获得与之绑定的重入锁,一旦成功获得,就可以继续执行了。因此,一般调用完condition.signal()后,都需要释放重入锁。

允许多个线程同时访问:信号量(Semaphore)

广义上讲,信号量是对锁的扩展。无论是内部锁synchronized还是重入锁ReentrantLock,一次都只允许一个线程访问一个资源,而信号量可以指定多个线程,同时访问某个资源

主要提供了两个构造函数:

public Semaphore(int permits)
public Semaphore(int permits, boolean fair) //第二个参数可以指定是否公平

在构造信号量对象时,必须要指定信号量的准入数,即同时能申请多少个许可。信号量的主要逻辑方法有:

public void acquire()
public void acquireUninterruptibly()
public boolean tryAcquire()
public boolean tryAcquire(long, TimeUnit unit)
public void release()
  • acquire():尝试获得一个准入的许可。若无法获得,则线程会等待,直到有线程释放一个许可或当前线程被中断。
  • acquireUninterruptibly():和acquire()方法类似,但是不响应中断。
  • tryAcquire():尝试获得一个许可,立即返回结果
  • release():释放一个许可。
public class SemapDemo implements Runnable{
final Semaphore semp = new Semaphore(5); //3
@Override
public void run() {
try {
semp.acquire(); //1
//模拟耗时操作
Thread.sleep(2000);
System.out.println(Thread.currentThread().getId()+":done!"); //2
semp.release(); //4
} catch (InterruptedException e) {
e.printStackTrace();
}
} 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);
}
}
}

上述代码中,1处到2处为临界区管理代码,程序会限制这段代码的线程数。在第3处,申明了一个包含5个许可的信号量。这意味着1~2处只能同时有5个线程进入。线程在使用完acquire(),在离开时,务必使用release()释放信号量。这和释放锁是一个道理。

读写锁:ReadWriteLock

ReadWriteLock是JDK5中提供的读写分离锁。读写分离锁可以有效地减少锁竞争,以提升系统性能。用锁分离的机制来提升性能很容易理解,如果使用重入锁或内部锁,理论上所有读—读、读—写、写—写都是串行操作。而读写锁,允许多个线程同时读

比如A1、A2、A3进行写操作,B1、B2、B3进行读操作。读写锁允许B1、B2、B3之间并行。但是,考虑数据完整性,写写操作和读写操作间依然是需要相互等待和持有锁的。总结如下:

  • 读-读不互斥:可并行;

  • 读-写互斥;

  • 写-写互斥;

    public class ReadWriteLockDemo {
    private static Lock lock = new ReentrantLock();
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private static Lock readLock = readWriteLock.readLock();
    private static Lock writeLock = readWriteLock.writeLock();
    private int value; public Object handleRead(Lock lock) throws InterruptedException {
    try {
    lock.lock(); // 模拟读操作
    Thread.sleep(1000); // 读操作的耗时越多,读写锁的优势越明显
    System.out.println(Thread.currentThread().getName()+" read end!");
    return value;
    } finally {
    lock.unlock();
    }
    } public void handleWrite(Lock lock, int index) throws InterruptedException {
    try {
    lock.lock(); // 模拟写操作
    Thread.sleep(1000);
    System.out.println(Thread.currentThread().getName()+" wrait end!");
    value = index;
    } finally {
    lock.unlock();
    System.out.println(value);
    }
    } public static void main(String[] args) {
    // TODO Auto-generated method stub
    final ReadWriteLockDemo demo = new ReadWriteLockDemo();
    Runnable readRunnable = () -> {
    // TODO Auto-generated method stub
    try {
    demo.handleRead(readLock);
    // demo.handleRead(lock);
    } catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    }
    };
    Runnable writeRunnable = () -> {
    // TODO Auto-generated method stub
    try {
    demo.handleWrite(writeLock, new Random().nextInt());
    // demo.handleWrite(lock, new Random().nextInt());
    } catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    } };
    for (int i = 0; i < 18; i++) {
    new Thread(readRunnable).start(); //1
    }
    for (int i = 18; i < 20; i++) {
    new Thread(writeRunnable).start(); //2
    }
    }
    }

上面代码中,读和写的线程使用耗时的操作来模拟,在1处开启同时读的线程,可以从结果看出读的速度可以是并行的,而2处则不行。

倒计时锁:CountDownLatch

这个工具称为倒计数器:通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。

使用场景:比如火箭就很适合使用CountDownLatch。火箭发射前,往往要进行各项设备、仪器检查,只有检查完毕后,引擎才能点火。

CountDownLatch的构造函数接收一个整数,即当前这个计数器的计数个数:

public CountDownLatch(int count)

演示:

public class CountDownLatchDemo implements Runnable {
static final CountDownLatch end = new CountDownLatch(10); //1
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(); //2
} catch (InterruptedException e) {
e.printStackTrace();
}
} public static void main(String[] args) throws InterruptedException {
ExecutorService exec = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
exec.submit(demo);
}
//等待检查
end.await(); //3
//发射火箭
System.out.println("Fire!");
exec.shutdown();
}
}

在1处,生成一个CountDownLatch,计数数量为10,表示需要10个线程完成任务,等待在CountDownLatch上的线程才能继续执行。2处表示一个线程已完成,计数器减一。在3处,要求主线程等待10个线程全部完成任务后,主线程才继续执行。

主线程在CountDownLatch上等待,当所有检查任务全部完成后,主线程方能继续执行。

循环栅栏:CyclicBarrier

循环栅栏(CyclicBarrier)和倒计时锁(CountDownLatch)非常类似:只是循环栅栏的计数器可以反复使用。比如假设我们将计数器设置为10,那么凑齐第一批10个线程后,计数器就会归零,然后接着凑齐下一批10个线程,这就是循环栅栏的内在含义。

使用场景:比如司令下达命令,要10个士兵去完成一项任务,士兵要先集合报道完,接着去执行任务。当10个士兵把手头任务都执行完成了,司令才能对象宣布,任务完成!

这里有两步:1,士兵集合报道;2,士兵把任务完成。当这两步先后完成,司令才认为任务完成。

构造函数:比CountDownLatch稍微强大一些。CyclicBarrier可以接收一个参数作为barrierAction(系统当计数器一次计数完成后,系统会执行的动作):

public class CyclicBarrierDemo {
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 {
//士兵报道
System.out.println(soldier + " 报道");
//等待所有士兵到齐
cyclic.await(); //2
doWork();
//等待所有士兵完成任务
cyclic.await(); //3
} 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[]) {
final int N = 10;
Thread[] allSoldier = new Thread[N];
boolean flag = false;
CyclicBarrier cyclic = new CyclicBarrier(N, new BarrierRun(flag, N)); //1
//设置屏障点,主要是为了执行这个方法
System.out.println("队伍集合");
for (int i = 0; i < N; ++i) {
allSoldier[i] = new Thread(new Soldier(cyclic, "士兵 " + i));
allSoldier[i].start(); //4
}
}
}

在1处,创建了CyclicBarrier实例,并将计数器设置为10,并要求在计数器达到指标时,执行BarrierRun。在2处,每一个士兵线程都会等待,知道所有士兵集合完毕,集合完毕后,意味着CyclicBarrier的一次计数完成,当再一次调用CyclicBarrier()时,会进行下一次计数。在3处,会等待所有士兵完成任务。还可以第三次第四次调用 cyclic.await();

整个工作过程图示:

CyclicBarrier.await可能会抛出两个异常,第一是中断异常,可以响应外部紧急事件。大部分迫使线程等待的方法都可能抛出这个异常。第二是它特有的BrokenBarrierException,这个异常说明当前的CyclicBarrier已经破损了,可能没有办法等待所有线程到齐了。如果继续等待,就白等了。

可以在4处上方插入:

if (i == 5)
allSoldier[0].interrupt();

这样做,我们可以得到1个中断异常和9个BrokenBarrierException,1个士兵处于中断,其他9个需要等待这个线程,抛出BrokenBarrierException可以避免其他9个线程进行永久的,无谓的等待。

线程阻塞工具类:LockSupport

LockSupport是一个很实用的线程阻塞工具,可以在线程的任何位置让线程阻塞。

和Thread.suspend()相比,它尼补了由于resume()在前生成的导致线程无法继续执行的问题。和Object.wait()相比,它不需要先获得某个对象锁,也不会抛出InterruptedException。

public class LockSupportDemo {
public static Object u = new Object();
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() {
synchronized (u) {
System.out.println("in "+getName());
LockSupport.park(this); //1
}
}
}
public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(1000);
t2.start();
LockSupport.unpark(t1); //2
LockSupport.unpark(t2); //3
t1.join();
t2.join();
}
}

我们将原来的suspend和resume方法用park()和unpark()代替,在1处,我们挂起了当前线程,在2处,我们分别继续执行t1和t2,从结果可以看出,它不会因为unpark在park执行前而导致线程永久挂起。

为什么LockSupport不会导致线程永久挂起?

因为LockSupport使用了类似信号量的机制(不同的是不能累加),它为每个线程准备了一个许可。

  • 若许可可用—>park()会立即返回,将许可变为不可用—>线程阻塞;
  • 调用unpark()—>使许可变为可用

这个特点使得:即使unpark()操作发生在park()之前,它也可以使下一次park()操作立即返回。这就是不会导致线程永久挂起的原因。

同时,处于park()挂起状态的线程不会像suspend()那样给出令人费解的Runnable状态,它会非常明确的给出一个WAITING状态,甚至会标注是park()引起的。这让问题很容易分析。

LockSupport除了阻塞功能外,还支持中断响应。但是和其他接收中断的函数不一样,它不抛出中断异常,而是默默返回,但可以从Thread.interrupted()等方法获得中断标记。

public class LockSupportIntDemo {
public static Object u = new Object();
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);
} public void run() {
synchronized (u) {
System.out.println("in " + getName());
LockSupport.park();
if (Thread.interrupted()) {
System.out.println(getName() + " 被中断了!");
}
}
System.out.println(getName() + " 执行结束");
}
} public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(100);
t2.start();
t1.interrupt();
LockSupport.unpark(t2);
}
}

线程复用:线程池

多线程在多核的处理下有助于性能,但如果不加控制的使用线程,反而会对系统性能产生不利影响。

为什么会造成不利影响?

  1. 线程创建和关闭需要花费时间,少数不要紧,系统级别的(线程很多的)就很耗时了。
  2. 线程本身需要占用内存。有抛出out of memory的危险

在实际生产环境中,线程的数量必须得到控制。

什么是线程池

联想一下数据库连接池,就知道线程池是啥了。

在线程池中,总有那么几个活跃的线程。当需要时,就从池中取出空闲线程,当完成工作后,再还回去,方便其他人使用。

JDK对线程池的支持

JDK提供了一套Executor框架,用来对线程池的支持。它的核心成员如下:

上面是jdk并发包的核心类。其中ThreadPoolExecutor表示一个线程池。Excecutor扮演线程池工厂的角色,通过它可以取得一个拥有特定功能的线程池。

Executors提供了各种类型的线程池:

线程池类型 作用
newFixedThreadPool() 返回固定线程数量的线程池。线程池中线程数量保持不变。有新任务时,有空闲线程,则执行。若没有则暂存一个任务队列,等到有空闲线程,再执行
newSingleThreadExecutor() 返回只有一个线程的线程池。若有多余任务提交线程池,则存入任务队列,待线程空闲,按先入先出的顺序执行任务队列中的任务。
newCachedThreadPool() 返回一个可根据实际情况调整线程数量的线程池。若有空闲线程可用,则用空闲线程。若没有,创建新的线程处理任务。所有线程完成任务后,将返回线程池进行复用。
newSingleThreadScheduledExecutor() 返回一个ScheduleExecutorService对象,线程池大小为1。它会在给定时间执行某任务。如在固定延时之后,或周期性执行某个任务
newScheduledThreadPool() 返回ScheduleExecutorService对象,但可以指定线程池数量

1、固定大小的线程池

以newFixedThreadPool()为例,简单展示线程池的使用:

public class ThreadPoolDemo {
public static class MyTask implements Runnable {
@Override
public void run() {
System.out.println(System.currentTimeMillis() + ":Thread ID:"
+ Thread.currentThread().getId());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} public static void main(String[] args) {
MyTask task = new MyTask();
ExecutorService es = Executors.newFixedThreadPool(5); //1
for (int i = 0; i < 10; i++) {
es.submit(task); //2
}
}
}

在1处,创建了一个固定大小的线程池,内有5个线程。在2处,依次向线程池提交了10个任务。

上面程序,前5个任务和后5个任务的执行时间正好相差1秒。

2、计划任务

newScheduledThreadPool()。它返回一个ScheduledExecutorService对象,可以根据时间需要进行调度,它其实起到了计划任务的作用。它的一些主要方法如下:

public ScheduledFuture<?> schedule(Runnable command, long delay,
TimeUnit unit);
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay, long period, TimeUnit unit);
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay, long delay, TimeUnit unit);

scheduleAtFixedRate()和scheduleWithFixedDelay()都会对任务进行周期性调度,不过它们有小小区别:

下面是scheduleAtFixedRate()的例子,任务执行1秒,调用周期2秒。即每2秒,任务会被执行一次。

public class ScheduledExecutorServiceDemo {
public static void main(String[] args) {
ScheduledExecutorService ses = Executors.newScheduledThreadPool(10);
ses.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println(System.currentTimeMillis()/1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 0, 2, TimeUnit.SECONDS);
}
}

上面的执行结果是每次打印时间间隔为2秒。

那如果任务的执行时间超过调度时间,会发生什么呢?会出现堆叠的情况吗,不会,若出现这种情况,任务的周期将变成8秒,即任务完成那一刻才开始下一次任务的调度。

如果采用scheduleWithFixedDelay(),任务的实际间隔将是10秒。

刨根究底:核心线程池的内部实现

对于核心的几个线程池,其内部都使用了ThreadPoolExecutor实现。这里就不给出它们的实现方式了。

下面是ThreadPoolExecutor最重要的构造函数:

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)

参数含义:

  • corePoolSize:指定线程池中的线程数量。
  • maximumPoolSize:指定了线程池中的最大线程数量。
  • keepAliveTime:超过corePoolSize的空闲线程,在多长时间内,会被销毁。
  • unit:keepAliveTime的单位。
  • workQueue:任务队列,被提交但尚未被执行的任务。
  • threadFactory:线程工厂,用来创建线程,用默认的即可。
  • handler:拒绝策略。太多任务而处理不及时,如何拒绝任务。

参数workQueue:被提交但未被执行的任务队列,它是一个BlockingQueue接口的对象,仅用于存放Runnable对象。ThreadPoolExecutor的构造函数中可以使用下面几种BlockingQueue:

  • 直接提交的队列:该功能由SynchronousQueue提供。SynchronousQueue没有容量,若使用它,提交的任务不会被真实的保存,而总是将任务提交给线程执行。若没有空线程,则创建;若线程数量达到顶峰,则执行拒绝策略。通常,使用SynchronousQueue需要很大的maximumPoolSize值,否则很容易执行拒绝策略。

  • 有界的任务队列:可以使用ArrayBlockingQueue实现。它的构造函数必须带一个容量参数,表示最大容量。若实际线程数小于corePoolSize,则优先创建新线程,若大于corePoolSize,则将新任务加入等待队列。若等待队列已满,在总线程数<=maximumPoolSize的前提下,创建新的进行执行任务。若>maximumPoolSize,执行拒绝策略。

    可见,有界队列仅在任务队列装满时,才肯呢过将线程数提升至corePoolSize以上。换句话说,除非系统非常繁忙,否则核心线程数维持在corePoolSize。

  • 无界的任务队列:可通过LinkedBlockingQueue实现。与有界队列相比,除非系统资源耗尽,否则无界队列不存在任务入队失败的情况。

    当系统线程数<corePoolSize,创建新线程;

    若>=corePoolSize,不增加新线程,加入等待队列,若任务创建和处理速度差异太大,无界队列会保持快速增长,知道耗尽系统内存。

  • 优先任务队列(带有执行优先级的队列):通过PriorityBlockingQueue实现,可以控制任务的执行前后顺序。高优先级的任务线执行。

回顾一下:

newFixedThreadPool()方法:它的corePoolSize和maximumPoolSize大小一样,因为固定大小的线程池不存在线程数量的动态变化。同时,它使用无界队列存放任务列表,从而在任务提交频繁的情况下有可能耗尽系统资源。

newSingleThreadExecutor()返回单线程线程池,是newFixedThreadPool()的退化,只是简单将线程数设为1。

newCachedThreadPool()方法返回corePoolSize为0,maximumPoolSize无穷大的线程池。刚开始该线程池无线程,它会将提交的线程加入SynchronousQueue,这是一种立即提交的队列,它会迫使线程池增加新的线程执行任务。当任务执行完毕,在60秒内将线程池不用的线程回收(不留任何空闲线程)。因此,当同时有大量任务提交时,任务执行又不快,那么系统便会开启灯亮线程处理,很快就会耗尽系统资源。

超负载了?使用拒绝策略

JDK内置的拒绝策略:

  • AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作。
  • CallerRunsPolicy策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的线程。显然,不会真正丢弃任务,但是,任务提交线程的性能可能急剧下降。
  • DiscardOledestPolicy策略:该策略将丢弃最老的一个请求,即将要执行的一个任务,并尝试再次提交当前任务。
  • DiscardPolicy策略:该策略默默丢弃无法处理的任务,不予任何处理。(若允许丢失,可能是最好的一种方式了)

可以自己扩展RejectedExecutionHandle接口实现自己的拒绝策略,下面代码简单演示了自定义线程池和拒绝策略的使用:

public class RejectThreadPoolDemo {
public static class MyTask implements Runnable {
@Override
public void run() {
System.out.println(System.currentTimeMillis() + ":Thread ID:"
+ Thread.currentThread().getId());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} public static void main(String[] args) throws InterruptedException {
MyTask task = new MyTask();
ExecutorService es = new ThreadPoolExecutor(5, 5,
0L, TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>(),
Executors.defaultThreadFactory(),
new RejectedExecutionHandler(){
@Override
public void rejectedExecution(Runnable r,
ThreadPoolExecutor executor) {
System.out.println(r.toString()+" is discard");
}
}); //1
for (int i = 0; i < Integer.MAX_VALUE; i++) {
es.submit(task);
Thread.sleep(10);
}
}
}

上述1处自定义了一个线程池。该线程池有5个常驻线程,并且最大线程数量也是5个。这和固定大小的线程池是一样的。但是它却拥有一个只有10个容器的等待队列。在这里,我们自定义了拒绝策略,只是比DiscardPolicy高级一点点,把拒绝的信息打印出来,在实际应用中,我们可以将其记录到日志上。用来分析系统的负载和任务丢失情况。

自定义线程创建:ThreadFactory

线程池的主要作用是为了线程复用,也就是避免了线程的频繁创建。但是,线程池最开始的线程从何而来呢?答案就是ThreadFactory。

ThreadFactory是一个接口,它只有一个方法,用来创建线程:

复制Thread newThread(Runnable r);

当线程池需要新建线程时,就会调用这个方法。

我们使用自定义线程可以更自由地设置池子中所有线程的状态,甚至可以设置为守护线程:

public class TFThreadPoolDemo {
public static class MyTask implements Runnable {
@Override
public void run() {
System.out.println(System.currentTimeMillis() + ":Thread ID:"
+ Thread.currentThread().getId());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} public static void main(String[] args) throws InterruptedException {
MyTask task = new MyTask();
ExecutorService es = new ThreadPoolExecutor(5, 5,
0L, TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactory(){
@Override
public Thread newThread(Runnable r) {
Thread t= new Thread(r);
t.setDaemon(true);
System.out.println("create "+t);
return t;
}
}
);
for (int i = 0; i < 5; i++) {
es.submit(task);
}
Thread.sleep(2000);
}
}

扩展线程池

虽然JDK已经帮我们实现了这个稳定的高性能线程池。但如果我们需要对线程池进行一些扩展。比如,想监控每个任务执行的开始和结束时间,或者其他一些自定义增强功能,怎么办呢?

ThreadPoolExecutor:它也是一个可以扩展的线程池。它提供了beforeExecute()、afterExecute()和terminated()三个接口对线程池进行控制。

在默认的ThreadPoolExecutor实现中,提供了空的beforeExecute()、afterExecute()实现。在实际引用中,可以对其扩展实现对线程池运行状态的跟踪,输出一些有用的调试信息,用以帮助系统故障诊断。下面演示对线程池的扩展,在这个扩展中,将记录每一个任务的执行日志:

public class ExtThreadPool {
public static class MyTask implements Runnable {
public String name; public MyTask(String name) {
this.name = name;
} @Override
public void run() {
System.out.println("正在执行" + ":Thread ID:" + Thread.currentThread().getId()
+ ",Task Name=" + name);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
ExecutorService es = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()) {
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println("准备执行:" + ((MyTask) r).name);
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println("执行完成:" + ((MyTask) r).name);
}
@Override
protected void terminated() {
System.out.println("线程池退出");
}
};
for (int i = 0; i < 5; i++) {
MyTask task = new MyTask("TASK-GEYM-" + i);
es.execute(task);
Thread.sleep(10);
}
es.shutdown();
}
}/**
......
正在执行:Thread ID:13,Task Name=TASK-GEYM-2
准备执行:TASK-GEYM-3
正在执行:Thread ID:14,Task Name=TASK-GEYM-3
准备执行:TASK-GEYM-4
正在执行:Thread ID:15,Task Name=TASK-GEYM-4
执行完成:TASK-GEYM-0
执行完成:TASK-GEYM-1
执行完成:TASK-GEYM-2
执行完成:TASK-GEYM-3
执行完成:TASK-GEYM-4
线程池退出
*/

可以看到,所有任务执行前后都捕获到了。这对于应用的调试和诊断是非常有帮助的。

合理的选择:优化线程池线程数量

线程池的大小对系统的性能有一定的影响。过大或过小的线程数量都无法发挥最优的系统性能。但是也不用做得非常精确,只要避免极大和极小两种情况即可。

堆栈去哪里了:挖出线程池中被淹没的异常堆栈

先说明一下要解决的问题!

public class DivTask implements Runnable {
int a, b;
public DivTask(int a, int b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
double re = a / b; //1
System.out.println(re);
}
public static void main(String[] args) {
ThreadPoolExecutor pools = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
0L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
for(int i=0; i<5; i++)
pools.submit(new DivTask(100, i));
// pools.execute(new DivTask(100, i));
}
}/**
100.0
25.0
50.0
33.0
*/

在上面程序中,只有四个输出结果,少了一个,然而没有报错信息。使用submit会出现这样的情况(execute会抛出异常,具体原因后面再看吧)。

再说下解决方案!

对于程序员来说,没有异常堆栈是最头疼的事。我们可以通过两种方法来讨回异常堆栈:

1 是放弃submit()改用execute(),如注释所示;

pools.execute(new DivTask(100, i));

2 是改造submit():

 Future<?> submit = pools.submit(new DivTask(100, i));
submit.get();

以上两种都可以得到部分堆栈信息:

Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
at java.util.concurrent.FutureTask.report(FutureTask.java:122)
at java.util.concurrent.FutureTask.get(FutureTask.java:192)
at geym.ch3.DivTask.main(DivTask.java:21)
Caused by: java.lang.ArithmeticException: / by zero
at geym.ch3.DivTask.run(DivTask.java:13)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)

注意,上面说的是部分!我们只知道异常是在哪里抛出的,也就是代码的1处,但是不确定线程是在哪里提交的,任务的具体提交的位置被淹没了!

3、自己动手,扩展ThreadPoolExecutor线程池(彻底解决的办法)

为了少加班!我们还是自己动手,把堆栈的信息彻底挖出来吧!扩展我们的ThreadPoolExecutor线程池,让它在调度任务之前,先保存一下提交任务线程的堆栈信息。

public class TraceThreadPoolExecutor extends ThreadPoolExecutor {
public TraceThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue);
}
@Override
public void execute(Runnable task) {
// TODO Auto-generated method stub
super.execute(wrap(task, clientTrace(), Thread.currentThread().getName())); //包装器
}
@Override
public Future<?> submit(Runnable task) {
// TODO Auto-generated method stub
return super.submit(wrap(task, clientTrace(), Thread.currentThread().getName()));
}
private Exception clientTrace() {
return new Exception("Client stack trace");
}
private Runnable wrap(final Runnable task,final Exception clientStack, String clientThreadName) { //1
return new Runnable() {
@Override
public void run() {
try {
task.run();
} catch (Exception e) {
clientStack.printStackTrace();
try {
throw e;
} catch (Exception e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
};
}
public static class DivTask implements Runnable {
int a,b;
public DivTask(int a,int b) {
this.a = a;
this.b = b;
}
@Override
public void run() {
double re = a/b;
System.out.println(re);
}
}
public static void main(String[] args) {
ThreadPoolExecutor pools = new TraceThreadPoolExecutor(0, Integer.MAX_VALUE,
0L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
for(int i=0; i<5; i++)
pools.execute(new DivTask(100, i));
}
}

在wrap()(1处)方法的第2个参数为一个异常,里面保存着提交任务的线程的堆栈信息。该方法将我们传入的Runnable任务进行一层包装,使之能处理异常信息。当任务发生异常时,这个异常会被打印。

分而治之:Fork/Join框架

fork()用来开启分支线程来处理任务,一般会提交给ForkJoinPool线程池进行处理,以节省系统资源。

Join()用来等待fork()的执行分支执行结束。

使用Fork/Join进行数据处理时的总体结构如图所示:

由于线程池的优化,提交的任务和线程数量不是一对一的关系。通常是一个线程处理多个任务,每个线程都有一个任务队列。当线程A把任务完成,而线程B还在有一堆任务处理时,线程A会帮助B。B从任务队列顶部拿数据,而A则是从任务队列的底部拿数据,这样有利于避免数据竞争。

ForkJoinPool的一个重要的接口,可以提交一个ForkJoinTask,ForkJoinTask支持fork()分解以及join()等待的任务,它有两个重要子类:RecursiveAction(无返回值)和RecursiveTask(返回v类型)。

public <T> ForkJoinTask<T> submit(ForkJoinTask<T> task);

使用:

public class CountTask extends RecursiveTask<Long> {
private static final int THRESHOLD = 10000;
private long start;
private long end; public CountTask(long start, long end) {
this.start = start;
this.end = end;
} public Long compute() {
long sum = 0;
boolean canCompute = (end - start) < THRESHOLD;
if (canCompute) {
//求和总数小于THRESHOLD,直接求和
for (long i = start; i <= end; i++) {
sum += i;
}
} else {
//分成100个小任务
// 比如start=0,end=100,则每一小步计算2个数
//i=0,lastOne=0+2=2, pos=2+1=3
//i=1,lastOne=2+2=4, pos=4+1=5
//...
//i=100
long step = (start + end) / 100; //
long pos = start;
for (int i = 0; i < 100; i++) {
long lastOne = pos + step;
if (lastOne > end) lastOne = end;
CountTask subTask = new CountTask(pos, lastOne);
pos = lastOne + 1;
subTask.fork();
sum += subTask.join();
}
}
return sum;
} public static void main(String[] args) {
ForkJoinPool forkJoinPool = new ForkJoinPool();
CountTask task = new CountTask(0, 200000L);
ForkJoinTask<Long> result = forkJoinPool.submit(task); //1
try {
long res = result.get();
System.out.println("sum=" + res);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}

在1处,使用forkJoinPool提交了CountTask,CountTask构造一个计算1到200000求和的任务。在compute()方法中,遵循了下面的逻辑:

if (canCompute) {
//求和总数够小,直接求和
} else {
//分成若干个小任务
}

在使用ForkJoinPool时需要注意,如果任务的划分层次很深,一直没有返回,可能出现两种情况:

  1. 系统内线程数量越积越多,导致性能严重下降。
  2. 函数调用层次变得很深,导致栈溢出。

JDK的并发容器

JDK提供了好用的并发容器类,使用也很方便,这里主要讲讲这些工具的具体实现。

并发集合介绍

先简单认识一下并发集合:

  • ConcurrentHashMap:高效的并发HashMap。即线程安全的HashMap
  • CopyOnWriteArrayList:属于List,和ArrayList是一族的。在读多少写的场合性能非常好,远远好于Vector。
  • ConcurrentLinkedQueue:线程安全的LinkedList。
  • BlockingQueue:这是一个接口,JDK内部通过链表、数组等方式实现这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。
  • ConcurrentSkipListMap:跳表的实现。这是一个Map,使用跳表的数据结构进行快速查找。
  • Vector也是线程安全的,另外Collections工具类可以帮助我们将任意集合包装成线程安全的工具类

线程安全的HashMap:ConcurrentHashMap

如果获得一个线程安全的HashMap?

第一种方法是:使用Collections.synchronizedMap()方法来包装HashMap

static Map<String, String> map = Collections.synchronizedMap(new HashMap<String, String>());

Collections.synchronizedMap()会生成一个名为SynchronizedMap的Map。它使用委托,将自己所有Map相关的功能交给传入的HashMap实现,自己则主要负责保证线程安全。

第二种方法是使用ConcurrentHashMap代替HashMap,这种方式更专业,更适合并发场合。

线程安全的List

Vector是线程安全的List,也可以使用Collections.synchronizedList()方法来包装任意List。

高效读写队列:ConcurrentLinkedQueue

ConcurrentLinkedQueue算是高并发中性能最好的队列了。

具体实现:

1、节点

作为一个链表,自然需要定义一个节点:

private static class Node<E>{
volatile E item;
volatile Node<E> next;

item用来表示目标元素,比如:放入String,item就是String元素。next表示Node的下一个元素。这样Node就环环相扣,串在一起了。

2、CAS操作

首先,说明一下CAS操作的原理:CAS操作包含三个操作数—— 内存位置的值(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

CAS是一种乐观锁。乐观锁在少写的情况下适用,若是多写的情况,会导致CAS算法不断的进行retry,反而降低了系统性能,多写的情况适合适用悲观锁。

casItem()表示设置当前Node的item值。cmp为期望值,第二个参数val为目标值。当当前值等于cmp期望值时,就会将目标值设置为val。第二个方法类似。只是它用来设置next字段。

ConcurrentLinkedQueue内部有两个重要的字段,head和tail,分别表示头部和尾部。tail的更新不是及时的,而是有延迟,每次更新会跳跃两个元素。如下图:

原书中的源码分析我没怎么看懂,有看懂的童鞋欢迎在评论中分享心得

高效读取:不变模式下的CopyOnWriteArrayList

在很多应用场景中,读操作往往会远远大于写操作。所以这种情况下,我们希望读的性能好些,而写的性能差些也无所谓。

我们知道:在读写锁ReadWriteLock中,读读不互斥,而读写,写写是互斥的。

而现在,JDK还提供了另外一个读写工具类,将读取性能发挥到极致CopyOnWriteArrayList,它的读读不阻塞,读写也不会互相阻塞,只有写写需要同步等待。

它是怎么做到读写不阻塞的?

CopyOnWrite在写入操作时,对原有的数据进行复制成一个副本(而不修改原来的数据),将修改的内容写入复制后的副本中。写完后,再用副本替换原来的数据,这样就不会影响读了。

读取的实现:

读取没有任何同步和锁的操作,理由是内部数组array不会发生修改,只会被另外一个array替换。

写人的实现:

public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1); //1
newElements[len] = e;//2
setArray(newElements);//3
return true;
} finally {
lock.unlock();
}
}

首先,写入操作使用锁,这个锁仅限于控制写-写的情况。重点在1处,进行了内部元素的复制,而生成一个新数组newElements。2处,将新元素增加到数组末尾,然后,将新数组替换老数组,修改就完成了。整个过程不会影响读取,并且读取线程会实时查看到这个修改(array变量是volatile的)

数据共享通道:BlockingQueue

如何实现多个线程间的数据共享呢?比如,线程A希望给线程B发一个消息,用什么方式好呢?

我们希望线程A能够通知到线程B,又希望线程A不知道线程B的存在。这样对于以后线程B的升级或维护,而不用再修改线程A有帮助。为了实现这一点,我们可以使用一个中间件BlockingQueue来实现。它就相当于一个意见箱,用来作为发表意见者与接收意见者沟通的桥梁。

BlockingQueue和之前提到的ConcurrentLinkedQueue和CopyOnWriteArrayList不同,它是一个接口,而不是具体的实现。它的主要实现如下图:

ArrayBlockingQueue:基于数组实现。更适合做有界队列,扩展比较不方便

LinkedBlockingQueue:基于链表。更适合做无界队列,因为其内部元素可动态增加。

BlockingQueue为什么适合作为数据共享的通道呢?原因在于Blocking(阻塞)。

当服务线程(指获取队列中消息并进行处理的线程)处理完队列中所有的消息后,服务线程是如何知道下一条消息的到来的?BlockingQueue会让服务线程在队列为空时,进行等待,当有新的消息进入队列后,自动将线程唤醒。

它是如何工作的?以ArrayBlockingQueue为例说明:

写入数据:

它有一个items,items就是用来存放数据的队列。offer()在列队满时,返回false。我们关注的是put()方法,put()也是将元素压入队列队尾,但队列满了,它会一直等待,直到队列中有空闲位置。

读取数据:

poll()、take()两个方法都能从队列中的头部弹出一个元素。不同的是:如果队列为空poll()方法直接返回null。而take()方法会等待,直到队列内有可用元素。

从上面可以看出,put()take()方法才是Blocking的关键。为了做好通知和等待两件事,ArrayBlockingQueue定义了三个字段:

take()操作:

当队列为空时,让当前线程等待在notEmpty,新元素入队时,则进行一次notEmpty上的通知。

public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await(); //1
return dequeue();
} finally {
lock.unlock();
}
}

在1处,要求线程在notEmpty对象中等待。下面是元素入队的一段代码:

/**
* 在当前put位置插入元素、进给和信号。
* 只有在持有锁时才调用。
*/
private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal(); //1
}

在1处,当新元素入列后,需要通知等待在notEmpty上的线程,让它们继续工作。

put()操作:

当队列满时,需要让 压入线程 等待:

public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await(); //1
enqueue(e);
} finally {
lock.unlock();
}
}

在1处,队列满时,在notFull对象中等待。

当然,当元素从队列中挪走时,队列中有空位时,自然也要通知等待入队的线程:

private E dequeue() {
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal(); //1
return x;
}

我们还会在“5.3 生产者消费者”一节中,看到他们的身影。在那里,我们可以更清楚地看到如何使用BlockingQueue解耦生产者和消费者。

随机数据结构—跳表:SkipList

介绍跳表

除了常用的哈希表外,还有一种有趣的数据结构:跳表。跳表的本质是同时维护了多个链表,并且链表是分层的。跳表的查询性能要比哈希表好。如下图

最低层的链表维护了跳表中所有的元素,每上面一层都是下面一层的子集,一个元素插入哪一层完全随机,运气不好可能得到性能最差的结构。但是实际工作中,它还是表现得很好的。

跳表内所有元素都是排序的。查找时,从顶级链表开始找,一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续查找。比如要查找上面跳表结构中的7。查找过程如下图所示:

跳表显然是一种空间换时间的算法。

使用跳表实现的Map和使用哈希算法实现的Map的另一个不同之处是:跳表实现的Map是会排序的,而哈希实现的Map不排序。若需要一个有序的Map,那就选择跳表。

使用:ConcurrentSkipListMap

实现这一数据结构的类是ConcurrentSkipListMap。简单使用:

复制Map<Integer, Integer> map = new ConcurrentSkipListMap<>();
for (int i = 0; i<30; i++)
map.put(i,i);
for (Map.Entry<Integer, Integer> entry: map.entrySet()
) {
System.out.println(entry.getKey());
}

跳表有三个关键的数据结构组成:

  • Node<K,V>:(节点,含有key、value、next元素,对Node的所有操作,都使用CAS方法)
  • Index<K,V>:(表示索引,它的内部包装了node,同时增加了向下和向右的引用),整个跳表就是根据Index进行全网的组织的。
  • HeadIndex:表示链表头部的第一个Index。它继承自Index。
 

周会材料:高并发程序设计<二>的更多相关文章

  1. 【实战Java高并发程序设计 7】让线程之间互相帮助--SynchronousQueue的实现

    [实战Java高并发程序设计 1]Java中的指针:Unsafe类 [实战Java高并发程序设计 2]无锁的对象引用:AtomicReference [实战Java高并发程序设计 3]带有时间戳的对象 ...

  2. 【实战Java高并发程序设计6】挑战无锁算法:无锁的Vector实现

    [实战Java高并发程序设计 1]Java中的指针:Unsafe类 [实战Java高并发程序设计 2]无锁的对象引用:AtomicReference [实战Java高并发程序设计 3]带有时间戳的对象 ...

  3. 【实战Java高并发程序设计 5】让普通变量也享受原子操作

    [实战Java高并发程序设计 1]Java中的指针:Unsafe类 [实战Java高并发程序设计 2]无锁的对象引用:AtomicReference [实战Java高并发程序设计 3]带有时间戳的对象 ...

  4. 【实战Java高并发程序设计 4】数组也能无锁:AtomicIntegerArray

    除了提供基本数据类型外,JDK还为我们准备了数组等复合结构.当前可用的原子数组有:AtomicIntegerArray.AtomicLongArray和AtomicReferenceArray,分别表 ...

  5. 【实战Java高并发程序设计 3】带有时间戳的对象引用:AtomicStampedReference

    [实战Java高并发程序设计 1]Java中的指针:Unsafe类 [实战Java高并发程序设计 2]无锁的对象引用:AtomicReference AtomicReference无法解决上述问题的根 ...

  6. 【实战Java高并发程序设计 1】Java中的指针:Unsafe类

    是<实战Java高并发程序设计>第4章的几点. 如果你对技术有着不折不挠的追求,应该还会特别在意incrementAndGet() 方法中compareAndSet()的实现.现在,就让我 ...

  7. 《实战java高并发程序设计》源码整理及读书笔记

    日常啰嗦 不要被标题吓到,虽然书籍是<实战java高并发程序设计>,但是这篇文章不会讲高并发.线程安全.锁啊这些比较恼人的知识点,甚至都不会谈相关的技术,只是写一写本人的一点读书感受,顺便 ...

  8. JAVA高并发程序设计笔记

    第二章 Java并行程序基础 1.join()的本质是让调用线程wait()在当前线程的对象上 2.Thread.yiedl()会使当前线程让出CPU 3.volatile保证可见性,无法保证原子性( ...

  9. 《实战Java高并发程序设计》读书笔记

    文章目录 第二章 Java并行程序基础 2.1 线程的基本操作 2.1.1 线程中断 2.1.2 等待(wait)和通知(notify) 2.1.3 等待线程结束(join)和谦让(yield) 2. ...

随机推荐

  1. hdu 6301 Distinct Values (贪心)

    Distinct Values Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)T ...

  2. 《Java练习题》习题集一

    编程合集: https://www.cnblogs.com/jssj/p/12002760.html Java总结:https://www.cnblogs.com/jssj/p/11146205.ht ...

  3. Java基础接口和抽象类区别(二)

    抽象类 在了解抽象类之前,先来了解一下抽象方法.抽象方法是一种特殊的方法:它只有声明,而没有具体的实现.抽象方法的声明格式为: 抽象方法必须用abstract关键字进行修饰.如果一个类含有抽象方法,则 ...

  4. 第三次作业-Python网络爬虫与信息提取

    1.注册中国大学MOOC 2.选择北京理工大学嵩天老师的<Python网络爬虫与信息提取>MOOC课程 3.学习完成第0周至第4周的课程内容,并完成各周作业 过程. 5.写一篇不少于100 ...

  5. Java 异常规范

    1. 只针对异常情况使用异常,不要用异常来控制流程 try { int i = 0; while (true) { range[i++].doSomething(); } } catch (Array ...

  6. c++之运算符

    运算符分为:算数运算符.赋值运算符.比较运算符.逻辑运算符 算数运算符:+(正) -(负) + - * / % i++(先赋值后自增) ++i(先自增后赋值) i--(先赋值后自减) --i(先自减后 ...

  7. Cannot read property 'createElement' of undefined

    场景: 架构:React+TS+DVA   具体场景: 在将之前后缀为jsx的组件转化为tsx后缀的组件时,抛出Cannot read property 'createElement' of unde ...

  8. ArcGIS Runtime SDK for WPF学习笔记(一)

    本节主要讲解如何安装ArcGIS Runtime SDK,以及移除注释与水印. 附上ArcGIS Runtime SDK for .NET的官方操作手册网址:https://developers.ar ...

  9. Linux MySQL的root无法登录数据库ERROR 1045 (28000)

    Linux环境下,脚本自动安装完数据库,命令行用mysql -uroot -ppasswaord 登录却报了这么个错: ERROR 1045 (28000): Access denied for us ...

  10. Linux kernel中常见的宏整理

    0x00 宏的基本知识 // object-like #define 宏名 替换列表 换行符 //function-like #define 宏名 ([标识符列表]) 替换列表 换行符 替换列表和标识 ...