多线程

一、进程和线程

  • 进程:正在运行的程序实例。比如打开一个程序,就开启了一个进程
  • 线程:线程是进程内部的一个独立执行单元,一个进程可以同时并发多个线程。比如浏览器开启多个页面。

多线程就是多个线程并发执行

二、线程创建

Java中创建线程的四种方式:

  • 继承Thread类
  • 实现Runable接口
  • 实现Callable接口
  • 线程池

1、继承Thread类

1)创建自定义线程类,重写run方法
package com.multithread.thread;

import java.util.Date;

public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i<10; i++){
            System.out.println("mythread线程正在执行:"+new Date().getTime());
        }
    }
}
2)测试类,创建自定义线程,调用其start方法来开启线程
package com.multithread.demos;

import com.multithread.thread.MyThread;

import java.util.Date;

public class ThreadCreateDemo {
    public static void main(String[] args){
        //1.创建自定义线程
        MyThread thread = new MyThread();//没有传入参数
        thread.start();
        //2.主线程循环打印
        for (int i=0; i<10; i++){
            System.out.println("main主线程正在执行:"+new Date().getTime());
        }
    }
}

2、实现Runable接口

1)创建自定义类实现Runable接口
package com.multithread.thread;

import java.util.Date;

public class MyRunable implements Runnable {
    public void run() {
        for (int i=0; i<10; i++){
            System.out.println("MyRunnable线程正在执行:"+new Date().getTime());
        }
    }
}
2)测试类,创建自定义线程,传入Runable接口实现类的对象
package com.multithread.demos;

import com.multithread.thread.MyRunable;
import com.multithread.thread.MyThread;

import java.util.Date;

public class ThreadCreateDemo {
    public static void main(String[] args){
        //1.创建自定义线程
        Thread thread = new Thread(new MyRunable());//注意传入参数
        thread.start();
        //2.主线程循环打印
        for (int i=0; i<10; i++){
            System.out.println("main主线程正在执行:"+new Date().getTime());
        }
    }
}

3、实现Callable接口

注意:Callable需要使用FutureTask类帮助执行

Future接口:

  • 判断任务是否完成:isDone()
  • 能够中断任务:cancle()
  • 能够获取任务执行结果:get()
1)创建自定义类实现Callable接口
package com.multithread.thread;

import java.util.Date;
import java.util.concurrent.Callable;

public class MyCallable implements Callable<String> {//注意泛型,传入的类型就是call方法的返回值类型
    public String call() throws Exception {
        for (int i=0; i<10; i++){
            System.out.println("MyCallable正在执行:"+new Date().getTime());
        }
        return "MyCallable执行完毕!";
    }
}
2)测试类,需要FutureTask类帮助
package com.multithread.demos;

import com.multithread.thread.MyCallable;
import com.multithread.thread.MyRunable;
import com.multithread.thread.MyThread;

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

public class ThreadCreateDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask task = new FutureTask(new MyCallable());//注意需要创建FutureTask对象,传入Callable接口实现类的对象
        Thread thread = new Thread(task);//传入FutureTask对象
        thread.start();

        for (int i=0; i<10; i++){
            System.out.println("main主线程正在执行:"+new Date().getTime());
        }
        System.out.println(task.get());
    }
}

4、线程池

1)为什么要使用线程池

多线程的缺点:

  • 处理任务的线程创建和销毁都非常耗时并消耗资源。
  • 多线程之间的切换也会非常耗时并消耗资源。

解决方法:采用线程池

  • 使用时线程已存在,消除了线程创建的时耗
  • 通过设置线程数目,防止资源不足
  • 提高响应速度
    • 因为线程池中的线程数没有超过线程池的最大上限时,有的线程处于等待分配任务的状态,当任务来时无需创建新的线程就能执行。
  • 提高线程可管理性
    • 线程池会根据当前系统特点对线程池内的线程进行优化处理,减少创建和销毁线程带来的系统开销。无限的创建和销毁线程不仅消耗系统资源,还降低系统的稳定性,使用线程池进行统一分配便可解决该问题
2)关系图:

线程池.png

说明:

Executor接口:

声明了execute(Runnable runnable)方法,执行任务代码

ExecutorService接口:

继承Executor接口,声明方法:submit、invokeAll、invokeAny以及shutDown等

AbstractExecutorService抽象类:

实现ExecutorService接口,基本实现ExecutorService中声明的所有方法

ScheduledExecutorService接口:

继承ExecutorService接口,声明定时执行任务方法

ThreadPoolExecutor类:

继承类AbstractExecutorService,实现execute、submit、shutdown、shutdownNow方法

ScheduledThreadPoolExecutor类:

继承ThreadPoolExecutor类,实现ScheduledExecutorService接口并实现其中的方法

Executors类:

提供快速创建线程池的方法
3)ThreadPoolExecutor的全参构造函数介绍

在Java中创建线程池常用的类是ThreadPoolExecutor,该类的全参构造函数如下:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
}

参数介绍:

  • corePoolSize:线程池中核心线程数的最大值。(核心线程就是永久保留的、不会被销毁的线程)

  • maximumPoolSize:线程池中能拥有最多线程数

  • workQueue:用于缓存任务的阻塞队列,对于不同的应用场景我们可能会采取不同的排队策略,这就需要不同类型的阻塞队列,在线程池中常用的阻塞队列有以下2种:

    • SynchronousQueue:此队列中不缓存任何一个任务。向线程池提交任务时,如果没有空闲线程来运行任务,则入列操作会阻塞。当有线程来获取任务时,出列操作会唤醒执行入列操作的线程。从这个特性来看,SynchronousQueue是一个无界队列,因此当使用SynchronousQueue作为线程池的阻塞队列时,参数maximumPoolSizes没有任何作用。
    • LinkedBlockingQueue:顾名思义是用链表实现的队列,可以是有界的,也可以是无界的,但在Executors中默认使用无界的。
    以上三个参数之间的关系如下:
    1)如果没有空闲的线程执行该任务且当前运行的线程数少于corePoolSize,则添加新的线程执行该任务。
    2)如果没有空闲的线程执行该任务且当前的线程数等于corePoolSize同时阻塞队列未满,则将任务入队列,而不添加新的线程。
    3)如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数小于maximumPoolSize,则创建新的线程执行任务。
    如果没有空闲的线程执行该任务且阻塞队列已满同时池中的线程数等于maximumPoolSize,则根据构造函数中的handler指定的策略来拒绝新的任务。
    
  • keepAliveTime:表示空闲线程的存活时间。

  • unit:表示keepAliveTime的单位。

  • handler:表示当workQueue已满,且池中的线程数达到maximumPoolSize时,线程池拒绝添加新任务时采取的策略。一般可以采取以下四种取值。

    取值作用
    ThreadPoolExecutor.AbortPolicy()抛出RejectedExecutionException异常
    ThreadPoolExecutor.CallerRunsPolicy()由向线程池提交任务的线程来执行该任务
    ThreadPoolExecutor.DiscardOldestPolicy()抛弃最旧的任务(最先提交而没有得到执行的任务)
    ThreadPoolExecutor.DiscardPolicy()抛弃当前的任务
  • threadFactory:指定创建线程的工厂

4)四种常用线程池

ThreadPoolExecutor构造函数的参数很多,使用起来很麻烦,为了方便的创建线程池,JavaSE中又定义了Executors类,Eexcutors类提供了四个创建线程池的方法,分别如下:

  • newCachedThreadPool
  • newFixedThreadPool
  • newSingleThreadExecutor
  • newScheduleThreadPool
1.newCachedThreadPool

该方法创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。适用于少数量的线程。

此类型线程池特点是:

  • 工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE)
  • 空闲的工作线程会自动销毁,有新任务会重新创建
  • 在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。

示例:

public static void main(String[] args) {  
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();  //newCachedThreadPool
        for (int i = 0; i < 10; i++) {  
            final int index = i;  
            try {  
                Thread.sleep(10);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
            cachedThreadPool.execute(new Runnable() { //执行CachedThreadPool的execute方法 
                public void run() {  
                    System.out.println(index);  
                }  
            });  
        }  
    }
2.newFixedThreadPool

该方法创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。适合一直有任务执行,且任务量基本固定。

  • 优点:提高程序效率和节省创建线程时所耗的开销。
  • 缺点:在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。

示例:

public static void main(String[] args) {  
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);  //newFixedThreadPool
        for (int i = 0; i < 10; i++) {  
            final int index = i;  
            fixedThreadPool.execute(new Runnable() {//执行FixedThreadPool的execute方法
                public void run() {  
                    try {  
                        System.out.println(index);  
                        Thread.sleep(10);  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                }  
            });  
        }  
    }  
3.newSingleThreadExecutor

该方法创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO,优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。

单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。

这样子好像和单线程木有区别了..

示例:

public static void main(String[] args) {  
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();  //newSingleThreadExecutor
        for (int i = 0; i < 10; i++) {  
            final int index = i;  
            singleThreadExecutor.execute(new Runnable() { //执行SingleThreadExecutor的execute方法 
                public void run() {  
                    try {  
                        System.out.println(index);  
                        Thread.sleep(2000);  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                }  
            });  
        }  
    }  
4.newScheduleThreadPool

该方法创建一个定长的线程池,而且支持定时的以及周期性的任务执行。

示例:

public static void main(String[] args) {  
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);  //newScheduledThreadPool
        for (int i = 0; i < 10; i++) {  
            scheduledThreadPool.schedule(new Runnable() {// ScheduledThreadPool的 schedule方法
                public void run() {  
                    System.out.println("delay 3 seconds");  
                }  
            }, 3, TimeUnit.SECONDS);  
        }  
  
    } 

5)线程池使用自定义接口

这里以newFixedThreadPool为例

1)创建自定义类实现Runable接口(实现Callable接口也行)
package com.multithread.thread;

import java.util.Date;

public class MyRunable implements Runnable {
    public void run() {
        for (int i=0; i<10; i++){
            System.out.println("MyRunnable线程正在执行:"+new Date().getTime());
        }
    }
}
2)测试类
package com.multithread.demos;

import com.multithread.thread.MyCallable;
import com.multithread.thread.MyRunable;
import com.multithread.thread.MyThread;

import java.util.Date;
import java.util.concurrent.*;

public class ThreadCreateDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1.使用Executors创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(10);//参数是线程池里的线程数
        //2.通过线程池执行线程
        executorService.execute(new MyRunable());
        //3.主线程循环打印
        for (int i=0; i<10; i++){
            System.out.println("main主线程正在执行:"+new Date().getTime());
        }
    }
}

5、线程创建总结

1)实现接口和继承Thread类比较
  • 接口更适合多个相同的程序代码的线程去共享同一个资源。
  • 接口可以避免Java中单继承的局限性
  • 接口代码可以被多个线程共享,代码和线程独立
  • 线程池只能放入实现Runable接口或Callable接口的线程,不能直接放入继承Thread的类

注意!!!

在Java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。

2)Runable接口和Callable接口比较

相同点:

  • 都是接口(和不说一样)
  • 都可用来编写多线程程序
  • 都需要Thread.start()启动线程

不同点:

  • 实现Callable接口的线程能执行返回结果;而实现Runable接口的线程不能返回结果
  • Callable接口的call()方法允许抛出异常;而Runable接口的run()方法不允许抛出异常
  • 实现Callable接口的线程可以调用Future.cancle取消执行,而实现Runable接口的线程不能

注意:

Callable接口支持返回执行结果,此时调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取到结果;当不调用此方法时,主线程不会阻塞。

三、线程生命周期

线程生命周期.png

1、新建

  • new关键字创建了一个线程之后,该线程就处于新建状态
  • JVM为线程分配内存,初始化成员变量值

2、就绪

  • 当线程对象调用了start()方法之后,该线程就处于就绪状态
  • JVM为线程创建方法栈和程序计数器,等待线程调度器调度

3、运行

  • 就绪状态的线程获得CPU资源,开始运行run()方法,该线程进入运行状态

4、阻塞

当发生如下情况时,线程将会进入阻塞状态:

  • 线程调用sleep()方法主动放弃所占用的处理器资源
  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
  • 线程试图获得一个同步锁(同步监视器),但该同步锁正被其他线程所持有
  • 线程正在等待某个通知
  • 程序调用了线程的suspend()方法将该线程挂起。注意,这个方法容易导致死锁,所以应该尽量避免使用该方法。

5、死亡

线程有以下3种结束方式,结束后就处于死亡状态

  • run()或call()方法执行完成,线程正常结束
  • 线程抛出一个未捕获的Exception或Error
  • 调用该线程的stop()方法结束该线程,该方法容易导致死锁,不推荐使用。

代码书写世界,吉他演奏生活