11_02.【Java】多线程概述

一、线程的运行模式

为了实现在同一个时间运行多个任务,Java引入了多线程的概念。在Java中可以通通过方便、快捷的方式启动多线程模式。多线程常被应用在符合并发机制的程序中,例如网络程序等。

人体可以同时进行呼吸、血液循环、思考问题等活动,可以边听歌边聊天…这种机制在Java中被称为并发机制,通过并发机制可以实现多个线程并发执行,这样多线程就应运而生了。

以多线程在Windows操作系统中的运行模式为例,Windows操作系统事多任务操作系统,它以进程为单位。每个独立执行的程序都被称为进程,比如只在运行的QQ、微信、谷歌浏览器等,每一个都是一个进程,每个进程都包含多个线程。系统可以分配给每个进程一段使用CPU的时间,然后CPU在这段时间执行某个进程,进程中的每个线程也被分配到一小段执行时间,这样一个进程就可以具有多个线程并发执行的线程。接下来,下一个CPU时间段又执行另外另一个进程。由于CPU转换的较快,可以使每个进程好像是被同时执行一样。

多线程在Windows操作系统中的运行模式如下:

在这里插入图片描述

二、多线程的实现方式

Java提供了三种实现多线程的方式,分别是继承java.lang.Thread类实现,通过java.lang.Runnable接口实现以及通过Callable和Future创建线程。

(1)继承Thread类覆写run()方法实现多线程

  • 1)创建一个继承Thread类的子类。

  • 2)覆写Thread类的run()方法。

  • 3)创建线程类的一个对象。

  • 4)通过线程类的对象调用start()方法启动线程(启动之后会自动调用覆写的run()方法执行线程)。

(2)通过Runnable接口实现多线程

  • 1)创建实现Runnable接口的类

    • ①实现Runnable接口
    • ②重写run方法,告知系统程序应该如何运行
    • ③在实现类中使用参数为Runnable对象的构造方法创建Thread对象,使任务在线程中执行。
  • 2)创建测试程序实现的类

    • ④创建Runnable实现类的测试类
    • ⑤实例化测试对象,并为实例化对象分配线程
    • ⑥调用start()方法,启动线程

(3)通过Callable和Future创建线程

  • 1)创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
  • 2)创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
  • 3)使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
  • 4)调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。

采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。

使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。

三、线程的优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。

Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。

默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

四、线程的生命周期

线程是具有生命周期的,其中包含5种状态:出生状态、就绪状态、运行状态、暂停状态(包括休眠状态、等待状态和阻塞状态)、死亡状态。

1、线程生命周期的5个状态

(1)出生状态

出生状态就是线程被创建时的状态,是指线程已经被创建但尚未被指定(start()尚未被调用)。

我们使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于出生状态。它保持这个状态直到程序 start() 这个线程。

(2)就绪状态

当线程对象调用了start()方法之后,该线程就进入就绪状态(也称为可执行状态)。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。这个状态下中的线程虽然不一定正在执行,但CPU时间随时可能被分配给该线程。

(3)运行状态

如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

(4)阻塞状态

阻塞状态:是指线程没有被分配到CPU时间,无法执行。如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

  • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
  • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
  • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

(5)死亡状态

一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

正常情况下,run()方法返回使得线程死亡,这时候会产生异常。调用stop()或者destory()也有同样的效果,但是不推荐使用stop和destory,因为属于强制种植,不会释放锁。

2、线程生命周期状态图

在这里插入图片描述

五、线程的操作

1、创建线程

new()
  • 继承 Thread 类
  • 实现 Runnable 接口

2、启动线程

.start()
  • 调用 start() 方法

3、线程的休眠

.sleep()

能控制线行为的方法之一就是调用sleep()方法让线程休眠,线程休眠指的是让线程暂缓执行,等到预计时间之后再恢复执行。

sleep()方法可以指定线程休眠时间,线程休眠的时间以毫秒为单位。

线程休眠逻辑如下:

  • 线程休眠会交出CPU,让CPU去执行其他的任务。

  • 调用sleep()方法让线程进入休眠状态后,sleep()方法并不会释放锁,即当前线程持有某个对象锁时,即使调用sleep()方法其他线程也无法访问这个对象。

  • 调用sleep()方法让线程从运行状态转换为阻塞状态;sleep()方法调用结束后,线程从阻塞状态转换为可执行状态。

4、线程的加入

.join()

假设当前程序为多线程程序,并且存在一个线程A,现在需要插入线程B,并要求线程B执行完毕后再执行线程A,此时可以使用Thread类中的join()方法来实现。这就好比A正在看电视,突然B上门收水费,A必须敷完水费之后才能继续看电视。

当某个线程使用join()方法加入到另一个线程时,另一个线程会等待该线程执行完毕后再继续执行。

5、线程让步

.yield()

线程让步是指:暂停当前正在执行的线程对象,并执行其他线程,需要用到yield()方法。

线程让步的逻辑:

  • 调用yield()方法让当前线程交出CPU权限,让CPU去执行其他线程。

  • yield()方法和sleep()方法类似,不会释放锁,但yield()方法不能控制具体交出CPU的时间。

  • yield()方法只能让拥有相同优先级的线程获取CPU执行的机会。

  • 使用yield()方法不会让线程进入阻塞状态,而是让线程从运行状态转换为就绪状态,只需要等待重新获取CPU执行的机会。

6、线程的中断

多线程中停止线程有三种方式:

(1)设置标记位,让线程正常停止。

我们可以给线程设置标记位,修改标记位的值可以让我们自己创建的线程停止。

现在提倡在run()方法中使用无限循环的形式,然后使用一个布尔类型标记控制循环的停止。

private class InterruptedTest implements Runnable{
	private boolean isContine = false;     //设置一个标记变量,默认值为false

  public void run(){                     //重写run()方法
    while(true){             
      //code
      if(isContinue)                     //当isContinue变量为true时,停止线程
        break;
    }
  }

  public void setContinue(){             //定义设置isContinue变量为true的方法
    this.isContinue = true;
  }
}

如果线程是因为使用了sleep()方法或者wait()方法进入就绪状态,可以使用Thread类中的interrupt()方法使线程进离开run(),同时结束线程。但此时程序会抛出InterruptedException异常,可以在处理该线程异常时完成线程的中断业务,如终止while循环。

(2)使用Thread类的interrupt()方法中断线程

.stop()
  • interrupt()方法只是改变中断状态而已,它不会中断一个正在运行的线程。具体来说就是,调用interrupt()方法只会给线程设置一个为true的中断标志,而设置之后,则根据线程当前状态进行不同的后续操作。
  • 如果线程的当前状态出于非阻塞状态,那么仅仅将线程的中断标志设置为true而已;
  • 如果线程的当前状态出于阻塞状态,那么将在中断标志设置为true后,还会出现wait()、sleep()、join()方法之一引起的阻塞,那么会将线程的中断标志位重新设置为false,并抛出一个InterruptedException异常。
  • 如果在中断时,线程正处于非阻塞状态,则将中断标志修改为true,而在此基础上,一旦进入阻塞状态,则按照阻塞状态的情况来进行处理。例如,一个线程在运行状态时,其中断标志设置为true之后,一旦线程调用了wait()、sleep()、join()方法中的一种,立马抛出一个InterruptedException异常,且中断标志被程序自动清除,重新设置为false。

调用Thread类的interrupted()方法,其本质只是设置该线程的中断标志,将中断标志设置为true,并根据线程状态决定是否抛出异常。因此,通过interrupted()方法真正实现线程的中断原理是 :开发人员根据中断标志的具体值来决定如何退出线程。

(3)使用stop()方法强制使线程退出

.stop()

stop()方法的原理是:解除由线程获得的所有锁。当在一个线程对象上调用stop()方法时,这个线程对象所运行的线程会立即停止。

因此stop()方法是不安全的,例如当一个线程执行同步方法时,被调用的stop()方法会让同步方法中的线程停止,造成程序的不安全。

该已经被废弃了,不建议使用!

六、线程安全与线程同步

在单线程程序中,每次只做一件事,后面的事情需要等待前面的事情完成后才能执行。如果需要用多线程程序,就会发生两个线程抢占资源的问题,例如两个人以相反的方向同时过一个独木桥,这时候就会涉及到多线程编程中的资源抢占问题。

1、线程安全

在实际开发中,使用多线程程序的情况很多,如银行排号系统、火车站售票系统等。

多线程的程序通常会存在一些安全问题,以火车站售票系统为例:有一段判断当前的票数是否大于0的程序,如果程序判断大于0就执行把火车票售给乘客操作。

现在发生了一种常见的情况:如果当前仅剩下少量的票,多个线程同时访问判断程序后得到当前票数大于0的结果,如果此时访问的线程数大于剩余票数,多个线程又根据判断结果(大于0)都执行了售票操作,那么系统票数就会为负数,明显这种情况是不能被允许的。因此,我们在编写多线程程序时,应该考虑到线程安全问题。

而实际上,线程安全问题大多来源于多个线程同时操作单一对象数据的情况。

2、线程同步机制

当出现线程安全问题时,我们应该如何解决资源抢占问题呢?这时候我们就需要借助线程同步机制来防治资源冲突。

所有解决线程资源冲突问题的方法都是在指定时间只允许一个线程访问共享资源,这时候就需要给共享资源上一道锁,这就好比一个人上洗手间时,进入洗手间就上锁,防止其他人进入;出来时再将锁打来,然后其他人就可以进去了。

Java的线程同步机制,主要有两方面的内容:

(1)同步块

同步机制使用synchronized关键字,使用该关键字的代码块称为同步块,也成临界区。

synchronized(Object){
  
}

通常将共享资源的操作放置在synchronized定义的区域内,这样当其他线程获取到这个锁时,就必须等待锁被释放后才能进入该区域。Object为任意一个对象,每个对象都存在一个标识位置,并具有两个值,分别为0和1。

代码块中程序执行逻辑如下:

  • 一个线程运行到同步代码块时首先检查该对象的标识位,如果为0状态,表明此同步块内存在其他线程,这时当前线程处于就绪状态。
  • 直到处于同步代码块中的线程执行玩同步代码块中的代码后,这时该对象的标识位设置为1,当前线程才能开始执行同步代码块中的代码。
  • 执行同步块中的代码的同时,将Object对象的标识位设为0,防止其他线程执行同步代码块中的代码。

(2)同步方法

同步方法就是被synchronized关键字修饰的方法。

synchronized void f(){
  
}

当某个对象调用了同步方法时,该对象的其他同步方法就必须等待该同步方法执行完毕后才能被执行。必须将每个能访问共享资源的方法都修饰为synchronized,否在会出现错误。

将共享资源放置在同步方法总中,运行结果与使用代码块的结果是一致的。

七、多线程的使用原则

  • 有效利用多线程的关键是理解程序是并发执行而不是串行执行的。例如:程序中有两个子系统需要并发执行,这时候就需要利用多线程编程。

  • 通过对多线程的使用,可以编写出非常高效的程序。不过需要注意,如果创建太多的线程,程序执行的效率实际上是降低了,而不是提升了。

  • 需要注意:上下文的切换开销也很重要,如果你创建了太多的线程,CPU 花费在上下文的切换的时间将多于执行程序的时间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值