热门标签 | 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中多线程编程方法的详细解析(附实例)的详细内容,更多请关注其它相关文章!


推荐阅读
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • Centos7.6安装Gitlab教程及注意事项
    本文介绍了在Centos7.6系统下安装Gitlab的详细教程,并提供了一些注意事项。教程包括查看系统版本、安装必要的软件包、配置防火墙等步骤。同时,还强调了使用阿里云服务器时的特殊配置需求,以及建议至少4GB的可用RAM来运行GitLab。 ... [详细]
  • 禁止程序接收鼠标事件的工具_VNC Viewer for Mac(远程桌面工具)免费版
    VNCViewerforMac是一款运行在Mac平台上的远程桌面工具,vncviewermac版可以帮助您使用Mac的键盘和鼠标来控制远程计算机,操作简 ... [详细]
  • Linux服务器密码过期策略、登录次数限制、私钥登录等配置方法
    本文介绍了在Linux服务器上进行密码过期策略、登录次数限制、私钥登录等配置的方法。通过修改配置文件中的参数,可以设置密码的有效期、最小间隔时间、最小长度,并在密码过期前进行提示。同时还介绍了如何进行公钥登录和修改默认账户用户名的操作。详细步骤和注意事项可参考本文内容。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 本文介绍了在rhel5.5操作系统下搭建网关+LAMP+postfix+dhcp的步骤和配置方法。通过配置dhcp自动分配ip、实现外网访问公司网站、内网收发邮件、内网上网以及SNAT转换等功能。详细介绍了安装dhcp和配置相关文件的步骤,并提供了相关的命令和配置示例。 ... [详细]
  • 搭建Windows Server 2012 R2 IIS8.5+PHP(FastCGI)+MySQL环境的详细步骤
    本文详细介绍了搭建Windows Server 2012 R2 IIS8.5+PHP(FastCGI)+MySQL环境的步骤,包括环境说明、相关软件下载的地址以及所需的插件下载地址。 ... [详细]
  • 本文介绍了使用AJAX的POST请求实现数据修改功能的方法。通过ajax-post技术,可以实现在输入某个id后,通过ajax技术调用post.jsp修改具有该id记录的姓名的值。文章还提到了AJAX的概念和作用,以及使用async参数和open()方法的注意事项。同时强调了不推荐使用async=false的情况,并解释了JavaScript等待服务器响应的机制。 ... [详细]
  • PHP设置MySQL字符集的方法及使用mysqli_set_charset函数
    本文介绍了PHP设置MySQL字符集的方法,详细介绍了使用mysqli_set_charset函数来规定与数据库服务器进行数据传送时要使用的字符集。通过示例代码演示了如何设置默认客户端字符集。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • 本文介绍了如何使用php限制数据库插入的条数并显示每次插入数据库之间的数据数目,以及避免重复提交的方法。同时还介绍了如何限制某一个数据库用户的并发连接数,以及设置数据库的连接数和连接超时时间的方法。最后提供了一些关于浏览器在线用户数和数据库连接数量比例的参考值。 ... [详细]
  • 本文介绍了在Hibernate配置lazy=false时无法加载数据的问题,通过采用OpenSessionInView模式和修改数据库服务器版本解决了该问题。详细描述了问题的出现和解决过程,包括运行环境和数据库的配置信息。 ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • 本文介绍了如何找到并终止在8080端口上运行的进程的方法,通过使用终端命令lsof -i :8080可以获取在该端口上运行的所有进程的输出,并使用kill命令终止指定进程的运行。 ... [详细]
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社区 版权所有