第十章多线程(暑假啃书提升计划)

(端午节给自己放了两天假,另外swing章节先跳过了)

多线程

一. 线程和多线程

1. 线程的概念

  • 进程

    • 程序的一次动态执行,对应了从代码加载,执行至执行完毕的一个完整过程
    • 一个进程既包括所要执行的指令又包括执行指令需要的任何系统资源(CPU,内存空间等)
    • 不同进程所占用的系统资源相对独立
  • 线程

    • 线程是进程执行过程中产生的多条执行线索比进程单位更小的执行单位
    • 没有出入口,因此自身不能自动运行,必须栖身于一个进程之中
    • 同一进程的所有线程共享该进程的资源(例如qq是一个进程,qq里面的功能是线程,它们之间的资源是共享的)
    • 线程间的切换速度比进程快得多
  • 多线程

    • 宏观角度来说就是使多个作业同时执行

    • 多线程可以使系统资源特别是CPU的利用率得到提高,整个程序的执行效率得到提高

    • Java把线程或执行环境当作一个封装对象,包含CPU以及自己的程序代码和数据,由虚拟机提供控制

    • Thread允许创建这样的线程,并可控制所创建的线程

2. 线程的结构

  • 线程的组成
    • 虚拟CPU,封装在Thread类中,控制整个线程的运行
    • 执行的代码,传递给Thread类,由Thread控制按序执行
    • 处理的数据,传递给Thread,在代码执行过程中需要处理的数据
  • 当一个线程被构造时,由构造方法参数,执行代码,操作数据来初始化
  • 线程与进程的区别
    • 多线程编程简单,效率高,使用多线程可以在线程间直接共享数据和资源
    • 多线程适合开发有多种交互接口的程序
    • 其机制可以减轻编写交互频繁,涉及面多的程序问题(如侦听网络端口的程序)
    • 提供了Thread类来实现多线程

3. 线程的状态

  • 线程的4种状态图解

    • **Thread类本身只是线程的虚拟CPU,**线程所要完成的功能是通过run()来完成的
    • run称为线程体,实现线程体的特定对象是在初始化线程时传递给线程的
    • 在一个线程被建立并初始化之后,Java运行时系统自动调用run( )方法

在这里插入图片描述

  • 新建

    • 线程刚创建还未启动,此时处于新建状态,但已有了相应的内存空间以及其他资源
  • 可运行状态

    • 这种情况下线程可能正在运行,也可以没运行,只要CPU一空闲,马上运行
    • 可运行但没在运行的线程都排在一个队列中,称为就绪队列
    • 调用线程的 start()方法可使线程处于可运行状态
  • 死亡

    • run( )方法执行完毕,结束运行
    • 当线程遇到异常退出
  • 阻塞

    • 阻塞时线程不能进入就绪队列 , 必须等引起阻塞的原因消除,才可重新进入队列
    • sleep 和 wait 是两个常用的引起阻塞的办法

二. 创建线程

1. 继承Thread类创建线程

  • Thread类中一个典型的构造方法

    • Thread(ThreadGroup group, Runnable target, String name)
      
      • name作为新线程的名字,且是group中的一员
      • target必须实现Runnable接口, 它是另外一个线程对象, 当本线程启动时, 将调用target的run( )方法
      • 当target为null时, 启动本线程的run( )方法
  • Thread类本身实现了Runnable接口

    • 任何实现Runnable接口的对象都可以作为一个线程的目标对象
    • 构造方法中的各参数都可以缺省
    • 在Thread类派生出一个子类, 在类中一定要实现run( )方法
  • 定义一个线程类,继承Thread类并重写run( ), Java只支持单重继承, 用这种方法定义的类不能再继承其他类

    • Thread的构造方法中有一个Runnable实例的参数
  • 使用Thread类的子类创建线程

    • public class Test extends Thread {
      
          @Override
          public void run() {                 // 线程体
              for (int i = 0; i < 5; i++) {
                  System.out.println("我是钢铁侠");     // 输出6次信息
                  try {
                      sleep(500);                // 线程等待一会鹅
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          }
      }
      
      class Test2 extends Thread {
      
          @Override
          public void run() {
              for (int i = 0; i < 5; i++) {
                  System.out.println("我是美国队长");
                  try {
                      sleep(300);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          }
      }
      
      class ThreadTest {
      
          public static void main(String[] args) {
              Test test = new Test();     // 创建线程
              Test2 test2 = new Test2();
              test.start();   // 启动线程
              test2.start();
          }
      
      }
      
      
      控制台:
      我是钢铁侠
      我是美国队长
      我是美国队长
      我是钢铁侠
      我是美国队长
      我是美国队长
      我是钢铁侠
      我是美国队长
      我是钢铁侠
      我是钢铁侠
      
      • 定义了Test 和 Test2两个类, 都是Thread类的子类, 都是线程类
      • 覆盖了父类中的run( )方法
      • 启动线程时, 它们的线程体是不一样的
      • 执行顺序按系统来决定

2. 实现Runnable接口创建线程

  • 任何实现线程功能的类都必须实现该接口

  • 使用Runnable接口实现多线程时, 也必须实现run( )方法,必须使用start( )启动线程

  • Runnable接口创建线程

    • public class Test implements Runnable {
      
          private int i;
      
          @Override
          public void run() {
              for (; i < 20; i++) {
                  System.out.println(Thread.currentThread().getName()+"\t"+i);
                  if (i == 20) {
                      System.out.println(Thread.currentThread().getName()+"\t"+"over");
                  }
              }
          }
      }
      
      class MyThreadTest{
          public static void main(String[] args) {
              for (int i = 0; i < 10; i++) {
                  System.out.println(Thread.currentThread().getName()+"\t"+i);
                  if (i == 5) {
                      Test t1 = new Test();
                      Thread thread1 = new Thread(t1, "线程1");
                      Thread thread2 = new Thread(t1, "线程2");
                      thread1.start();
                      thread2.start();
                  }
              }
          }
      }
      
      
      控制台:
       main	0
      main	1
      main	2
      main	3
      main	4
      main	5
      main	6
      main	7
      main	8
      main	9
      线程1	0
      线程1	1
      线程1	2
      线程1	3
      线程1	4
      线程1	5
      线程1	6
      线程1	7
      线程1	8
      线程1	9
      线程1	10
      线程1	11
      线程1	12
      线程1	13
      线程1	14
      线程1	15
      线程1	16
      线程1	17
      线程1	18
      线程1	19
      线程2	0
      线程2	over
      
      • Test是实现了Runnable接口的类,所以可以使用它创建线程t1
      • 然后又将t1作为构造方法中的target创建了两个线程thread1 和 thread2
      • 这两个线程使用的是同一个t1 , 它们共享t1, 即都执行t1的run( )方法
      • 先输出主线程main的, 然后两个新线程共享的信息随机输出

3. 创建线程的两种方法的适用条件

  • 适用于采用实现Runnable接口方法的情况

    • 如果一个类已经继承了Thread, 就不能再继承其他类, 就被迫采用实现Runnable的方法
    • 原本的线程是采用实现Runnable接口的方法**,为了保持风格一致继续使用这种方法**
  • 适用于采用继承Thread方法的情况

    • 当一个run( )方法置于Thread的子类中时, this实际上引用的是控制当前运行系统的Thread实例

    • Thread.currentThread().getState();    
      
    • 可简写为

      • getState();
        
    • 代码比较简洁, 所以很多情况下愿意使用继承Thread的方法

三. 线程的基本控制

1. 线程的启动

  • 要使线程真正的运行, 必须通过 start( )方法来启动, 此时线程中的虚拟CPU已经就绪

  • API 提供了以下有关线程的操作方法

    • start( ); 启动线程对象, 让线程从新建状态转为就绪状态
    • run( ); 用来定义线程对象被调度之后所执行的操作, 用户必须重写run( )方法
    • yield( ); 强制终止线程的执行
    • isAlive( ); 测试当前线程是否在活动
    • sleep( ); 使线程休眠一段时间
    • void wait( ); 使线程处于等待状态

2. 线程调度

  • Java中线程调度是抢占式

    • 抢占式调度是指可能有多个线程准备运行, 只有一个在真正的运行
    • 一个线程获得执行权, 这个线程将持续运行下去
  • 直到运行结束或者某种原因阻塞, 或者有另外一个高优先级线程就绪

  • 线程调度的优先级策略

    • 优先级高的先执行
    • 每个线程创建时会自动分配一个优先级, 默认时,继承其父类的优先级
    • 任务紧急的线程, 其优先级较高
    • 同优先级的线程 按照 先进先出 的调度原则
  • 线程优先级的静态量

    • MAX_PRIORITY 最高优先级, 10
    • MIN_PRIORITY 最低优先级, 1
    • NORM_PRIORITY 默认优先级 5
  • 优先级的几个常用方法

    • void setPriority(int newPriority)	// 重置线程优先级
      
    • int getPriority()	// 获取当前线程的优先级
      
    • void yield()		// 停止当前正在执行的线程,即让当前线程放弃执行权
      
  • 线程被阻塞的原因可能:

    • 执行了sleep调用, 暂停一段时间
    • 也可能需要等待一个较慢的外部设备, 例如磁盘或者用户操作的键盘
  • 所有被阻塞的线程按次序排列, 组成一个阻塞队列

  • 所有就绪但没运行的线程根据其优先级进入一个就绪队列

  • 当CPU空闲时, 就绪队列中第一个具有最高优先级的线程将运行

  • 当一个线程被抢占而停止运行时, 它的运行状态会被改变并放到就绪队列的队尾

  • 一个被阻塞的线程就绪后也会放到就绪队列的队尾

  • 调用sleep方法

    • 由于Java线程调度不是时间片式, 在程序设计时要合理安排不同线程之间的运行顺序

    • 保证其他线程留有执行的机会

    • @Override
      public void run() {
          
          while (true) {
              // 执行若干操作
              // 给其他线程运行的机会
              try {
                  Thread.sleep(10);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
          
      }
      
      • sleep中的参数指定了必须休眠的时间
      • 最小时间之后只保证它回到就绪状态, 能否获到CPU运行, 要视线程调度而定
  • yield方法

    • yield方法可以给其他同优先级线程一个运行的机会
    • 如果在就绪队列中有其他同优先级的线程, yield把调用者放入就绪队列队尾, 并允许其他线程运行
    • 如果没有同优先级, 则yield不做任何工作
    • sleep 允许低优先级线程运行, yield 只给同优先级线程允许运行

3. 结束线程

  • 线程的死亡

    • 当一个线程从run( )方法的结尾处返回时, 自动消亡并不能再被运行, 可理解为自然死亡
    • 遇到异常使得线程结束, 将其理解为强迫死亡, 可以使用interrupt( )方法中断线程的执行
  • 可以利用Thread类中的静态方法currentThread( )来引用正在运行的线程

  • 可以使用 isAlive( )来获取一个线程是否还在活动状态的信息

  • 活动状态不意味着这个线程正在执行, 只说明这个线程已经被启动

4. 挂起线程

  • 暂停一个线程也称为挂起, 挂起之后必须重新唤醒线程进入就绪状态

  • 挂起线程的方法有以下几种

    • sleep

      • 暂时停止一个线程的执行, 不是休眠期满就立刻被唤醒, 此时有可能其他线程正在执行
      • 重新调度只在以下几种情况发生
        • 被唤醒的线程具有更高的优先级
        • 正在执行的线程因为其他原因被阻塞
        • 程序处于支持时间片的系统中
    • wait notify notifyAll

      • wait方法导致当前线程等待, 直到其他线程调用此对象的notify 或者 notifyAll方法, 才可唤醒线程
    • join

      • 将引起当前正在执行的线程等待, 直到join方法所调用的线程结束

      • 比如在线程B中调用了线程A的join方法, 直到线程A执行完毕, 才会继续执行线程B(强行插队)

      • join方法可以在调用时使用一个以毫秒计的时间值

      • void join(long millis)
        
        • 此时join方法将挂起线程millis毫秒, 或者直到线程的结束

四. 线程的互斥

1. 互斥问题的提出

  • 同时运行的线程需要共享数据, 此时每个线程就必须要考虑与它一起共享数据线程的状态和行为

  • 否则就无法保证共享数据的一致性,因此就不能保证正确性

  • 设计一个代表栈的类

    • public class Stack {
      
          int idx = 0;
          char data[] = new char[6];
      
          public void push(char c) {
              data[idx] = c;
              idx ++;
          }
      
          public char pop() {
              idx --;
              return data[idx];
          }
      
      }
      
      • 栈具有后进先出模式,使用 下标值idx 表示栈中下一个放置元素的位置
      • 设想有两个独立的 线程A 和 B 都具有对这个类同一对象的引用,线程A 负责入栈,线程B 负责出栈
      • 要求 线程A 放入栈中的数据都要由 线程B 读出,看起来可以将数据成功移出移入
      • 但因为入栈方法push()和出栈方法 pop()中含有多条语句,执行过程中存在问题
        • 假设此时栈中已有字符 1 和 2 ,当前线程 A 要入栈一个字符 3 ,调用push(3)
        • 执行了语句 data[idx] = c; 后被其他线程抢占了,此时还没执行 idx++ 语句
        • 如果此时线程被唤醒,可以继续修正 idx 的值,否则,入栈操作执行了一半,恰巧线程B此时正占有CPU,调用 pop(),执行出栈操作,则它返回的字符是2,因为它先执行 idx- - 语句,idx的值变为 1 ,返回的是 data[1] 处的字符,即字符串 2 ,字符 3 被漏掉了
    • 以上例子说明的就是 多线程访问共享数据时通常会引发的问题

      • 原因是:对共享资源访问的不完整性
      • 有一种机制保证对共享数据的完整性
        • 对共享数据操作的同步
        • 共享数据称为条件变量
    • 可以选择禁止线程在完成关键代码部分时被切换

      • 对于线程A就是 入栈操作 以及 下标值增加操作
      • 对于线程B就是 下标值递减 和 出栈操作(它们要么一起完成,要么都不执行

2. 对象的锁定标志

  • 对象互斥锁阻止多个线程同时访问同一个条件变量

  • 有两种方法可以实现 对象互斥锁

    • 关键字 volatile声明一个共享数据(变量)
    • 关键字 synchronization声明操作共享数据的一个方法或一段代码
  • 设想一个场景

    • 有一间实验室,实验人员都可以来用,但任何时候实验室只允许一组实验人员在里面做实验
    • 否则就会引发混乱,为了进行控制,在门口设置了一把锁
    • 实验室没人的时候锁是开放的,有人员进入之后第一件事就是将门锁上,然后开始工作
    • 之后如果有人再想进入,会因为门已被锁只能等待,直到里面的实验人员完成工作后将锁打开才能进入
    • 这种机制保证了在一组人员工作的过程中不会被另一组人员打断
    • 保证了数据操作的完整性,在同一时刻只能有一个任务访问的代码区称为临界区
  • 修改栈实例

    • public class Stack {
      
          int idx = 0;
          char data[] = new char[6];
      
          public void push(char c) {
              synchronized (this) {   // 增加同步标志
                  data[idx] = c;
                  idx ++;
              }
          }
      
          public char pop() {
              synchronized (this) {   // 增加同步标志
                  idx --;
                  return data[idx];
              }
          }
      
      }
      
      • 当线程执行到被同步的语句时,将传递的对象参数设为锁定标志,禁止其他线程对该对象的访问
      • 对pop()和 push()操作的部分增加了一个对 synchronization(this)的调用,在第一个线程拥有锁定标记时,如果另一个线程企图执行 synchronization(this)中的语句时
      • 将从对象 this 中索取锁定标记,因为这个标记不可得,该线程不能继续执行
      • 实际上将这个线程加入了一个等待队列,当标志被返回给对象时,等待标志的第一个线程得到该标志继续运行
      • 当持有锁定标志的线程运行完 synchronization()调用包含的程序块之后
      • 不管这个程序块是否发生了异常,或者循环中断跳出了该程序块,这个标志都将自动返还
      • 如果同一个线程两次调用了同一个对象,在退出最外层后这个标志也会被正确释放
      • 在退出内层时不会执行释放
  • synchronization关键字

    • 参数必须是this,因此可以使用下面第二种简洁的写法:

      • public char pop() {
           synchronized (this) {
        
           }
        }
        
        public synchronized char pop() {
        
        }
        
      • 把synchronization作为方法的修饰字,将整个方法都视为同步块

      • 但是可能使持有锁定标记的时间比实际需要的时间要长,从而降低效率

五. 线程的同步

1. 同步问题的提出

  • 生产者和消费者问题
  • 有两个人各自代表一个线程,一个人在刷盘子,另一个人在烘干,它们之间有一个共享对象—盘架
  • 显然盘架上有刷好的盘子时,烘干的人才能开始工作,如果刷盘子的太快,占满了盘架,就不能再继续了,得等盘架有空位置才行
  • 以上例子说明一个问题
    • 生产者生产一个产品后放入共享对象中,不去管共享对象是否有产品
    • 消费者从共享对象中取产品,不检测是否已经取过
  • 若共享对象中只能存放一个数据,可能会出现以下问题
    • 生产者比消费者快时,消费者会漏掉一些数据
    • 反之,消费者会取相同重复的数据

2. 解决方法

  • 每个对象实例都有两个线程队列和它相连

    • 第一个用来排列等待锁定标志的线程
    • 第二个用来实现 wait()和 notify()交互机制
  • Java中可以使用Object类中的 wait()和 notify() / notifyAll()方法来协调线程间的运行速度关系

    • wait()方法导致当前线程等待,让当前线程释放其所持有的 对象互斥锁 ,进入 wait队列(等待队列)
    • notify() / notifyAll()方法的作用是唤醒一个或所有 正在等待队列中等待的线程,并将 它 移入同一个对象互斥锁 的队列
    • notify()和 notifyAll()方法只能在被声明为 synchronization的方法 或 代码段中调用
  • 再来看刷盘子的例子

    • 线程 A 代表刷盘子,线程 B 代表烘干,都有对盘架 drainingBoard 的访问权,假设 线程B 想要进行烘干操作,此时盘架是空的,则应该如下表示

      • synchronized (drainingBoard) {
            if (drainingBoard.isEmpty()) {
                drainingBoard.wait();
            }
        }
        
        • **当 线程B 执行了 wait()操作后,不可再执行,并加入到对象 drainingBoard 的等待队列中,**在有线程将它从这个队列中释放之前,不能再次运行
    • 烘干线程想要重新运行,则需要由 洗刷线程A 来通知它已经有工作可以做了,运行 drainingBoard 的 notify()方法可以做到

      • synchronized (drainingBoard) {
            drainingBoard.addItem(plate);
            if (!drainingBoard.isEmpty()) {
                drainingBoard.notify();
            }
         
        }
        
  • 线程执行被同步的语句时必须要拥有对象的锁定标志

  • 不会出现wait()方法时阻塞

    • 因为在执行 wait 调用时,首先将锁定标志返回给对象
    • 即使一个线程由于执行 wait 调用被阻塞,也不会影响到其他等待锁定标志的线程的运行
  • 为了避免打断程序的运行

    • 当一个线程被 notify()之后,并不会立即变为可执行状态,仅仅是从等待队列中移入锁定标志队列中

    • 在获得锁定标志之前,不能继续运行
      执行,并加入到对象 drainingBoard 的等待队列中,**在有线程将它从这个队列中释放之前,不能再次运行

    • 烘干线程想要重新运行,则需要由 洗刷线程A 来通知它已经有工作可以做了,运行 drainingBoard 的 notify()方法可以做到

      • synchronized (drainingBoard) {
            drainingBoard.addItem(plate);
            if (!drainingBoard.isEmpty()) {
                drainingBoard.notify();
            }
         
        }
        
  • 线程执行被同步的语句时必须要拥有对象的锁定标志

  • 不会出现wait()方法时阻塞

    • 因为在执行 wait 调用时,首先将锁定标志返回给对象
    • 即使一个线程由于执行 wait 调用被阻塞,也不会影响到其他等待锁定标志的线程的运行
  • 为了避免打断程序的运行

    • 当一个线程被 notify()之后,并不会立即变为可执行状态,仅仅是从等待队列中移入锁定标志队列中
    • 在获得锁定标志之前,不能继续运行
  • wait()既可以被 notify终止,也可以通过调用线程的 interrupt()方法终止

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值