初识Java多线程

一、多线程概述

1.1、程序、进程、线程概念

1)程序

是为完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象

2)进程

是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

3)线程

进程内部的一个独立执行单元;一个进程可以同时并发的运行多个线程,可以理解为一个进程便相当于一个单 CPU 操作系统,而线程便是这个系统中运行的多个任务。

1.2、并行与并发

1)并行

多个CPU同时执行多个任务;指两个或多个时间在同一时刻发生(同时发生)。

比如:多个人同时做不同的事

2)并发

一个CPU(采用时间片)同时执行多个任务,指两个或多个事件在一个时间段内发生。

比如秒杀平台,多个人做同件事

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

而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行

目前电脑市场上说的多核 CPU,便是多核处理器,核 越多,并行处理的程序越多,能大大的提高电脑运行的效率。

注意:单核处理器的计算机肯定不能并行的处理多个任务,只能是多个任务交替的在单个 CPU 上运行

1.3、多线程的作用

1)为了更好的利用cpu的资源,如果只有一个线程,则第二个任务必须等到第一个任务结束后才能进行,如果使用多线程则在主线程执行任务的同时可以执行其他任务,而不需要等待;

2)进程之间不能共享数据,线程可以;

3)系统创建进程需要为该进程重新分配系统资源,创建线程代价比较小;

4) Java语言内置了多线程功能支持,简化了java多线程编程。

1.4、进程与线程的区别

进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。

线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。

注意:

1)因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是干涉不了的。而这也就造成的多线程的随机性。

2)Java 程序的进程里面至少包含两个线程,主进程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。

3)由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建多线程,而不是创建多进程。

二、线程的生命周期

1.1、线程生命周期图

1)线程状态图

2)线程状态转换图

3)具体运行转换图

1.2、线程生命周期详解

1.2.1、新建状态(New)

用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间。

备注:不能对已经启动的线程再次调用start()方法,否则会出现Java.lang.IllegalThreadStateException异常。

1.2.2、就绪状态(Runnable)

线程对象创建后,其他线程调用了该对象的start()方法时进入就绪状态。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。

详解:

处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。

备注:如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一伙儿,转去执行子线程。

1.2.3、运行状态(Running)

就绪状态的线程获取了CPU,执行程序代码。

详解:

处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态

处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。

1.2.4、阻塞状态(Blocked)

阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。

详解:

处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。

在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。

阻塞的情况分类:

​ 1)等待阻塞:运行的线程执行 wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
​ 2)同步阻塞:运行的线程在获取对象的 同步锁 时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
​ 3)其他阻塞:运行的线程执行 sleep()或join()方法 ,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超 时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)

1.2.5、死亡状态(Dead)

线程执行完了或者因异常退出了run()方法,该线程结束生命周期。

当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。

详解:

当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。

三、从JVM内存结构中看线程

1)Java虚拟机内存结构图

2)解释

程序(一段静态的代码)——————》加载到内存中——————》进程(加载到内存中的代码,动态的程序)
进程可细分为多个线程,一个线程代表一个程序内部的一条执行路径
每个线程有其独立的程序计数器(PC,指导着程序向下执行)与运行栈(本地变量等,本地方法等)

3)线程的分类

java中的线程分为两类:1.守护线程(如垃圾回收线程,异常处理线程),2.用户线程(如主线程)

若JVM中都是守护线程,当前JVM将退出。

四、线程开销

多线程中两个必要的开销:线程的创建、上下文切换

4.1、创建线程开销

创建线程使用是直接向系统申请资源的,对操作系统来说,创建一个线程的代价是十分昂贵的, 需要给它分配内存、列入调度,同时在线程切换的时候还要执行内存换页,CPU 的缓存被 清空,切换回来的时候还要重新从内存中读取信息,破坏了数据的局部性。

关于资源:Java线程的线程栈所占用的内存是在Java堆外的,所以是不受java程序控制的,只受系统资源限制,默认一个线程的线程栈大小是1M(当然这个可以通过设置-Xss属性设置,但是要注意栈溢出问题),但是,如果每个用户请求都新建线程的话,1024个用户光线程就占用了1个G的内存,如果系统比较大的话,一下子系统资源就不够用了,最后程序就崩溃了。

同样的道理在java程序中也不要随意开启新的线程,特别是高频业务尽量使用线程池,不然很容易导致内存不足,程序崩溃的问题

4.2、上下文切换开销

1.概念

当前任务执行一个时间片后会切换到下一个任务。在切换之前,上一个任务的状态会被保存下来,下次切换回这个任务时,可以再加载这个任务的状态,任务从保存到再加载的过程就是一次上下文切换。

2.说明

1)时间片是CPU分配给各个线程的时间,时间片一般是几十毫秒。

2)CPU通过给每个线程分配CPU时间片,并且不停地切换线程来实现多线程。因为时间片非常短,所以感觉多个线程是在同时执行。

3.减少上下文切换的方法

1)无锁并发编程

多线程竞争锁时,会引起上下文切换,所以在使用多线程处理数据时,可以采用一些策略来避免使用锁。

常见的策略:将数据按照id的哈希值进行切分,不同的线程处理不同段的数据。

2)锁分离技术

举例:ConcurrentHashMap

3)CAS算法

java的Atomic包使用CAS算法来更新数据,而不需要加锁。

4)使用最少的线程

避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。

举例:

通过减少大量WAITING的线程,来减少上下文切换次数

转储堆栈信息

jstack PID > dumpfile

统计所有线程的状态

grep java.lang.Thread.State dumpfile | awk '{print $2" "$3" "$4" "$5}' | sort | uniq -c

如果存在大量waiting的线程,则查看dumpfile文件进行分析:

1)如果是服务器的工作线程大量等待,则修改服务器配置文件中线程池的配置信息,然后重启查看效果。

posted @ 2021-03-10 14:29  华仔Coding  阅读(207)  评论(0编辑  收藏  举报
levels of contents