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

java的多线程用法编程总结

本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。

一、进程与线程

1、进程是什么?

狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。

广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

2、线程是什么?

线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。

3、进程和线程的区别?

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。

进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。

线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

简言之,线程与进程的区别就是:

(1)一个程序至少有一个进程,一个进程至少有一个线程;
(2) 线程的划分尺度小于进程,使得多线程程序的并发性高。
(3)进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
(4)线程在执行过程中与进程是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
(5)从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。

这就是进程和线程的重要区别。

二、线程的生命周期及五种基本状态

Java线程具有五种基本状态:

(1)新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread();

(2)就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,只是说明此线程已经做好了准备,随时等待CPU调度执行,并不是说执行了t.start()此线程立即就会执行;

(3)运行状态(Running):当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就 绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;

(4)阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:

①等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;

②同步阻塞:线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;

③其他阻塞 : 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

(5)死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

三、Java多线程的实现

在Java中,如果要实现多线程的程序,那么必须依靠一个线程的主体类(好比主类的概念一样,表示一个线程的主类),但是这个线程的主体类在定义的时候需要有一些特殊的要求,这个类可以继承Thread类或实现Runnable接口来完成定义。

1、继承Thread类实现多线程

java.lang.Thread是一个负责线程操作的类,任何的类继承了Thread类就可以成为一个线程的主类。既然是主类,必须有它的使用方法,而线程启动的主方法需要覆写Thread类中的run()方法才可以。

定义一个线程的主体类:

class MyThread extends Thread { // 线程的主体类
 private String title;

 public MyThread(String title) {
 this.title = title;
 }

 @Override
 public void run() { // 线程的主方法
 for (int x = 0; x <10; x++) {
  System.out.println(this.title + "运行,x = " + x);
 }
 }
}

现在已经有了线程类,并且里面也存在了相应的操作方法,那么就应该产生对象并调用里面的方法,于是编写出了下的程序:

public class TestDemo {
 public static void main(String[] args) {
 MyThread mt1 = new MyThread("线程A");
 MyThread mt2 = new MyThread("线程B");
 MyThread mt3 = new MyThread("线程C");
 mt1.run();
 mt2.run();
 mt3.run();
 }

 运行结果:

线程A运行,x = 0
线程A运行,x = 1
线程A运行,x = 2
线程A运行,x = 3
线程A运行,x = 4
线程A运行,x = 5
线程A运行,x = 6
线程A运行,x = 7
线程A运行,x = 8
线程A运行,x = 9
线程B运行,x = 0
线程B运行,x = 1
线程B运行,x = 2
线程B运行,x = 3
线程B运行,x = 4
线程B运行,x = 5
线程B运行,x = 6
线程B运行,x = 7
线程B运行,x = 8
线程B运行,x = 9
线程C运行,x = 0
线程C运行,x = 1
线程C运行,x = 2
线程C运行,x = 3
线程C运行,x = 4
线程C运行,x = 5
线程C运行,x = 6
线程C运行,x = 7
线程C运行,x = 8
线程C运行,x = 9

我们发现:以上的操作并没有真正的启动多线程,因为多个线程彼此之间的执行一定是交替的方式运行,而此时是顺序执行,每一个对象的代码执行完之后才向下继续执行。

如果要想在程序之中真正的启动多线程,必须依靠Thread类的一个方法:public void start(),表示真正启动多线程,调用此方法后会间接调用run()方法:

public class TestDemo {
 public static void main(String[] args) {
 MyThread mt1 = new MyThread("线程A");
 MyThread mt2 = new MyThread("线程B");
 MyThread mt3 = new MyThread("线程C");
 mt1.start();
 mt2.start();
 mt3.start();
 }

}

 运行结果:

线程C运行,x = 0
线程A运行,x = 0
线程B运行,x = 0
线程A运行,x = 1
线程C运行,x = 1
线程A运行,x = 2
线程B运行,x = 1
线程A运行,x = 3
线程A运行,x = 4
线程A运行,x = 5
线程C运行,x = 2
线程C运行,x = 3
线程C运行,x = 4
线程C运行,x = 5
线程C运行,x = 6
线程C运行,x = 7
线程C运行,x = 8
线程C运行,x = 9
线程A运行,x = 6
线程A运行,x = 7
线程A运行,x = 8
线程A运行,x = 9
线程B运行,x = 2
线程B运行,x = 3
线程B运行,x = 4
线程B运行,x = 5
线程B运行,x = 6
线程B运行,x = 7
线程B运行,x = 8
线程B运行,x = 9

此时可以发现:多个线程之间彼此交替执行,但每次的执行结果是不一样的。通过以上的代码就可以得出结论:要想启动线程必须依靠Thread类的start()方法执行,线程启动之后会默认调用了run()方法。

在调用start()方法之后,发生了一系列复杂的事情:
(1)启动新的执行线程(具有新的调用栈);
(2)该线程从新状态转移到可运行状态;
(3)当该线程获得机会执行时,其目标run()方法将运行。
注意:对Java来说,run()方法没有任何特别之处。像main()方法一样,它只是新线程知道调用的方法名称(和签名)。因此,在Runnable上或者Thread上调用run方法是合法的,但并不启动新的线程。

说明:为什么线程启动的时候必须调用start()而不是直接调用run()?

我们发现,在调用了start()之后,实际上它执行的还是覆写后的run()方法,那为什么不直接调用run()方法呢?为了解释此问题,下面打开Thread类的源代码,观察一下start()方法的定义:

 public synchronized void start() {
 if (threadStatus != 0)
  throw new IllegalThreadStateException();
 group.add(this);
 boolean started = false;
 try {
  start0();
  started = true;
 } finally {
  try {
  if (!started) {
   group.threadStartFailed(this);
  }
  } catch (Throwable ignore) {
  }
 }
 }
 private native void start0();

 打开此方法的源代码首先可以发现:方法会抛出一个“IllegalThreadStateException”异常。一般来讲,如果一个方法中使用了throw抛出一个异常对象,那么这个异常应该使用try…catch捕获,或者是方法的声明上使用throws抛出,但是这块都没有,为什么呢?因为这个异常类是属于运行时异常(RuntimeException)的子类:

java.lang.Object
   |- java.lang.Throwable
     |- java.lang.Exception
       |- java.lang.RuntimeException
         |- java.lang.IllegalArgumentException
           |- java.lang.IllegalThreadStateException

当一个线程对象被重复启动之后会抛出此异常,即:一个线程对象只能启动一次。

在start()方法之中有一个最为关键的部分就是start0()方法,而且这个方法上使用了一个native关键字的定义。

native关键字指的是Java本地接口调用(Java Native Interface),即:是使用Java调用本机操作系统的函数功能完成一些特殊的操作,而这样的代码开发在Java之中几乎很少出现,因为Java的最大特点是可移植性,如果一个程序只能在固定的操作系统上使用,那么可移植性就将彻底的丧失,所以,此操作一般不用。

多线程的实现一定需要操作系统的支持,那么以上的start0()方法实际上就和抽象方法很类似没有方法体,而这个方法体交给JVM去实现,即:在windows下的JVM可能使用A方法实现了start0(),而在Linux下的JVM可能使用了B方法实现了start0(),但是在调用的时候并不会去关心具体是何方式实现了start0()方法,只会关心最终的操作结果,交给JVM去匹配了不同的操作系统。

所以在多线程操作之中,使用start()方法启动多线程的操作是需要进行操作系统函数调用的。

2、实现Runnable接口实现多线程

使用Thread类的确是可以方便的进行多线程的实现,但是这种方式最大的缺点就是单继承的问题。为此,在java之中也可以利用Runnable接口来实现多线程。这个接口的定义如下:

public interface Runnable {
 public void run();
}

 通过Runnable接口实现多线程:

class MyThread implements Runnable { // 线程的主体类
 private String title;

 public MyThread(String title) {
 this.title = title;
 }

 @Override
 public void run() { // 线程的主方法
 for (int x = 0; x <10; x++) {
  System.out.println(this.title + "运行,x = " + x);
 }
 }
}

 这和之前继承Thread类的方式区别不大,但是有一个优点就是避免了单继承局限。

不过问题来了。之前说过,如果要启动多线程,需要依靠Thread类的start()方法完成,之前继承Thread类的时候可以将此方法直接继承过来使用,但现在实现的是Runable接口,没有这个方法可以继承了,怎么办?

要解决这个问题,还是需要依靠Thread类完成。在Thread类中定义了一个构造方法,接收Runnable接口对象:

public Thread(Runnable target);

利用Thread类启动多线程:

public class TestDemo {
 public static void main(String[] args) throws Exception {
 MyThread mt1 = new MyThread("线程A");
 MyThread mt2 = new MyThread("线程B");
 MyThread mt3 = new MyThread("线程C");
 new Thread(mt1).start();
 new Thread(mt2).start();
 new Thread(mt3).start();
 }
}

运行结果:

线程A运行,x = 0
线程B运行,x = 0
线程B运行,x = 1
线程C运行,x = 0
线程B运行,x = 2
线程A运行,x = 1
线程B运行,x = 3
线程C运行,x = 1
线程C运行,x = 2
线程B运行,x = 4
线程B运行,x = 5
线程A运行,x = 2
线程A运行,x = 3
线程A运行,x = 4
线程A运行,x = 5
线程A运行,x = 6
线程A运行,x = 7
线程A运行,x = 8
线程A运行,x = 9
线程B运行,x = 6
线程B运行,x = 7
线程B运行,x = 8
线程B运行,x = 9
线程C运行,x = 3
线程C运行,x = 4
线程C运行,x = 5
线程C运行,x = 6
线程C运行,x = 7
线程C运行,x = 8
线程C运行,x = 9

此时,不但实现了多线程的启动,而且没有了单继承局限。

3、实现多线程的第三种方法:.使用Callable接口实现多线程

使用Runnable接口实现的多线程可以避免单继承局限,但是有一个问题,Runnable接口里面的run()方法不能返回操作结果。为了解决这个问题,提供了一个新的接口:Callable接口(java.util.concurrent.Callable)。

public interface Callable{
 public V call() throws Exception;
}

执行完Callable接口中的call()方法会返回一个结果,这个返回结果的类型由Callable接口上的泛型决定。

实现Callable接口来实现多线程的具体操作是:
创建Callable接口的实现类,并实现clall()方法;然后使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。

定义一个线程主体类:

import java.util.concurrent.Callable;

class MyThread implements Callable{

 private int ticket = 10;
 @Override
 public String call() throws Exception {
 for(int i = 0 ; i <20 ; i++){
  if(this.ticket > 0){
  System.out.println("卖票,剩余票数为"+ this.ticket --);
  }
 }
 return "票已卖光";
 }

}

Thread类没有直接支持Callable接口。而在JDK1.5之后,提供了一个类:

java.util.concurrent.FutureTask

这个类主要负责Callable接口对象操作。其定义结构如下:

public class FutureTask
extends Object
implements RunnableFurture

而RunnableFurture这个接口又有如下定义:

public interface RunnableFurture
extends Runnable,Future

在FutureTask 类里面定义有如下构造方法:

public FutureTask(Callable callable)

现在,终于可以通过FutureTask类来接收Callable接口对象了,接收的目的是为了取得call()方法的返回结果。

从上面分析我们可以发现:
FutureTask类可以接收Callable接口对象,而FutureTask类实现了RunnableFurture接口,RunnableFurture接口又继承了Runnable接口。

于是,我们可以这样来启动多线程:

public class TestDemo {
 public static void main(String[] args) throws Exception {
 MyThread mt1 = new MyThread();
 MyThread mt2 = new MyThread();

 FutureTask task1 = new FutureTask(mt1);//取得call()方法返回结果
 FutureTask task2 = new FutureTask(mt2);//取得call()方法返回结果

 //FutureTask是Runnable接口的子类,可以使用Thread类的构造来接收task对象
 new Thread(task1).start();
 new Thread(task2).start();

 //多线程执行完毕后,可以使用FutureTask的父接口Future中的get()方法取得执行结果
 System.out.println("线程1的返回结果:"+task1.get());
 System.out.println("线程2的返回结果:"+task2.get());
 }
}

运行结果:

卖票,剩余票数为10
卖票,剩余票数为10
卖票,剩余票数为9
卖票,剩余票数为8
卖票,剩余票数为7
卖票,剩余票数为9
卖票,剩余票数为6
卖票,剩余票数为8
卖票,剩余票数为5
卖票,剩余票数为7
卖票,剩余票数为4
卖票,剩余票数为6
卖票,剩余票数为3
卖票,剩余票数为5
卖票,剩余票数为2
卖票,剩余票数为4
卖票,剩余票数为1
卖票,剩余票数为3
卖票,剩余票数为2
卖票,剩余票数为1
线程1的返回结果:票已卖光
线程2的返回结果:票已卖光

小结:

上述讲解了三种实现多线程的方式,对于线程的启动而言,都是调用线程对象的start()方法,需要特别注意的是:不能对同一线程对象两次调用start()方法。


推荐阅读
  • 从 .NET 转 Java 的自学之路:IO 流基础篇
    本文详细介绍了 Java 中的 IO 流,包括字节流和字符流的基本概念及其操作方式。探讨了如何处理不同类型的文件数据,并结合编码机制确保字符数据的正确读写。同时,文中还涵盖了装饰设计模式的应用,以及多种常见的 IO 操作实例。 ... [详细]
  • 在现代网络环境中,两台计算机之间的文件传输需求日益增长。传统的FTP和SSH方式虽然有效,但其配置复杂、步骤繁琐,难以满足快速且安全的传输需求。本文将介绍一种基于Go语言开发的新一代文件传输工具——Croc,它不仅简化了操作流程,还提供了强大的加密和跨平台支持。 ... [详细]
  • 解决微信电脑版无法刷朋友圈问题:使用安卓远程投屏方案
    在工作期间想要浏览微信和朋友圈却不太方便?虽然微信电脑版目前不支持直接刷朋友圈,但通过远程投屏技术,可以轻松实现在电脑上操作安卓设备的功能。 ... [详细]
  • 本文详细介绍了如何在Ubuntu系统中下载适用于Intel处理器的64位版本,涵盖了不同Linux发行版对64位架构的不同命名方式,并提供了具体的下载链接和步骤。 ... [详细]
  • 本文介绍如何在Linux服务器之间使用SCP命令进行文件传输。SCP(Secure Copy Protocol)是一种基于SSH的安全文件传输协议,支持从远程机器复制文件到本地服务器或反之。示例包括从192.168.45.147复制tomcat目录到本地/home路径。 ... [详细]
  • Composer Registry Manager:PHP的源切换管理工具
    本文介绍了一个用于Composer的源切换管理工具——Composer Registry Manager。该项目旨在简化Composer包源的管理和切换,避免与常见的CRM系统混淆,并提供了详细的安装和使用指南。 ... [详细]
  • PHP中去除换行符的多种方法及应用场景
    本文将详细介绍在PHP中去除换行符的各种方法,并结合实际应用场景进行说明。通过本文,您将了解如何根据不同操作系统的特点,选择最合适的换行符处理方式。 ... [详细]
  • 基于KVM的SRIOV直通配置及性能测试
    SRIOV介绍、VF直通配置,以及包转发率性能测试小慢哥的原创文章,欢迎转载目录?1.SRIOV介绍?2.环境说明?3.开启SRIOV?4.生成VF?5.VF ... [详细]
  • 本文将深入探讨PHP编程语言的基本概念,并解释PHP概念股的含义。通过详细解析,帮助读者理解PHP在Web开发和股票市场中的重要性。 ... [详细]
  • 本文探讨了如何优化和正确配置Kafka Streams应用程序以确保准确的状态存储查询。通过调整配置参数和代码逻辑,可以有效解决数据不一致的问题。 ... [详细]
  • Linux设备驱动程序:异步时间操作与调度机制
    本文介绍了Linux内核中的几种异步延迟操作方法,包括内核定时器、tasklet机制和工作队列。这些机制允许在未来的某个时间点执行任务,而无需阻塞当前线程,从而提高系统的响应性和效率。 ... [详细]
  • 深入探讨CPU虚拟化与KVM内存管理
    本文详细介绍了现代服务器架构中的CPU虚拟化技术,包括SMP、NUMA和MPP三种多处理器结构,并深入探讨了KVM的内存虚拟化机制。通过对比不同架构的特点和应用场景,帮助读者理解如何选择最适合的架构以优化性能。 ... [详细]
  • 本文探讨了如何在 PHP 的 Eloquent ORM 中实现数据表之间的关联查询,并通过具体示例详细解释了如何将关联数据嵌入到查询结果中。这不仅提高了数据查询的效率,还简化了代码逻辑。 ... [详细]
  • 在多线程编程环境中,线程之间共享全局变量可能导致数据竞争和不一致性。为了解决这一问题,Linux提供了线程局部存储(TLS),使每个线程可以拥有独立的变量副本,确保线程间的数据隔离与安全。 ... [详细]
  • 本文将详细介绍如何在Linux操作系统中执行PHP脚本,包括环境配置、命令使用及验证方法。对于需要在Linux环境下开发或部署PHP应用的用户来说,这是一篇非常实用的文章。 ... [详细]
author-avatar
菜鸟
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有