Java基础:多线程

Posted Jack-Chan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java基础:多线程相关的知识,希望对你有一定的参考价值。

1. 多线程概述

人们在日常生活中,很多事情都是可以同时进行的。例如,一个人可以一边听音乐,一边打扫房间,可以一边吃饭,一边看电视。在使用计算机时,很多任务也是可以同时进行的。例如,可以一边浏览网页,一边打印文档,还可以一边聊天,一边复制文件等。计算机这种能够同时完成多项任务的技术,就是多线程技术。Java是支持多线程的语言之一,它内置了对多线程技术的支持,可以使程序同时执行多个执行片段。

1.1 多线程引入

由上图中程序的调用流程可知,这个程序只有一个执行流程,所以这样的程序就是单线程程序。假如一个程序有多条执行流程,那么,该程序就是多线程程序。

1.2 多线程概述

1.2.1 什么是进程?

进程就是正在运行的程序,是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。

在一个操作系统中,每个独立执行的程序都可称之为一个进程,也就是“正在运行的程序”。目前大部分计算机上安装的都

是多任务操作系统,即能够同时执行多个应用程序,最常见的有Windows、Linux、Unix等。在本教材使用的Windows操作系统下,鼠标右键单击任务栏,选择【启动任务管理器】选项可以打开任务管理器面板,在窗口的【进程】选项卡中可以看到当前正在运行的程序,也就是系统所有的进程,如chrome.exe、QQ.exe等。任务管理器的窗口如图所示。

在多任务操作系统中,表面上看是支持进程并发执行的,例如可以一边听音乐一边聊天。但实际上这些进程并不是同时运行的。在计算机中,所有的应用程序都是由CPU执行的,对于一个CPU而言,在某个时间点只能运行一个程序,也就是说只能执行一个进程。操作系统会为每一个进程分配一段有限的CPU使用时间,CPU在这段时间中执行某个进程,然后会在下一段时间切换到另一个进程中去执行。由于CPU运行速度很快,能在极短的时间内在不同的进程之间进行切换,所以给人以同时执行多个程序的感觉。

1.2.2 多进程有什么意义呢?

单进程的计算机只能做一件事情,而我们现在的计算机都可以做多件事情。举例:一边玩游戏(游戏进程),一边听音乐(音乐进程)。也就是说现在的计算机都是支持多进程的,可以在一个时间段内执行多个任务。并且呢,可以提高CPU的使用率,解决了多部分代码同时运行的问题。

其实,多个应用程序同时执行都是CPU在做着快速的切换完成的。这个切换是随机的。CPU的切换是需要花费时间的,从而导致了效率的降低。

1.2.3 什么是线程?

每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时运行,这些执行单元可以看作程序执行的一条条线索,被称为线程。操作系统中的每一个进程中都至少存在一个线程。例如当一个Java程序启动时,就会产生一个进程,该进程中会默认创建一个线程,在这个线程上会运行main()方法中的代码。

线程是程序的执行单元,执行路径;是进程中的单个顺序控制流,是一条执行路径;一个进程如果只有一条执行路径,则称为单线程程序。一个进程如果有多条执行路径,则称为多线程程序。

代码都是按照调用顺序依次往下执行,没有出现两段程序代码交替运行的效果,这样的程序称作单线程程序。如果希望程序中实现多段程序代码交替运行的效果,则需要创建多个线程,即多线程程序。所谓的多线程是指一个进程在执行过程中可以产生多个单线程,这些单线程程序在运行时是相互独立的,它们可以并发执行。

多线程程序的执行过程如图所示

图中所示的多条线程,看似是同时执行的,其实不然,它们和进程一样,也是由CPU轮流执行的,只不过CPU运行速度很快,故而给人同时执行的感觉。

1.2.4 多线程有什么意义呢?

多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率。程序的执行其实都是在抢CPU的资源,CPU的执行权。多个进程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。我们是不敢保证哪一个线程能够在哪个时刻抢到,所以线程的执行有随机性。

1.2.5 线程与进程的关系

线程是CPU调度的最小单元,同时线程是一种有限的系统资源。而进程一般指一个执行单元,在PC和移动设备上指一个程序或者一个应用。一个进程可以包含多个线程,因此进程和线程是包含与被包含的关系。

  • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
  • 资源分配给进程,同一进程的所有线程共享该进程的所有资源。
  • 处理机分给线程,即真正在处理机上运行的是线程。
  • 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
  • 线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

1.2.6 进程与线程的区别

  • 调度:线程作为CUP调度和分配的基本单位,进程作为拥有资源的基本单位。
  • 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行。
  • 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
  • 系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。在进程切换时,耗费资源较大,效率要差一些。
  • 健壮性:进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个进程死掉就等于所有的线程死掉,所以多进程的程序要比多线程的程序健壮。进程间的crash不会相互影响,但是一个线程的crash会使整个进程都crash,其他线程也跟着都挂了。

总的来讲:进程只是一些资源的集合,真正的程序执行都是线程来完成的,程序启动的时候操作系统就帮你创建了一个主线程。每个线程有自己的堆栈。

android在程序启动的时候,会我们分配一个主线程(UI线程),如果没有特殊处理,我们所有的操作都是在UI线程中完成的。

1.2.7 什么是并行、并发呢?

前者是逻辑上同时发生,指在某一个时间内同时运行多个程序;后者是物理上同时发生,指在某一个时间点同时运行多个程序。那么,我们能不能实现真正意义上的并发呢?答案是可以的,多个CPU就可以实现,不过你得知道如何调度和控制它们。

PS:

  • 一个进程中可以有多个执行路径,称之为多线程。
  • 一个进程中至少要有一个线程。
  • 开启多个线程是为了同时运行多部分代码,每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务。

1.3 Java程序运行原理

java 命令会启动 java 虚拟机,启动 JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法。所以 main方法运行在主线程中。在此之前的所有程序都是单线程的。

思考:JVM虚拟机的启动是单线程的还是多线程的?

答案:JVM启动时启动了多条线程,至少有两个线程可以分析的出来

  • 执行main函数的线程,该线程的任务代码都定义在main函数中。

  • 负责垃圾回收的线程。System类的gc方法告诉垃圾回收器调用finalize方法,但不一定立即执行。

2. 多线程的实现方案

由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。但是呢?Java可以去调用C/C++写好的程序来实现多线程程序。由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,然后提供一些类供我们使用。我们就可以实现多线程程序了。

2.1 多线程的实现方案一:继承Thread类,重写run()方法

  • 定义一个类继承Thread类
  • 覆盖Thread类中的run方法
  • 直接创建Thread的子类对象创建线程
  • 调用start方法开启线程并调用线程的任务run方法执行
package cn.itcast;

//多线程的实现方案一:继承Thread类,重写run()方法
//1、定义一个类继承Thread类。
class MyThread extends Thread 
    private String name;

    MyThread(String name) 
        this.name = name;
    

    // 2、覆盖Thread类中的run方法。
    public void run() 
        for (int x = 0; x < 5; x++) 
            System.out.println(name + "...x=" + x + "...ThreadName="
                    + Thread.currentThread().getName());
        
    


class ThreadTest 
    public static void main(String[] args) 
        // 3、直接创建Thread的子类对象创建线程。
        MyThread d1 = new MyThread("黑马程序员");
        MyThread d2 = new MyThread("中关村在线");
        // 4、调用start方法开启线程并调用线程的任务run方法执行。
        d1.start();
        d2.start();
        for (int x = 0; x < 5; x++) 
            System.out.println("x = " + x + "...over..."
                    + Thread.currentThread().getName());
        
    

运行结果:

黑马程序员...x=0...ThreadName=Thread-0
中关村在线...x=0...ThreadName=Thread-1
x = 0...over...main
中关村在线...x=1...ThreadName=Thread-1
黑马程序员...x=1...ThreadName=Thread-0
中关村在线...x=2...ThreadName=Thread-1
x = 1...over...main
中关村在线...x=3...ThreadName=Thread-1
黑马程序员...x=2...ThreadName=Thread-0
中关村在线...x=4...ThreadName=Thread-1
x = 2...over...main
x = 3...over...main
x = 4...over...main
黑马程序员...x=3...ThreadName=Thread-0
黑马程序员...x=4...ThreadName=Thread-0

2.1.2 为什么要重写run()方法?

Thread类用于描述线程,线程是需要任务的。所以Thread类也有对任务的描述。这个任务就是通过Thread类中的run方法来体现。也就是说,run方法就是封装自定义线程运行任务的函数,run方法中定义的就是线程要运行的任务代码。所以只有继承Thread类,并复写run方法,将运行的代码定义在run方法中即可。

2.1.3 启动线程使用的是那个方法

启动线程调用的是start()方法,不是run()方法,run()方法只是封装了被线程执行的代码,调用run()只是普通方法的调用,无法启动线程。

2.1.4 线程能不能多次启动

不能,会出现IllegalThreadStateException非法的线程状态异常。

2.1.5 run()和start()方法的区别

  • run():仅仅是封装了被线程执行的代码,直接调用就是普通方法
  • start():首先是启动了线程,然后再由jvm去调用了该线程的run()方法

2.1.6 Thread类的基本获取和设置方法

  • public final String getName():获取线程的名称
  • public final void setName(String name):设置线程的名称
  • Thread(String name) :通过构造方法给线程起名字

思考:如何获取main方法所在的线程名称呢?

public static Thread currentThread() // 获取任意方法所在的线程名称

2.2 多线程的实现方案二:实现Runnable接口

  • 定义类实现Runnable接口
  • 覆盖接口中的run方法,将线程的任务代码封装到run方法中
  • 通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。为什么?因为线程的任务都封装在Runnable接口子类对象的run方法中。所以要在线程对象创建时就必须明确要运行的任务
  • 调用线程对象的start方法开启线程
package cn.itcast;

//多线程的实现方案二:实现Runnable接口
//1、定义类实现Runnable接口。
class MyThread implements Runnable 
    // 2、覆盖接口中的run方法,将线程的任务代码封装到run方法中。
    public void run() 
        show();
    

    public void show() 
        for (int x = 0; x < 5; x++) 
            System.out.println(Thread.currentThread().getName() + "..." + x);
        
    


class ThreadTest 
    public static void main(String[] args) 
        MyThread d = new MyThread();
        // 3、通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。
        Thread t1 = new Thread(d);
        Thread t2 = new Thread(d);
        // 4、调用线程对象的start方法开启线程。
        t1.start();
        t2.start();
    

运行结果:

Thread-0...0
Thread-1...0
Thread-0...1
Thread-1...1
Thread-0...2
Thread-1...2
Thread-0...3
Thread-1...3
Thread-0...4
Thread-1...4
  • 如何获取线程名称:Thread.currentThread().getName()
  • 如何给线程设置名称:setName()、Thread(Runnable target, String name)
  • 实现接口方式的好处:
    • 可以避免由于Java单继承带来的局限性,所以,创建线程的第二种方式较为常用。
    • 适合多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码,数据有效分离,较好的体现了面向对象的设计思想。

2.3 多线程程序实现方案三:实现Callable接口

  • 创建一个线程池对象,控制要创建几个线程对象。
public static ExecutorService newFixedThreadPool(int nThreads)
  • 这种线程池的线程可以执行:可以执行Runnable对象或者Callable对象代表的线程。
  • 调用如下方法即可
    • Future<?> submit(Runnable task)
      提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future
    • <T> Future<T> submit(Callable<T> task)
      提交一个Callable任务用于执行,返回一个表示任务的未决结果的 Future
  • 结束线程:shutdown() 关闭线程池
package cn.itcast;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import java.util.concurrent.Callable;

//Callable:是带泛型的接口。
//这里指定的泛型其实是call()方法的返回值类型。
class MyCallable implements Callable 

    public Object call() throws Exception 
        for (int x = 0; x < 100; x++) 
            System.out.println(Thread.currentThread().getName() + ":" + x);
        
        return null;
    



/*
 * 多线程实现的方式3: A:创建一个线程池对象,控制要创建几个线程对象。 public static ExecutorService
 * newFixedThreadPool(int nThreads) B:这种线程池的线程可以执行:
 * 可以执行Runnable对象或者Callable对象代表的线程 做一个类实现Runnable接口。 C:调用如下方法即可 Future<?>
 * submit(Runnable task) <T> Future<T> submit(Callable<T> task) D:我就要结束,可以吗? 可以。
 */
public class CallableDemo 
    public static void main(String[] args) 
        // 创建线程池对象
        ExecutorService pool = Executors.newFixedThreadPool(2);

        // 可以执行Runnable对象或者Callable对象代表的线程
        pool.submit(new MyCallable());
        pool.submit(new MyCallable());

        // 结束
        pool.shutdown();
    

运行结果:

实现Callable的优缺点

  • 好处:可以有返回值;可以抛出异常。
  • 弊端:代码比较复杂,所以一般不用

2.4 匿名内部类方式使用多线程

//新创建一个线程,重写run()方法
new Thread() 
    @Override
    public void run() 
        super.run();
        // code
    
.start();

//新创建一个线程,传递一个Runnable对象
new Thread(new Runnable() 
    @Override
    public void run() 
        // code
    
).start();

3. 线程调度和线程控制

程序中的多个线程是并发执行的,某个线程若想被执行必须要得到CPU的使用权。Java虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制被称作线程的调度。

在计算机中,线程调度有两种模型,分别是分时调度模型和抢占式调度模型。所谓分时调度模型是指让所有的线程轮流获得CPU的使用权,并且平均分配每个线程占用的CPU的时间片。抢占式调度模型是指让可运行池中优先级高的线程优先占用CPU,而对于优先级相同的线程,随机选择一个线程使其占用CPU,当它失去了CPU的使用权后,再随机选择其他线程获取CPU使用权。Java虚拟机默认采用抢占式调度模型,通常情况下程序员不需要去关心它,但在某些特定的需求下需要改变这种模式,由程序自己来控制CPU的调度。

3.1 线程调度

假如我们的计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到 CPU时间片,也就是使用权,才可以执行指令。那么Java是如何对线程进行调用的呢?

3.1.1 线程有两种调度模型

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

Java使用的是抢占式调度模型。

3.1.2 如何设置和获取线程优先级

在应用程序中,如果要对线程进行调度,最直接的方式就是设置线程的优先级。优先级越高的线程获得CPU执行的机会越大,而优先级越低的线程获得CPU执行的机会越小。线程的优先级用1~10之间的整数来表示,数字越大优先级越高。除了可以直接使用数字表示线程的优先级,还可以使用Thread类中提供的三个静态常量表示线程的优先级,如表所示。

程序在运行期间,处于就绪状态的每个线程都有自己的优先级,例如main线程具有普通优先级。然而线程优先级不是固定不变的,可以通过Thread类的setPriority(int newPriority)方法对其进行设置,该方法中的参数newPriority接收的是1~10之间的整数或者Thread类的三个静态常量。

public final int getPriority(); //获取线程的优先级
public final void setPriority(int newPriority); //设置线程的优先级

注意:线程默认的优先级是5;线程优先级的范围是:1-10;线程优先级高仅仅表示线程获取CPU的时间片的几率高,但是要在次数比较多,或者多次运行的时候才能卡到比较好的结果。

package cn.itcast.chapter10.example06;
/**
 * 不同优先级的两个线程在程序中运行情况
 */
public class Example06 
    public static void main(String[] args) 
        //创建两个线程
        Thread minPriority = new Thread(new Task(), "优先级较低的线程 ");
        Thread maxPriority = new Thread(new Task(), "优先级较高的线程 ");
        minPriority.setPriority(Thread.MIN_PRIORITY); //设置线程的优先级为1  
        maxPriority.setPriority(Thread.MAX_PRIORITY); //设置线程的优先级为10
        //开启两个线程
        minPriority.start();
        maxPriority.start();
    


//定义一个线程的任务类
class Task implements Runnable 
    @Override
    public void run() 
        for (int i = 0; i < 10; i++) 
            System.out.println(Thread.currentThread().getName() + "正在输出" + i);
        
    

3.2 线程控制

方法声明功能描述
sleep(long millis)线程休眠,让当前线程暂停,进入休眠等待状态
join()线程加入,调用该方法的线程会插入优先先执行
yield()线程礼让,暂停当前正在执行的线程对象,并执行其他线程。
setDaemon(boolean on)将该线程标记为守护线程(后台线程)或用户线程。
当正在运行的线程都是守护线程时,Java 虚拟机退出。
该方法必须在启动线程前调用。
stop()让线程停止,过时了,但是还可以使用
interrupt()中断线程。 把线程的状态终止,并抛出一个InterruptedException。
setPriority(int newPriority)更改线程的优先级
isInterrupted()线程是否被中断

线程休眠 sleep()

优先级高的程序会先执行,而优先级低的程序会后执行。如果希望人为地控制线程,使正在执行的线程暂停,将CPU让给别的线程,这时可以使用静态方法sleep(long millis),该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态。当前线程调用sleep(long millis)方法后,在指定时间(参数millis)内该线程是不会执行的,这样其他的线程就可以得到执行的机会了。

sleep(long millis)方法声明会抛出InterruptedException异常,因此在调用该方法时应该捕获异常,或者声明抛出该异常。

package cn.itcast.chapter10.example07;
/**
 * sleep(long millis) 方法在程序中的使用 
 */
public class Example07 
    public static void main(String[] args) throws Exception 
        //创建一个线程
        new Thread(new Task()).start();     
        for (int i = 1; i <= 10; i++) 
            if (i == 5) 
                Thread.sleep(2000); //当前main主线程 休眠2秒
             else 
                Thread.sleep(500); 
            
            System.out.println("main主线程正在输出:" + i);
        
    

//定义线程的任务类
class Task implements Runnable 
    @Override
    public void run() 
        for (int i = 1; i <= 10; i++) 
            try
                if (i == 3) 
                    Thread.sleep(2000);//当前线程休眠2秒
                 else 
                    Thread.sleep(500);
                
                System.out.println("线程一正在输出:" + i);
             catch (Exception e)
                e.printStackTrace();
            
        
    

线程插队 join()

现实生活中经常能碰到“插队”的情况,同样,在Thread类中也提供了一个join()方法来实现这个“功能”。当在某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后它才会继续运行。

线程加入,等待目标线程执行完之后再继续执行。阻塞当前调用join函数时所在的线程,直到接收函数执行完毕后再继续执行

Worker worker1 = new Worker("work-1");
Worker worker2 = new Worker("work-2");

worker1.start();
System.out.println("启动线程1");
try 
    worker1.join();
    System.out.println("启动线程2");
    worker2.start();
    worker2.join();
 catch (InterruptedException e) 
    e.printStackTrace();


System.out.println("主线程继续执行");
class Worker extends Thread 

    public Worker(String name) 
        super(name);
    

    @Override
    public void run() 
        try 
            Thread.sleep(2000);
         catch (InterruptedException e) 
            e.printStackTrace();
        
        System.out.println("work in " + getName());
    

输出结果

启动线程1
work in work-1
启动线程2
work in work-2
主线程继续执行

案例代码2

package cn.itcast.chapter10.example09;
/**
 * 线程插队,join()方法的使用
 */
public class Example09
    public static void main(String[] args) throws Exception 
        // 创建线程
        Thread t = new Thread(new EmergencyThread(),"线程一");
        t.start(); // 开启线程
        for (int i = 1; i < 6; i++) 
            System.out.println(Thread.currentThread().getName()+"输出:"+i);
            if (i == 2) 
                t.join(); // 调用join()方法
        
            Thread.sleep(500); // 线程休眠500毫秒
        
    

class EmergencyThread implements Runnable 
    public void run() 
        for (int i = 1; i < 6; i++) 
            System.out.println(Thread.currentThread().getName()+"输出:"+i);
            try 
                Thread.sleep(500); // 线程休眠500毫秒
             catch (InterruptedException e) 
                e.printStackTrace();
            
        
    

线程让步 yield()

在篮球比赛中,我们经常会看到两队选手互相抢篮球,当某个选手抢到篮球后就可以拍一会,之后他会把篮球让出来,其他选手重新开始抢篮球,这个过程就相当于Java程序中的线程让步。所谓的线程让步是指正在执行的线程,在某些情况下将CPU资源让给其他线程执行。

线程让步可以通过yield()方法来实现,该方法和sleep()方法有点相似,都可以让当前正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。当某个线程调用yield()方法之后,只有与当前线程优先级相同或者更高的线程才能获得执行的机会。

使调用yield()函数的线程让出执行时间给其它已就绪状态的线程,也就是主动让出线程的执行权给其它的线程

class YieldThread extends Thread 
    public YieldThread(String name) 
        super(name);
    

    public synchronized void run() 
        for (int i = 0; i < MAX; i++) 
            System.out.printf("%s ,优先级为 : %d ----> %d\\n", this.getName(), this.getPriority(), i);
            // i整除4时,调用yield
            if (i == 2) 
                Thread.yield();
            
        
    
YieldThread t1 = new YieldThread("thread-1");
YieldThread t2 = new YieldThread("thread-2");
t1.start();
t2.start();
thread-1 ,优先级为:5 ----> 0
thread-1 ,优先级为:5 ----> 1
thread-1 ,优先级为:5 ----> 2
thread-1 ,优先级为:5 ----> 0
thread-1 ,优先级为:5 ----> 1
thread-1 ,优先级为:5 ----> 2
thread-1 ,优先级为:5 ----> 3
thread-1 ,优先级为:5 ----> 4
thread-1 ,优先级为:5 ----> 3
thread-1 ,优先级为:5 ----> 4

案例代码2

package cn.itcast.chapter10.example08;
/**
 * 线程让步,yield()方法的使用
 */
// 定义YieldThread类继承Thread类
class YieldThread extends Thread 
     // 定义一个有参的构造方法
    public YieldThread(String name)  
        super(name); // 调用父类的构造方法
    
    public void run() 
        for (int i = 0; i < 6; i++) 
            System.out.println(Thread.currentThread().getName() + "---" + i);
            if (i == 3) 
                System.out.print("线程让步:");
                Thread.yield(); // 线程运行到此,作出让步
            
        
    

public class Example08 
    public static void main(String[] args) 
         // 创建两个线程
        Thread t1 = new YieldThread("线程A");
        Thread t2 = new YieldThread("线程B");
         // 开启两个线程
        t1.start();
        t2.start();
    

4. 线程的生命周期

在Java中,任何对象都有生命周期,线程也不例外,它也有自己的生命周期。当Thread对象创建完成时,线程的生命周期便开始了。当run()方法中代码正常执行完毕或者线程抛出一个未捕获的异常(Exception)或者错误(Error)时,线程的生命周期便会结束。线程整个生命周期可以分为五个阶段,分别是新建状态(New)、就绪状态(Runnable)、运行状态(Running)、阻塞状态(Blocked)和死亡状态(Terminated),线程的不同状态表明了线程当前正在进行的活动。在程序中,通过一些操作,可以使线程在不同状态之间转换,如图所示。

图中展示了线程各种状态的转换关系,箭头表示可转换的方向,其中,单箭头表示状态只能单向的转换,例如线程只能从新建状态转换到就绪状态,反之则不能;双箭头表示两种状态可以互相转换,例如就绪状态和运行状态可以互相转换。通过一张图还不能完全描述清楚线程各状态之间的区别,接下来针对线程生命周期中的五种状态分别进行详细讲解,具体如下:

1.新建状态(New)

创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,没有表现出任何线程的动态特征。

2.就绪状态(Runnable)

当线程对象调用了start()方法后,该线程就进入就绪状态。处于就绪状态的线程位于线程队列中,此时它只是具备了运行的条件,能否获得CPU的使用权并开始运行,还需要等待系统的调度。

3.运行状态(Running)

如果处于就绪状态的线程获得了CPU的使用权,并开始执行run()方法中的线程执行体,则该线程处于运行状态。一个线程启动后,它可能不会一直处于运行状态,当运行状态的线程使用完系统分配的时间后,系统就会剥夺该线程占用的CPU资源,让其他线程获得执行的机会。需要注意的是,只有处于就绪状态的线程才可能转换到运行状态。

4.阻塞状态(Blocked)

一个正在执行的线程在某些特殊情况下,如被人为挂起或执行耗时的输入/输出操作时,会让出CPU的使用权并暂时中止自己的执行,进入阻塞状态。线程进入阻塞状态后,就不能进入排队队列。只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。

线程由运行状态转换成阻塞状态的原因,以及如何从阻塞状态转换成就绪状态。

  • 当线程试图获取某个对象的同步锁时,如果该锁被其他线程所持有,则当前线程会进入阻塞状态,如果想从阻塞状态进入就绪状态必须得获取到其他线程所持有的锁。
  • 当线程调用了一个阻塞式的IO方法时,该线程就会进入阻塞状态,如果想进入就绪状态就必须要等到这个阻塞的IO方法返回。
  • 当线程调用了某个对象的wait()方法时,也会使线程进入阻塞状态,如果想进入就绪状态就需要使用notify()方法唤醒该线程。
  • 当线程调用了Thread的sleep(long millis)方法时,也会使线程进入阻塞状态,在这种情况下,只需等到线程睡眠的时间到了以后,线程就会自动进入就绪状态。
  • 当在一个线程中调用了另一个线程的join()方法时,会使当前线程进入阻塞状态,在这种情况下,需要等到新加入的线程运行结束后才会结束阻塞状态,进入就绪状态。

需要注意的是,线程从阻塞状态只能进入就绪状态,而不能直接进入运行状态,也就是说结束阻塞的线程需要重新进入可运行池中,等待系统的调度。

5.死亡状态(Terminated)

当线程调用stop()方法或run()方法正常执行完毕后,或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入死亡状态。一旦进入死亡状态,线程将不再拥有运行的资格,也不能再转换到其他状态。

4.1 线程的状态

线程状态状态说明
新建状态等待状态,调用start()方法启动
就绪状态有执行资格,但是没有执行权
运行状态具有执行资格和执行权
阻塞状态遇到sleep()方法和wait()方法时,失去执行资格和执行权,
sleep()方法时间到或者调用notify()方法时,获取执行资格,变为临时状态
死亡状态中断线程,或者run()方法结束

4.2 线程的生命周期图

5. 线程安全问题

5.1 判断一个程序是否会有线程安全问题的标准

  • 是否是多线程环境
  • 是否有共享数据
  • 是否有多条语句操作共享数据

5.2 如何解决多线程安全问题呢?

基本思想:让程序没有安全问题的环境。
解决办法:同步机制:同步代码块、同步方法。把多个语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可。

5.2.1 解决线程安全问题实现1:同步代码块,格式如下

当多个线程使用同一个共享资源时,可以将处理共享资源的代码放在一个使用synchronized关键字来修饰的代码块中,这个代码块被称作同步代码块

synchronized(lock)
  需要同步的代码;

lock是一个锁对象,它是同步代码块的关键。当某一个线程执行同步代码块时,其它线程将无法执行当前同步代码块,会发生阻塞,等当前线程执行完同步代码块后,所有的线程开始抢夺线程的执行权,抢到执行权的线程将进入同步代码块,执行其中的代码。循环往复,直到共享资源被处理完为止。这个过程就好比一个公用电话亭,只有前一个人打完电话出来后,后面的人才可以打。

package cn.itcast;

//卖票程序的同步代码块实现示例
class Ticket implements Runnable 
    private int num = 10;
    Object obj = new Object();

    public void run() 
        while (true) 
            // 给可能出现问题的代码加锁
            synchronized (obj) 
                if (num > 0) 
                    // 显示线程名及余票数
                    System.out.println(Thread.currentThread().getName()
                            + "...sale..." + num--);
                
            
        
    


class TicketDemo 
    public static void main(String[] args) 
        // 通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。
        Ticket t = new Ticket();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        Thread t4 = new Thread(t);
        // 调用线程对象的start方法开启线程。
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    

运行结果:

Thread-0...sale...10
Thread-0...sale...9
Thread-0...sale...8
Thread-0...sale...7
Thread-0...sale...6
Thread-0...sale...5
Thread-0...sale...4
Thread-0...sale...3
Thread-0...sale...2
Thread-0...sale...1
  • 同步可以解决安全问题的根本原因就在那个对象上。该对象如同锁的功能。
  • 同步代码块的对象可以是哪些呢? 可以是任意对象,但每个线程都必须是同一对象。
  • 同步的前提:多个线程;多个线程使用的是同一个锁对象
  • 同步的好处:同步的出现解决了多线程的安全问题。
  • 同步的弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

5.

以上是关于Java基础:多线程的主要内容,如果未能解决你的问题,请参考以下文章

java核心技术-多线程基础

Java多线程并发环境下的synchronized死锁实例

Java 多线程基础多线程的实现方式

Java 多线程基础多线程的实现方式

Java基础教程:多线程基础——线程间的通信

java基础第八天_多线程