Java线程(一)——线程的介绍与创建

11.9已更新:ThreadPoolExecutor,下一次会更新更多线程池
这一篇作为Java线程系列的开篇吧,简单的介绍一下一些普遍的线程和线程的创建方式。


线程简介

先放一张随处可见的线程状态转换图:
Thread status
所以啥是线程?
平时我们用的程序他运行起来了,他就叫进程(运行中的程序),是一个动态的概念,
而进程内部可能包含很多顺序执行任务,那这每一个任务就是一个线程。
进程和线程很类似,但又区别很大:

相同点:

  • 进程的存在是为了让操作系统同时运行多个程序;线程的存在是为了让进程同时执行多个任务。
  • 他们都可以并发执行

区别:

  • 对于每一个进程系统都会分配一定内存空间与资源,而这些分配的内存空与资源在不同进程之间是很难共享的,所以说进程是地址独立、资源独立的;而对于每一个线程,他们只能共享进程中的资源,尤其是CPU(线程只有获得CPU才能够执行,说到底CPU也是系统分配给进程的)
  • 一个程序至少要有一个进程,一个进程至少要有一个线程

为什么使用线程?

  • 线程之间能共享资源,共享内存十分容易,而进程不行!
  • 创建线程代价比创建进程要小得多,所以用多线程来实现多任务并发比多进程效率要高。

线程的创建

除了众所周知的直接继承于Thread和实现Runnable的方法以外,还有Java 5后提供的Future+Callable和线程池,在这里写一下他们的方式与区别。

Thread

直接通过继承于Thread的方式来创建:

  1. 需要一个继承于Thread的类,并重写其中的run()方法
  2. 新建该类的对象
  3. 通过该对象的start()启动线程
    public class TestThread extends Thread{
    private int i ;
    //线程体
    @Override
    public void run() {
        for( ; i <100 ; i++){
           System.out.println(getName()+" "+i);
        }
    }
    public static void main(String[] args) {
        new TestThread().start();
        new TestThread().start();
    }
    }
    
    此时输出的效果部分如下:

    Thread-1 42
    Thread-0 66
    Thread-0 67
    Thread-1 43
    Thread-1 44
    Thread-0 68
    Thread-1 45
    Thread-0 69

可以看见,两个线程都混杂在一起,谁也不能影响到谁的变量i,这是以继承的方式。

Runnable

Runnable的定义可以理解为该方法的反型封装,即它执行能做的事情。Runnable的创建线程的方式:

  1. 需要一个实现Runnable的类,并实现其中的run()方法
  2. 创建这个类的实例,作为参数创建一个新的Thread对象
  3. 通过该对象的start()启动线程
    public class TestRunnable implements Runnable{
    //共用的成语属性
    private int i;
    //实现Runnable的run方法,线程体
    public void run() {
        for (; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
    public static void main(String[] args) {
        TestRunnable tr = new TestRunnable();       //创建Runnable实例
        new Thread(tr,"Runnable-Thread1").start();  //传参构建Thread
        new Thread(tr,"Runnable-Thread2").start();
    }
    }
    
    部分输出效果:

    Runnable-Thread1 33
    Runnable-Thread2 34
    Runnable-Thread1 35
    Runnable-Thread2 36
    Runnable-Thread2 38
    Runnable-Thread2 39
    Runnable-Thread2 40
    Runnable-Thread2 41
    Runnable-Thread1 37

显然Thread1和Thread2明显共用了属性i,可见,当两个两个线程同时以同一个Runnable的实现类作为参数的时候,他们能共用那个Runnable的属性。(当然要是你创建两个Runnable实例效果会跟Thread一样,但是那样就没用Runnable的意义了)
但因为getName()是Thread类里的方法,所以必须用Thread的静态方法Thread.currentThread()先获得当前线程对象才能调用getName()。

Callable

Callable创建线程的方式跟Runnable很类似,虽然Thread的构造方法参数没有Callable类参数,但FutureTask实现了Runnable接口:

  1. 需要一个实现Callable的类(Callable的泛型就是返回值的类型),并实现其中的call()方法
  2. 创建这个类的实例,作为参数包装成一个新的FutureTask对象
  3. FutureTask作为参数创建一个新的Thread对象
  4. 通过该对象的start()启动线程
  5. 通过get()可以获得返回值(记得捕获异常)

    public class TestCallable implements Callable<Integer> {
    
    private int i;
    //线程体
    @Override
    public Integer call(){
        for(; i < 100 ; i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
        return i;
    }
    
    public static void main(String[] args) {
        TestCallable tc = new TestCallable();       //创建Callable实例
        FutureTask<Integer> task = new FutureTask<>(tc);        //包装成FutureTask
        new Thread(task,"Callable-Thread").start();             //当target传参进去
        try {
            System.out.println("返回值:"+task.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    }
    

    部分输出效果:

    Callable-Thread 96
    Callable-Thread 97
    Callable-Thread 98
    Callable-Thread 99
    返回值:100

虽然用Callable创建起来比Runnable要复杂,但从这里可以看出来了:利用Callable不仅可以抛出异常,而且可以通过get()方法获得线程执行返回的信息。
其次要注意的是,这里我没有创建两个线程,只有一个主线程和一个Callable的线程,是因为使用同一个FutureTask来创建线程会被认为是“不安全的”,在Callable的线程体call()执行一次之后会将status改成不再是NEW,而不是NEW的callable将直接退出,无法执行线程体:

public void run() {
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,null, Thread.currentThread()))
    return;

所以如果用像前面Runnable那样用一个target创建两个线程的话,第二个线程将会在执行线程体之前就挂掉。

线程池

线程池Java5在之前只能自己手动去创建,现在已经有了多种方法如Executors工厂,ForkJoinPool,在这里先写Executors工厂类吧,利用线程池的方式:

  1. 先调用Executors静态工厂方法创建一个ExecutorService或ScheduledExecutorService线程池对象(创建时可传入ThreadFactory来进行规定创建线程,下面CacheThreadPool会有例子)
  2. 创建Runnable/Callable实例,利用线程池对象的execute()和submit()或者schedule()来传入线程池并执行(callable必须用submit())
  3. 当任务完成后,执行shutdown()来关闭线程池

附上一张线程池状态图:
Thread Pool Status

其中newCacheThreadPool(), newFixedThreadPool(int nThreads), newSingleThreadExecutor()是ExecutorService对象,而newScheduledThreadPool(int corePoolSize), newSingleScheduledThreadPool()为ScheduledExecutorService对象,下面我会为前三个常用的做简单的例子。

###FixedThreadPool

    public class PoolTest {
        static class PoolRunnable implements Runnable{
            private int i;
            public void run() {
                for (; i < 100; i++) {
                    System.out.println(Thread.currentThread().getName() + " " + i);
                }
            }
        }

        public static void main(String[] args) {
            ExecutorService executorService = Executors.newFixedThreadPool(3);
            PoolRunnable p = new PoolRunnable();
            executorService.execute(p);
            executorService.submit(p);
            executorService.shutdown();
        }
    }

部分输出效果:

pool-1-thread-1 41
pool-1-thread-2 45
pool-1-thread-1 46
pool-1-thread-2 47
pool-1-thread-2 49
pool-1-thread-2 50
pool-1-thread-2 51
pool-1-thread-2 52
pool-1-thread-1 48
pool-1-thread-2 53
pool-1-thread-1 54

可见与用Runnable的方式效果相差无几,只是方式不一样了而已。
而三种Executor都有他们特别的地方,用Thinking in Java的原话来说的话就是这样:

CachedThreadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程的时候停止创建新线程,因此它是合理的Executor首选。只有在这种方式会引发问题的时候,才需要用到FixedThreadPool。
FixedThreadPool可以一次性预先执行代价高昂的线程分配,然后限制现成的数量不用每个任务都要付出创建线程的开销,直接从池中获取线程,且不会滥用可获得的资源。
SingleThreadExecutor就像是数量为1的FixedThreadPool,如果向它提交了多个任务,这些任务都将排队,所有任务将使用同一个进程。(但还是有区别,他提供了一种重要的并发保证,保证不会有两个线程被并发调用)

###SingleThreadExecutord

public class PoolTest2 {
    static class PoolRunnable2 implements Runnable{
        public void run() {
            int random = (int) Math.round(Math.random()*9+1);
            System.out.println("我是"+Thread.currentThread().getName()+", 我先睡"+random+"秒");
            try {
                Thread.sleep(random*1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"已经溜了");
        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();//换成newCachedThreadPool试试
        for(int i = 0 ; i < 5 ; i++) {
            PoolRunnable2 p = new PoolRunnable2();
            executorService.execute(p);
        }
        executorService.shutdown();
    }
}

效果如图:

我是pool-1-thread-1, 我先睡6秒
pool-1-thread-1已经溜了
我是pool-1-thread-1, 我先睡6秒
pool-1-thread-1已经溜了
我是pool-1-thread-1, 我先睡7秒
pool-1-thread-1已经溜了
我是pool-1-thread-1, 我先睡6秒
pool-1-thread-1已经溜了
我是pool-1-thread-1, 我先睡5秒
pool-1-thread-1已经溜了

可见该线程池中始终只有一个线程,一直使用的也是那一个线程。

###CacheThreadPool
在利用线程池创建线程的时候我们很容易发现,几种创建线程池的方式都可以传入类型为ThreadFactory的参数,实际上线程池默认会有一个ThreadFactory的成员属性,当我们不传这个参数时默认为Executors.defaultThreadFactory(),里面包含了他所规定的创建线程时的操作:

    static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

那么我们实际上也可以定义一个自己的ThreadFactory来定义线程池在创建线程时候的操作(这就是ThreadFactory的作用)
在这里既然讲到了ThreadFactory,那么也顺带讲一讲线程的异常捕获吧。线程的异常由于线程的本质特性,使得不能捕获到线程中逃逸的异常(你大可以尝试在run中抛出一个异常,然后在main中尝试去捕获它),在Java SE5以前只能使用线程组进行捕获(而线程组已被Sun公司的架构师表示——“最好把线程组看成是一次不成功的尝试,你只要忽略它就行了”),而Java SE5线程池的出现已经能完美的解决这一问题。
我们通过前面所提到的ThreadFactory能修改线程池生产线程的方式——在生产时候为每个Thread对象附上一个异常处理器UncaughtExceptionHandler,那么在抛出异常的时候便会通过处理器进行捕获处理:

public class PoolTest3 {
    static class ExecptionThread implements Runnable{
        @Override
        public void run() {
            throw new RuntimeException();
        }
    }

    static class EHThreadFactory implements ThreadFactory{

        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
                @Override
                public void uncaughtException(Thread t, Throwable e) {
                    System.out.println("catch " + e);
                }
            });//你也可以创建一个类继承于Thread.UncaughtExceptionHandler来进行使用而不使用匿名类
            return t;
        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool(new EHThreadFactory());
        executorService.execute(new ExecptionThread());
        executorService.shutdown();
    }
}

效果如下

catch java.lang.RuntimeException

你甚至可以调用静态设置Thread.setDefaultUncaughtExceptionHandler()来让所有没有设置未捕获异常处理器的线程调用默认的未捕获异常处理器。

ThreadPoolExecutor

我们已经知道是通过工厂方法来创建的线程池,那那些线程池实际上都是什么?
只要你看了看源代码就知道,无论是创建Fixed的还是Cached的线程池,其实都是new 一个 ThreadPoolExecutor(但是是ExecutorService的实现,具体可看下图),只是他们new出的ThreadPoolExecutor参数不同。

如图可见他们的关系:
relation

而他有以下几个核心参数用于初始化:

corePoolSize:核心线程数
maximumPoolSize:最大线程数
workQueue:阻塞队列,存储等待执行的任务,有三种
keepAlive:线程没有任务时最多保持多久才终止(单位为unit)
threadFactory:线程工厂,用来设置线程池生产线程的方式
rejectHandler:拒绝任务的时候的策略

他有几个可用于监控的重要方法:

getTaskCount():获得线程池已执行和未执行的任务总数
getCompletedTaskCount():获得以完成的任务数量
getPoolSize():线程池中当前线程的数量
getActiveCount():当前线程池中正在执行任务的线程数量

比较

直接用Thread的方式创建多线程的优势:

编程简单,像获得当前线程直接通过实例的方法即可,十分适合那些短小精悍的线程。

用实现Runnable的方式创建多线程的优势:

1.能继承与实现更多的类
2.能通过共用target来进行共享资源

用实现Callable的方式创建多线程的优势:

1.能继承与实现更多的类
2.能捕获异常进行处理
3.能获得线程执行的返回值,在编程上能有更多空间

用线程池的方式的优势:

1.target通过submit()或schedule()传入线程池后,线程池会启用一个线程来执行target里的run()或call(),但执行结束后不会死亡,将进入空闲状态保留在线程池中,等待下一个target时使用。这样在创建大量的短暂线程的时候可以不用像Thraed那样用一个创建一个,性能差又浪费资源,直接从线程池中使用。
2.可以通过控制线程池的最大线程数来控制系统并发线程数,统一管理线程,防止线程创建过多占用过多系统资源而导致的JVM崩溃,也避免的资源竞争
3.利用ScheduledExecutorService能指定延迟或周期性的执行线程任务
4.集合Runnable和Callable的优点——能共享资源,能处理返回值等
5.使用Executor能明显感到线程与任务是不同的概念,因为它甚至替你创建与管理线程,他将用户提交与运行分离开来,当然你也可以通过ThreadFactory来自定义线程池中线程的创建方式


上一篇
下一篇