【Java基础系列教程】第十二章 Java多线程(下)_线程组、ThreadLocal、线程池

一、使用Callable接口创建线程

         使用Callable接口创建线程是JDK5.0新增的;

        与使用Runnable相比,Callable功能更强大些:
                相比run()方法,可以有返回值;
                方法可以抛出异常;
                支持泛型的返回值;
                需要借助FutureTask类,比如获取返回结果;

        具体是创建Callable接口的实现类,并实现call()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。

        看着好像有点复杂,直接来看一个例子就清晰了。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadTest {

    public static void main(String[] args) {
        // 创建MyCallable对象
        Callable<Integer> myCallable = new MyCallable();

        // 使用FutureTask来包装MyCallable对象
        FutureTask<Integer> ft = new FutureTask<Integer>(myCallable);

        for (int i = 0; i < 1000; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 30) {
                // FutureTask对象作为Thread对象的target创建新的线程
                Thread thread = new Thread(ft);
                // 线程进入到就绪状态
                thread.start();
            }
        }

        System.out.println("主线程for循环执行完毕..");

        try {
            // 取得新创建的新线程中的call()方法返回的结果
            int sum = ft.get();
            System.out.println("sum = " + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

class MyCallable implements Callable<Integer> {

    // 与run()方法不同的是,call()方法具有返回值
    @Override
    public Integer call() {
        int sum = 0;
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            sum += i;
        }
        return sum;
    }

}

        首先,我们发现,在实现Callable接口中,此时不再是run()方法了,而是call()方法,此call()方法作为线程执行体,同时还具有返回值!在创建新的线程时,是通过FutureTask来包装MyCallable对象,同时作为了Thread对象的target。那么看下FutureTask类的定义:

public class FutureTask<V> implements RunnableFuture<V> {   
    //.... 
}

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

        于是,我们发现FutureTask类实际上是同时实现了Runnable和Future接口,由此才使得其具有Future和Runnable双重特性。通过Runnable特性,可以作为Thread对象的target,而Future特性,使得其可以取得新创建线程中的call()方法的返回值。

        Future接口:
                可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
                FutrueTask是Futrue接口的唯一的实现类。
                FutureTask同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

        执行下此程序,我们发现sum = 4950永远都是最后输出的。而“主线程for循环执行完毕..”则很可能是在子线程循环中间输出。由CPU的线程调度机制,我们知道,“主线程for循环执行完毕..”的输出时机是没有任何问题的,那么为什么sum =4950会永远最后输出呢?

        原因在于通过ft.get()方法获取子线程call()方法的返回值时,当子线程此方法还未执行完毕,ft.get()方法会一直阻塞,直到call()方法执行完毕才能取到返回值。

        目前主要讲解了三种常见的线程创建方式,对于线程的启动而言,都是调用线程对象的 start()方法,需要特别注意的是:不能对同一线程对象两次调用start()方法。

二、线程组

2.1 线程组概述

        可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式,如图所示:

2.2 ThreadGroup

        public class ThreadGroup 
                extends Object 
                implements Thread.UncaughtExceptionHandler

        Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。

2.2.1 主线程组

        默认情况下,所有的线程都属于主线程组。

        public final ThreadGroup getThreadGroup()    通过线程对象获取他所属于的组

        public final String getName()    通过线程组对象获取他组的名字

public class ThreadGroupTest {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        MyRunnable mr = new MyRunnable();
        Thread t1 = new Thread(mr, "张三");
        Thread t2 = new Thread(mr, "李四");

        // 获取线程组
        // 线程类里面的方法:public final ThreadGroup getThreadGroup()
        ThreadGroup tg1 = t1.getThreadGroup();
        ThreadGroup tg2 = t2.getThreadGroup();

        // 线程组里面的方法:public final String getName()
        String name1 = tg1.getName();
        String name2 = tg2.getName();
        System.out.println(name1);
        System.out.println(name2);

        // 通过结果我们知道了:线程默认情况下属于main线程组
        // 通过下面的测试,你应该能够看到,默任情况下,所有的线程都属于同一个组
        System.out.println(Thread.currentThread().getThreadGroup().getName());
    }

}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        // TODO Auto-generated method stub

    }
}

2.2.2 自定义线程组

        1、ThreadGroup(String name)    构造一个新线程组。
               ThreadGroup(ThreadGroup parent, String name)    创建一个新线程组。

        2、创建线程对象

        3、Thread(ThreadGroup group, Runnable target, String name) 
                分配新的 Thread 对象,以便将 target 作为其运行对象,将指定的 name 作为其名称,并作为 group 所引用的线程组的一员。

        4、设置整组的优先级或者守护线程

public class ThreadGroupTest {

	public static void main(String[] args) {
		// ThreadGroup(String name)
		ThreadGroup tg = new ThreadGroup("这是一个新的组");

		MyRunnable mr = new MyRunnable();
		// Thread(ThreadGroup group, Runnable target, String name)
		Thread t1 = new Thread(tg, mr, "张三");
		Thread t2 = new Thread(tg, mr, "李四");

		System.out.println(t1.getThreadGroup().getName());
		System.out.println(t2.getThreadGroup().getName());

		// 通过组名称设置守护线程,表示该组的线程都是守护线程 (也叫后台线程)
		tg.setDaemon(true);
	}

}

class MyRunnable implements Runnable {
	@Override
	public void run() {
		// TODO Auto-generated method stub

	}
}

        线程组More:https://blog.csdn.net/qq_23585245/article/details/80990281

三、ThreadLocal

3.1 ThreadLocal概述

        其实 ThreadLocal 并非是一个线程的本地实现版本,它并不是一个 Thread,而是 threadlocalvariable (线程局部变量)。也许把它命名为ThreadLocalVar更加合适。

        线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。
 
        从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
 
        通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。
 
        ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。
 
        概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

3.2 API说明

        public class ThreadLocal<T> extends Object

        该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。 

3.2.1 构造函数

        ThreadLocal()    创建一个线程本地变量。

3.2.2 成员方法

        T get()    返回此线程局部变量的当前线程副本中的值,如果这是线程第一次调用该方法,则创建并初始化此副本。
    
        protected  T initialValue()    返回此线程局部变量的当前线程的初始值。最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用 get() 方法访问变量的时候。如果线程先于 get 方法调用 set(T) 方法,则不会在线程中再调用 initialValue 方法。
     若该实现只返回 null;如果程序员希望将线程局部变量初始化为 null 以外的某个值,则必须为 ThreadLocal 创建子类,并重写此方法。通常,将使用匿名内部类。initialValue 的典型实现将调用一个适当的构造方法,并返回新构造的对象。

        void remove()     移除此线程局部变量当前线程的值.这可能有助于减少线程局部变量的存储需求。如果再次访问此线程局部变量,那么在默认情况下它将拥有其 initialValue。

        void set(T value)     将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于 initialValue() 方法来设置线程局部变量的值。
    
        在程序中一般都重写initialValue方法,以给定一个特定的初始值。

3.3 典型实例

3.3.1 框架应用

        Hiberante的Session 工具类HibernateUtil

        这个类是Hibernate官方文档中HibernateUtil类,用于session管理。

        注:并不需要大家知道这个案例怎么去写,只需要知道这项技术是有用的;

public class HibernateUtil {
    private static Log log = LogFactory.getLog(HibernateUtil.class);
    private static final SessionFactory sessionFactory;     //定义SessionFactory

    static {
        try {
            // 通过默认配置文件hibernate.cfg.xml创建SessionFactory
            sessionFactory = new Configuration().configure().buildSessionFactory();
        } catch (Throwable ex) {
            log.error("初始化SessionFactory失败!", ex);
            throw new ExceptionInInitializerError(ex);
        }
    }

    // 创建线程局部变量session,用来保存Hibernate的Session
    // ThreadLocal可以隔离多个线程的数据共享,因此不需要对线程进行同步
    public static final ThreadLocal session = new ThreadLocal();

    /**
     * 获取当前线程中的Session
     * @return Session
     * @throws HibernateException
     */
    public static Session currentSession() throws HibernateException {
        Session s = (Session) session.get();
        // 如果Session还没有打开,则新开一个Session
        if (s == null) {
            s = sessionFactory.openSession();
            session.set(s);         //将新开的Session保存到线程局部变量中
        }
        return s;
    }

    public static void closeSession() throws HibernateException {
        //获取线程局部变量,并强制转换为Session类型
        Session s = (Session) session.get();
        session.set(null);
        if (s != null)
            s.close();
    }
}

        在这个类中,由于没有重写ThreadLocal的initialValue()方法,则首次创建线程局部变量session其初始值为null,第一次调用currentSession()的时候,线程局部变量的get()方法也为null。因此,对session做了判断,如果为null,则新开一个Session,并保存到线程局部变量session中,这一步非常的关键,这也是“public static final ThreadLocal session = new ThreadLocal()”所创建对象session能强制转换为Hibernate Session对象的原因。

3.3.2 应用案例

        创建一个Bean,通过不同的线程对象设置Bean属性,保证各个线程Bean对象的独立性。

public class ThreadLocalTest {
    public static void main(String[] args) {
        StudentThread studentThread = new StudentThread();

        // 创建线程对象
        Thread thread1 = new Thread(studentThread, "线程A");
        Thread thread2 = new Thread(studentThread, "线程B");

        // 启动线程
        thread1.start();
        thread2.start();
    }
}

/*
 * 定义一个学生实体类
 * */
class StudentEntity {
    private int age;

    public StudentEntity() {
    }

    public StudentEntity(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

/*
 * 定义一个线程类
 *   这个线程类是用来创建学生的,并且要把创建的学生放到线程局部变量
 * */
class StudentThread implements Runnable {
    // 声明线程局部变量,核心部分:线程局部变量是静态的
    private static ThreadLocal<StudentEntity> threadLocal = new ThreadLocal();

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "正在运行....");
        // 随机一个0-100的整数数值
        int randomNum = (int) (Math.random() * 100);
        System.out.println("随机的数值是:" + randomNum);

        // 在线程体里面获取学生对象
        StudentEntity student = createStudent();
        // 给学生对象设置年龄(上面的随机值)
        student.setAge(randomNum);

        System.out.println(Thread.currentThread().getName() + "第一次读取年龄:" + student.getAge());

        // 延时操作
        try {
            Thread.sleep(1500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "第二次读取年龄:" + student.getAge());
    }

    /*
     * 创建学生的方法
     * */
    public StudentEntity createStudent() {
        // 此线程局部变量的当前线程副本中的值
        StudentEntity studentEntity = threadLocal.get();
        if (studentEntity == null) {
            // 如果没有学生对象,那么就初始化一个学生对象
            studentEntity = new StudentEntity();
            // 将其放到线程局部变量里面
            threadLocal.set(studentEntity);
        }
        return studentEntity;
    }
}

运行结果:

线程A正在运行....
线程B正在运行....
随机的数值是:35
随机的数值是:88
线程A第一次读取年龄:35
线程B第一次读取年龄:88
线程B第二次读取年龄:88
线程A第二次读取年龄:35

        可以看到a、b两个线程age在不同时刻打印的值是完全相同的。这个程序通过妙用ThreadLocal,既实现多线程并发,由兼顾数据的安全性。

3.4 ThreadLocal使用步骤

        1、在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx(private static final),用来保存线程间需要隔离处理的对象xxx。

        2、在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。

        3、在ThreadDemo类的run()方法中,通过getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。

3.5 总结

        ThreadLocal使用场合主要解决多线程中数据因并发产生不一致问题。ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,但大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。
 
        ThreadLocal不能使用原子类型(基本类型),只能使用Object类型。ThreadLocal的使用比synchronized要简单得多。
 
        ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
 
        Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
 
        当然ThreadLocal并不能替代synchronized,它们处理不同的问题域。Synchronized用于实现同步机制,比ThreadLocal更加复杂。

        源码解析: https://my.oschina.net/clopopo/blog/149368

四、线程池(重点)

4.1 线程池概述

4.1.1 概念理解

        在前面的章节中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间,而创建并销毁线程的过程势必会消耗内存。而在Java中,内存资源是及其宝贵的。

        那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

        在Java中可以通过线程池来达到这样的效果。看名字就知道是装有线程的池子,我们可以提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。

        Java中开辟出了一种管理线程的概念,这个概念叫做线程池,从概念以及应用场景中,我们可以看出,线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗。

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

4.1.2 线程池的好处

        我们知道不用线程池的话,每个线程都要通过new Thread(xxRunnable).start()的方式来创建并运行一个线程,线程少的话这不会是问题,而真实环境可能会开启多个线程让系统和程序达到最佳效率,当线程数达到一定数量就会耗尽系统的CPU和内存资源,也会造成GC频繁收集和停顿,因为每次创建和销毁一个线程都是要消耗系统资源的,如果为每个任务都创建线程这无疑是一个很大的性能瓶颈。所以,线程池中的线程复用极大节省了系统资源,当线程一段时间不再有任务处理时它也会自动销毁,而不会长驻内存。

        第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

        第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

        第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

4.1.3 什么时候使用线程池

        1:单个任务处理时间比较短。

        2:需要处理的任务数量很大。

4.2 线程池原理

        那么,我们应该如何创建一个线程池呢?Java中已经提供了创建线程池的一个接口:Executor;而我们创建时,一般使用它的子类:ThreadPoolExecutor。

        这里使用JDK1.8中的ThreadPoolExecutor分析线程池的原理;

        public interface Executor
                Executor执行已提交的 Runnable 任务的对象。

        public interface ExecutorService extends Executor
                ExecutorService提供了管理终止的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。 

        public abstract class AbstractExecutorService extends Object implements ExecutorService
                提供 ExecutorService 执行方法的默认实现。

        public class ThreadPoolExecutor extends AbstractExecutorService
                一个 ExecutorService,它使用可能的几个池线程之一执行每个提交的任务,通常使用 Executors 工厂方法配置。 

        线程池可以解决两个不同问题:由于减少了每个任务调用的开销,它们通常可以在执行大量异步任务时提供增强的性能,并且还可以提供绑定和管理资源(包括执行任务集时使用的线程)的方法。每个 ThreadPoolExecutor 还维护着一些基本的统计数据,如完成的任务数。 

4.2.1 Executor

        Executor:执行提交的线程任务的对象。这个接口提供了一种将任务提交与每个任务将如何运行实现了分离,包括线程使用、调度等细节。该接口只定义了一个execute()方法。

        execute():将任务提交给线程池,由线程池为该任务创建线程并启动。注意这个方法没有返回值,获取不到线程执行结果

        void execute(Runnable command)     在未来某个时间执行给定的命令。 

4.2.2 ExecutorService

        提供用于管理终止的方法如 shutDown()和shutDownNow()用于关闭线程池的方法以及判断线程池是否关闭的方法如,isShutdown(),isTerminated()的方法

        提供了可以生成用于跟踪一个或多个异步任务进度的方法如 invokeAll(),submit()。这些方法的返回值都是Future类型,可以获取线程的执行结果。

        ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
                void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
                <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行Callable
                void shutdown() :关闭连接池

4.2.3 AbstractExecutorService

        提供 ExecutorService 执行方法的默认实现。 没太多要介绍的

4.2.4 ThreadPoolExecutor

        线程池可以解决两个不同问题:由于减少了每个任务调用的开销,它们通常可以在执行大量异步任务时提供增强的性能,并且还可以提供绑定和管理资源(包括执行任务集时使用的线程)的方法。每个 ThreadPoolExecutor 还维护着一些基本的统计数据,如完成的任务数。 

        核心构造函数:
                public ThreadPoolExecutor(int corePoolSize,  
                              int maximumPoolSize,  
                              long keepAliveTime,  
                              TimeUnit unit,  
                              BlockingQueue<Runnable> workQueue,  
                              ThreadFactory threadFactory,  
                              RejectedExecutionHandler handler)
                                  
        这是其中最重要的一个构造方法,这个方法决定了创建出来的线程池的各种属性,下面依靠一张图来更好的理解线程池和这几个参数:

        由图中,我们可以看出:
                线程池中的corePoolSize就是线程池中的核心线程数量,这几个核心线程,即使在没有用的时候,也不会被回收。
                maximumPoolSize就是线程池中可以容纳的最大线程的数量。
                keepAliveTime,就是线程池中除了核心线程之外的其他的最长可以保留的时间,因为在线程池中,除了核心线程即使在无任务的情况下也不能被清除,其余的都是有存活时间的,意思就是非核心线程可以保留的最长的空闲时间。
                unit,就是计算这个时间的一个单位。
                workQueue,就是等待队列,任务可以储存在任务队列中等待被执行,执行的是FIFIO原则(先进先出)。
                threadFactory,就是创建线程的线程工厂,最后一个handler,是一种拒绝策略,我们可以在任务满了时候,拒绝执行某些任务。

成员变量

    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    private static final int COUNT_BITS = Integer.SIZE - 3;
    private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

    // runState is stored in the high-order bits
    private static final int RUNNING    = -1 << COUNT_BITS;
    private static final int SHUTDOWN   =  0 << COUNT_BITS;
    private static final int STOP       =  1 << COUNT_BITS;
    private static final int TIDYING    =  2 << COUNT_BITS;
    private static final int TERMINATED =  3 << COUNT_BITS;

        ctl是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段,ctl是一个Integer, 它包含两部分的信息: 高三位表示线程池的运行状态(runState)和低29位表示线程池内有效线程的数量(workerCount);

线程池的生命周期
        RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务;线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!

        SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。

        STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态;

        TIDYING:如果所有的任务都已终止了,workerCount (有效线程数) 为0,当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。 

        TERMINATED:在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做。线程池彻底终止,就变成TERMINATED状态

        进入TERMINATED的条件如下:
                线程池不是RUNNING状态;
                线程池状态不是TIDYING状态或TERMINATED状态;
                如果线程池状态是SHUTDOWN并且workerQueue为空;
                workerCount为0;
                设置TIDYING状态成功。

线程池的生命周期流程图

 还有三个关于ctl的方法:

// Packing and unpacking ctl
private static int runStateOf(int c)     { return c & ~CAPACITY; }
private static int workerCountOf(int c)  { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }

        runStateOf:获取运行状态;

        workerCountOf:获取活动线程数;

        ctlOf:获取运行状态和活动线程数的值;

ThreadPoolExecutor构造函数

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
        null :
    AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

下面解释构造函数的参数含义:

    corePoolSize:核心线程数量,当有新任务在execute()方法提交时,会执行以下判断:
        a):如果运行的线程小于 corePoolSize,则创建新线程来处理任务,即使线程池中的其他线程是空闲的;
        b):如果线程池中的线程数量大于等于 corePoolSize 且小于 maximumPoolSize,当workQueue未满的时候任务添加到workQueue中,当workQueue满时才创建新的线程去处理任务;
        c):如果设置的corePoolSize 和 maximumPoolSize相同,则创建的线程池的大小是固定的,这时如果有新任务提交,若workQueue未满,则将请求放入workQueue中,等待有空闲的线程去从workQueue中取任务并处理;
        d):如果运行的线程数量大于等于maximumPoolSize,这时如果workQueue已经满了,则通过handler所指定的策略来处理任务;
        所以,任务提交时,判断的顺序为 corePoolSize –> workQueue –> maximumPoolSize。
        corePoolSize:线程池的核心大小,也可以理解为最小的线程池大小。

    maximumPoolSize:最大线程数量;

    workQueue:等待队列,当任务提交时,如果线程池中的线程数量大于等于corePoolSize的时候,把该任务封装成一个Worker对象放入等待队列;保存等待执行的任务的阻塞队列,当提交一个新的任务到线程池以后, 线程池会根据当前线程池中正在运行着的线程的数量来决定对该任务的处理方式,主要有以下几种处理方式:
        直接切换:这种方式常用的队列是SynchronousQueue。
        使用无界队列:一般使用基于链表的阻塞队列LinkedBlockingQueue。如果使用这种方式,那么线程池中能够创建的最大线程数就是corePoolSize,而maximumPoolSize就不会起作用了。当线程池中所有的核心线程都是RUNNING状态时,这时一个新的任务提交就会放入等待队列中。
        使用有界队列:一般使用ArrayBlockingQueue。使用该方式可以将线程池的最大线程数量限制为maximumPoolSize,这样能够降低资源的消耗,但同时这种方式也使得线程池对线程的调度变得更困难,因为线程池和队列的容量都是有限的值,所以要想使线程池处理任务的吞吐率达到一个相对合理的范围,又想使线程调度相对简单,并且还要尽可能的降低线程池对资源的消耗,就需要合理的设置这两个数量。
        如果要想降低系统资源的消耗(包括CPU的使用率,操作系统资源的消耗,上下文环境切换的开销等), 可以设置较大的队列容量和较小的线程池容量, 但这样也会降低线程处理任务的吞吐量。
        如果提交的任务经常发生阻塞,那么可以考虑通过调用 setMaximumPoolSize() 方法来重新设定线程池的容量。
        如果队列的容量设置的较小,通常需要将线程池的容量设置大一点,这样CPU的使用率会相对的高一些。但如果线程池的容量设置的过大,则在提交的任务数量太多的情况下,并发量会增加,那么线程之间的调度就是一个要考虑的问题,因为这样反而有可能降低处理任务的吞吐量。

    unit:销毁时间单位

    keepAliveTime:线程池维护线程所允许的空闲时间。当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime;

    threadFactory:它是ThreadFactory类型的变量,用来创建新线程。默认使用Executors.defaultThreadFactory() 来创建线程。使用默认的ThreadFactory来创建线程时,会使新创建的线程具有相同的NORM_PRIORITY优先级并且是非守护线程,同时也设置了线程的名称。

    handler:它是RejectedExecutionHandler类型的变量,表示线程池的饱和策略。如果阻塞队列满了并且没有空闲的线程,这时如果继续提交任务,就需要采取一种策略处理该任务。线程池提供了4种策略:
        AbortPolicy:直接抛出异常,这是默认策略;
        CallerRunsPolicy:用调用者所在的线程来执行任务;
        DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
        DiscardPolicy:直接丢弃任务;

4.3 线程池完整结构解析

        线程池中的核心线程和非核心线程,没有什么区别,都是线程,只不过人为的规则线程池中的一部分线程叫核心线程

4.4 线程池的流程

        ThreadPoolExecutor执行execute方法分下面4种情况。

        1、如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。

        2、如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。

        3、如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(注意,执行这一步骤需要获取全局锁)。

        4、如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用

        RejectedExecutionHandler.rejectedExecution()方法。

        ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行步骤2,而步骤2不需要获取全局锁。

4.6 线程池分类

        Executors是jdk里面提供的创建线程池的工厂类,它默认提供了4种常用的线程池应用,而不必我们去重复构造。

4.6.1 newFixedThreadPool

        newFixedThreadPool: 固定线程池,核心线程数和最大线程数固定相等,而空闲存活时间为0毫秒,说明此参数也无意义,工作队列为最大为Integer.MAX_VALUE大小的阻塞队列。

        当执行任务时,如果线程都很忙,就会丢到工作队列等有空闲线程时再执行,队列满就执行默认的拒绝策略。

/**
  * Creates a thread pool that reuses a fixed number of threads
  * operating off a shared unbounded queue, using the provided
  * ThreadFactory to create new threads when needed. 
  */
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}

4.6.2 newCachedThreadPool

        带缓冲线程池,从构造看核心线程数为0,最大线程数为Integer最大值大小,超过0个的空闲线程在60秒后销毁,SynchronousQueue这是一个直接提交的队列,意味着每个新任务都会有线程来执行,如果线程池有可用线程则执行任务,没有的话就创建一个来执行,线程池中的线程数不确定,一般建议执行速度较快较小的线程,不然这个最大线程池边界过大容易造成内存溢出。

/**
  * Creates a thread pool that creates new threads as needed, but
  * will reuse previously constructed threads when they are
  * available.  These pools will typically improve the performance
  * of programs that execute many short-lived asynchronous tasks.
  */
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

4.6.3 newSingleThreadExecutor

        创建一个单线程化的Executor,核心线程数和最大线程数均为1,空闲线程存活0毫秒同样无意思,意味着每次只执行一个线程,多余的先存储到工作队列,一个一个执行,保证了线程的顺序执行。
        如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

/**
 * Creates an Executor that uses a single worker thread operating
 * off an unbounded queue. (Note however that if this single
 * thread terminates due to a failure during execution prior to
 * shutdown, a new one will take its place if needed to execute
 * subsequent tasks.)  Tasks are guaranteed to execute
 * sequentially, and no more than one task will be active at any
 * given time. Unlike the otherwise equivalent
 */
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

4.6.4 newScheduledThreadPool

        调度线程池,即按一定的周期执行任务,即定时任务,对ThreadPoolExecutor进行了包装而已。

/**
 * Creates a thread pool that can schedule commands to run after a
 * given delay, or to execute periodically.
 */
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

4.6.5 newSingleThreadScheduledExecutor

        创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。线程池中最多执行1个线程,之后提交的线程活动将会排在队列中以此执行并且可定时或者延迟执行线程活动。

4.7 线程池案例

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolTest {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            executor.execute(new Runnable() { 
                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    System.out.println("thread id is: " + Thread.currentThread().getId());
                    try {
                        Thread.sleep(1000L);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}
class NumberThread implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread1 implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

public class ThreadPool {

    public static void main(String[] args) {
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        //设置线程池的属性
        // System.out.println(service.getClass());
        // service1.setCorePoolSize(15);
        // service1.setKeepAliveTime();


        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        service.execute(new NumberThread1());//适合适用于Runnable

        // service.submit(Callable callable);//适合使用于Callable
        //3.关闭连接池
        service.shutdown();
    }

}

submit和execute分别有什么区别呢?
        execute没有返回值,如果不需要知道线程的结果就使用execute方法,性能会好很多。
        submit返回一个Future对象,如果想知道线程结果就使用submit提交,而且它能在主线程中通过Future的get方法捕获线程中的异常。

4.8 总结

        线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到多个任务上,而且由于在请求到达时线程已经存在,所以消除线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足。

        最后说一句,线程池可以算是面试必考题!!!

五、面试题

1、Runnable 和 Callable创建线程对比(***)?
    无论是Runnable 还是 Callable都是用来创建线程的;
    与使用Runnable相比, Callable功能更强大些:
        相比run()方法,可以有返回值;
        方法可以抛出异常;
        支持泛型的返回值;

2、什么是线程组?
    可以把线程归属到某一个线程组中,线程组中可以有线程对象,也可以有线程组,组中还可以有线程,这样的组织结构有点类似于树的形式。
    Java中使用ThreadGroup来表示线程组,它可以对一批线程进行分类管理,Java允许程序直接对线程组进行控制。

3、什么ThreadLocal?
    其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个 Thread,而是线程局部变量。
    线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。
    通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。
    对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

4、同步机制和ThreadLocal的区别(***)?
    Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
    对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

5、什么是线程池?
    装有线程的池子,通过维护一定数量的线程池来达到多个线程的复用。

6、什么时候使用线程池(***)?
    几乎所有需要并发执行任务的程序都可以使用线程池。
    1:单个任务处理时间比较短
    2:需要处理的任务数量很大

7、为什么要使用线程池和线程池的作用?
    如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间,而创建并销毁线程的过程势必会消耗内存。
    作用:
        第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
        第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
        第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

8、线程池生命周期及五种状态(***)?
    RUNNING: 运行状态,能接受新提交的任务,并且也能处理阻塞队列中的任务;线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!
    SHUTDOWN: 关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。 在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。
    STOP: 停止状态,不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态;
    TIDYING: 整理状态,如果所有的任务都已终止了,workerCount (有效线程数) 为0,当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
    TERMINATED: 终止状态,在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做。线程池彻底终止,就变成TERMINATED状态

9、线程池的原理(***)?
    1、我们通过execute()或submit()把任务提交给线程池;
    2、如果线程池中的线程数量小于corePoolSize(核心线程数量),则创建新线程来处理任务,即使线程池中的其他线程是空闲的;我们可以简单的认为corePoolSize就是线程池里面的最少线程数,而且核心线程不会销毁;
    3、如果线程池中的线程数量大于等于corePoolSize且小于maximumPoolSize(最大线程数),当workQueue未满的时候任务添加到workQueue(队列)中,当workQueue满时才创建新的线程去处理任务;
    4、如果线程池中的线程数量大于等于maximumPoolSize,这时如果workQueue已经满了,则通过RejectedExecutionHandler(拒绝策略)所指定的策略来处理任务;
    5、当线程池的线程执行完任务之后,不会被立马销毁,而是又放到线程池等待重复使用;
    6、当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,非核心线程等待的时间超过了keepAliveTime(结合TimeUnit来使用),那么就会被销毁;
    7、当线程不够的时候,通过Executors.defaultThreadFactory()来创建线程;
    8、如果阻塞队列满了并且没有空闲的线程,这时如果继续提交任务,就需要采取一种饱和(拒绝)策略处理该任务。
        AbortPolicy: 直接抛出异常,这是默认策略;
        CallerRunsPolicy: 用调用者所在的线程来执行任务;
        DiscardOldestPolicy: 丢弃阻塞队列中靠最前的任务,并执行当前任务;
        DiscardPolicy: 直接丢弃任务;

10、线程池的分类(***)?
    newFixedThreadPool: 固定线程池,核心线程数和最大线程数固定相等,而空闲存活时间为0毫秒,说明此参数也无意义,工作队列为最大为Integer.MAX_VALUE大小的阻塞队列。
    newCachedThreadPool: 带缓冲线程池,从构造看核心线程数为0,最大线程数为Integer最大值大小,超过0个的空闲线程在60秒后销毁,SynchronousQueue这是一个直接提交的队列,意味着每个新任务都会有线程来执行,如果线程池有可用线程则执行任务,没有的话就创建一个来执行,线程池中的线程数不确定,一般建议执行速度较快较小的线程,不然这个最大线程池边界过大容易造成内存溢出。
    newSingleThreadExecutor: 创建一个单线程化的Executor,核心线程数和最大线程数均为1,每次只执行一个线程,多余的先存储到工作队列,一个一个执行,保证了线程的顺序执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。
    newScheduledThreadPool: 调度线程池,即按一定的周期执行任务,即定时任务,对ThreadPoolExecutor进行了包装而已。
    newSingleThreadScheduledExecutor: 创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。

11、线程池都有哪几种工作队列(*)?
    ArrayBlockingQueue: 是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
    LinkedBlockingQueue: 一个基于链表结构的阻塞队列,此队列按FIFO(先进先出)排序元素,吞吐量通常要高于ArrayBlockingQueue。
    SynchronousQueue: 一个不存储元素的阻塞队列。
    PriorityBlockingQueue: 一个具有优先级的无限阻塞队列。

12、submit和execute分别有什么区别呢?
    execute没有返回值,如果不需要知道线程的结果就使用execute方法,性能会好很多。
    submit返回一个Future对象,如果想知道线程结果就使用submit提交,而且它能在主线程中通过Future的get方法捕获线程中的异常。

    其他常见面试题:https://www.jianshu.com/p/6c6f396fc88e

    如何合理配置线程池的大小:https://www.zhihu.com/question/38128980

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是波哩个波

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值