Java 基础 —— 线程概念

1、进程与线程

(1)进程

进程是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。确切的来说,当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序,并且具有一定独立功能。

在这里插入图片描述
(2)线程

线程是操作系统能够独立调度和分派的最小单位,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流。线程(Thread)是进程中的基本执行单元,在进程入口执行的第一个线程被视为这个进程的主线程。

一个进程可以有一个或多个线程,各个线程之间共享程序内存空间(也就是所在进程的内存空间)的堆和方法区。一个标准的线程由线程ID,当前指令指针PC,寄存器和堆栈组成。而进程由内存空间(代码,数据,进程空间,打开的文件)和一个或多个线程组成。
进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见。

线程和进程关系示意图
在这里插入图片描述

2、线程的内存管理

在这里插入图片描述

主内存: Java内存规定了所有变量都存储在主内存(Main Memory)中,各个线程又有自己的本地内存(工作内存),本地内存保存着主内存中部分变量。

具体访问方式如下:
在这里插入图片描述

● lock加锁:为了保证访问主内存变量的线程安全性,在访问前一般会加锁处理;
● read读:从主内存中读取一个变量到工作内存;
● load加载:把read读到的变量加载到工作内存的变量副本中;
● use使用:此时线程可以使用其工作内存中的变量了;
● assign赋值:将处理后的变量赋值给工作内存中的变量;
● store存储:将工作内存中的变量存储到主内存中,以新建new 一个新变量的方式存储;
● write写:将store存在的新变量的引用赋值给被处理的变量;
● unload解锁:所有的工作做完,最后解锁释放资源。

3、主线程、用户线程与守护线程

(1)主线程

当 Java 程序启动时,一个线程立刻运行,该线程通常叫做程序的主线程(main thread),它是程序开始时就被自动创建执行的线程,是程序执行的入口每个进程只有一个主线程。主线程的重要性体现在两方面:

1)子线程都从主线程中被创建;
2)通常它必须最后完成执行,因为它执行各种关闭动作。

主线程在程序启动时会被自动创建,为了能够控制它我们必须获取到它的引用,我们可以在当前类中调用 currentThread() 方法来获取到一个当前线程的引用,该方法是 Thread 类的公有的静态方法。
main() 函数可以认为是主线程,但不是守护线程。

(2)守护线程

守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收(GC)线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。
当 JVM 中不存在任何一个正在运行的非守护线程时,则 JVM 进程即会退出。

1)守护线程经常被用来执行一些后台任务。
2)如果用户线程已经全部退出运行了,连 main 线程也执行完毕,守护线程存也会随之结束,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。
3)如果有用户自定义线程存在的话,JVM 就不会退出。此时,守护线程也不能退出,也就是它还要运行,干嘛呢,就是为了执行垃圾回收的任务啊。
4)守护线程拥有自动结束自己生命周期的特性,而非守护线程不具备这个特点。如果你希望在程序退出时( JVM 退出时),线程能够自动关闭,那么可以将该线程设置为守护线程

创建一个守护线程,可以使用 thread.setDaemon(true) 方法将非守护线程设置为守护线程。

1)thread.setDaemon(true) 必须在 thread.start() 之前设置,否则会跑出一个 IllegalThreadStateException 异常:你不能把正在运行的常规线程设置为守护线程。
2)在守护线程中创建的线程也是守护线程。

(3)用户线程

由用户创建。

关系

① 主线程、守护线程和非守护线程互不影响。
② 守护线程与用户线程没有本质区别,它们是可以相互切换的。默认情况下启动的线程是用户线程,通过setDaemon(true)将线程设置成守护线程,这个函数必须在线程启动前进行调用,否则会报java.lang.IllegalThreadStateException异常,启动的线程无法变成守护线程,而是用户线程。
③ 用户线程是独立存在的,不会因为其他用户线程退出而退出;守护线程是依赖于用户线程,当所有用户线程结束了,守护线程也会结束,JVM 退出,典型的守护线程如垃圾回收线程;如果 JVM 中还存在用户线程,守护线程就不会终止,那么JVM就会一直存活,不会退出。
4)虚拟机必须确保用户线程结束才能退出;虚拟机不必守护线程结束就可以直接退出

4、父线程与子线程

对于父子线程,有两种情况:

第一种是父线程是进程的主线程,子线程由主线程创建;

第二种情况是父线程为进程主线程创建的一个子线程,而这个子线程又创建了一个孙线程,这种情况大多被称为子孙线程。

一个线程与被他创建出来的线程,除了在创建的时候(init)会有一定的依赖交互之外,对 JVM 来说,他们并没有什么特别的依赖联系,是两个独立的线程

不管是父线程还是子线程,这只不过是在运行时谁建了谁时用的,一旦所谓的子线程被启动,这两个线程是没有联系的。任何线程是没有办法把另外一个线程终止的。

5、单线程与多线程

(1)单线程

单线程就是进程只有一个线程。若有多个任务只能依次执行,当上一个任务执行结束后,下一个任务开始执行。

(2)多线程

多线程就是进程有多个线程。在一个程序中可以同时运行多个不同的线程来执行不同的任务。

单线程与多线程的比较

单线程(同步)应用程序的开发比较容易,但由于需要在上一个任务完成后才能开始新的任务,所以其效率通常比多线程应用程序低。
多线程可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。
线程也是程序,所以线程需要占用内存,线程越多占用内存也越多; 多线程需要协调和管理,所以需要CPU时间跟踪线程; 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;线程太多会导致控制太复杂,最终可能造成很多Bug。

6、线程池

在我们的开发中经常会使用到多线程。例如在Android中,由于主线程的诸多限制,像网络请求等一些耗时的操作我们必须在子线程中运行。我们往往会通过new Thread来开启一个子线程,待子线程操作完成以后通过Handler切换到主线程中运行。这么以来我们无法管理我们所创建的子线程,并且无限制的创建子线程,它们相互之间竞争,很有可能由于占用过多资源而导致死机或者OOM。所以在Java中为我们提供了线程池来管理我们所创建的线程。

(1)采用线程池的好处

① 重用线程池中已经存在的线程,减少了线程的创建和消亡多造成的性能开销。
② 能够有效控制最大的并发线程数,提高了系统资源的使用率,并且还能够避免大量线程之间因为相互抢占系统资源而导致阻塞。
③ 能够对线程进行简单管理,并提供定时执行、定期执行、单线程、并发数控制等功能。
④ 提供更强大的功能,延时定时线程池。

(2)创建线程池

我们可以通过ThreadPoolExecutor来创建一个线程池。构造函数:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

ThreadPoolExecutor参数含义

corePoolSize   
线程池中的核心线程数,默认情况下,核心线程一直存活在线程池中,即便他们在线程池中处于闲置状态。除非我们将ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这时候处于闲置的核心线程在等待新任务到来时会有超时策略,这个超时时间由keepAliveTime来指定。一旦超过所设置的超时时间,闲置的核心线程就会被终止。
maximumPoolSize   
线程池中所容纳的最大线程数,如果活动的线程达到这个数值以后,后续的新任务将会被阻塞。
keepAliveTime   
非核心线程闲置时的超时时长,对于非核心线程,闲置时间超过这个时间,非核心线程就会被回收。只有对ThreadPoolExecutor的allowCoreThreadTimeOut属性设为true的时候,这个超时时间才会对核心线程产生效果。
unit   
用于指定keepAliveTime参数的时间单位。他是一个枚举,可以使用的单位有天(TimeUnit.DAYS),小时(TimeUnit.HOURS),分钟(TimeUnit.MINUTES),毫秒(TimeUnit.MILLISECONDS),微秒(TimeUnit.MICROSECONDS, 千分之一毫秒)和毫微秒(TimeUnit.NANOSECONDS, 千分之一微秒);
workQueue   
线程池中保存等待执行的任务的阻塞队列。通过线程池中的execute方法提交的Runable对象都会存储在该队列中。我们可以选择下面几个阻塞队列。ArrayBlockingQueue:基于数组实现的有界的阻塞队列,该队列按照FIFO(先进先出)原则对队列中的元素进行排序。 LinkedBlockingQueue:基于链表实现的阻塞队列,该队列按照FIFO(先进先出)原则对队列中的元素进行排序。SynchronousQueue: 内部没有任何容量的阻塞队列。在它内部没有任何的缓存空间。对于SynchronousQueue中的数据元素只有当我们试着取走的时候才可能存在。 PriorityBlockingQueue:具有优先级的无限阻塞队列。 我们还能够通过实现BlockingQueue接口来自定义我们所需要的阻塞队列。
threadFactory   
线程工厂,为线程池提供新线程的创建。ThreadFactory是一个接口,里面只有一个newThread方法。
handler   
他是RejectedExecutionHandler对象,而RejectedExecutionHandler是一个接口,里面只有一个rejectedExecution方法。当任务队列已满并且线程池中的活动线程已经达到所限定的最大值或者是无法成功执行任务,这时候ThreadPoolExecutor会调用RejectedExecutionHandler中的rejectedExecution方法。在ThreadPoolExecutor中有四个内部类实现了RejectedExecutionHandler接口。在线程池中它默认是AbortPolicy,在无法处理新任务时抛出RejectedExecutionException异常。下面是在ThreadPoolExecutor中提供的四个可选值。 CallerRunsPolicy:只用调用者所在线程来运行任务。AbortPolicy:直接抛出RejectedExecutionException异常。 DiscardPolicy:丢弃掉该任务,不进行处理 DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。 我们也可以通过实现RejectedExecutionHandler接口来自定义我们自己的handler。如记录日志或持久化不能处理的任务。

(3)线程池流程
在这里插入图片描述

① 首先线程池判断基本线程池是否已满?没满,创建一个工作线程来执行任务。满了,则进入下个流程。
② 其次线程池判断工作队列是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程。
③ 最后线程池判断整个线程池是否已满?没满,则创建一个新的工作线程来执行任务,满了,则交给饱和策略来处理这个任务。

线程池为什么要使用阻塞队列而不使用非阻塞队列?

阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。
使得在线程不至于一直占用cpu资源。
(线程执行完任务后通过循环再次从任务队列中取出任务进行执行,代码片段如下
while (task != null || (task = getTask()) != null) {})。
不用阻塞队列也是可以的,不过实现起来比较麻烦而已,有好用的为啥不用呢?

(4)如何配置线程池

CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
IO密集型任务
可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
混合型任务
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。 因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。 因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。

(5)线程池分类

newCachedThreadPool
用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)
newFixedThreadPool
创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
newSingleThreadExecutor
创建一个单线程的线程池,适用于需要保证顺序执行各个任务。
newScheduledThreadPool
适用于执行延时或者周期性任务。

7、并行与并发

(1)并行

并行:指两个或多个线程在同一时刻发生( 同时发生)。
在这里插入图片描述

(2)并发

并发:指两个或多个线程在同一个时间段内发生(同一时间段)。
在这里插入图片描述

并发是在一段时间内宏观上多个程序同时运行,并行是在某一时刻,真正有多个程序在运行。
决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行。只有在多CPU或者一个CPU多核的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。

8、线程的生命周期

在这里插入图片描述
(1)新建(new)

当程序使用 new 关键字创建了一个线程之后,该线程就处于一个新建状态(初始状态),此时它和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,并初始化了其成员变量值,但是尚未分配的到系统资源或者CPU试用权。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体

(2)就绪(Runnable)

线程对象调用了start()方法之后,该线程处于就绪状态Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。在就绪状态时,线程必须争夺CPU的使用权,只有获得CPU使用权的线程才能调用run()来执行

(3)运行(Running)

如果处于就绪状态的线程获得了CPU资源,就开始执行run方法的线程执行体,则该线程处于运行状态。

(4)阻塞 (Blocked)

阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况大概三种:
① 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
② 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
③ 线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
④ 线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。唤醒线程后,就转为就绪(Runnable)状态。
⑤ 线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程

(5)死亡(Dead)

线程会以以下三种方式之一结束,结束后就处于死亡状态:
① run()方法执行完成,线程正常结束
② 线程抛出一个未捕获的Exception或Error
③ 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用。

9、线程的实现

10、线程调度

线程不一定立即执行,由 CPU 安排调度,只有获得 cpu 的使用权才能执行指令

每个线程只有获得 cpu 的使用权才能执行指令。线程调度是指系统为线程分配处理器使用权的过程,主要调度方式分两种,分别是协同式线程调度和抢占式线程调度。
Java 线程调度就是抢占式调度

1、协同式调度

协同式线程调度,线程执行时间由线程本身来控制,线程把自己的工作执行完之后,要主动通知系统切换到另外一个线程上。

好处是实现简单,且切换操作对线程自己是可知的,没有线程同步问题
坏处是线程执行时间不可控制,如果一个线程有问题,可能一直阻塞在那里

2、抢占式线程调度

抢占式调度,每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(Java中,Thread.yield()可以让出执行时间,但无法获取执行时间)。线程执行时间系统可控,也不会有一个线程导致整个进程阻塞。

希望系统能给某些线程多分配一些时间,给一些线程少分配一些时间,可以通过设置线程优先级来完成。Java语言一共10个级别的线程优先级(Thread.MIN_PRIORITY至Thread.MAX_PRIORITY),在两线程同时处于ready状态时,优先级越高的线程越容易被系统选择执行。但优先级并不是很靠谱,因为Java线程是通过映射到系统的原生线程上来实现的,所以线程调度最终还是取决于操作系统

Java中线程会按优先级分配CPU时间片运行,那么线程什么时候放弃CPU的使用权?可以归类成三种情况:

当前运行线程主动放弃CPU,JVM暂时放弃CPU操作(基于时间片轮转调度的JVM操作系统不会让线程永久放弃CPU,或者说放弃本次时间片的执行权),例如调用yield()方法。

当前运行线程因为某些原因进入阻塞状态,例如阻塞在I/O上。

当前运行线程结束,即运行完run()方法里面的任务。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值