JAVA多线程(超详细)

1.多线程概述

世间万物中每个个体都可以同时完成很多工作。例如,人体可以同时进行呼吸、血液循环、思考题等活动。用户既可以使用计算机听歌,也可以编写文档、发送文件等,这些活动可以同时进行。在领域里,这种同时执行多个操作的行为模式被称为并发。在操作系统中同时运行着多个独立的任务每个任务对应一个进程,每个进程可产生多个线程。通过这种并发运行的方式,计算机的性能被挖掘至了极限。

1.进程与线程

  1. 进程是程序的一次动态执行过程,它是从代码加载、执行中到执行完毕的一个完整过程,也是过程本身从产生、发展到最终消亡的过程。操作系统同时管理一个计算机系统中的多个进程,让计算中的多个进程轮流使用中央处理器(Central Processing Unit, CPU)资源,或者共享操作系统的其他资源。由于CPU执行速度非常快,所有程序好像是在"同时"运行一样。
  2. 在操作系统中可以有多个进程,这些进程包括系统进程(由操作系统内部建立的进程)和用户进程(由用户程序建立的进程)。可以从Windows任务管理器中查看已启动的进程,进程是系统运行程序的最小单元。各进程之间是独立的,每个进程的内部数据和状态也是完全独立的。
  3. 线程是进程中执行运算的最小单位,暗在进程基础上的进一步划分,一个线程可以完成一个独立的顺序控制流程。下面看一简单的问题,假设一个水箱有五个排水孔,打开一个排水孔清空水箱需要一小时,怎样才能使水箱迅速清空并计算最快的清空时间?如果把水箱排水比作一个进程,那么一个排水孔就是一个线程。
  4. 与进程不同,同一进程内的多个线程共享同一块内存空间(包括代码空间、数据空间)和一块系统资源,所有系统在产生一个线程或在各线程之间切换工作时,其负担要比在进程间切换小得多。
  5. 综上所述,进程和线程是两个不同的概念,应用程序有单进程单线程的,有多进程但每个进程只有一个线程的,有单进程包含多线程的,还有多进程且每个进程有多个线程等四种情况。
  6. 如果在同一个进程中同时有多个线程,用于执行不同的工作,则称之为“多线程”。这些线程可以同时存在、同时执行。例如,只有一个排水孔的水箱可比作单线程程序,有多个排水孔的水箱可比作多线程程序。下面详细介绍多线程的运行机制。

2.多线程的运行机制 

  •  以往开发的程序大多是单线程的,即一个程序只有从开始到结束这一条执行路径。而多线程是指一个进程同时存在几条执行路径且并发执行的工作方式。
  • 并发运行与并行运行不同。并行运行通常表示同一个时刻有多条指令代码在处理器上同时运行,这种情况往往需要多个处理器支持。而并发运行表示在一个处理器中,操作系统为了提高程序的运行效率,将CPU的执行时间分成多个时间片,分配给同一进程的不同线程。当执行完一个时间片后,当前运行的线程就可能交付出CPU权限,让其他线程执行下一个时间片,当然CPU也有可能相邻的时间片分配给同一线程,即多个线程分享CPU时间,交替运行。之所以从表面上看是多个线程同时运行的,是因为不同线程之间切换的时间非常短,也许仅仅是几毫秒,对普通人来说是难以感知的,即所谓的“宏观并行,微观串行”。

3.多线程的优势 

多线程作为一种多作务并发的工作方式,有着广泛的应用。合理使用线程,将减少开发和维护的成本,甚至可以改善复杂应用程序的性能。使用多线程的优势如下。

  1. 充分利用CPU的资源。运行单线程程序时,若程序发生阻塞,则CPU可能会处于空闲状态,这将造成计算机资源浪费。而使用多线程可以在某个线程处于休眠或阻塞的状态时运行其他线程,这样将大大提高资源利用率。
  2. 简化编程模型:可以考虑将一个既长又复杂的进程分为多个线程,成为几个独立执行的模块,如使用时、分、秒来描述当前时间。如果是单线程程序,则需要多重判断。如果使用多线程,时、分、秒各使用一个线程控制,每个线程仅需实现 简单的流程,简化了程序逻辑,更方便编码和维护。
  3. 良好的用户体验。由于多个线程可以交替运行,减少或避免了程序阻塞或意外情况造成的响应过慢现象,减少了用户等待时间,提升了用户体验。
  4. 多线程日常技术实际开发中是非常有价值的。例如,Word文本编辑工具需提供一边编辑一边保存的功能,且同时进行规范化格式和标记错别字检查;一个浏览器必须能同时下载多个图片;一个Web服务器必须能同时响应多个用户请求;JVM本身就在后台提供了一个超级线程进行垃圾回收……总之,多线程在实际编程中的应用是非常广泛的。

2.多线程编程 

java语言提供了java.lang.Thread类支持多线程程序,下面介绍Thread类的用法。

1.Thread类介绍 

Thread类提供是了大量的方法来控制和操作过程。Thread类的常用方法如下表所示。

 

Thread类的静态方法currentThread()返回当前线程对象的引用。在java程序启动时,一个线程立即随之启动,这个线程通常被称为程序的主线程。public static void main()方法是主线程的入口,每个进程至少有一个主线程。它的重要性如下。

  • 主线程是产生其他子线程的线程。
  • 主线程通常必须最后完成运行,因为它执行各种关闭动作。尽管主线程是自动创建的,但是可以由一个Thread对象控制。因此,可以使用Thread类的方法获取主线程信息。 
  • 在java语言中,实现多线程的方式有两种:一种是继承Thread类,另一种是实现Runnable接口下面分别介绍这两种创建线程类的方法。 

2.继承Thread类创建线程类 

继承Thread类是实现线程的一种方式。在使用此方法自定义线程类时,必须在格式上满足如下要求。

  • 此类必须继承Thread类
  • 将线程执行的代码写在run()方法中 

线程从它的run()方法开始执行,即run()方法是线程执行的起点,就像main()方法是应用程序的起点一样。因为run()方法定义在Thread类中,所有的自定义线程类中必须重写run()方法,为线程提供实现具体任务的代码。使用Thread类创建并启动线程的代码结构如下。

//继承Thread类的方式创建自定义线程类
public class MyThread extends Thread{
    //省略成员变量和成员方法代码
    //重写Thread类中的run()方法
   public void run(){
      //线程执行任务的代码
    }
}

 3.实现Runnable接口创建线程类

在使用继承Thread类的方式创建线程的过程中,子类无法再继承其他父类。这是因为java语言不支持多重继承。在这种情况下,可以通过实现Runnable接口的方式创建线程。这种方式更具有灵活性,用户线程还可以通过继承,再具有其他类的特性,这是开发中经常使用的方式。

Runnable接口位于java.lang包中,其中只提供一个抽象方法run()的声明,Thread类也实现了Runnable接口。使用Runnable接口时离不开Thread类,这是因为它要用到Thread类中的statr()方法。在Runnable接口中只有run()方法,其他操作都要借助于Thread类。使用Runnable接口创建线程的一般格式如下。

//实现Runnable接口方式创建线程类
class MyThread implements Runnable{
  public void run(){
    //这里写线程的内容
    }
}
//测试类
public class RunnableTest{
   public static void main(String[] args){
   //通过Thread类创建线程对象
   MyTherad myThread=new MyThread();
   Thread thread =new Thread(myThread);
   thread.start();
    }
}

在以上代码中,虽然MyThread类为线程提供了run()方法,但它本身不是线程类。如果要创建一个专门执行run()方法的线程对象,则需要创建一个Thread对象。

在上面的代码中,MyThread类实现了Runnable接口,在run()方法中编写线程所执行的代码。如果MyThread还需继承其他类(如Base类),也完全可以实现。关键代码如下。

class MyThread extends Base implements Runnable{
    public void run(){
       //线程执行任务的代码
   }
}

3.线程的状态转换 

通过前面的内容,大家学习了java中多线程的基础知识及如何创建和启动线程。如果要实现多线程,则需要在主线程中创建新的线程类对象。java语言使用Thread类及其子类的对象表示线程,新建的线程通常会在五种状态中转换,即新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead).这五种状态组成了线程的生命周期,如下图所示。

 

  1. 新建状态。一个Thread类或其子类的对象被声明并创建,但其在还未启动的这段时间里处于一种特殊的新建状态中。些时,线程类对象已经被分配了内存空间和资源,并且已被初始化,但是该线程沿未被调试。
  2. 就绪状态。就绪状态也被称为可运行状态的线程被启动后,也就是调用start()方法后,新建线程将进入线程队列,排队等待CUP时间片。此时它已具备运行的条件,一旦轮到它使用CPU资源,它就可以脱离创建它的主线程独立开始它的生命周期。这就好比一批作物种子被运输到农场,处于等待果农撒种的状态。
  3. 运行状态。当就绪状态的进程被调试关获得处理器资源后便进入运行状态。每个Thread类及其子类都拥有一个线程操作方法run(),当线程类对象被调度运行时,它将自动调用此run()方法,从该run()方法的一条语句开始执行,一直到执行完毕。处于运行状态的线程在以下四种情况会让出CPU的控制权。
  4. 阻塞状态。一个正在运行的线程在某些特殊 情况下需要让出CPU并暂时中止运行。这时,线程处于不运行的状态被称为阻塞状态。这就好比作物在生长过程中由于干旱或糟遇虫害停滞生长。一个线程当被阻塞时不能进入就绪状态的排队队列,只有当阻塞的原因被取消时,线程才可以转为就绪状态,当一个线程执行此方法时,它就会放弃CPU使用权,转为阻塞状态。处于阻塞状态的线程的线程通常需要在某些时刻被唤醒,至于用什么事件唤醒该线程,取决于其阻塞的原因。处于休眠状态的线程必须被阻塞一段固定的时间,这段时间结束后即转为就绪状态。
  5. 死亡状态。一个线程的run()方法运行完毕,表明该线程已死亡。处于死亡的线程不具有继承运行的能力。导致线程死亡的原因有两个:一是正常运行的线程完成了它的全部工作,即运行完run()方法的最后一条语句并退出;二是当进程停止运行时,该进程中的所有线程将被强行中止。线程处于死亡状态并且没有该线程的引用时,JVM会从内存中删除该线程类对象。
  6. 简而言之,忽略操作系统底层的操作,在程序中可视的线程生命周期是新建线程、start()方法启动线程、调用run()方法运行线程,运行完毕后线程的生命周期就结束了。
  • 线程运行完毕。
  • 有比当前线程优先级更高的线程抢占了CPU.
  • 线程休眠。
  • 线程因等待某个资源而处于阻塞状态。

 4.线程调试相关方法

下面介绍线程的调度,常用的线程操作方法如下表所示。

1.线程的优先级 

每个线程运行时都具有一定的优先级,优先级高的线程获得较多的运行机会,而优先级低的线程获得较少的运行机会。每个线程的默认优先级都与创建它的线程的优先级相同,在默认情况下,主线程main具有普通优先级,由主线程创建和子线程也具有普通优先级。

Thread类提供了setPriority(int newPriority)方法、getPriority()方法用于设置和返回指定线程的优先级,其中setPriority(int newPriority)方法的参数newPriority可以是一个整数,范围是1~10;也可以使用Thread类的如下三静态常量设置线程的优先级。

  • MAX_PRIORITY:其值是10,表示优先级最高
  • MIN_PRIORITY:其值是1,表示优先级最低
  • NORM_PRIORITY:其值是5,表示普通优先级

 2.线程的强制运行

在线程操作中,可以使用join()方法让一个线程强制运行。在线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续运行。它有三个重载方法,定义如下。

public final void join()
public final void join(long mills)
public final void join(long mills,int nanos)

需要注意的是,调用join()方法需要处理InterruptedException异常。

3.线程的礼让 

当一个线程在运行中执行了Thread类的yield()静态方法中,如果此时还有相同或更高优先级的其他线程处于就绪状态,系统将会选择其他相同或更高优先级的线程运行,如果不存在这样的线程,则该线程继续运行。yield()方法如下。

public static void yield()

 5.线程同步

当多个线程共享数据时,由于CPU负责线程的调试,所以程序无法精确地控制多线程的交替次序。如果没有特殊控制,则多线程对共享数据的修改和访问将导致数据的不一致。

1.为什么需要线程同步

前面学习的线程都是独立且异步运行的,也就是说每个线程都包含了运行时所需要的数据或方法,不必关心其他线程的状态和行为。但是经常会有一些同时运行的线程需要操作共同数据,此时就要考虑其他线程的状态和行为;否则,不能保证程序运行结果的正确性。下面通过具体示例进行分析。

 一年一度的国庆佳节快到了,果园里,这边果农们在采摘应季水果,那边销售人员在仓库向酵素梅和商超发货。为保持水果新鲜,仓库最多可暂存三车水果,当仓库的水果全部发货完毕后,就会有运输车将果农闪采摘的水果成箱地运关到仓库,如下图所示。

2.实现线程的同步 

当两个或多个线程需要访问同一资源时,需要以某种顺序来确保该资源某一时刻只能被一个线程使用,这称为线程同步。线程同步相当于为线程中需要一次性完成不允许中断的操作加上一把锁,从而解决冲突。

在执行向仓库运输水果和果商采购水果操作中加一把锁,不允许其他线程进入。只有在每次在每次修改数据和显示数据两步操作都执行完毕后,才允许打开这把锁。在试衣间的例子中,一个人进入试衣间后就为其加上一把锁 ,只有试衣完毕后才能打开,在这过程中不允许任何人进入。

以上加锁的过程使用线程同步实现,有同步代码块和同步方法两种方式,这两种方式都使用到synchronized关键字。

1.同步代码块

代码块即使用"{}"括起来的一段代码,使用synchronized关键字修饰的代码块被称为同步代码块。其语法如下。

synchronized(obj){
  //需要同步的代码
}

如果一个代码块带有synchronized(obj)标记,那么当线程执行代码时,必须先获得obj变量所引用的对象的锁,其可针对任何代码块,并且可以任意指定上锁的对象,因此灵活性更高。

2.同步方法 

如果一个方法的的呢代码都属于需同步的代码,那么这个方法定义处可以直接使用synchronized关键字修饰,即同步方法。其语法如下。

访问修饰符 synchronized 返回类型 方法名(参数列表) {//省略方法体……}
或
synchronized 访问修饰符 返回类型 方法名(参数列表) {//省略方法体……}

3.线程同步的特征 

所谓线程之间保持同步,是指不同的线程在执行以同一个对象作为锁标记的同步代码块或同步方法时,因为要获得这个对象的锁而相互牵制,线程同步具有以下特征。

  1. 当多个并发线程访问同一对象的同步代码块或同步方法时,同一时刻只能有一个线程运行,其他线程必须等待当前线程运行完毕后才能运行。
  2. 如查多个线程访问的不是同一共享资源,则无需同步 。
  3. 当一个线程访问Object对象的同步代码块或同步方法时,其他线程仍可以访问该Object 对象的非同步代码块及非同步方法。

综上所述,synchronized关键字就是为当前手代码块声明一把锁,获得这把锁的线程可以执行代码块里的指令,其他的线程只能等待获取锁,然后才能执行相同的操作。

以上学习了使用同步方法和同步代码块实现线程同步,这两者从实现结果上看没有区别,只是同步方法便于阅读理解,而同步代码块可以更精确地限制访问区域,这样会更高效。

 4.线程安全的类型

若程序所在的进程中有多个线程,而当这些线程同时运行时,每次的运行结果和单线程时的运行结果是一样的,而且其他变量的值也和预期相同,那么当前程序就是线程安全的。

一个类的被多线程访问时,不管运行时对这些线程安全的呢?答案是ArrayList是非线程安全的类型。那么应该如休判断 ?

从以上ArrayList类源码中可以看出,其实现主要是使用一个Object类型数组保存所有元素,使用一个int类型变量size保存当前数组中已经存储的元素个数。

由此可以看出,添加一个元素主要完成如下两步操作。

  1. 判断列表容量是否足够,是否需要扩容。
  2. 将元素添加到列表的元素数组里。

以上两步操作并非不可分割,这样也就出现了导致线程不安全的隐患。在多个线程执行add()方法进行添加元素操作时,可以会导致elementData数组越界。

 1.对比Hashtable和HashMap

1.是否线程安全

Hashtable是线程安全的,其方法是同步的,可查看Hashtable类型源码中操作数据的方法为同步方法

而HashMap中的方法在默认情况下是非同步的。在多线程并发的环境下,可以直接使用Hashtable.如果使用HashMap.就要自行增加同步处理。

2.效率比较

由于Hashtable是线程安全的,其方法是同步的,而HashMap是非线程安全的,重速度,轻安全,所以当只需单线程时,使用HashMap的执行速度要高过Hashtable。

 2.对比StringBuffer和StringBuilder

StringBuffer和StringBuilder都可用来存储字符串变量,是可变的对象。它们的区别是StringBuffer是线程安全的,而StringBuilder是非线程安全的。因此,在单线程环境下StringBuilder执行效率更高

 

 

 

  • 30
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Java多线程是指在一个Java程序中同时执行多个线程,它可以提高程序的并发性和响应能力。Java中实现多线程的方式有多种,包括继承Thread类、实现Runnable接口、使用Executor框架和Callable/Future等。\[1\] 在继承Thread类的方式中,可以创建一个自定义的线程类,重写run方法,并通过调用start方法来启动线程。\[2\] 使用线程池是一种最佳的多线程实现方式,它可以避免系统不断创建和销毁新的线程,从而减少系统资源的消耗。通过使用Executors类的静态方法,可以创建不同类型的线程池,如可缓存的线程池。\[3\] 在多线程并发中,存在线程安全问题,即多个线程同时访问共享资源可能导致数据不一致或其他错误。为了解决这个问题,可以使用同步机制,如synchronized关键字或Lock接口来保证线程的安全性。\[6\] 希望以上信息对您有所帮助。 #### 引用[.reference_title] - *1* [【Java系列】深入解析Java多线程](https://blog.csdn.net/weixin_36755535/article/details/130558474)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [java多线程详细)](https://blog.csdn.net/zdl66/article/details/126297036)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值