热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

关于JAVA中多线程编程方法的详细解析(附实例)

多线程编程就是以线程为基本抽象单位的一种编程范式。但是,多线程编程又不仅仅是使用多个线程进行编程那么简单,其自身又有需要解决的问题。多线程编程和面向对象编程是可以相容的,即我们可以在面向对象编程的基础上实现多线程编程。
一、程序、进程、线程

程序是一组指令的有序集合,也可以将其通俗地理解为若干行代码。它本身没有任何运行的含义,它只是一个静态的实体,它可能只是一个单纯的文本文件,也有可能是经过编译之后生成的可执行文件。
  从狭义来说,进程是正在运行的程序的实例;从广义上来说,进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。进程是操作系统进行资源分配的基本单位。
  线程是进程中可独立执行的最小单位,它也是处理器进行独立调度和分派的基本单位。一个进程可以包含多个线程,每个线程执行自己的任务,同一个进程中的所有线程共享该进程中的资源,如内存空间、文件句柄等。

二、多线程编程简介

1、什么是多线程编程

多线程编程技术是Java语言的重要特点。多线程编程的含义是将程序任务分成几个并行的子任务,并将这些子任务交给多个线程去执行。
  多线程编程就是以线程为基本抽象单位的一种编程范式。但是,多线程编程又不仅仅是使用多个线程进行编程那么简单,其自身又有需要解决的问题。多线程编程和面向对象编程是可以相容的,即我们可以在面向对象编程的基础上实现多线程编程。事实上,Java平台中的一个线程就是一个对象

2、为什么要使用多线程编程

现在的计算机动辄就是多处理器核心的,而每一个线程同一时间只能运行在一个处理器上。如果只采用单线程进行开发,那么就不能充分利用多核处理器的资源来提高程序的执行效率。而使用多线程进行编程时,不同的线程可以运行在不同的处理器上。这样一来,不仅大大提高了对计算机资源的利用率,同时也提高了程序的执行效率。

三、JAVA线程API简介

java.lang.Thread类就是Java平台对线程的实现。Thread类或其子类的一个实例就是一个线程。

1、线程的创建、启动、运行

在Java平台中,创建一个线程就是创建一个Thread类(或其子类)的示例。每个线程都有其要执行的任务。线程的任务处理逻辑可以在Thread类的run方法中直接实现或者通过该方法进行调用,因此run方法相当于线程的任务处理逻辑的入口方法,它应该由Java虚拟机在运行相应线程时直接调用,而不应该由应用代码进行调用。
  运行一个线程实际上就是让Java虚拟机执行该线程的run方法,从而使任务处理逻辑代码得以执行。如果一个线程没有启动,它的run方法是绝对不会被执行的。为此,首先需要启动线程。Thread类的start方法的作用是启动相应的线程。启动一个线程的实质是请求虚拟机运行相应的线程,而这个线程具体何时能够运行是由线程调度器(线程调度器是操作系统的一部分)决定的。因此,调用线程的start方法并不意味着线程已经开始运行,这个线程可能马上开始运行,也有可能稍后才被运行,也有可能永远不运行。
  下面介绍两种创建线程的方式(实际上还有其他方式,后续文章中会详细介绍)。在此之前我们先来看一下Thread类的run方法的源码:

// Code 1-1@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

这个run方法是在接口Runnable中定义的,它不接受参数也没有返回值。事实上Runnable接口中也只有这一个方法,因此这个接口是一个函数式接口,这意味着我们可以在需要Runnable的地方使用lambda表达式。Thread类实现了这个接口,因此它必须实现这个方法。target是Thread类中的一个域,它的类型也是Runnable。target域表示这个线程需要执行的内容,而Thread类的run方法所做的也只是执行target的run方法。
  我们刚刚提到,Java虚拟机会自动调用线程的run方法。但是,Thread类的run方法已经定义好了,我们没有办法将自己需要执行的代码放在Thread类的run方法中。因此,我们可以考虑其他的方式来影响run方法的行为。第一种就是继承Thread类并重写run方法,这样JVM在运行线程时就会调用我们重写的run方法而不是Thread类的run方法;第二种方法是将我们要执行的代码传递给Thread类的target方法,而刚好Thread类有几个构造器可以直接对target进行赋值,这样一来,JVM在调用run方法时执行的仍然是我们传递的代码。
  在Java平台中,每个线程都可以拥有自己默认的名字,当然我们也可以在构造Thread类的实例时为我们的线程起一个名字,这个名字便于我们区分不同的线程。
  下面的代码使用上述的两种方式创建了两个线程,它们要执行的任务很简单——打印一行欢迎信息,并且要包含自己的名字。

public class WelcomeApp {
    public static void main(String[] args) {
        Thread thread1 = new WelcomeThread();
        Thread thread2 = new Thread(() -> System.out.println("2. Welcome, I'm " + Thread.currentThread().getName()));
        thread1.start();
        thread2.start();
    }
}class WelcomeThread extends Thread {
    @Override
    public void run() {
        System.out.println("1. Welcome, I'm " + Thread.currentThread().getName());
    }
}

下面是这个程序运行时输出的内容:

1. Welcome, I'm Thread-0
2. Welcome, I'm Thread-1

多次运行这个程序,我们可以发现这个程序的输出也有可能是:

2. Welcome, I'm Thread-1
1. Welcome, I'm Thread-0

这说明,虽然thread1的启动在thread2之前,但这并不意味着thread1会在thread2之前被运行。
  不管采用哪种方式创建线程,一旦线程的run方法执行(由JVM调用)结束,相应线程的运行也就结束了。当然,run方法执行结束包括正常结束(run方法正常返回)和代码中抛出异常而导致的终止。运行结束的线程所占用的资源(如内存空间)会如同其他Java对象一样被JVM回收。
  线程属于“一次性用品”,我们不能通过重新调用一个已经运行结束的线程的start方法来使其重新运行。事实上,start方法也只能够被调用一次,多次调用同一个Thread实例的start方法会导致其抛出IllegalThreadStateException异常。

2、线程的属性

线程的属性包括线程的编号、名称、类别和优先级, 详情如下表所示:

  Java虚拟机启动的时候会创建一个主线程(main线程),该线程负责执行Java程序的入口方法(main方法)。下面的程序打印出主线程的名称:

public class MainThreadDemo {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName());
    }
}

  该程序会输出“main”,这说明main方法是由一个名为“main”的线程调用的,这个线程就是主线程,它是由JVM创建并启动的。
  在多线程编程中,弄清楚一段代码具体是由哪个(或者哪种)线程去负责执行的这点很重要,这关系到性能、线程安全等问题。本系列的后续文章会体现这点。
  Java 虚拟机垃圾回收器(Garbage Collector)负责对Java程序中不再使用的内存空间进行回收,而这个回收的动作实际上也是通过专门的线程(垃圾回收线程)实现的,这些线程由Java虚拟机自行创建。
  为了提高Java代码的执行效率,Java虚拟机中的JIT(Just In Time)编译器会动态地将Java字节码编译为Java虚拟机宿主机处理器可直接执行的机器码。这个动态编译的过程实际上是由Java虚拟机创建的专门的线程负责执行的。
  Java平台中的线程随处可见,这些线程各自都有其处理任务。

五、线程的层次关系

Java平台中的线程不是孤立的,线程与线程之间总是存在一些联系。假设线程A所执行的代码创建了线程B, 那么,习惯上我们称线程B为线程A的子线程,相应地线程A就被称为线程B的父线程。例如, Code 1-2中的线程thread1和thread2是main线程的子线程,main线程是它们的父线程。子线程所执行的代码还可以创建其他线程,因此一个子线程也可以是其他线程的父线程。所以,父线程、子线程是一个相对的称呼。理解线程的层次关系有助于我们理解Java应用程序的结构,也有助于我们后续阐述其他概念。
  在Java平台中,一个线程是否是一个守护线程默认取决于其父线程:默认情况下父线程是守护线程,则子线程也是守护线程;父线程是用户线程,则子线程也是用户线程。另外,父线程在创建子线程后启动子线程之前可以调用该线程的setDaemon方法,将相应的线程设置为守护线程(或者用户线程)。
  一个线程的优先级默认值为该线程的父线程的优先级,即如果我们没有设置或者更改一个线程的优先级,那么这个线程的优先级的值与父线程的优先级的值相等。
  不过,Java平台中并没有API用于获取一个线程的父线程,或者获取一个线程的所有子线程。并且,父线程和子线程之间的生命周期也没有必然的联系。比如父线程运行结束后,子线程可以继续运行,子线程运行结束也不妨碍其父线程继续运行。

六、线程的生命周期状态

 在Java平台中,一个线程从其创建、启动到其运行结束的整个生命周期可能经历若干状态。如下图所示:

多线程编程具有以下优势:

提高系统的吞吐率:多线程编程使得一个进程中可以有多个并发(即同时进行的)的操作。例如,当一个线程因为I/0操作而处于等待时,其他线程仍然可以执行其操作。

提高响应性:在使用多线程编程的情况下,对于GUI软件(如桌面应用程序)而言,一个慢的操作(比如从服务器上下载一个大的文件)并不会导致软件的界面出现被“冻住”的现象而无法响应用户的其他操作;对于Web应用程序而言,一个请求的处理慢了并不会影响其他请求的处理。

充分利用多核处理器资源:如今多核处理器的设备越来越普及,就算是手机这样的消费类设备也普遍使用多核处理器。实施恰当的多线程编程有助于我们充分利用设备的多核处理器资源,从而避免了资源浪费。

多线程编程也有自身的问题与风险,包括以下几个方面:

线程安全问题。多个线程共享数据的时候,如果没有采取相应的并发访问控制措施,那么就可能产生数据一致性问题,如读取脏数据(过期的数据)、丢失更新(某些线程所做的更新被其他线程所做的更新覆盖)等。

线程活性问题。一个线程从其创建到运行结束的整个生命周期会经历若于状态。从单个线程的角度来看,RUNNABLE状态是我们所期望的状态。但实际上,代码编写不当可能导致某些线程一直处于等待其他线程释放锁的状态(BLOCKED状态),这种情况称为死锁(Deadlock)。当然,一直忙碌的线程也可能会出现问题,它可能面临活锁(Livelock)问题,即一个线程一直在尝试某个操作但就是无法进展。另外,线程是一种稀缺的计算资源,一个系统所拥有的处理器数最相比于该系统中存在的线程数量而言总是少之又少的。某些情况下可能出现线程饥饿(Starvation)的问题,即某些线程永远无法获取处理器执行的机会而永远处于RUNNABLE状态的READY子状态。

上下文切换。处理器从执行一个线程转向执行另外一个线程的时候操作系统所需要做的一个动作被称为上下文切换。由于处理器资源的稀缺性,因此上下文切换可以被看作多线程编程的必然副产物,它增加了系统的消耗,不利于系统的吞吐率。

相了解更多相关问题请访问:JAVA视频教程

以上就是关于JAVA中多线程编程方法的详细解析(附实例)的详细内容,更多请关注其它相关文章!


推荐阅读
  • TortoiseSVN与VisualSVN Server的安装及基本操作指南
    本文详细介绍了如何安装VisualSVN Server以及TortoiseSVN客户端,并提供了基本的操作步骤,包括配置仓库、用户管理及权限设置等关键环节。 ... [详细]
  • 为何我选择了华为云GaussDB数据库
    本文分享了作者选择华为云GaussDB数据库的理由,详细介绍了GaussDB(for MySQL)的技术特性和优势,以及它在金融和互联网行业的应用场景。 ... [详细]
  • HTTPS与TLS/SSL协议详解:握手及记录协议
    HTTPS,即HTTP over TLS/SSL,通过在HTTP通信层引入安全协议,确保数据传输的安全性。本文将深入探讨TLS/SSL协议的基本概念、HTTPS的必要性,以及TLS握手和记录协议的工作原理。 ... [详细]
  • 应对.avast后缀勒索病毒:全面指南
    本文详细介绍了.avast后缀勒索病毒的特性、感染途径、恢复方法及预防措施,旨在帮助用户有效应对这一威胁。 ... [详细]
  • NFS(Network File System)即网络文件系统,是一种分布式文件系统协议,主要用于Unix和类Unix系统之间的文件共享。本文详细介绍NFS的配置文件/etc/exports和相关服务配置,帮助读者理解如何在Linux环境中配置NFS客户端。 ... [详细]
  • 如何从python读取sql[mysql基础教程]
    从python读取sql的方法:1、利用python内置的open函数读入sql文件;2、利用第三方库pymysql中的connect函数连接mysql服务器;3、利用第三方库pa ... [详细]
  • 深入解析:OpenShift Origin环境下的Kubernetes Spark Operator
    本文探讨了如何在OpenShift Origin平台上利用Kubernetes Spark Operator来管理和部署Apache Spark集群与应用。作为Radanalytics.io项目的一部分,这一开源工具为大数据处理提供了强大的支持。 ... [详细]
  • MySQL 'Too Many Connections' 错误处理及优化方案
    本文详细介绍了如何诊断和解决MySQL数据库中出现的‘Too Many Connections’错误,包括查看当前连接状态、调整配置文件以及优化应用代码等方法。 ... [详细]
  • 深入解析BookKeeper的设计与应用场景
    本文介绍了由Yahoo在2009年开发并于2011年开源的BookKeeper技术。BookKeeper是一种高效且可靠的日志流存储解决方案,广泛应用于需要高性能和强数据持久性的场景。 ... [详细]
  • 本文详细探讨了Java中Volatile关键字的工作原理、优化技巧及其在实际开发中的应用场景,特别是在提高多线程环境下数据可见性和减少锁竞争方面的优势。 ... [详细]
  • 想搭建一个能够稳定支持每日500万页面浏览量(PV)的网站架构吗?了解500万PV的实际意义,以及如何计算服务器需要处理的并发请求量,是成功构建高效架构的关键。本文将从基础概念出发,深入探讨实现这一目标所需的技术细节和策略。 ... [详细]
  • 理解文档对象模型(DOM)
    本文介绍了文档对象模型(DOM)的基本概念,包括其作为HTML文档的节点树结构,以及如何通过JavaScript操作DOM来实现网页的动态交互。 ... [详细]
  • 请看|间隔时间_Postgresql 主从复制 ... [详细]
  • 本文详细探讨了在服务器上运行的PostgreSQL数据库出现'内存不足'错误的具体情况,并提供了一系列有效的解决策略。通过本文,读者将能够更好地理解这一常见问题及其背后的原理。 ... [详细]
  • 本文详细介绍了JSP(Java Server Pages)的九大内置对象及其功能,探讨了JSP与Servlet之间的关系及差异,并提供了实际编码示例。此外,还讨论了网页开发中常见的编码转换问题以及JSP的两种页面跳转方式。 ... [详细]
author-avatar
拍友2502885255
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有