多线程学习(一)基础线程入门

基础概念

什么是进程?

一个进程对应一个应用程序。例如:在 windows 操作系统启动 Word 就表示启动了一个进程。在java的开发环境下启动JVM,就表示启动了一个进程。现代的计算机都是支持多进程的,在同一个操作系统中,可以同时启动多个进程。
对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程,打开两个记事本就启动了两个记事本进程,打开一个Word就启动了一个Word进程。
有些进程还不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。

进程是操作系统进行资源分配的单位,进程是计算机系统分配资源的最小单位。每个进程都有自己的一部分独立的系统资源,彼此是隔离的。
进程(英语:Process)是计算机中已运行程序的实体。进程本身不会运行,是线程的容器。程序本身只是指令的集合,进程才是程序(那些指令)的真正运行。若干进程有可能与同一个程序相关系,且每个进程皆可以同步(循序)或不同步(平行)的方式独立运行。进程为现今分时系统的基本运作单位。

什么是线程?

线程指进程中的一个执行场景,也就是执行流程。同一个进程中的线程共享其进程中的内存和资源(共享的内存是堆内存和方法区内存,栈内存不共享,每个线程有自己的。)

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

多线程的作用

多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。线程是在同一时间需要完成多项任务的时候实现的。

反应“多角色”的程序代码,最起码每个角色要给他一个线程吧,否则连实际场景都无法模拟,当然也没法说能用单线程来实现:比如最常见的“生产者,消费者模型”。

并行与并发

  • 并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
  • 并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS(Transactions Per Second(每秒传输的事物处理个数))或者QPS(每秒查询率QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准)来反应这个系统的处理能力。

    总线程数<= CPU数量:并行运行
    总线程数> CPU数量:并发运行

    什么是线程安全?

    指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。

    线程不安全的原因:

    多个线程访问了相同的资源,同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件,并发生写操作。

    竞态条件 & 临界区

    当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。

    共享资源

    允许被多个线程同时执行的代码称作线程安全的代码。线程安全的代码不包含竞态条件。当多个线程同时更新共享资源时会引发竞态条件。因此,了解Java线程执行时共享了什么资源很重要。

    什么是线程同步?

    保证多线程共享资源但是对资源的操作先后执行。

    线程的创建

    此部分内容参考至sunddenly

  • 通过继承Thread类来创建并启动多线程的方式
  • 通过实现Runable接口来创建并启动多线程的方式
  • 通过实现Callable接口来创建并启动线程的方式

    继承Thread类来创建线程类

    Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建并启动多线程的步骤如下:
  • 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务因此把run()方法称为线程执行体;
  • 创建Thread子类的实例,即创建了线程对象;
  • 调用线程对象的start()方法来启动该线程
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
     public class ExtendsThreadDemo {
    public static void main(String[] args) {
    for (int i=0;i<300;i++){
    System.out.println("打游戏"+i);
    if(i==10){
    MusicThread t =new MusicThread();
    t.start();
    }
    }
    }
    }
    class MusicThread extends java.lang.Thread{
    @Override
    public void run(){
    for(int i=0;i<300;i++){
    System.out.println("播放音乐"+i);
    }
    }
    }

    实现Runnable接口创建线程类

    实现Runnable接口来创建并启动多线程的步骤如下:
  • 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体
  • 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象
  • 调用线程对象的start()方法来启动线程
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class ImplementRunnableDemo {
    public static void main(String[] args) {
    for (int i = 0; i < 100; i++) {
    System.out.println(Thread.currentThread().getName()+"--打游戏" + i);
    if (i == 1) {
    Runnable target = new MusicRunnableImpl();
    Thread t = new Thread(target,"线程1");
    t.start();
    Thread t1 = new Thread(target,"线程2");
    t1.start();
    }
    }
    }
    }
    class MusicRunnableImpl implements java.lang.Runnable{
    @Override
    public void run() {
    for(int i=0;i<100;i++){
    System.out.println(Thread.currentThread().getName()+"--播放音乐"+i);
    }
    }
    }

    1. 在这种方式下,程序所创建的Runnable对象只是线程的target,而多个线程可以共享同一个target。2. 所以多个线程可以共享同一个线程类即线程的target类的实例属性.
    2. 这个时候target的全局变量会受到线程资源抢夺的问题。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    public class ImplementRunnableDemo {
    public static void main(String[] args) {
    for (int i = 0; i < 100; i++) {
    System.out.println(Thread.currentThread().getName()+"--打游戏" + i);
    if (i == 1) {
    Runnable target = new MusicRunnableImpl();
    Thread t = new Thread(target,"线程1");
    t.start();
    Thread t1 = new Thread(target,"线程2");
    t1.start();
    }
    }

    System.out.println("主线程结束");
    }
    }
    class MusicRunnableImpl implements java.lang.Runnable{
    private int i=0;
    void print(){
    System.out.println(Thread.currentThread().getName()+"--i--"+(i));
    }

    @Override
    public void run() {
    for(;i<500;i++){
    System.out.println(Thread.currentThread().getName()+"--播放音乐"+i);
    print();
    }
    }
    }

    使用Callable和Future创建线程

    Callable和Future接口概述

    从Java 5开始,Java提供了Callable接口,该接口怎么看都像是Runnable接口的增强版,Callable接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更强大。
  • call()方法可以有返回值;
  • call()方法可以声明抛出异常;

因此我们完全可以提供一个Callable对象作为Thread的target,而该线程的线程执行体就是该Callable对象的call()方法。问题是:Callable接口是Java 5新增的接口,而且它不是Runnable接口的子接口,所以Callable对象不能直接作为Thread的target。而且call()方法还有一 个返回值—–call()方法并不是直接调用,它是作为线程执行体被调用的。那么如何获取call()方法的返回值呢?

Java 5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口和Runnable接口可以作为Thread类的target。在Future接口里定义了如下几个公共方法来控制它关联的Callable任务:

  • boolcan cancel(boolean maylnterruptltRunning):试图取消该Future里关联的Callable任务
  • V get():返回Callable任务里call()方法的返回值。调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值
  • V get(long timeout,TimeUnit unit):返回Callable任务里call()方法的返回值。该方法让程序最多阻塞timeout和unit指定的时间,如果经过指定时间后Callable任务依然没有返回值,将会抛出TimeoutExccption异常
  • boolean isCancelled():如果在Callable任务正常完成前被取消,则返回true
  • boolean isDone():妇果Callable任务已完成,则返回true

    注意:Callable接口有泛型限制,Callable接口里的泛型形参类型与call()方法返回值类型相同。

创建并启动有返回值的线程的步骤:

  • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值

  • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象该FutureTask对象封装了该Callable对象的call()方法的返回值

  • 使用FutureTask对象作为Thread对象的target创建并启动新线程

  • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    public class ImplementCallableDemo implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
    int i = 0;
    for ( ; i < 100 ; i++ ){
    System.out.println(Thread.currentThread().getName()+ "--i--" + i);
    }
    // call()方法可以有返回值
    return i;
    }

    public static void main(String[] args) {
    //创建callable对象
    ImplementCallableDemo callableDemo=new ImplementCallableDemo();
    //使用FutureTask来包装Callable对象
    FutureTask<Integer> task = new FutureTask<Integer>(callableDemo);
    for (int i=0;i<100;i++){
    System.out.println(Thread.currentThread().getName()+"\t"+i);
    if (i==1){
    new Thread(task,"callable").start();
    }
    }
    try{
    // 获取线程返回值
    System.out.println("callable返回值:" + task.get());
    }
    catch (Exception ex){
    ex.printStackTrace();
    }

    }
    }

    (1) 采用实现Runnable、Callable接口的方式创建多线程
    线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
    在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
    劣势:编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread()方法。
    (2) 采用继承Thread类的方式创建多线程劣势:因为线程类已经继承了Thread类,所以不能再继承其他父类

    线程的生命周期

  • 新建状态:采用new语句创建完成

  • 就绪状态:执行start之后,进入就绪状态(就是有权去获取CPU的运行时间,可以被JVM调度)

  • 运行状态:在被JVM调度,获取CPU运行时间,执行run方法。此时是运行状态

  • 阻塞状态:执行了wait语句、执行了sleep语句和等待某个对象锁,等待输入的场合。

  • 消亡状态:run 方法执行完。

    来源于知乎(来源于知乎作者:泥瓦匠

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    public enum State {
    /**
    * 创建了线程,但是还没有启动
    */
    NEW,
    /**
    * 这个状态包含了线程在JVM中执行或则在等待争抢执行的权利
    */
    RUNNABLE,
    /**
    * 线程的这个状态表示阻塞状态,等待获取一个监视器锁进入到同步方法或同步代码块,或者等待方法返回后重新获取。
    */
    BLOCKED,
    /**
    *表示线程处于无限制等待状态,等待一个特殊的事件来重新唤醒,
    *如通过wait()方法进行等待的线程等待一个notify()或者notifyAll()方法,
    *通过join()方法进行等待的线程等待目标线程运行结束而唤
    */
    WAITING,
    /**
    *表示线程进入了一个有时限的等待,如sleep(3000),等待3秒后线程重新进行RUNNABLE状态继续运行。或者join(3000),等待线程的加入,等待一定时间。
    */
    TIMED_WAITING,
    /**
    * 表示线程执行完毕后,进行终止状态。
    *需要注意的是,一旦线程通过start方法启动后就再也不能回到初始NEW状态,线程终止后也不能再回到RUNNABLE状态。
    */
    TERMINATED;
    }

线程阻塞:

  • 线程调用sleep()方法主动放弃所占用的处理器资源
  • 线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
  • 线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有。
  • 线程在等待某个通知(notify)

当前正在执行的线程被阻塞之后,其他线程就可以获得执行的机会。被阻塞的线程会在合适的时候重新进入就绪状态,注意是就绪状态而不是运行状态。也就是说,被阻塞线程的阻塞解除后,必须重新等待线程调度器再次调度它。
解除阻塞:

  • 用sleep()方法的线程经过了指定时间。
  • 线程调用的阻塞式IO方法已经返回。
  • 线程成功地获得了试图取得的同步监视器。
  • 线程正在等待某个通知时,其他线程发出了个通知。

线程调度与控制

线程的调度模型分为: 分时调度模型和抢占式调度模型,Java使用抢占式调度模型

  • 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
  • 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。

线程的join

Thread提供了让一个线程等待另一个线程完成的方法join()方法。当在某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。join()方法通常由使用线程的程序调用,以将大问题划分成许多小问题,每个小问题分配一个线程。当所有的小问题都得到处理后,再调用主线程来进一步操作。

后台线程

有一种线程,它是在后台运行的,它的任务是为其他的线程提供服务,这种线程被称为后台线程(Daemon Thread),又称为守护线程或精灵线程。JVM的垃圾回收线程就是典型的后台线程。后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡。

调用Thread对象的setDaemon(true)方法可将指定线程设置成后台线程。下面程序将执行线程设置成后台线程,可以看到当所有的前台线程死亡时,后台线程随之死亡。当整个虚拟机中只剩下后台线程时,程序就没有继续运行的必要了,所以虚拟机也就退出了。

线程睡眠 sleep

如果需要让当前正在执行的线程暂停一段时,并进入阻塞状态,则可以通过调用Thread类的静态sleep()方法来实现。当当前线程调用sleep()方法进入阻塞状态后,在其睡眠时间段内,该线程不会获得执行的机会,即使系统中没有其他可执行的线程,处于sleep()中的线程也不会执行,因此sleep()方法常用来暂停程序的执行。

线程让步 yield

yield()方法是一个和sleep()方法有点相似的方法,它也是Threard类提供的一个静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。yield()只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行。

关于sleep()方法和yield()方法的区别如下:

  1. sleep()方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程的优先级;但yield()方法只会给优先级相同,或优先级更高的线程执行机会
  2. sleep()方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态:而yield()不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程调用yield()方法暂停之后,立即再次获得处理器资源被执行
  3. sleep()方法声明抛出了InterruptcdException异常,所以调用sleep()方法时要么捕捉该异常,要么显式声明抛出该异常;而yield()方法则没有声明抛出任何异常
  4. sleep()方法比yield()方法有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行

线程的优先级

线 程 优 先 级 主 要 分 三 种 :

  • MAX_PRIORITY( 最 高 级 );
  • MIN_PRIORITY ( 最 低 级 )
  • NORM_PRIORITY(标准)默认