多线程快速入门

线程介绍

线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。

每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。
一个进程可以有很多线程,每条线程并行执行不同的任务。

总结:进程是所有线程的集合,每一个线程是进程中的一条执行路径。

线程的意义

为了解决负载均衡问题,充分利用CPU资源.为了提高CPU的使用率,采用多线程的方式去同时完成几件事情而不互相干扰.为了处理大量的IO操作时或处理的情况需要花费大量的时间等等,比如:读写文件,视频图像的采集,处理,显示,保存等

线程相比进程的优点:

  1. 线程在程序中是独立的,并发的执行流,但是,与分隔的进程相比,进程中的线程之间的隔离程度要小。它们共享内存,文件句柄和其他每个进程应有的状态。
  2. 线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性:多个线程将共享同一个进程虚拟空间。线程共享的环境包括:进程代码段,进程的公有数据等。利用这些共享的数据等,线程很容易实现相互之间的通信。
  3. 当操作系统创建一个进程时,必须为进程分配独立的内存空间,并分配大量相关资源:但创建一个线程则简单很多,因此使用多线程来实现并发比使用多进程实现并发

使用多线程的影响:

  1. cpu在多个线程之间进行切换会牺牲一部分性能.
  2. 创建线程需要占用一定的内存空间
  3. 线程之间共享数据会产生线程安全问题
  4. 线程竞争资源会导致死锁

线程创建方式

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。
Java可以以下四种方式来创建线程:

  1. 继承Thread类创建线程
  2. 实现Runnable接口创建线程
  3. 使用Callable和Future创建线程
  4. 使用线程池Executor框架

1、继承Thread类创建线程

通过继承Thread类来创建并启动多线程的一般步骤如下

  1. 定义Thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。
  2. 创建Thread子类的实例,也就是创建了线程对象
public class ThreadExt extends Thread{

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

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

2、实现Runnable接口创建线程

通常推荐使用实现Runnable接口创建线程的方式,原因是一个类实现了接口还可以继续继承,继承了Thread类则不能再继承其它类了。

通过实现Runnable接口创建并启动线程一般步骤如下:

  1. 定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体
  2. 创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象
 public class RunnableImpl implements Runnable{
    @Override
    public void run() {
        for(int i = 0;i<10;i++){
            System.out.println(Thread.currentThread().getName() + " : " + i);
        }
    }

    public static void main(String[] args) {
        //将实现Runnable接口的类传入Thread构造器创建,推荐使用此方式
        Thread thread = new Thread(new RunnableImpl());
        //当然你也可以使用匿名内部类的形式实现Runnable接口,一般不推荐这么做
        thread = new Thread(()->{
            for(int i = 0;i<10;i++){
                System.out.println(Thread.currentThread().getName() + " : " + i);
            }
        });
        //
        thread.start();
    }
}

3、 使用Callable和Future创建线程

和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。

Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。

  1. 创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
  2. 使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
  3. 使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
  4. 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
public class CallableImpl implements Callable {

    // 与run()方法不同的是,call()方法具有返回值
    @Override
    public Integer call() throws InterruptedException {
        int sum = 0;
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + " : " + i);
            sum += i;
        }
        Thread.sleep(3000);   //计算完成sum后,我们让线程休眠3秒
        return sum;
    }

    public static void main(String[] args) {

        Callable<Integer> callableImpl = new CallableImpl();    // 创建CallableImpl对象
        FutureTask<Integer> ft = new FutureTask<>(callableImpl); //使用FutureTask来包装CallableImpl对象

        Thread thread = new Thread(ft);       //FutureTask对象作为Thread对象的target创建新的线程
        thread.start();                       //线程进入到就绪状态

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

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

    }
}

4、 使用线程池Executor框架

Executor接口中之定义了一个方法execute(Runnable command),该方法接收一个Runable实例,它用来执行一个任务,任务即一个实现了Runnable接口的类。

关于线程池框架的详细介绍请参考这篇博文:Java线程池ThreadPoolExecutor的使用及其原理

下面是一个自定义线程池的示例:

    public static void main(String[] args) {
        ExecutorService threadPool = new ThreadPoolExecutor(// 自定义一个线程池
                1, // 核心线程数
                2, // 最大线程数
                60, // 超过核心线程数的额外线程存活时间
                TimeUnit.SECONDS, // 线程存活时间的时间单位
                new ArrayBlockingQueue<>(3) // 有界队列,容量是3个
                , Executors.defaultThreadFactory()    // 线程工厂
                , new ThreadPoolExecutor.AbortPolicy() //线程的拒绝策略
        );
        //执行一个任务
        threadPool.execute(()->{
            //线程执行的具体逻辑
            //Runnable to do something.
            System.out.println("hello world");
        });
        threadPool.shutdown();
        threadPool.shutdownNow();
    }

启动线程

  • 使用start()方法开始执行线程
  • 注意开启线程不是调用run()方法,直接使用run()方法相当于在当前线程下执行了

其中start()方法原码如下

	/**
 	*使该线程开始执行;Java虚拟机调用此线程的run方法。
 	*结果是两个线程同时运行:
 	*当前线程(执行start方法)和另一个线程(执行其run方法)。
 	*多次启动线程是不合法的。
	 *特别是,线程一旦完成就不能重新启动执行。
	**/
    public synchronized void start() {
   		 /**
   	 	 *0状态值对应于状态“NEW”。
    	 */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /*通知组此线程即将启动
		*以便将其添加到组的线程列表中
		*组的未开始计数可以减少*/
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

    private native void start0();

守护线程

Java中有两种线程,一种是用户线程,另一种是守护线程。

  • 用户线程是指用户自定义创建的线程,主线程停止,用户线程不会停止守护线程
  • 当进程不存在或主线程停止,守护线程也会被停止。

使用setDaemon(true)方法设置为守护线程

多线程运行状态

在这里插入图片描述

线程从创建、运行到结束总是处于下面五个状态之一:新建状态、就绪状态、运行状态、阻塞状态及死亡状态。

新建状态

当用new操作符创建一个线程时, 例如new Thread(),线程还没有开始运行,此时线程处在新建状态。
线程中的代码此时并未被执行。

就绪状态

一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。

处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。

运行状态

当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.

阻塞状态

阻塞状态是指线程因为某些原因放弃CPU,暂时停止运行。当线程处于阻塞状态时,Java虚拟机不会给线程分配CPU。直到线程重新进入就绪状态,它才有机会转到运行状态。

阻塞状态可分为以下3种:

  1. 位于对象等待池中的阻塞状态(Blocked in object’s wait pool):当线程处于运行状态时,如果执行了某个对象的wait()方法,Java虚拟机就会把线程放到这个对象的等待池中,这涉及到“线程通信”的内容。
  2. 位于对象锁池中的阻塞状态(Blocked in object’s lock pool):当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java虚拟机就会把这个线程放到这个对象的锁池中,这涉及到“线程同步”的内容。
  3. 其他阻塞状态(Otherwise Blocked):当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了I/O请求时,就会进入这个状态。

死亡状态

有两个原因会导致线程死亡:

  • run方法正常退出而自然死亡,
  • 一个未捕获的异常终止了run方法而使线程猝死。

为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true;
如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false.

线程的常用方法

常用线程api方法
start()启动线程
currentThread()获取当前线程对象
getID()获取当前线程ID Thread-编号 该编号从0开始
getName()获取当前线程名称
sleep(long mill)休眠线程,单位毫秒
stop()停止线程,
常用线程构造函数
Thread()分配一个新的 Thread对象。自动生成的名称的格式为“Thread-”+n,其中n是整数。
Thread(String name)分配一个新的 Thread对象,指定线程名称name。
Thread(Runable r)分配一个新的 Thread对象
Thread(Runable r, String name)分配一个新的 Thread对象 指定线程名称name

join()方法作用

当在当前线程中执行到t1.join()方法时,就认为当前线程会把把执行权让给t1

    public static void main(String[] args) {
        Thread t1 = new Thread(() ->{ System.out.println(Thread.currentThread().getName()); },"t1");
        Thread t2 = new Thread(() -> {
            try {
                t1.join();
            } catch (Exception e) {
                // TODO: handle exception
            }
            System.out.println(Thread.currentThread().getName());
        },"t2");
        Thread t3 = new Thread(() -> {
            try {
                t2.join();
            } catch (Exception e) {
                // TODO: handle exception
            }
            System.out.println(Thread.currentThread().getName());
        },"t3");
        t3.start();
        t2.start();
        t1.start();
    }

输出:

t1
t2
t3

Yield方法

Thread.yield()方法的作用:暂停当前正在执行的线程,并执行其他线程。(可能没有效果)

yield()让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。

结论:大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

线程调度

优先级

现代操作系统基本采用时分的形式调度运行的线程,线程分配得到的时间片的多少决定了线程使用处理器资源的多少,也对应了线程优先级这个概念。在JAVA线程中,通过一个int
priority来控制优先级,范围为1-10,其中10最高,默认值为5。下面是源码(基于1.8)中关于priority的一些量和方法。

优先级高的线程并不一定先执行
  • java线程是通过映射到系统的原生线程上来实现的,所以线程的调度最终还是取决于操作系统,操作系统的优先级与java的优先级并不一一对应,如果操作系统的优先级级数大于java的优先级级数(10级)还好,但是如果小于得的话就不行了,这样会导致不同优先级的线程的优先级是一样的。
  • 优先级可能会被系统自动改变,比如windows系统中就存在一个线程调度器,大致功能就是如果一个线程执行的次数过多的话,可能会越过优先级为他分配执行时间
线程调度器

线程调度器是一个操作系统服务,它负责为 Runnable 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。同上一个问题,线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是 更好的选择(也就是说不要让你的程序依赖于线程的优先级)。时间分片是指将可用的 CPU 时间分配给可用的 Runnable 线程的过程。分配 CPU 时间可以基于线程优先级或者线程等待的时间。

线程调度算法

计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令.所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务.在运行池中,会有多个处于就绪状态的线程在等待CPU,JAVA虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配CPU的使用权.

有两种调度模型:分时调度模型和抢占式调度模型。

分时调度模型是指让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的CPU的时间片这个也比较好理解。

java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。

多线程中的上下文切换

在上下文切换过程中,CPU会停止处理当前运行的程序,并保存当前程序运行的具体位置以便之后继续运行。从这个角度来看,上下文切换有点像我们同时阅读几本书,在来回切换书本的同时我们需要记住每本书当前读到的页码。在程序中,上下文切换过程中的“页码”信息是保存在进程控制块(PCB)中的。PCB还经常被称作“切换桢”(switchframe)。“页码”信息会一直保存到CPU的内存中,直到他们被再次使用。
上下文切换是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。

线程间的通信

请参考:Java多线程通讯

面试题

  1. 进程与线程的区别?
  2. 为什么要用多线程?
  3. 多线程创建方式?
  4. 是继承Thread类创建线程好还是实现Runnable接口线程好?为什么?
  5. 多线程的应用场景?
  6. 现在有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值