最新Java基础系列课程--Day14-多线程编程(一)

简介: 最新Java基础系列课程--Day14-多线程编程

一、多线程

1.1 基本概念

线程其实是程序中的一条执行路径。

多线程(Multithread)是指在同一个程序中同时存在几个执行体,按几条不同的执行路径共同工作的情况。

先来区分几个概念:

程序(Program):程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,程序是静态的代码。
进程(Process):进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。
多任务(Multi task):多任务是指在一个系统中可以同时运行多个程序,即有多个独立运行的任务,每一个任务对应一个进程。
线程(Thread):线程是一个比进程更小的执行单位。一个进程在其执行过程中可以产生多个线程,形成多条执行线路。

我们之前写过的程序,其实都是单线程程序,如下图代码,如果前面的for循环没有执行完,for循环下面的代码是不会执行的。

怎样的程序才是多线程程序呢? 如下图所示,12306网站就是支持多线程的,因为同时可以有很多人一起进入网站购票,而且每一个人互不影响。再比如百度网盘,可以同时下载或者上传多个文件。这些程序中其实就有多条执行路径,每一条执行执行路径就是一条线程,所以这样的程序就是多线程程序。


1.2 线程的状态与生命周期

接下来,我们学习最后一个有关线程的知识点,叫做线程的生命周期。所谓生命周期就是线程从生到死的过程中间有哪些状态,以及这些状态之间是怎么切换的。

为了让大家同好的理解线程的生命周期,先用人的生命周期举个例子,人从生到死有下面的几个过程。在人的生命周期过程中,各种状态之间可能会有切换,线程也是一样的。

接下来就来学习线程的生命周期。在Thread类中有一个嵌套的枚举类叫Thread.Status,这里面定义了线程的6中状态。如下图所示

NEW: 新建状态,线程还没有启动
RUNNABLE: 可以运行状态,线程调用了start()方法后处于这个状态
BLOCKED: 锁阻塞状态,没有获取到锁处于这个状态
WAITING: 无限等待状态,线程执行时被调用了wait方法处于这个状态
TIMED_WAITING: 计时等待状态,线程执行时被调用了sleep(毫秒)或者wait(毫秒)方法处于这个状态
TERMINATED: 终止状态, 线程执行完毕或者遇到异常时,处于这个状态。

这几种状态之间切换关系如下图所示

1.3 线程的调度与优先级

调度:指在各个线程之间分配CPU资源。线程调度有两种模型:分时模型和抢占模型。

优先级:决定了线程被CPU执行的优先顺序。

Java语言中线程的优先级从低到高以整数1~10表示,共分为10级。Thread类有三个关于线程优先级的静态变量,MIN_PRIORITY表示最小优先级,通常为1;MAX_PRIORITY表示最高优先级,通常为10;NORM_PRIORITY表示普通优先级,缺省值为5。

对应一个新建的线程,系统会遵循如下的原则为其指定优先级:

(1)新建线程将继承创建它的父线程的优先级。父线程是指执行创建新线程对象语句所在的线程,它可能是程序的主线程,也可能是某一个用户自定义的线程。

(2)一般情况下,主线程具有普通优先级。

1.4 线程创建方式1

Java语言中实现多线程的方法有两种,一种是继承java.lang包中的Thread类,另一种是用户在定义自己的类中实现Runnable接口。但不管采用哪种方法,都要用到Java语言类库中的Thread类以及相关的方法。

Java为开发者提供了一个类叫做Thread,此类的对象用来表示线程。创建线程并执行线程的步骤如下

1.定义一个子类继承Thread类,并重写run方法
2.创建Thread的子类对象
3.调用start方法启动线程(启动线程后,会自动执行run方法中的代码)

代码如下

public class MyThread extends Thread{
    // 2、必须重写Thread类的run方法
    @Override
    public void run() {
        // 描述线程的执行任务。
        for (int i = 1; i <= 5; i++) {
            System.out.println("子线程MyThread输出:" + i);
        }
    }
}

再定义一个测试类,在测试类中创建MyThread线程对象,并启动线程

public class ThreadTest1 {
    // main方法是由一条默认的主线程负责执行。
    public static void main(String[] args) {
        // 3、创建MyThread线程类的对象代表一个线程
        Thread t = new MyThread();
        // 4、启动线程(自动执行run方法的)
        t.start(); 
        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程main输出:" + i);
        }
    }
}

打印结果如下图所示,我们会发现MyThread和main线程在相互抢夺CPU的执行权(注意:哪一个线程先执行,哪一个线程后执行,目前我们是无法控制的,每次输出结果都会不一样

最后我们还需要注意一点:不能直接去调用run方法,如果直接调用run方法就不认为是一条线程启动了,而是把Thread当做一个普通对象,此时run方法中的执行的代码会成为主线程的一部分。此时执行结果是这样的。

1.5 线程创建方式2

接下来我们学习线程的第二种创建方式。Java为开发者提供了一个Runnable接口,该接口中只有一个run方法,意思就是通过Runnable接口的实现类对象专门来表示线程要执行的任务。具体步骤如下

1.先写一个Runnable接口的实现类,重写run方法(这里面就是线程要执行的代码)
2.再创建一个Runnable实现类的对象
3.创建一个Thread对象,把Runnable实现类的对象传递给Thread
4.调用Thread对象的start()方法启动线程(启动后会自动执行Runnable里面的run方法)

代码如下:先准备一个Runnable接口的实现类

/**
 * 1、定义一个任务类,实现Runnable接口
 */
public class MyRunnable implements Runnable{
    // 2、重写runnable的run方法
    @Override
    public void run() {
        // 线程要执行的任务。
        for (int i = 1; i <= 5; i++) {
            System.out.println("子线程输出 ===》" + i);
        }
    }
}

再写一个测试类,在测试类中创建线程对象,并执行线程

public class ThreadTest2 {
    public static void main(String[] args) {
        // 3、创建任务对象。
        Runnable target = new MyRunnable();
        // 4、把任务对象交给一个线程对象处理。
        //  public Thread(Runnable target)
        new Thread(target).start();
        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程main输出 ===》" + i);
        }
    }
}

运行上面代码,结果如下图所示**(注意:没有出现下面交替执行的效果,也是正常的)**

主线程main输出 ===》1
主线程main输出 ===》2
主线程main输出 ===》3
子线程输出 ===》1
子线程输出 ===》2
子线程输出 ===》3
子线程输出 ===》4
子线程输出 ===》5
主线程main输出 ===》4
主线程main输出 ===》5

1.6 线程创建方式2—匿名内部类

同学们注意了,现在这种写法不是新知识。只是将前面第二种方式用匿名内部类改写一下。因为同学们在看别人写的代码时,有可能会看到这种写法。你知道是怎么回事就可以了。

刚刚我们学习的第二种线程的创建方式,需要写一个Runnable接口的实现类,然后再把Runnable实现类的对象传递给Thread对象。

现在我不想写Runnable实现类,于是可以直接创建Runnable接口的匿名内部类对象,传递给Thread对象。

代码如下

public class ThreadTest2_2 {
    public static void main(String[] args) {
        // 1、直接创建Runnable接口的匿名内部类形式(任务对象)
        Runnable target = new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程1输出:" + i);
                }
            }
        };
        new Thread(target).start();
        // 简化形式1:
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程2输出:" + i);
                }
            }
        }).start();
        // 简化形式2:
        new Thread(() -> {
                for (int i = 1; i <= 5; i++) {
                    System.out.println("子线程3输出:" + i);
                }
        }).start();
        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程main输出:" + i);
        }
    }
}

1.7 线程的创建方式3

接下来,我们学习线程的第三种创建方式。已经有两种了为什么还有要第三种呢? 这样,我们先分析一下前面两种都存在的一个问题。然后再引出第三种可以解决这个问题。

  • 假设线程执行完毕之后有一些数据需要返回,前面两种方式重写的run方法均没有返回结果。
public void run(){
    ...线程执行的代码...
}
  • JDK5提供了Callable接口和FutureTask类来创建线程,它最大的优点就是有返回值。
    在Callable接口中有一个call方法,重写call方法就是线程要执行的代码,它是有返回值的
public T call(){
    ...线程执行的代码...
    return 结果;
}

第三种创建线程的方式,步骤如下

1.先定义一个Callable接口的实现类,重写call方法
2.创建Callable实现类的对象
3.创建FutureTask类的对象,将Callable对象传递给FutureTask
4.创建Thread对象,将Future对象传递给Thread
5.调用Thread的start()方法启动线程(启动后会自动执行call方法)
   等call()方法执行完之后,会自动将返回值结果封装到FutrueTask对象中
6.调用FutrueTask对的get()方法获取返回结果

代码如下:先准备一个Callable接口的实现类

/**
 * 1、让子类继承Thread线程类。
 */
public class MyThread extends Thread{
    // 2、必须重写Thread类的run方法
    @Override
    public void run() {
        // 描述线程的执行任务。
        for (int i = 1; i <= 5; i++) {
            System.out.println("子线程MyThread输出:" + i);
        }
    }
}

再定义一个测试类,在测试类中创建线程并启动线程,还要获取返回结果

public class ThreadTest3 {
    public static void main(String[] args) throws Exception {
        // 3、创建一个Callable的对象
        Callable<String> call = new MyCallable(100);
        // 4、把Callable的对象封装成一个FutureTask对象(任务对象)
        // 未来任务对象的作用?
        // 1、是一个任务对象,实现了Runnable对象.
        // 2、可以在线程执行完毕之后,用未来任务对象调用get方法获取线程执行完毕后的结果。
        FutureTask<String> f1  = new FutureTask<>(call);
        // 5、把任务对象交给一个Thread对象
        new Thread(f1).start();
        Callable<String> call2 = new MyCallable(200);
        FutureTask<String> f2  = new FutureTask<>(call2);
        new Thread(f2).start();
        // 6、获取线程执行完毕后返回的结果。
        // 注意:如果执行到这儿,假如上面的线程还没有执行完毕
        // 这里的代码会暂停,等待上面线程执行完毕后才会获取结果。
        String rs = f1.get();
        System.out.println(rs);
        String rs2 = f2.get();
        System.out.println(rs2);
    }
}

二、多线程常用方法

下面我们演示一下getName()setName(String name)currentThread()sleep(long time)这些方法的使用效果。

public class MyThread extends Thread{
    public MyThread(String name){
        super(name); //1.执行父类Thread(String name)构造器,为当前线程设置名字了
    }
    @Override
    public void run() {
        //2.currentThread() 哪个线程执行它,它就会得到哪个线程对象。
        Thread t = Thread.currentThread();
        for (int i = 1; i <= 3; i++) {
            //3.getName() 获取线程名称
            System.out.println(t.getName() + "输出:" + i);
        }
    }
}

再测试类中,创建线程对象,并启动线程

public class ThreadTest1 {
    public static void main(String[] args) {
        Thread t1 = new MyThread();
        t1.setName(String name) //设置线程名称;
        t1.start();
        System.out.println(t1.getName());  //Thread-0
        Thread t2 = new MyThread("2号线程");
        // t2.setName("2号线程");
        t2.start();
        System.out.println(t2.getName()); // Thread-1
        // 主线程对象的名字
        // 哪个线程执行它,它就会得到哪个线程对象。
        Thread m = Thread.currentThread();
        m.setName("最牛的线程");
        System.out.println(m.getName()); // main
        for (int i = 1; i <= 5; i++) {
            System.out.println(m.getName() + "线程输出:" + i);
        }
    }
}

执行上面代码,效果如下图所示,我们发现每一条线程都有自己了名字了。

最后再演示一下join这个方法是什么效果。

public class ThreadTest2 {
    public static void main(String[] args) throws Exception {
        // join方法作用:让当前调用这个方法的线程先执行完。
        Thread t1 = new MyThread("1号线程");
        t1.start();
        t1.join();
        Thread t2 = new MyThread("2号线程");
        t2.start();
        t2.join();
        Thread t3 = new MyThread("3号线程");
        t3.start();
        t3.join();
    }
}

执行效果是1号线程先执行完,再执行2号线程;2号线程执行完,再执行3号线程;3号线程执行完就结束了。


最新Java基础系列课程--Day14-多线程编程(二)https://developer.aliyun.com/article/1423551

相关文章
|
1天前
|
缓存 Java 调度
Java并发编程:深入理解线程池
【4月更文挑战第30天】 在Java并发编程中,线程池是一种重要的工具,它可以帮助我们有效地管理线程,提高系统性能。本文将深入探讨Java线程池的工作原理,如何使用它,以及如何根据实际需求选择合适的线程池策略。
|
1天前
|
Java
Java并发编程:深入理解线程池
【4月更文挑战第30天】 本文将深入探讨Java中的线程池,解析其原理、使用场景以及如何合理地利用线程池提高程序性能。我们将从线程池的基本概念出发,介绍其内部工作机制,然后通过实例演示如何创建和使用线程池。最后,我们将讨论线程池的优缺点以及在实际应用中需要注意的问题。
|
1天前
|
设计模式 算法 安全
Java多线程编程实战:从入门到精通
【4月更文挑战第30天】本文介绍了Java多线程编程的基础,包括线程概念、创建线程(继承`Thread`或实现`Runnable`)、线程生命周期。还讨论了线程同步与锁(同步代码块、`ReentrantLock`)、线程间通信(等待/通知、并发集合)以及实战技巧,如使用线程池、线程安全设计模式和避免死锁。性能优化方面,建议减少锁粒度和使用非阻塞算法。理解这些概念和技术对于编写高效、可靠的多线程程序至关重要。
|
1天前
|
Java 调度 开发者
Java中的多线程编程:基础知识与实践
【4月更文挑战第30天】 在现代软件开发中,多线程编程是提高程序性能和响应能力的关键。Java作为一款广泛使用的编程语言,提供了丰富的多线程支持。本文将介绍Java多线程的基础概念、实现方法以及常见问题的解决策略。我们将从线程的创建和管理入手,逐步深入到同步机制、死锁避免以及高级并发工具类的应用。通过实例代码演示和理论分析,旨在帮助读者掌握Java多线程编程的核心技能,提升软件项目的并行处理能力。
|
1天前
|
Java
java多线程售票例子
java多线程售票例子
|
1天前
|
Java 程序员
Java中的多线程编程与性能优化
【4月更文挑战第30天】本文主要探讨了Java中的多线程编程以及如何通过多线程技术来提升程序的性能。首先,我们将介绍多线程的基本概念和原理,然后深入探讨Java中实现多线程的两种主要方式:继承Thread类和实现Runnable接口。接着,我们将讨论多线程中的同步问题,包括synchronized关键字和Lock锁。最后,我们将探讨如何通过线程池来管理和优化线程,以及如何避免常见的多线程问题。
|
1天前
|
Java
Java并发编程:深入理解线程池
【4月更文挑战第30天】本文将深入探讨Java并发编程中的一个重要主题——线程池。我们将从线程池的基本概念入手,了解其工作原理和优势,然后详细介绍如何使用Java的Executor框架创建和管理线程池。最后,我们将讨论一些高级主题,如自定义线程工厂和拒绝策略。通过本文的学习,你将能够更好地理解和使用Java的线程池,提高你的并发编程能力。
|
1天前
|
存储 安全 Java
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第30天】在Java开发中,并发编程是一个复杂而又关键的领域。它允许多个线程同时执行,从而提高程序性能和资源利用率。然而,并发编程也带来了许多挑战,如数据不一致、死锁和线程安全问题。本文将深入探讨Java并发编程的核心概念,包括线程安全和性能优化策略。我们将通过实例分析如何在保证线程安全的同时提高程序性能,为Java开发者提供实用的指导。
|
1天前
|
Java 程序员 开发者
深入理解Java并发编程:线程同步与锁机制
【4月更文挑战第30天】 在多线程的世界中,确保数据的一致性和线程间的有效通信是至关重要的。本文将深入探讨Java并发编程中的核心概念——线程同步与锁机制。我们将从基本的synchronized关键字开始,逐步过渡到更复杂的ReentrantLock类,并探讨它们如何帮助我们在多线程环境中保持数据完整性和避免常见的并发问题。文章还将通过示例代码,展示这些同步工具在实际开发中的应用,帮助读者构建对Java并发编程深层次的理解。
|
1天前
|
安全 Java 调度
深入理解Java并发编程:线程安全与性能优化
【4月更文挑战第30天】本文将深入探讨Java并发编程的核心概念,包括线程安全、同步机制、锁优化以及性能调优。我们将通过实例分析如何确保多线程环境下的数据一致性,同时介绍一些常见的并发模式和最佳实践,旨在帮助开发者在保证线程安全的同时,提升系统的性能和响应能力。