Java并发技术研究

1.JAVA并发编程基础

Java从诞生开始就选择了内置对多线程的支持。但是过多地创建线程和对线程的不当管理也容易造成问题。因而对于开发人员如何编写优秀的并发程序是一个不小的挑战。

1.1 多线程

1.1.1 多线程简单示例

一个Java程序从main()方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上Java程序天生就是多线程程序,因为执行main()方法的是一个名称为main的线程

package ThreadDemo;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

public class MultiThread{

    public static void main(String[] args) {
        // 获取Java线程管理MXBean
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 不需要获取同步的monitor和synchronizer信息,仅获取线程和线程堆栈信息
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        // 遍历线程信息,仅打印线程ID和线程名称信息
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.
                    getThreadName());
        }
    }
}

控制台输入结果:
[5] Monitor Ctrl-Break   // console控制线程
[4] Signal Dispatcher    // 分发处理发送给JVM信号的线程
[3] Finalizer            // 调用对象finalize方法的线程
[2] Reference Handler    // 清除reference引用线程
[1] main                 // 主线程

1.1.2 为什么使用多线程

  • 更多的处理器核心
  • 更快的响应时间
  • 更好的编程模型

1.1.3 线程的状态

  • Java线程的状态
    这里写图片描述

  • Java线程状态变迁
    这里写图片描述
    线程创建之后,调用start()方法开始运行。当线程执行wait()方法之后,线程进入等待状态。

    进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而超时等待状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时将会返回到运行状态。

    当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到阻塞状态。线程在执行Runnable的run()方法之后将会进入到终止状态。

1.1.4 Daemon线程

Daemon线程是一种后台支持型线程。当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。

通过调用Thread.setDaemon(true)将线程设置为Daemon线程。注意:设置线程的Demon属性需要在启动线程之前设置。

1.1.5 ThreadLocal(易混淆)

ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递复杂度。

    public static class ThreadShareList {
        private static final ThreadLocal<ArrayList<String>> threadLocal = new ThreadLocal<ArrayList<String>>();  

        public static ArrayList<String> getCurrList() {  
            // 获取当前线程内共享的arrayList  
            ArrayList<String> arrayList = threadLocal.get();  
            if(null==arrayList) {  
                arrayList = new ArrayList<String>();
                threadLocal.set(arrayList);  
            }       
            return arrayList;  
        }  
    }

Jdk7提供的类ThreadLocalRandom使用Threadlocal机制实现创建单线程相关的random。

1.1.6 线程安全

在多线程编程中,可能会出现多个线程并发访问同一个资源(内存区(变量,数组,或对象)、系统(数据库,web services等)、文件)的情况。如果不对这样的访问做控制,就可能出现不可预知的结果。这就是线程安全问题。

在Java多线程编程当中,提供了以下几种方式来实现线程安全:

  • 隐形锁(Synchronized)和显式锁(Lock):属于互斥同步方法,是重量级的多线程同步机制,可能会引起上下文切换和线程调度,它同时提供内存可见性、有序性和原子性。

  • volatile:轻量级多线程同步机制,不会引起上下文切换和线程调度。仅提供内存可见性、有序性保证,不提供原子性。

  • CAS原子指令:属于非阻塞同步方法,轻量级多线程同步机制,不会引起上下文切换和线程调度。它同时提供内存可见性、有序性和原子化更新保证。比如后面分享的Atomic机制

1.2 volatile关键字

Java支持多个线程同时访问一个对象或者对象的成员变量,但是程序在执行过程中,一个线程看到的变量并不一定是最新的。
这里写图片描述

关键字volatile可以用来修饰成员变量,就是告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。

过多地使用volatile是不必要的,因为它会降低程序执行的效率。

注意:volatile关键字无法保证原子操作的原子性,比如多个线程同时操作count++,如果需要保证原子性的话需要使用synchronized,Atomic,Lock。

1.3 synchronized关键字

关键字synchronized(隐形锁)可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。

用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现
为以下3种形式:

  • 对于普通同步方法,锁是当前实例对象。
  • 对于静态同步方法,锁是当前类的Class对象。
  • 对于同步方法块,锁是Synchonized括号里配置的对象。

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁(synchronized自身控制)。

典型的双重锁示例:

/**
 * 延迟初始化,效率优化(双重锁机制)
 */
public class SingleInstance04 {

    private static volatile SingleInstance04 singleInstance04;

    private SingleInstance04() { }

    public static SingleInstance04 getSingleInstance04() {

        if (null == singleInstance04) {
            synchronized (singleInstance04) {
                if (null == singleInstance04) {
                    singleInstance04 = new SingleInstance04();
                }
            }
        }
        return singleInstance04;
    }
}

2.Java中的锁

在Java中,使用synchronized关键字将会隐式地获取锁,但是它将锁的获取和释放固化了,扩展性不好。比如synchronized无法解决生产者消费者模式场景。

在Jdk5以后,并发包新增了Lock接口(以及相关实现类)用来实现锁功能,和synchronized具有类似的功能,Lock可以显示的获取和释放锁,更加灵活。如下图所示:
这里写图片描述

2.1 Lock接口

Lock正常使用的方式如下:

        Lock lock = new ReentrantLock();
        lock.lock();
        try {
            System.out.println("Lock接口使用模式");
        } finally {
            lock.unlock();
        }

Lock接口提供的synchronized关键字所不具备的主要特性:

  • 尝试非阻塞的获取锁
  • 能够响应中断地获取锁
  • 超时获取锁

尝试非阻塞的获取锁示例:

    public static void main(String[] argv) {
        Lock lock = new ReentrantLock();
        lock.lock();
        try {
            new Thread() {
                @Override
                public void run() {
                    System.out.println("Lock接口非阻塞测试1");
                    lock.tryLock();
                    System.out.println("Lock接口非阻塞测试2");
                }
            }.start();

            Thread.sleep(4000);
            System.out.println("Lock接口使用模式");
        } catch (Exception e) {
            System.out.println(e);
        } finally {
            lock.unlock();
        }
    }   

    测试结果:
    Lock接口非阻塞测试1
    Lock接口非阻塞测试2
    Lock接口使用模式 

Lock相关接口,可参考jdk中接口Lock定义。

2.2 队列同步器(AbstractQueuedSynchronizer)

AbstractQueuedSynchronizer,是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获
取线程的排队工作。

同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。

  • getState():获取当前同步状态
  • setState(int newState):设置当前同步状态
  • compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性

同步器提供的模板方法基本上分为3类:

- 独占式获取与释放同步状态
- 共享式获取与释放同步状态
- 查询同步队列中的等待线程情况。

自定义同步组件将使用同步器提供的模板方法来实现自己的同步语义。下面给出一个独占锁的实例:

class Mutex implements Lock {

    // 静态内部类,自定义同步器
    private static class Sync extends AbstractQueuedSynchronizer {
        // 是否处于占用状态
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
        // 当状态为0的时候获取锁
        public boolean tryAcquire(int acquires) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        // 释放锁,将状态设置为0
        protected boolean tryRelease(int releases) {
            if (getState() == 0) throw new
                    IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
        // 返回一个Condition,每个condition都包含了一个condition队列
        Condition newCondition() { return new ConditionObject(); }
    }

    // 仅需要将操作代理到Sync上即可
    private final Sync sync = new Sync();

    public void lock() {
        sync.acquire(1);
    }

    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    public void unlock() {
        sync.release(1);
    }

    public Condition newCondition() {
        return sync.newCondition();
    }

    public boolean isLocked() {
        return sync.isHeldExclusively();
    }

    public boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }
    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }
    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

运行测试结果:
Thread-0acquire lock
Thread-0release lock
Thread-1acquire lock
Thread-1release lock

2.3 重入锁(ReentrantLock)

ReentrantLock是支持重进入的锁,该锁能够支持一个线程对资源的重复加锁。换句话说重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞。

ReentrantLock中的锁存在两种形式

  • 公平加锁
  • 非公平加锁

测试重进入特性示例:

public static void main(String[] argv) {
        Lock lock = new ReentrantLock();
        lock.lock();
        lock.lock();
        try {
            new Thread() {
                @Override
                public void run() {
                    System.out.println("Lock接口非阻塞测试1");
                    lock.lock();
                    System.out.println("Lock接口非阻塞测试2");
                }
            }.start();

            Thread.sleep(4000);
            System.out.println("Lock接口使用模式");
        } catch (Exception e) {
            System.out.println(e);
        } finally {
            lock.unlock();
            lock.unlock();
        }
    }

2.4 读写锁(ReentrantReadWriteLock)

之前提到的ReentrantLock或者Mutex都是互斥排他锁,某一时刻只允许一个线程获取到锁(即使是共享锁也是存在限制的,只能最多其允许的线程并发访问),对于读写模式显然都不适应。

jdk提供了读写锁,能够实现在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一个读锁,一个写锁。

ReentrantReadWriteLock的特性:

  • 公平性选择
  • 重进入
  • 锁降级

ReentrantReadWriteLock示例:

public class ReentrantReadWriteLockTest {

    static Map<String, Object> map = new HashMap<>();
    static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    // 构建一个读锁,一个写锁
    static Lock r = rwl.readLock();
    static Lock w = rwl.writeLock();

    // 获取一个key对应的value
    public static Object get(String key) {
        r.lock();
        try {
            System.out.println("get: key="+key+", value="+map.get(key));
            return map.get(key);
        } finally {
            r.unlock();
        }
    }
    // 设置key对应的value,并返回旧的value
    public static Object put(String key, Object value) {
        w.lock();
        try {
            System.out.println("put: key="+key+", value="+value);
            return map.put(key, value);
        } finally {
            w.unlock();
        }
    }

    // 清空所有的内容
    public static void clear() {
        w.lock();
        try {
            map.clear();
        } finally {
            w.unlock();
        }
    }

}

2.5 StampedLock

读写锁虽然分离了读和写的功能,使得读与读之间可以并发。但是,读和写之间依然是冲突的。读锁会完全阻塞写锁,它使用的依然是悲观锁的策略,如果有大量的读线程,它也有可能引起写线程的“饥饿”。

StampedLock是并发包里面Jdk8版本新增的一个锁,可以很好的解决上述问题。

  • 写锁
  • 乐观读锁
  • 悲观读锁

Jdk8提供的一个示例:

public class Point {

    // 成员变量
    private double x, y;

    // 锁实例
    private final StampedLock sl = new StampedLock();

    // 排它锁-写锁(writeLock)
    void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }

    // 乐观读锁(tryOptimisticRead)
    double distanceFromOrigin() {

        // 尝试获取乐观读锁
        long stamp = sl.tryOptimisticRead();
        // 将全部变量拷贝到方法体栈内
        double currentX = x, currentY = y;
        // 检查在获取到读锁票据后,锁有没被其他写线程排它性抢占
        if (!sl.validate(stamp)) {
            // 如果被抢占则获取一个共享读锁(悲观获取)
            stamp = sl.readLock();
            try {
                // 将全部变量拷贝到方法体栈内
                currentX = x;
                currentY = y;
            } finally {
                // 释放共享读锁
                sl.unlockRead(stamp);
            }
        }
        // 返回计算结果
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    // 使用悲观锁获取读锁,并尝试转换为写锁
    void moveIfAtOrigin(double newX, double newY) {
        // 这里可以使用乐观读锁替换
        long stamp = sl.readLock();
        try {
            // 如果当前点在原点则移动
            while (x == 0.0 && y == 0.0) {
                // 尝试将获取的读锁升级为写锁
                long ws = sl.tryConvertToWriteLock(stamp);
                // 升级成功,则更新票据,并设置坐标值,然后退出循环
                if (ws != 0L) {
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                } else {
                    // 读锁升级写锁失败则释放读锁,显示获取独占写锁,然后循环重试
                    sl.unlockRead(stamp);
                    stamp = sl.writeLock();
                }
            }
        } finally {
            // 释放锁(6)
            sl.unlock(stamp);
        }
    }
}

2.6 Condition接口

Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁。

public class ConditionUseCase {

    private Lock lock;
    private Condition condition;

    public ConditionUseCase() {
        lock = new ReentrantLock();
        condition = lock.newCondition();
    }


    public void conditionWait() throws InterruptedException {
        lock.lock();
        try {
            System.out.println("conditionWait start");
            condition.await();
            System.out.println("conditionWait end");
        } finally {
            lock.unlock();
        }
    }


    public void conditionSignal() throws InterruptedException {
        lock.lock();
        try {
            System.out.println("conditionSignal start");
            Thread.sleep(3000);
            condition.signal();
            System.out.println("conditionSignal start");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] argv) throws Exception{
        ConditionUseCase conditionUseCase = new ConditionUseCase();
        new Thread() {
            @Override
            public void run() {
                try {
                    conditionUseCase.conditionWait();
                } catch (Exception e) {

                }
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                try {
                    conditionUseCase.conditionSignal();
                } catch (Exception e) {

                }
            }
        }.start();

    }
}
测试结果:
conditionWait start
conditionSignal start
conditionSignal start
conditionWait end

3.Java并发容器及并发工具类

3.1 ConcurrentHashMap

ConcurrentHashMap是线程安全且高效的HashMap。相对于HashMap,它是线程安全的,相对于HashTable(所有访问线程间竞争一把锁)更加高效。

ConcurrentHashMap采用的是锁分段技术。如需深入了解,可以阅读源码(设计的很巧妙)

3.2 Java中的阻塞队列

对于Java中的阻塞队列,均是使用通知模式实现。所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。

Jdk1.7中提供了7个阻塞队列,如下:

  • ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列

    // 
    public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }
  • LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列,此队列的默认和最大长度为Integer.MAX_VALUE,平时使用的最多

  • PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列,可以自定义类实现compareTo()接口或者指定构造参数Comparator来对元素进行排序

  • DelayQueue:一个使用优先级队列实现的无界阻塞队列,支持延时获取元素。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。

    • 缓存系统的设计中
    • 定时任务调度
  • SynchronousQueue:一个不存储元素的阻塞队列,每一个put操作必须等待一个take操作,否则不能继续添加元素

  • LinkedTransferQueue:一个由链表结构组成的无界阻塞队列

  • LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列

3.3 Fork/Join框架

Jdk1.7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。
这里写图片描述

3.4 并发工具类

Jdk还提供了几个比较实用的并发工具类:CountDownLatch、CyclicBarrier和Semaphore。

3.4.1 CountDownLatch

CountDownLatch允许一个或多个线程等待其他线程完成操作。

示例代码:

public class CountDownLatchTest {

    // 计数器定义为3,表示需要三个线程执行c.countDown,否则调用c.wait的线程会一直阻塞
    static CountDownLatch c = new CountDownLatch(3);

    public static void main(String[] args) throws InterruptedException {

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(1);
                c.countDown();
                System.out.println(2);
                c.countDown();
            }
        }).start();
        c.await();
        System.out.println("3");
    }
}

3.4.2 CyclicBarrier

CyclicBarrier实现让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景

示例代码:

public class CyclicBarrierTest {

    // 定义屏障线程数为3,即需要达到三个线程进入阻塞,屏障才会打开
    static CyclicBarrier c = new CyclicBarrier(3);

    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    c.await();
                } catch (Exception e) {
                }
                System.out.println(1);
            }
        }).start();

        try {
            c.await();
        } catch (Exception e) {
        }
        System.out.println(2);
    }
}

3.4.3 Semaphore

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。

4.Atomic

Java从JDK 1.5开始提供了java.util.concurrent.atomic包(以下简称Atomic包),这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。具体如下:
这里写图片描述

  • 基本类型:AtomicBoolean、AtomicInteger、AtomicLong

  • 引用类型(带有标记位):AtomicReference、AtomicStampedRerence、AtomicMarkableReference,后面两个是为了解决ABA问题的

  • 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray,注意:Atomic数组类型会将当前数组
    复制一份,内部修改不会影响传入的数组

  • 更新指定对象的属性:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

  • Jdk1.8新增:DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder,在高并发相比于AtomicLonggen更高效,LongAdder更多地用于收集统计数据,而不是细粒度的同步控制

具体示例:


public class CounterThread extends Thread {

    // 使用原子类型
    private static AtomicLong counter = new AtomicLong(0);

    public static long addOne() {
        return counter.incrementAndGet();
    }

    @Override
    public void run() {
        try {
            Thread.sleep(100);
            if (addOne() == 100) {
                System.out.println("计数器值最终值为100");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class AtomicMain {

    public static void main(String[] argv) {

        AtomicInteger atomicInteger = new AtomicInteger();
        atomicInteger.set(1);
        atomicInteger.incrementAndGet();
        System.out.println(atomicInteger.get());
    }
}

5.Java中的线程池及Executor框架

Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。

  • 降低资源消耗
  • 提高响应速度
  • 提高线程的可管理性

5.1 ThreadPoolExecutor

5.1.1 线程池实现原理

这里写图片描述

 /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */

5.1.2 线程池实用

  • 1.线程池创建
/**
     * Creates a new {@code ThreadPoolExecutor} with the given initial
     * parameters.
     *
     * @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
     * @param maximumPoolSize the maximum number of threads to allow in the
     *        pool
     * @param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.
     * @param unit the time unit for the {@code keepAliveTime} argument
     * @param workQueue the queue to use for holding tasks before they are
     *        executed.  This queue will hold only the {@code Runnable}
     *        tasks submitted by the {@code execute} method.
     * @param threadFactory the factory to use when the executor
     *        creates a new thread
     * @param handler the handler to use when execution is blocked
     *        because the thread bounds and queue capacities are reached
     * @throws IllegalArgumentException if one of the following holds:<br>
     *         {@code corePoolSize < 0}<br>
     *         {@code keepAliveTime < 0}<br>
     *         {@code maximumPoolSize <= 0}<br>
     *         {@code maximumPoolSize < corePoolSize}
     * @throws NullPointerException if {@code workQueue}
     *         or {@code threadFactory} or {@code handler} is null
     */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  • corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。

  • maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没什么效果。

  • keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以,如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程的利用率。

  • TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)。

  • BlockingQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。

    • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
    • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
    • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    • PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
  • ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。

  • RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。在JDK 1.5中Java线程池框架提供了以下4种策略。

    • AbortPolicy:直接抛出异常。
    • CallerRunsPolicy:只用调用者所在线程来运行任务。
    • DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
    • DiscardPolicy:不处理,丢弃掉。
      当然,也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化存储不能处理的任务。
  • 2.任务提交

存在两个方法向线程池提交任务,分别为execute()和submit()方法。

  • execute():不需要返回值的任务
  • submit():需要返回值的任务

  • 3.关闭线程池

通过调用线程池的shutdown或shutdownNow方法来关闭线程池。

  • shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
  • shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

具体采用哪种方式,需要根据任务的特性。

  • 4.具体示例
public class NamedThreadFactory implements ThreadFactory {
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;
    private final boolean daemon;

    public NamedThreadFactory(String namePrefix) {
        this.namePrefix = namePrefix;
        this.daemon = false;
    }

    public NamedThreadFactory(String namePrefix, boolean isDaemon) {
        this.namePrefix = namePrefix;
        this.daemon = isDaemon;
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(r, this.namePrefix + "-" + this.threadNumber.getAndIncrement());
        t.setDaemon(this.daemon);
        if (t.getPriority() != 5) {
            t.setPriority(5);
        }

        return t;
    }
}

public class WorkThread implements Runnable {

    private String command;

    public WorkThread(String s){
        this.command=s;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+" Start. Command = "+command);
        processCommand();
        System.out.println(Thread.currentThread().getName()+" End.");
    }

    private void processCommand() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public String toString(){
        return this.command;
    }
}

public class ThreadPoolExecutorTest {

    private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10, 10,
            20L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(1),
            new NamedThreadFactory("Thread-Worker"),
            new ThreadPoolExecutor.CallerRunsPolicy());

    public static void main (String[] argv) {

        for (int i = 0; i < 10; i++) {
            Runnable worker = new WorkThread(String.valueOf(i));
            poolExecutor.execute(worker);
        }
        poolExecutor.shutdown();
        while (!poolExecutor.isTerminated()) {
            try {
                Thread.sleep(1000);
            } catch (Exception e) {

            }
        }
        System.out.println("All Threads have finished!!");

    }
}
  • 5.ThreadPoolExecutor延伸的三种类型

    • CachedThreadPool:创建一个可缓存的线程池,如果当前线程池的规模超出了处理需求,将回收空闲的线程;当需求增加时,会增加工作线程数量;线程池规模无限制;空闲线程被保留60秒。
    • FixedThreadPool:创建一个固定长度的线程池,当到达线程最大数量时,线程池的规模将不再变化。
    • SingleThreadExecutor:线程池中任意时刻只有一个线程,队列中的任务按照顺序执行。

5.2 ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor继承自ThreadPoolExecutor。它主要用来在给定的延迟之后运行任务,或者定期执行任务。

注意ScheduledThreadPoolExecutor使用的阻塞队列是DelayQueue-无界阻塞队列。

  • 1.线程池执行原理
    这里写图片描述

  • 4.具体示例

示例同5.2
public class ScheduledThreadPoolExecutorTest {

    public static void main(String[] argv) throws Exception {

        ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(5,
                new NamedThreadFactory("scheduler-pool"));

        for (int i = 0; i < 10; i++) {
            WorkThread workThread = new WorkThread(String.valueOf(i));

            threadPoolExecutor.scheduleAtFixedRate(workThread, 2, 3, TimeUnit.SECONDS);

        }

        Thread.sleep(30000);

        threadPoolExecutor.shutdown();
        while (!threadPoolExecutor.isTerminated()) {
            Thread.sleep(1000);
        }
        System.out.println("All Threads have finished!!");
    }
}

5.3 FutureTask

FutureTask一个可取消的异步计算,实现了Future的基本方法,提供启动start 和cancel操作。可以查询计算是否已经完成,获取计算的结果。结果只能在计算完成之后调用get()获取,否则会阻塞,直到计算完成。

FutureTask除了实现Future接口外,还实现了Runnable接口。因此,FutureTask可以交给Executor执行,或者调用线程直接执(FutureTask.run())

FutureTask的get方法和cancel方法
这里写图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值