Java SE :”深挖 “多线程(上)

简介: Java SE :”深挖 “多线程(上)

编译软件:IntelliJ IDEA 2019.2.4 x64

运行环境:win10 家庭中文版

jdk版本:1.8.0_361

提示:以下是本篇文章正文内容,下面案例可供参考

一、进程与线程的相关概念

1.1 程序

它是选择一种编程语言,完成一个功能/任务,而编写的一段代码,这段代码最后被编译/解释为指令程序是一组指令的集合

程序是静态的。当我们电脑、手机安装了一个程序之后,只是占用硬盘/存储卡的空间。

1.2 进程

①一个程序的一次运行

按快捷键 CTRL+SHIFT+ESC ,调出任务管理器,如下所示:

当程序启动后,操作系统部会给这个程序分配一个进程的ID, 并且会给他分配一块独立的内存空间。

②如果一个程序被启动了多次,那么会有多个进程。

如下所示:

多个进程之问是无法共享数据。

如果两段代码需要进行数据的交互,成本比较高,要么遇过一个硬盘的文件,要么通过网络。

进程之间的切换成术也比较高。现在的操作系统都支持多任务,嫌作系统在每一个任务之同进行切换,要给整个进程做镜像,要记录当前进程的状态,执行到哪个指令的.

1.3 线程

进程中的其中一条执行路径。

多个线程会同属于一个进程。
这多个线程会共享同一个进程中的一些资源。

比如Java中堆内存的数据,方法区的数据。

如下所示:

如果同一个进程的两段代码需要进行数据的交互,非常方便,可以直接在内存中共享。当然,同时要考虑安全问题。进程之间的切换,需要记录的信息要少很多,因为很多线程之间的数据是共享的,这些数据就不用单独在做镜像了,只需要记录每一个线程要执行的下一条指令。


二、并发与并行

  • 并行(parallel):指两个或多个事件在同一时刻发生(同时发生)。指在同一时刻,有多条指令在多个处理器上同时执行。
  • 并发(concurrency):指两个或多个事件在同一个时间段内发生。指在同一个时刻只能有一条指令执行,但多个进程的指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。

如下图所示:

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

而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核越多,并行处理的程序越多,能大大的提高电脑运行的效率。

例子:

  • 并行:多项工作一起执行,之后再汇总,例如:泡方便面,电水壶烧水,一边撕调料倒入桶中
  • 并发:同一时刻多个线程在访问同一个资源,多个线程对一个点,例如:春运抢票、电商秒杀…

三、线程调度

有两种调度模型:分时调度模型和抢占式调度模型。

  • 分时调度模型
    所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
  • 抢占式调度模型优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
  • 抢占式调度详解
    大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。
    实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
    其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高

java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,斜体样式使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。


四 、线程的创建与使用

4.1 线程的创建

java中开启一个线程的方共有四种:

1.继承Thread类

2.实现Runnable接口

3.实现Callable接口

4. 线程池

ps:本文今天暂且只介绍前两种,后两种在后期再行探讨。

4.1.1 继承Thread类

Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建启动多线程的步骤如下:

(1) 编与一个类,让它继承Thread类

(2)重写父类的public void run()}这个run()方法不是由程序员调用的,而且线程调度时,自动调用。要让一个线程做什么事,必须把这个代码写到run 中把run()方法的方法体,称为线程体。

(3) 创建自定义线程类的对象

(4)启动线程,调用自定义线程类的对象的start()

代码演示如下:

public class TestMyThread {
    public static void main(String[] args) {
        MyThread myThread=new MyThread();
        myThread.start();//启动线程
        for (int i = 1; i <=10 ; i+=2) {
            System.out.println("main线程打印【1-10】之间的奇数:"+i);
        }
    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 1; i <=10 ; i++) {
            if (i%2==0){
                System.out.println("自定义线程打印【1-10】之间的偶数:"+i);
            }
        }
    }
}

4.1.2 实现Runnable接口

Java有单继承的限制,当我们无法继承Thread类时,那么该如何做呢?在核心类库中提供了Runnable接口,我们可以实现Runnable接口,重写run()方法,然后再通过Thread类的对象代理启动和执行我们的线程体run()方法

步骤如下:

(1)编写线程类,实现Runnable接口

(2)重写接口的抽象方法public void run()

(3) 创建自定义线程类的对象

(4)创建一个Thread类的对象,同时让Thread对象代理我们的自定义线程对象创建它的目的是为了调用start方法

(5)启动线程

线程调度器会调用t对象的run方法,因为这里启动的是t线程 ,(t.start())

Thread类的run()
  @Override
  public void run() {
    if(target != null) 
    {
      (target.run();
    }
   }

这里的target对象就是创建Thread类对象时传入的Runnable接口的实现类对象,即被代理对象。

代码演示如下:

public class test {
    public static void main(String[] args) {
        TestRunnable tr=new TestRunnable();
        Thread thread=new Thread(tr);
        thread.start();  //启动线程
    }
}
class TestRunnable implements Runnable{
    @Override
    //线程体
    public void run() {
        for (int i = 1; i <=10 ; i++) {
            if (i%2==0){
                System.out.println("自定义线程打印【1-10】之间的偶数:"+i);
            }
        }
    }
}

通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。

在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。

实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现,Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

💡tips:

Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。

4.3 使用匿名内部类对象来实现线程的创建和启动

①使用匿名内部类对象继承Thread类

代码演示如下:

new Thread(){
            @Override
            public void run() {
                //依次打印1-10
                System.out.println("使用匿名内部类创建Thread对象打印1-10:");
                for (int i = 1; i <=10 ; i++) {
                    System.out.print(i+"\t");
                }
            }
        }.start();

②使用匿名内部类对象实现Runnable接口

代码演示如下:

new Thread(new Runnable(){
            @Override
            public void run() {
                //依次打印1-10
                System.out.println("使用匿名内部类实现Runnable接口创建的线程打印1-10:");
                for (int i = 1; i <=10 ; i++) {
                    System.out.print(i+"\t");
                }
            }
        }).start();

4.2 线程的使用

4.2.1 Thread类

4.2.1.1 构造方法

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

4.2.1.2 常用方法①

  • public void run() :此线程要执行的任务在此处定义代码。
  • public String getName() :获取当前线程名称。

如果没有手动指定线程名称,默认是Thread-编号,从0开始。 如果需要手动指定线程名称,可以通过构造器,或者setName(String name)方法设置线程名称。

  • public static Thread currentThread() :返回对当前正在执行的线程对象的引用。
  • public final boolean isAlive()测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。
  • public final int getPriority()返回线程优先级
  • public final void setPriority(int newPriority)改变线程的优先级
  • 每个线程都有一定的优先级,优先级高的线程将获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。Thread类提供了setPriority(int newPriority)和getPriority()方法类设置和获取线程的优先级,其中setPriority方法需要一个整数,并且范围在[1,10]之间,通常推荐设置Thread类的三个优先级常量:
  • MAX_PRIORITY(10):最高优先级
  • MIN _PRIORITY (1):最低优先级
  • NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级。

案例:声明一个匿名内部类继承Thread类,重写run方法,在run方法中获取线程名称和优先级。设置该线程优先级为最高优先级并启动该线程。

代码演示如下:

Thread  t=  new Thread(){
            @Override
            public void run() {
                System.out.println(getName()+"的优先级:"+getPriority());
            }
        };
        t.setPriority(10);
        t.start();

4.2.1.3 常用方法②

  • public static void sleep(long millis) throws InterruptedException: 线程休眠,单位毫秒
  • public static void yield(): 让当前线程暂停一下当前线程暂停下,让出CPU,但是下一次CPU有可能还是调用它。
  • void join() throws InterruptedException : 等待该线程终止。该线程是调用ioin 方法的线程。

阻塞当前线程的执行,等到被调用join的线程对象执行完毕才执行继续执行当前线程

  • void join(long millis) : 等待该线程终止的时间最长为 millis 毫秒。如果millis时间到,将不再等待。
  • void join(long millis,int nanos) : 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。

案例:

声明一个PrintEvenThread线程类,继承Thread类,重写run方法,实现打印[1,100]之间的偶数,要求每隔1毫秒打印1个偶数。

声明一个PrintOddThread线程类,继承Thread类,重写run方法,实现打打印[1,100]之间的奇数在main线程中:

(1) 创建两个线程对象,并启动两个线程

(2)当打印奇数的线程结束了,让偶数的线程也停下来,就算偶数线程没有全部打印完[1,100]之间的偶数.

案例演示代码如下:

package test;
//打印1-100之间的偶数
public class PrintEvenThread extends Thread {
    private boolean flag=true;
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
    @Override
    public void run() {
        for (int i = 2; i <=100 & flag; i+=2) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("自定义异常打印【1-100】之间的偶数:"+i);
        }
    }
}
//打印1-100之间的奇数
public class PrintOddThread extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 100 ; i+=2) {
            System.out.println("自定义异常打印【1-100】之间的奇数:"+i);
        }
    }
}
public class TestPrint {
    public static void main(String[] args) {
        PrintEvenThread p1=new PrintEvenThread();//偶数进程
        PrintOddThread p2=new PrintOddThread();//奇数进程
        p1.start();
        p2.start();
        try {
            p2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        p1.setFlag(false);
    }
}

相关文章
|
3小时前
|
安全 Java 调度
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第12天】 在现代软件开发中,多线程编程是提升应用程序性能和响应能力的关键手段之一。特别是在Java语言中,由于其内置的跨平台线程支持,开发者可以轻松地创建和管理线程。然而,随之而来的并发问题也不容小觑。本文将探讨Java并发编程的核心概念,包括线程安全策略、锁机制以及性能优化技巧。通过实例分析与性能比较,我们旨在为读者提供一套既确保线程安全又兼顾性能的编程指导。
|
3小时前
|
Java
Java中的多线程编程:基础知识与实践
【5月更文挑战第13天】在计算机科学中,多线程是一种使得程序可以同时执行多个任务的技术。在Java语言中,多线程的实现主要依赖于java.lang.Thread类和java.lang.Runnable接口。本文将深入探讨Java中的多线程编程,包括其基本概念、实现方法以及一些常见的问题和解决方案。
|
3小时前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第13天】 在Java开发中,并发编程是一个复杂且重要的领域。它不仅关系到程序的线程安全性,也直接影响到系统的性能表现。本文将探讨Java并发编程的核心概念,包括线程同步机制、锁优化技术以及如何平衡线程安全和性能。通过分析具体案例,我们将提供实用的编程技巧和最佳实践,帮助开发者在确保线程安全的同时,提升应用性能。
10 1
|
3小时前
|
Java 调度
Java一分钟之线程池:ExecutorService与Future
【5月更文挑战第12天】Java并发编程中,`ExecutorService`和`Future`是关键组件,简化多线程并提供异步执行能力。`ExecutorService`是线程池接口,用于提交任务到线程池,如`ThreadPoolExecutor`和`ScheduledThreadPoolExecutor`。通过`submit()`提交任务并返回`Future`对象,可检查任务状态、获取结果或取消任务。注意处理`ExecutionException`和避免无限等待。实战示例展示了如何异步执行任务并获取结果。理解这些概念对提升并发性能至关重要。
17 5
|
3小时前
|
Java 开发框架 XML
JDK、JRE、Java SE、Java EE和Java ME有什么区别?
JDK、JRE、Java SE、Java EE和Java ME有什么区别?
|
3小时前
|
Java
Java一分钟:线程协作:wait(), notify(), notifyAll()
【5月更文挑战第11天】本文介绍了Java多线程编程中的`wait()`, `notify()`, `notifyAll()`方法,它们用于线程间通信和同步。这些方法在`synchronized`代码块中使用,控制线程执行和资源访问。文章讨论了常见问题,如死锁、未捕获异常、同步使用错误及通知错误,并提供了生产者-消费者模型的示例代码,强调理解并正确使用这些方法对实现线程协作的重要性。
14 3
|
3小时前
|
安全 算法 Java
Java一分钟:线程同步:synchronized关键字
【5月更文挑战第11天】Java中的`synchronized`关键字用于线程同步,防止竞态条件,确保数据一致性。本文介绍了其工作原理、常见问题及避免策略。同步方法和同步代码块是两种使用形式,需注意避免死锁、过度使用导致的性能影响以及理解锁的可重入性和升级降级机制。示例展示了同步方法和代码块的运用,以及如何避免死锁。正确使用`synchronized`是编写多线程安全代码的核心。
56 2
|
3小时前
|
安全 Java 调度
Java一分钟:多线程编程初步:Thread类与Runnable接口
【5月更文挑战第11天】本文介绍了Java中创建线程的两种方式:继承Thread类和实现Runnable接口,并讨论了多线程编程中的常见问题,如资源浪费、线程安全、死锁和优先级问题,提出了解决策略。示例展示了线程通信的生产者-消费者模型,强调理解和掌握线程操作对编写高效并发程序的重要性。
43 3
|
3小时前
|
安全 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第11天】在Java并发编程中,线程安全和性能优化是两个重要的主题。本文将深入探讨这两个方面,包括线程安全的基本概念,如何实现线程安全,以及如何在保证线程安全的同时进行性能优化。我们将通过实例和代码片段来说明这些概念和技术。
4 0
|
3小时前
|
Java 调度
Java并发编程:深入理解线程池
【5月更文挑战第11天】本文将深入探讨Java中的线程池,包括其基本概念、工作原理以及如何使用。我们将通过实例来解释线程池的优点,如提高性能和资源利用率,以及如何避免常见的并发问题。我们还将讨论Java中线程池的实现,包括Executor框架和ThreadPoolExecutor类,并展示如何创建和管理线程池。最后,我们将讨论线程池的一些高级特性,如任务调度、线程优先级和异常处理。