JAVA基础-线程、Thread和线程池

100 阅读12分钟

线程状态

image.png

新建(New)

创建后尚未启动。

可运行(Runnable)

Java虚拟机中处于该状态的线程,对应操作系统状态中的Running和Ready,表示该线程正在执行或者等待操作系统分配执行时间

阻塞(Blocking)

当多个线程进行资源争抢,等待获取一个排他锁的线程会处于该状态,当线程获取到锁之后状态会变为RUNNABLE状态,其他等待锁的线程继续处于BLOCKED状态,一般被synchronized关键字修饰或者ReentrantLock包裹的代码快会触发不可到到达线程处于该状态。

无限期等待(Waiting)

处于该状态的线程不会被分配执行时间,需要等待被其他线程显式唤醒,调用Thread中以下方法会触发线程处于该状态:

  • Object.wait() with no timeout
  • Thread.join() with no timeout
  • LockSupport.park() |

限期等待(Timed Waiting)

计时等待状态的线程,不会被操作系统分配执行时间,也不需要被其他线程显式唤醒,在计时结束后由操作系统自动唤醒,Thread中的以下方法会触发线程进入此状态:

  • Thread.sleep()
  • Object.wait() with timeout
  • Thread.join() with timeout
  • LockSupport.parkNanos()
  • LockSupport.parkUntil()

死亡(Terminated)

可以是线程结束任务之后自己结束,或者产生了异常而结束。

Thread

构造方法:

  1. Thread() :创建一个默认设置的线程对象实例。
  2. Thread(Runnable target) :创建一个包含可执行对象的线程实例。
  3. Thread(Runnable target, String name) :创建一个包含可执行对象,指定名称的线程对象。
  4. Thread(String name):创建一个指定名称的线程对象。
  5. Thread(ThreadGroup group, Runnable target) :创建一个指定线程组,包含可执行对象的线程对象实例。
  6. Thread(ThreadGroup group, Runnable target, String name) :创建一个指定线程组,包含可执行对象,指定线程名称的线程对象实例。
  7. Thread(ThreadGroup group, Runnable target, String name, long stackSize) :创建一个指定线程组、包含可执行对象、指定名称以及堆栈大小的线程对象实例。
  8. Thread(ThreadGroup group, String name):创建一个指定线程组,线程名称的线程实例。

关于线程组(ThreadGroup类),一个线程组代表一组线程。此外,一个线程组还可以包括其他线程组。线程组形成一棵树,其中除了初始线程组之外的每个线程组都有一个父级。允许线程访问有关其自己的线程组的信息,但不能访问有关其线程组的父线程组或任何其他线程组的信息。

优先级

JAVA中创建的线程,每个线程都有一个优先级,具有较高优先级的线程优先于具有较低优先级的线程执行。当在某个线程中运行的代码创建一个新的Thread对象时,新线程的优先级最初设置为等于创建线程的优先级。
使用setPriority()设置优先级。优先级的范围1~10,值越大优先级越高。注意:设置的优先级并不能保证高优先级的线程先执行。Thread类中有三种优先级:

//最大优先级
public static final int MAX_PRIORITY = 10;
//默认优先级
public static final int NORM_PRIORITY = 5;
//最小优先级
public static final int MIN_PRIORITY = 1;

创建线程

Java中创建一个线程Thread。可以继承Thread类、实现Runnable或者Callable和Future创建线程。

继承Thread

public class MyThread extends Thread{

    @Override
    public void run() {
       System.out.println(Thread.currentThread().getName());
    }
    
    public static  void main(String[] args){
        MyThread myThread=new MyThread();
        myThread.start();
    }
}

实现Runnable

public class MyThread implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        Thread thread = new Thread();
        thread.start();
    }
}

Callable和Future

public class MyThread implements Callable<String> {
    public static void main(String[] args) {
        MyThread callable = new MyThread();
        FutureTask<String> futureTask = new FutureTask<>(callable);
        new Thread(futureTask,"thread callable").start();
    }

    @Override
    public String call() throws Exception {
        return Thread.currentThread().getName();
    }
}

Thread中的run()和start()的区别

start方法用来启动相应的线程;run方法只是thread的一个普通方法。
线程的run()方法是由java虚拟机直接调用的,如果我们没有启动线程(没有调用线程的start()方法)而是在应用代码中直接调用run()方法,那么这个线程的run()方法其实运行在当前线程(即run()方法的调用方所在的线程)之中。

sleep()和wait()的区别

sleep为线程的方法,而wait为Object的方法,他们的功能相似,最大本质的区别是:sleep不释放锁,wait释放锁。

用法上的不同:sleep(milliseconds)可以用时间指定来使他自动醒过来,如果时间不到你只能调用interreput()来终止线程;wait()可以用notify()/notifyAll()直接唤起。

Thread常用方法

image.png

ThreadLocal

ThreadLocal是通过线程隔离的方式防止任务在共享资源上产生冲突, 线程本地存储是一种自动化机制,可以为使用相同变量的每个不同线程都创建不同的存储。

ThreadLocal简单用法

1.通过重写ThreadLocal中的initialValue方法,为每个线程产生线程本地数据。

public class ThreadLocalDemo {
    static Random random = new Random();
    static ThreadLocal<Integer> stringThreadLocal = new ThreadLocal<Integer>() {
        @Nullable
        @Override
        protected Integer initialValue() {
            return random.nextInt();
        }
    };


    public static void main(String[] args) {
        new Thread(() -> {
            //使用ThreadLocal  get()方法返回的值为使用ThreadLocal中initialValue方法的返回值。
            //同一个线程多次调用返回的值一样。
            //不同线程调用会产生不同的值,每个线程都用相对应的线程本地数据
            Integer value = stringThreadLocal.get();
            System.out.print(value);
        }).start();
    }
}

2.通过ThreadLocal set方法为每个线程设置线程本地数据。

public class ThreadLocalDemo {

    static ThreadLocal<Integer> stringThreadLocal = new ThreadLocal<Integer>();
    
    public static void main(String[] args) {
        stringThreadLocal.set(1);
        new Thread(() -> {
            System.out.print(stringThreadLocal.get());//输出值为null
            stringThreadLocal.set(2);
            System.out.print(stringThreadLocal.get());//输出值为2
        }).start();
        System.out.print(stringThreadLocal.get());//输出值为1
    }
}

ThreadLocal原理

ThreadLocal主要是用到了Thread对象中的一个ThreadLocalMap类型的变量threadLocals来实现线程隔离。
具体源码:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
  1. 首先获取当前线程对象t, 然后从线程t中获取到ThreadLocalMap的成员属性threadLocals
  2. 如果当前线程的threadLocals已经初始化(即不为null) 并且存在以当前ThreadLocal对象为Key的值, 则直接返回当前线程要获取的对象(本例中为Connection);
  3. 如果当前线程的threadLocals已经初始化(即不为null)但是不存在以当前ThreadLocal对象为Key的的对象, 那么重新创建一个Connection对象, 并且添加到当前线程的threadLocals Map中,并返回
  4. 如果当前线程的threadLocals属性还没有被初始化, 则重新创建一个ThreadLocalMap对象, 并且创建一个Connection对象并添加到ThreadLocalMap对象中并返回。

如果存在则直接返回,若不存在使用setInitialValue方法,并调用ThreadLocal中的initialValue方法创建value。

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

同时, ThreadLocal还提供了直接操作Thread对象中的threadLocals的方法。 这样我们也可以不实现initialValue, 也能对ThreadLocal设置value。

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

线程池(Executor)

Executors创建线程池方法:

方法名说明
newSingleThreadExecutor()创建只有一个线程的线程池
newFixedThreadPool(int nThreads)创建固定大小的线程池
newCachedThreadPool()创建一个不限制线程数量的线程池,任何提交的任务都将立即执行,但是闲置线程会得到及时回收
newScheduledThreadPool创建一个可定期或者延时执行任务的线程池

SingleThreadPool和FixedThreadPool

这两个工厂方法所创建的线程池,任务队列的长度都为Integer.MAX_VALUE,可能会堆积大量的任务,从而导致OOM。

CachedThreadPool和ScheduledThreadPool

这两个工厂方法所创建的线程池允许创建的线程数量为Integer.MAX_VALUE,可能会导致创建大量的线程,从而导致OOM。

newSingleThreadExecutor

该方法用于创建一个“单线程化线程池”,也就是只有一个线程的线程池,所创建的线程池用唯一的工作线程来执行任务,使用此方法创建的线程池能保证所有任务按照指定顺序(如FIFO)执行。

该线程池有以下特点:

  • 单线程化的线程池中的任务是按照提交的顺序执行的。

  • 池中的唯一线程的存活时间是无限的。

  • 当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列中,并且其阻塞队列是无界的。

    适用的场景是:任务按照提交次序,一个任务一个任务地逐个执行的场景。

newFixedThreadPool

该方法用于创建一个“固定数量的线程池”,其唯一的参数用于设置池中线程的“固定数量”。

  • 该线程池有以下特定:
  • 如果线程池没有达到“固定数量”,每次提交一个任务线程池内就创建一个新线程,直到线程达到线程池固定的数量。
  • 线程池的大小一旦达到“固定数量”就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
  • 在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,新任务会进入阻塞队列中(无界的阻塞队列)。

适用的场景是:需要任务长时间的场景。注意:内部使用无界队列来存放排队任务,当大量任务超过线程池最大容量需要处理时,队列无限增大,使服务器资源迅速耗尽。

newCachedThreadPool

该方法用于创建一个“可缓存线程池”,如果线程池内的某些线程无事可干成为空闲线程,“可缓存线程池”可灵活回收这些空闲线程。

该线程池有以下特点:

  • 在接收新的异步任务target执行目标实例时,如果池内所有线程繁忙,此线程池就会添加新线程来处理任务。
  • 此线程池不会对线程池大小进行限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
  • 如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,就会回收空闲(60秒不执行任务)线程。

适用的场景是:需要快速处理突发性强、耗时较短的任务场景。注意线程池没有最大线程数量限制,如果大量的异步任务执行目标实例同时提交,可能会因创建线程过多而导致资源耗尽。

newScheduledThreadPool

该方法用于创建一个“可调度线程池”,即一个提供“延时”和“周期性”任务调度功能的ScheduledExecutorService类型的线程池。

适用的场景是:周期性地执行任务的场景。

标准线程池创建(ThreadPoolExecutor)

Executors中创建线程池的快捷方法,实际上是调用了ThreadPoolExecutor的构造方法、定时任务使用的是ScheduledThreadPoolExecutor

ThreadPoolExecutor构造函数

public ThreadPoolExecutor(int corePoolSize,// 核心线程数,即使线程空闲,也不会回收
                          int maximumPoolSize,// 最大线程数
                          long keepAliveTime,// 线程最大空闲时长
                          TimeUnit unit,// 空闲时长单位
                          BlockingQueue<Runnable> workQueue,// 任务阻塞队列
                          ThreadFactory threadFactory,// 新线程的产生方式
                          RejectedExecutionHandler handler// 拒绝策略
                          ) 

参数详解:

  1. corePoolSize:核心线程数,也是线程池中常驻的线程数,线程池初始化时默认是没有线程的,当任务来临时才开始创建线程去执行任务
  2. maximumPoolSize:最大线程数,在核心线程数的基础上可能会额外增加一些非核心线程,需要注意的是只有当workQueue队列填满时才会创建多于corePoolSize的线程(线程池总线程数不超过maxPoolSize)
  3. keepAliveTime:非核心线程的空闲时间超过keepAliveTime就会被自动终止回收掉,注意当corePoolSize=maxPoolSize时,keepAliveTime参数也就不起作用了(因为不存在非核心线程);
  4. unit:keepAliveTime的时间单位
  5. workQueue:用于保存任务的队列,可以为无界、有界、同步移交三种队列类型之一,当池子里的工作线程数大于corePoolSize时,这时新进来的任务会被放到队列中。
    • SynchronousQueue(同步移交队列):队列不作为任务的缓冲方式,可以简单理解为队列长度为零
    • LinkedBlockingQueue(无界队列):队列长度不受限制,当请求越来越多时(任务处理速度跟不上任务提交速度造成请求堆积)可能导致内存占用过多或OOM
    • ArrayBlockintQueue(有界队列):队列长度受限,当队列满了就需要创建多余的线程来执行任务
  6. threadFactory:创建线程的工厂类,默认使用Executors.defaultThreadFactory(),也可以使用guava库的ThreadFactoryBuilder来创建
  7. handler:线程池无法继续接收任务(队列已满且线程数达到maximunPoolSize)时的饱和策略,取值有AbortPolicy、CallerRunsPolicy、DiscardOldestPolicy、DiscardPolicy。
    • AbortPolicy:中断抛出异常
    • DiscardPolicy:默默丢弃任务,不进行任何通知
    • DiscardOldestPolicy:丢弃掉在队列中存在时间最久的任务
    • CallerRunsPolicy:让提交任务的线程去执行任务(对比前三种比较友好一丢丢)

线程池线程创建流程图:

image.png

线程池的关闭

  • shutdownNow():立即关闭线程池(暴力),正在执行中的及队列中的任务会被中断,同时该方法会返回被中断的队列中的任务列表
  • shutdown():平滑关闭线程池,正在执行中的及队列中的任务能执行完成,后续进来的任务会被执行拒绝策略
  • isTerminated():当正在执行的任务及对列中的任务全部都执行(清空)完就会返回true

提交任务的三种方式

提交方式是否关心返回结果
Future submit(Callable task)
void execute(Runnable command)
Future<?> submit(Runnable task)否,虽然返回Future,但是其get()方法总是返回null

Future和FutureTask

Future接口用来表示执行异步任务的结果存储器,当一个任务的执行时间过长就可以采用这种方式:把任务提交给子线程去处理,主线程不用同步等待,当向线程池提交了一个Callable或Runnable任务时就会返回Future,用Future可以获取任务执行的返回结果。Future的主要方法包括:

  • get()方法:返回任务的执行结果,若任务还未执行完,则会一直阻塞直到完成为止,如果执行过程中发生异常,则抛出异常,但是主线程是感知不到并且不受影响的,除非调用get()方法进行获取结果则会抛出ExecutionException异常;
  • get(long timeout, TimeUnit unit):在指定时间内返回任务的执行结果,超时未返回会抛出TimeoutException,这个时候需要显式的取消任务;
  • cancel(boolean mayInterruptIfRunning):取消任务,boolean类型入参表示如果任务正在运行中是否强制中断;
  • isDone():判断任务是否执行完毕,执行完毕不代表任务一定成功执行,比如任务执行失但也执行完毕、任务被中断了也执行完毕都会返回true,它仅仅表示一种状态说后面任务不会再执行了;
  • isCancelled():判断任务是否被取消;

Future和FutureTask关系图

image.png

Future使用

public static void main(String[] args) {
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    Future<Object> submit = executorService.submit(new Callable<Object>() {
        @Override
        public Object call() throws Exception {
            return null;
        }
    });
    Object o = submit.get();//使用get(),程序将停止往下执行,一直等待结果,直到有返回值
}

FutureTask使用

public static void main(String[] args) {
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    FutureTask<Object> objectFutureTask = new FutureTask<Object>(new Callable<Object>() {
        @Override
        public Object call() throws Exception {
            return null;
        }
    });
    //使用execute执行
    executorService.execute(objectFutureTask);
    //ExecutorService的execute是没有返回值的,使用这种用法需要注意的是FutureTask的get方法会一直等待结果的返回,
    // 如果get的调用顺序在execute之前的话,那么程序将会停止在get这里。
    objectFutureTask.get();
    //通用也可以使用submit执行
    Future<?> submit = executorService.submit(objectFutureTask);
    submit.get();
}