乐趣区

关于java:JavaSE第18篇多线程上篇

核心内容:在理论开发中,若程序须要同时解决多个工作时,咱们该如何实现?此时多线程就可帮忙咱们实现。应用多线程能够进步 CPU 的利用率及程序的解决效率。本篇将会学习多线程相干概念、创立和应用、线程平安问题及线程状态的理解。

第一章:多线程根底

本章次要理解和多线程相干的一些概念。

想要设计一个程序,边打游戏边听歌,怎么设计?

要解决上述问题, 得应用多过程或者多线程来解决.

1.1- 并发和并行(理解)

并发

并发简而言之就是:指两个或多个事件在同一个时间段内产生(交替执行)。

在操作系统中,装置了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 零碎中,每 一时刻只能有一道程序执行,即宏观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的工夫是十分短的。

并行

简而言之,并行:是指两个或多个事件在同一时刻产生(同时产生)。

而在多个 CPU 零碎中,则这些能够并发执行的程序便能够调配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来解决一个能够并发执行的程序,这样多个程序便能够同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的进步电脑运行的效率。

1.2- 过程与线程(理解)

过程

是指一个内存中运行的应用程序,每个过程都有一个独立的内存空间,一个应用程序能够同时运行多个过程;过程也是程序的一次执行过程,是零碎运行程序的根本单位;零碎运行一个程序即是一个过程从创立、运行到沦亡的过程。

线程

线程是过程中的一个执行单元,负责以后过程中程序的执行,一个过程中至多有一个线程。一个过程

中是能够有多个线程的,这个应用程序也能够称之为多线程程序。

留神

一个程序运行后至多有一个过程,一个过程中能够蕴含多个线程。

因为创立一个线程的开销比创立一个过程的开销小的多,那么咱们在开发多任务运行的时候,通常思考创立多线程,而不是创立多过程。

多线程能够进步 cpu 利用率

大部分操作系统都反对多过程并发运行,当初的操作系统简直都反对同时运行多个程序。在同时运行的程序,”感觉这些软件如同在同一时刻运行着“。

实际上,CPU(中央处理器)应用抢占式调度模式在多个线程间进行着高速的切换。对于 CPU 的一个核而言,某个时刻,只能执行一个线程,而 CPU 的在多个线程间切换速度绝对咱们的感觉要快,看上去就是在同一时刻运行。其实,多线程程序并不能进步程序的运行速度,但可能进步程序运行效率,让 CPU 的使用率更高。

1.3- 线程调度(理解)

分时调度

所有线程轮流应用 CPU 的使用权,平均分配每个线程占用 CPU 的工夫。

抢占式调度

优先让优先级高的线程应用 CPU,如果线程的优先级雷同,那么会随机抉择一个(线程随机性),Java 应用的为抢占式调度。

第二章:Java 中创立和应用多线程

2.1- 继承 Thread 类形式创立线程(重要)

Java 应用 java.lang.Thread 类代表线程,所有的线程对象都必须是 Thread 类或其子类的实例。每个线程的作用是 实现肯定的工作,实际上就是执行一段程序流即一段程序执行的代码。Java 应用线程执行体来代表这段程流。

Java 中通过继承 Thread 类来创立并启动多线程的步骤如下

  1. 定义 Thread 类的子类,并重写该类的 run()办法,该 run()办法的办法体就代表了线程须要实现的工作, 因而把 run()办法称为线程执行体。
  2. 创立 Thread 子类的实例,即创立了线程对象。
  3. 调用线程对象的 start()办法来启动该线程。

Thread 类构造方法

  • public Thread() : 调配一个新的线程对象。
  • public Thread(String name) : 调配一个指定名字的新的线程对象。

示例代码

/* 测试类中的代码 */
public class DemoThread {public static void main(String[] args) {MyThread mt = new MyThread("线程 1");
    mt.start(); // 启动线程 1 的工作
    MyThread mt2 = new MyThread("线程 2");
    mt2.start(); // 启动线程 2 的工作}
}
/* 定义的线程类代码 */
public class MyThread extends Thread {public MyThread(String name) {super(name);
  }

  @Override
  public void run() {for (int i = 0; i < 20; i++) {System.out.println(getName() + "线程执行" + i);
    }
  }
}

2.2- 多线程原理(理解)

多个线程之间的程序不会影响彼此(比方一个线程解体了并不会影响另一个线程)。

在 Java 中,main 办法是程序执行的入口,也是 Java 程序的主线程。当在程序中开拓新的线程时,执行过程是这样的。

执行过程

  1. 首先 main 办法作为主程序先压栈执行。
  2. 在主程序的执行过程中,若创立了新的线程,则内存中会另开拓一个新的栈来执行新的线程。
  3. 每一个新的线程都会有一个新的栈来寄存新的线程工作。
  4. 栈与栈之间的工作不会相互影响。
  5. CPU 会随机切换执行不同栈中的工作。

图解执行过程(以上述代码为例)

2.3-Thread 类罕用办法(重要)

罕用办法

  • public String getName() : 获取以后线程名称。
  • public void start() : 导致此线程开始执行; Java 虚拟机调用此线程的 run 办法。
  • public void run() : 此线程要执行的工作在此处定义代码。
  • public static void sleep(long millis) : 使以后正在执行的线程以指定的毫秒数暂停(临时进行执行)。
  • public static Thread currentThread() : 返回对以后正在执行的线程对象的援用。

示例代码

//【代码测试类】public class Main01 {public static void main(String[] args) {MyThread mt = new MyThread("线程 1");
    mt.start();
    // 打印线程名称
    System.out.println(mt.getName());
    System.out.println("以后线程是" + Thread.currentThread().getName());
    // 每距离一秒钟打印一个数字
    for (int i = 0; i < 60; i++) {System.out.println(i);
      try {
        // sleep 抛出了异样,须要解决异样
        Thread.sleep(1000);
      } catch (InterruptedException e) {e.printStackTrace();
      }
    }
  }
}
//【MyThread 类】public class MyThread extends Thread{public  MyThread(){super();
  }
  // 构造函数中调用父类构造函数传入线程名称
  public MyThread(String name) {super(name);
  }
  @Override
  public void run() {
    // 打印线程名称
    System.out.println(this.getName());
    System.out.println("以后线程是" + Thread.currentThread().getName());
  }
}

run 办法和 start 办法

run()办法,是线程执行的工作办法,每个线程都会调用 run()办法执行,咱们将线程要执行的工作代码都写在 run()办法中就能够被线程调用执行。

start()办法,开启线程,线程调用 run()办法。start()办法源代码中会调用本地办法 start0()来启动线程:private native void start0(),本地办法都是和操作系统交互的,因而能够看出每次开启一个线程的线程都会和操作系统进行交互。

留神:一个线程只能被启动一次!

对于线程的名字

线程是有默认名字的,如果咱们不设置线程的名字,JVM 会赋予线程默认名字 Thread-0,Thread-1。

2.4- 实现 Runnable 接口方式创立线程(重要)

翻阅 API 后得悉创立线程的形式总共有两种,一种是继承 Thread 类形式,一种是实现 Runnable 接口方式。

Runnable 应用步骤

  1. 定义 Runnable 接口的实现类,并重写该接口的 run()办法,该 run()办法的办法体同样是该线程的线程执行体。
  2. 创立 Runnable 实现类的实例,并以此实例作为 Thread 的 target 来创立 Thread 对象,该 Thread 对象才是真正的线程对象。
  3. 调用线程对象的 start()办法来启动线程。

Thread 类构造函数

  • public Thread(Runnable target) : 调配一个带有指定指标新的线程对象。
  • public Thread(Runnable target,String name) : 调配一个带有指定指标新的线程对象并指定名字。

示例代码

// 测试类
public class Main01 {public static void main(String[] args) {
    // 创立 Runnable 对象
    RunnableImpl ra = new RunnableImpl();
    // 创立线程对象并传入 Runnable 对象
    Thread th = new Thread(ra);
    // 启动并执行线程工作
    th.start();}
}
//【Runnable 实现类】public class RunnableImpl implements Runnable {
  @Override
  public void run() {System.out.println("线程工作 1");
  }
}

总结

  • 通过实现 Runnable 接口,使得该类有了多线程类的特色。run()办法是多线程程序的一个执行指标。所有的多线程代码都在 run 办法外面。Thread 类实际上也是实现了 Runnable 接口的类。
  • 在启动的多线程的时候,须要先通过 Thread 类的构造方法 Thread(Runnable target) 结构出对象,而后调用 Thread 对象的 start()办法来运行多线程代码。
  • 实际上所有的多线程代码都是通过运行 Thread 的 start()办法来运行的。因而,不论是继承 Thread 类还是实现 Runnable 接口来实现多线程,最终还是通过 Thread 的对象的 API 来控制线程的,相熟 Thread 类的 API 是进行多线程编程的根底。
  • Runnable 对象仅仅作为 Thread 对象的 target,Runnable 实现类里蕴含的 run()办法仅作为线程执行体。而理论的线程对象仍然是 Thread 实例,只是该 Thread 线程负责执行其 target 的 run()办法。

2.5-Runnable 和 Thread 的关系(理解)

创立线程形式 2 如同比创立线程形式 1 操作要麻烦一些,为何要多此一举呢?

因为如果一个类继承 Thread,则不适宜资源共享。然而如果实现了 Runable 接口的话,则很容易的实现资源共享。

实现 Runnable 接口比继承 Thread 类所具备的劣势:

  • 适宜多个雷同的程序代码的线程去共享同一个资源。
  • 能够防止 java 中的单继承的局限性。
  • 减少程序的健壮性,实现解耦操作,代码能够被多个线程共享,代码和线程独立。
  • 线程池只能放入实现 Runable 或 Callable 类线程,不能间接放入继承 Thread 的类。(前面篇幅介绍)

扩大理解

在 java 中,每次程序运行至多启动 2 个线程。一个是 main 线程,一个是垃圾收集线程。因为每当应用 java 命令执行一个类的时候,实际上都会启动一个 JVM,每一个 JVM 其实在就是在操作系统中启动了一个过程。

2.6- 匿名外部类形式实现线程创立(重要)

应用线程的内匿名外部类形式,能够不便的实现每个线程执行不同的线程工作操作。

简而言之,应用匿名外部类能够简化代码。

    // 匿名外部类创立线程形式 1
    new Thread(){
      @Override
      public void run() {System.out.println(Thread.currentThread().getName());
      }
    }.start();
    // 匿名外部类创立线程形式 2
    new Thread(new Runnable() {
      @Override
      public void run() {System.out.println(Thread.currentThread().getName());
      }
    }).start();

第三章:线程平安问题

3.1- 线程平安概述(了解)

多个线程执行同一个工作并操作同一个数据时,就会造成数据的平安问题。咱们通过以下案例来看线程平安问题。

案例需要

电影院要卖票,咱们模仿电影院的卖票过程。假如要播放的电影是“皮卡丘大战葫芦娃”,本次电影的座位共 100 个 (本场电影只能卖 100 张票)。

咱们来模仿电影院的售票窗口,实现多个窗口同时卖“皮卡丘大战葫芦娃”这场电影票(多个窗口一起卖这 100 张票) 须要窗口,采纳线程对象来模仿;须要票,Runnable 接口子类来模仿。

案例代码

//【操作票的工作代码类】public class RunnableImpl implements Runnable {
  // 线程工作要操作的数据(100 张电影票)private int ticket = 100;
  // 线程要执行的工作
  @Override
  public void run() {while (true){if(ticket>0){System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket+"张票");
              ticket--;
        }else {break;}
    }
  }
}
//【测试类】public class Main01 {public static void main(String[] args) {
    // 创立线程工作
    RunnableImpl ra = new RunnableImpl();
    // 创立第一个线程执行线程工作
    new Thread(ra).start();
    // 创立第二线程执行线程工作
    new Thread(ra).start();
    // 创立第三个线程执行线程工作
    new Thread(ra).start();}
}

执行后果及问题

问题起因

争夺 cpu 执行权和线程执行工夫是不确定的,比方线程 0 抢到了 cpu 执行权并执行到了打印代码处,此时 cpu 又被线程 1 争夺,其余线程处于期待线程 1 页执行到了打印代码处,没等 ticket–,两个线程都打印了售票信息。

这种问题,几个窗口 (线程) 票数 不同步 了,这种问题称为线程不平安。

线程平安问题都是由全局变量及动态变量引起的。若每个线程中对全局变量、动态变量只有读操作,而无写 操作,一般来说,这个全局变量是线程平安的;若有多个线程同时执行写操作,个别都须要思考线程同步,否则的话就可能影响线程平安。

3.2- 线程平安解决方案(重要)

上述咱们晓得,线程平安问题是因为线程在操作数据时不同步造成的,所以只有可能实现操作数据同步,就能够解决线程平安问题。

同步指的就是,当一个线程执行指定同步的代码工作时,其余线程必须等该线程操作结束后再执行。

依据案例形容:窗口 1 线程进入操作的时候,窗口 2 和窗口 3 线程只能在外等着,窗口 1 操作完结,窗口 1 和窗口 2 和窗口 3 才有机会进入代码 去执行。也就是说在某个线程批改共享资源的时候,其余线程不能批改该资源,期待批改结束同步之后,能力去争夺 CPU 资源,实现对应的操作,保障了数据的同步性,解决了线程不平安的景象。

为了保障每个线程都能失常执行原子操作,Java 引入了线程同步机制(synchronize)。

那么怎么去应用呢?有三种形式实现同步操作:

  1. 同步代码块
  2. 同步办法
  3. 同步锁

3.3- 同步代码块(重要)

概述

同步代码块: synchronized关键字能够用于办法中的某个区块中,示意只对这个区块的资源履行互斥拜访。

格局

synchronized(同步锁){须要同步操作的代码}

  • 同步锁:对象的同步锁只是一个概念, 能够设想为在对象上标记了一个锁。
  • 锁对象能够是任意类型。
  • 多个线程对象 要应用同一把锁。
  • 留神:在任何时候, 最多容许一个线程领有同步锁, 谁拿到锁就进入代码块, 其余的线程只能在外等着。

示例代码

//【测试类】public class Main01 {public static void main(String[] args) {
    // 创立线程工作
    RunnableImpl ra = new RunnableImpl();
    // 创立第一个线程执行线程工作
    new Thread(ra).start();
    // 创立第二线程执行线程工作
    new Thread(ra).start();
    // 创立第三个线程执行线程工作
    new Thread(ra).start();}
}
//【线程工作类】public class RunnableImpl implements Runnable {
  // 线程工作要操作的数据
  private int ticket = 100;
  // 定义线程锁对象(任意对象)Object obj = new Object();
  // 线程工作
  @Override
  public void run() {while (true){synchronized (obj){if(ticket>0){
          try {Thread.sleep(10);
            System.out.println(Thread.currentThread().getName()+"在售卖第" + ticket + "张票");
            ticket--;
          } catch (InterruptedException e) {e.printStackTrace();
          }

        }else{break;}
      }

    }
  }
}

3.4- 同步办法(重要)

概述

同步办法:应用 synchronized 润饰的办法, 就叫做同步办法, 保障 A 线程执行该办法的时候, 其余线程只能在办法外等着

格局

public synchronized void method(){ // 可能会产生线程平安问题的代码}

同步锁是谁?

  • 对于非 static 办法, 同步锁就是 this
  • 对于 static 办法, 咱们应用以后办法所在类的字节码对象(类名.class)。

示例代码

//【测试类】public class Main01 {public static void main(String[] args) {
    // 创立线程工作
    RunnableImpl ra = new RunnableImpl();
    // 创立第一个线程执行线程工作
    new Thread(ra).start();
    // 创立第二线程执行线程工作
    new Thread(ra).start();
    // 创立第三个线程执行线程工作
    new Thread(ra).start();}
}
//【线程工作类】public class RunnableImpl implements Runnable {
  // 线程工作要操作的数据
  private int ticket = 100;
  @Override
  public void run() {while (true) {int flag = func();
      if(flag==0) {break;}
    }

  }
  public synchronized int func() {if (ticket > 0) {
      try {Thread.sleep(10);
      } catch (InterruptedException e) {e.printStackTrace();
      }
      System.out.println(Thread.currentThread().getName() + "在售卖第" + ticket + "张票");
      ticket--;
      return 1;
    }else {return 0;}
  }
}

3.5- 同步锁(重要)

概述

Lock:java.util.concurrent.locks.Lock 机制提供了比 synchronized 代码块和 synchronized 办法更宽泛的锁定操作, 同步代码块 / 同步办法具备的性能 Lock 都有, 除此之外更弱小, 更体现面向对象。

格局

Lock 锁也称同步锁,加锁与开释锁办法化了

  • public void lock() : 加同步锁。
  • public void unlock() : 开释同步锁。

示例代码

//【测试类】public class Main01 {public static void main(String[] args) {
    // 创立线程工作
    RunnableImpl ra = new RunnableImpl();
    // 创立第一个线程执行线程工作
    new Thread(ra).start();
    // 创立第二线程执行线程工作
    new Thread(ra).start();
    // 创立第三个线程执行线程工作
    new Thread(ra).start();}
}
//【线程工作类】public class RunnableImpl implements Runnable {
  // 线程工作要操作的数据
  private int ticket = 100;
  // 创立锁对象
  Lock lock = new ReentrantLock();
  @Override
  public void run() {while (true) {
      // 开启同步锁
      lock.lock();
      if (ticket > 0) {
        try {Thread.sleep(10);
          System.out.println(Thread.currentThread().getName() + "在售卖第" + ticket + "张票");
          ticket--;
        } catch (InterruptedException e) {e.printStackTrace();
        }finally {
          // 开释同步锁
          lock.unlock();}

      }else {break;}
    }

  }
}
 

第四章:线程状态

4.1- 线程状态介绍(理解)

当线程被创立并启动当前,它既不是一启动就进入了执行状态,也不是始终处于执行状态。在线程的生命周期中,有几种状态呢?在 API 中 java.lang.Thread.State 这个枚举中给出了 六种 线程状态:

咱们不须要去钻研这几种状态的实现原理,咱们只需晓得在做线程操作中存在这样的状态。那咱们怎么去了解这几 个状态呢,新建与被终止还是很容易了解的,咱们就钻研一下线程从 Runnable(可运行)状态与非运行状态之间 的转换问题。

4.2-TimedWaiting 计时期待(理解)

概述

Timed Waiting 在 API 中的形容为:一个正在限时期待另一个线程执行一个(唤醒)动作的线程处于这一状态。

独自 的去了解这句话,真是玄之又玄,其实咱们在之前的操作中曾经接触过这个状态了,在哪里呢?在咱们写卖票的案例中,为了缩小线程执行太快,景象不显著等问题,咱们在 run 办法中增加了 sleep 语句,这样就 强制以后正在执行的线程休眠(暂停执行),以“减慢线程”。

其实当咱们调用了 sleep 办法之后,以后执行的线程就进入到“休眠状态”,其实就是所谓的 Timed Waiting(计时等 待),那么咱们通过一个案例加深对该状态的一个了解。

示例

需要:实现一个计数器,计数到 100,在每个数字之间暂停 1 秒,每隔 10 个数字输入一个字符串。

public class MyThread extends Thread {public void run() {for (int i = 0; i < 100; i++) {if ((i) % 10 == 0) {System.out.println("‐‐‐‐‐‐‐" + i);
            }
            System.out.print(i);
            try {Thread.sleep(1000);
                System.out.print("线程睡眠 1 秒!\n");
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {new MyThread().start();}
}

通过案例能够发现,sleep 办法的应用还是很简略的。咱们须要记住上面几点:

  1. 进入 TIMED_WAITING 状态的一种常见情景是调用的 sleep 办法,独自的线程也能够调用,不肯定非要有协 作关系。
  2. 为了让其余线程有机会执行,能够将 Thread.sleep()的调用放线程 run()之内。这样能力保障该线程执行过程 中会睡眠。
  3. sleep 与锁无关,线程睡眠到期主动昏迷,并返回到 Runnable(可运行)状态。

留神:sleep()中指定的工夫是线程不会运行的最短时间。因而,sleep()办法不能保障该线程睡眠到期后就 开始立即执行。

图解

4.3-Blocked 锁阻塞(理解)

概述

Blocked 状态在 API 中的介绍为:一个正在阻塞期待一个监视器锁(锁对象)的线程处于这一状态。

咱们曾经学完同步机制,那么这个状态是十分好了解的了。比方,线程 A 与线程 B 代码中应用同一锁,如果线程 A 获 取到锁,线程 A 进入到 Runnable 状态,那么线程 B 就进入到 Blocked 锁阻塞状态。

这是由 Runnable 状态进入 Blocked 状态。除此 Waiting 以及 Time Waiting 状态也会在某种状况下进入阻塞状态。

图解

4.4-Waiting 有限期待(理解)

概述

Wating 状态在 API 中介绍为:一个正在无限期期待另一个线程执行一个特地的(唤醒)动作的线程处于这一状态。

示例

咱们通过一段代码来 学习一下:需要如下,消费者吃包子。过程如下:

  • 消费者问:包子好了吗?处于期待 …
  • 3 秒钟后 ….
  • 老板答:包子好了
  • 消费者:能够吃包子了

示例代码:

public class Test04 {
    // 锁对象
    public static Object obj = new Object();

    public static void main(String[] args) {
        //【消费者线程】new Thread(new Runnable() {
            @Override
            public void run() {while (true) {synchronized (obj) {System.out.println(Thread.currentThread().getName() + "- 顾客 1:老板包子好了吗?");
                        try {
                            // 期待,开释锁,处于阻塞状态
                            obj.wait();} catch (InterruptedException e) {e.printStackTrace();
                        }
                        // 唤醒之后要执行的代码
                        System.out.println(Thread.currentThread().getName() + "顾客 1:能够吃包子了。");
                        System.out.println("--------------------------------------");
                    }
                }
            }
        }).start();
        //【生产者线程】new Thread(new Runnable() {
            @Override
            public void run() {while (true) {
                    try {Thread.sleep(3000);
                    } catch (InterruptedException e) {e.printStackTrace();
                    }
                    synchronized (obj) {System.out.println("期待 3 秒后...");
                        System.out.println(Thread.currentThread().getName() + "老板说:包子好了!");
                        // 唤醒,唤醒其余被阻塞的线程
                        obj.notify();}
                }
            }
        }).start();}
}

剖析

通过上述案例咱们会发现,一个调用了某个对象的 Object.wait 办法的线程会期待另一个线程调用此对象的 Object.notify()办法 或 Object.notifyAll()办法。

其实 waiting 状态并不是一个线程的操作,它体现的是多个线程间的通信,能够了解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。就好比在公司里你和你的共事们,你们可能存在降职时的竞 争,但更多时候你们更多是一起单干以实现某些工作。

当多个线程合作时,比方 A,B 线程,如果 A 线程在 Runnable(可运行)状态中调用了 wait()办法那么 A 线程就进入 了 Waiting(有限期待)状态,同时失去了同步锁。如果这个时候 B 线程获取到了同步锁,在运行状态中调用了 notify()办法,那么就会将有限期待的 A 线程唤醒。留神是唤醒,如果获取到锁对象,那么 A 线程唤醒后就进入 Runnable(可运行)状态;如果没有获取锁对象,那么就进入到 Blocked(锁阻塞状态)。

图解

退出移动版