创建多线程有四种方式:

一、继承Thread类创建线程类

实现步骤:

    1)定义实现了Thread类的子类

    2)重写run方法,该run方法的方法体就代表了该线程需要完成的任务

    3)创建Thread类的实例,即创建了线程对象

    4)调用线程的start方法来启动线程

public class MyThread extend Thread{

    private int i;
    @Override
    public void run() {
        for(; i<10; i++) {
            System.out.println(getName()+"\t"+i);
        }
    }
}
public class Test{
    public static void main(String[] args){
        for(int i=0; i<10; i++){
            System.out.println(Thread.currentThread().getName()+"\t"+i);
            if(i==5){
                MyThread t1 = new MyThread();
                t1.start();

                new MyThread().start();
            }
        }
    }
}

二、实现Runnable接口

实现步骤:

    1)定义实现了Runnable接口的类

    2)重写run方法,run方法同样是该线程的执行体

    2)创建该实现类的实例

    3)将此实例作为Thread的参数创建一个Thread对象,该Thread对象才是真正的线程对象

    4)调用start方法启动该线程

//1、
public class MyThread implements Runnable(

    private int i;
    @Override
    public void run(){
        for(; i<20; i++){
            System.out.println(Thread.currentThread().getName()+"\t"+i);
        }
    }
}

public class Test{
    public staic void main(String[] args){
        for(int i=0; i<20; i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
            if(i==5){
                //2、
                MyThread t = new MyThread();
                //3、
                Thread thread1 = new Thread(t, "线程1");
                Thread thread2 = new Thread(t, "线程2");
                //4、
                thread1.start();
                thread2.start();
            }
        }
    }
}

结论:采用Runnable接口方式创建多个线程可以共享线程类的实例变量,这是因为在这种方式下,程序创建的Runnable对象只是线程的target,而多个线程可以共享一个target,所以多个线程可以共享一个实例变量,在一些情境中,使用实现了Runnable接口的方式较为方便,比如三个窗口共同售卖100张票。

三、使用callable和future创建线程

有返回值、call()方法可以抛出异常。Java5提供了Future接口来代表Callable接口的call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,也实现了Runnable接口——可以作为Thread的target

实现步骤:

    1)创建实现了Callable接口的类

    2)实现call()方法,该call()方法会成为线程执行体,并且call()方法具有返回值

    3)创建该实现类的对象

    4)使用FutureTask类来包装Callable对象,该FutureTask封装call()方法的返回值

    5)调用start方法启动线程

public class MyThread implements Callable<Integer>(

    int i=0;
    @Override
    public Integer call() throws Exception{
        for(; i<20; i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
        return i;
    }
}

public class Test{
    public staic void main(String[] args){
        MyThread t = new MyThread();
        Futuretask<Integer> ft = new futureTask<>(t);
        
        ThreeThread tt2 = new Thread(ft, "新线程");
        tt2.start();

        try{
            System.out.println(ft.get()); //get方法获取Call的返回值
        }catch(Exception e){
        }
    }
}

结论:采用Runnable、Callable的优势在于——线程类只是实现了Runnable或Callable接口,还可以继承其他类;在这种方法下,多个线程可以共享一个target对象,因此非常适合多个相同线程处理同一份资源的情况,从而将CPU、代码和数据分开,形参清晰的模型,体现了面对对象的编程思想。劣势在于编程复杂度略高。

四、使用线程池创建多线程

定义:线程池,其实就是一个可以容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

4.1 为什么要使用线程池:

在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。为了防止资源不足,需要采取一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务。

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

4.2 创建线程池的常用方法:

newFixedThreadPool(int threads):创建一个固定数目的线程池

newCachedThreadPool():创建一个可缓存的线程池,调用execute方法将重用以前创建的线程,如果没有可用线程则创建一个新的线程并添加到池中。终止并移除那些已经存在60s未被使用的线程。

newSingleThreadPoolExcutor():创建一个单线程化的Excutor

newScheduledThreadPool(int corePoolSize):创建一个定时及周期性执行任务的线程池。

4.3 源码分析:

java 方法里面创建多线程 java创建多线程的方法_thread

可以看到源码中创建线程池是通过实例化ThreadPoolExecutor类来实现的,阻塞队列类型是LinkedBlockingQueue

核心线程池(corePoolSize):

        当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的 prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。

最大线程池(maximumPoolSize):

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

保持存活时间(keepAliveTime):

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

线程活动保持时间的单位(TimeUnit):

        可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

阻塞队列:java中的阻塞队列有四种:

        ArrayBlockingQueue  以数组设计的有界队列,必须设置大小,按照先进先出(FIFO)进行排序

        LinkedBlockingQueue  以链表实现的有界阻塞队列,容量可以选择设置,如果不设置的话就是无界的,最大长度为Integer.MAX_VALUE,按照先进先出(FIFO)进行排序

        SynchronousQueue  不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态

        PriorityBlockingQueue   具有优先级得无限阻塞队列

        对于一个无边界队列来说是可以向其中无限添加任务的,这种情况下可能由于任务数太多而导致内存溢出。

4.4 向线程池提交任务

        使用submit 方法来提交任务,它会返回一个future,那么我们可以通过这个future来判断任务是否执行成功,通过future的get方法来获取返回值,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。

4.5 线程池的关闭

        通过调用线程池的shutdown或shutdownNow方法来关闭线程池,但是它们的实现原理不同,shutdown的原理是只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。shutdownNow的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdownNow会首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。

        只要调用了这两个关闭方法的其中一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow。