java特种兵读书笔记(5-1)——并发之基础介绍

Thread


所有java程序都是在进程中分配线程来处理的。

如果是一个main方法,则由一个主线程来处理。

如果不创建自定义线程,那么这个程序就是单线程的。

单独创建线程要使用Thread类,可以扩张Thread的run方法,也可以创建一个Thread,将一个Runnable任务实体传入。一个Runnable实体是一个任务,而不是一个线程

一个线程的启动需要通过Thread.start方法来完成,该方法会调用本地方法JNI来实现一个真正意义的线程。即只有start成功调用后,由OS分配线程资源,才能叫做线程。JVM中分配的Thread对象只是与之对应的外壳。

当大量分配线程后,可能会报错unable to create new native thread,说明线程使用的是堆外的内存空间,也说明了Thread本身对应的实例仅仅是JVM内部的一个普通java对象,是一个线程操作的外壳,而不是真正的线程

Thread与Runnable


可以把Runnable看成一个任务,如果仅仅与Thread配合使用,即在Thread构造函数时传入Runnable实例,那么它会被设置到Thread的target属性上。

Thread默认的run方法是调用target的run方法,这样Runnable的概念就与线程的概念隔离了——它本身是任务

线程可以执行任务,否则Thread需要通过子类去实现run方法描述任务内容

主线程与子线程


public static void main(String[] args) {

new Thread() {public void run(){System.out.println("nihao");}}

System.out.println("main");

}

这里测试“main”有时会在“nihao”之前打出来,而且概率很高。

因为main方法启动线程之后,就直接向下执行,不过启动线程(start)还需要做一些内核调用的处理,最后才会由C区域的方法回调java中的run,此时main线程可能已经输出了内容“main”。

结论:

①Thread的start方法启动了另一个线程来处理任务。

②线程的run方法调用并不是线程在调用start方法时被同步调用的,而是需要一个短暂延迟(因为需要做一些内核调用处理)

线程与进程


优点:

通常将线程理解为轻量级进程。它与进程的最大区别是,多个线程是共享一个进程资源的,对于OS的许多资源管理和分配(例如内存),通常是进程级别的。

线程只是OS调度的最小单位,相对进程线程更加轻量一些,上下文信息更少,创建于销毁更加简单。线程被挂起不会导致整个进程被挂起,一个进程中又可以分配许多线程。

缺点:

多线程也有问题,如果某个线程占用过多资源会导致整个进程宕机。

由于资源共享,线程之间会互相影响。多进程就不会有这个问题,因为它们共享服务器资源,相互影响的级别在服务器资源上,而不是进程内部。

选择:

具体选择需要视情况而定,类似Nginx这种负载均衡的软件就采用多进程模型,因为它的异步IO对于高并发来说,已经足以解决进程或者线程资源不足的问题,而且比多线程模型处理的更好,因为它是IO密集型的。

如果程序是计算密集型的,就不适合这样做了。

Thread的start方法启动线程


有好几种实现:

①Kernel线程与程序交互(KLT与LWP):基于Kernel Thread(KLT)的映射来实现。KLT是内核线程,内核线程由OS直接完成调度和切换,它相对于应用程序的线程来讲只是一个接口。外部程序会使用一种轻量级(Light Weight Process,LWP)来与KLT进行一对一的接口调用。即进程内部会尝试用OS的内核线程去参与实际的调度,使用API调用作为中间桥梁,与自己的程序交互。(应用线程->LWP->OS->KLT)

②基于用户线程(User Thread,UT):点对于实现一,没有中间这一层映射,自己的线程直接由CPU来调度,理论上效率更高。但是这样,用户进程需要关注的抽象层次会更低一些,跳过OS更加接近CPU(即自己要做许多原本OS做的事情,自然的OS调度算法、创建、销毁、上下文切换、挂起都要自己搞定,因为CPU只做计算)。这样做很麻烦。

③混合实现方式:既保留Kernel线程原有架构,又使用用户线程,轻量级进程依然与Kernel线程一一对应保持不变,唯一的变化是,轻量级进程不再与进程直接挂钩,而是与用户线程挂钩。用户线程与轻量级进程多对多,这样的好处是增加了一层来解除LWP与原进程之间的耦合,调度更灵活。

线程状态NEW


通过Thread的getState方法获得线程状态,返回值是一个枚举值。

这个线程还没有被start启动,或者说还不是一个真正意义上的线程。本质上说,这只是创建了一个java外壳,还没有真正的线程来运行。调用了start方法不代表立即改变,中间还有一些步骤(Kernel的调用)。在这个启动的过程中,如果有另一个线程来获取它的状态,其实是不确定的,要看中间步骤是否已经完成。

线程状态RUNNABLE


当NEW状态的线程发生start结束后,线程将变为RUNNABLE。获取状态得到的都是其他线程的状态,而不是自己的状态。该状态也可以理解为存活着正在尝试征用CPU的线程(有可能这个瞬间并没有占用CPU,但是它可能正在发送指令等待系统调度)。在真正的系统中,并不是开启了一个线程CPU就只为这一个线程服务,它必须使用某种调度算法来达到某种平衡,但这个时候线程依然处于RUNNABLE状态。

例如,当一个线程发生yield操作时,其实看到的线程状态也是RUNNABLE,只是它有一个细节的内部变化,就是做一个简单的让步。比如一个线程认为自己在做大量的CPU运算,会在较长一段时间内占用资源,这时如果调度算法有问题,就会一直占用CPU,所以适当的时候做下让步,让别人也来使用一下CPU(通过调度算法实现)

类似在超时买东西,高素质人(有调度算法的)如果发现有东西忘记买了,会让后面的人先结账,自己再去拿东西,然后回来重新排队。而没素质的人(没有调度算法的)会让后面的人干等着,自己去拿东西,然后会来付账。

BIO阻塞的时候,线程也是出于RUNNABLE状态,而在底层,线程已经被阻塞了。这是java内在一些不协调的问题所在。所以不光要看状态本身,也要看java与计算机之间的关系和计算机底层的东西。

线程状态BLOCKED


BLOCKED称为阻塞状态,或者说线程已经被挂起了,它睡着了。原因通常是因为它在等待一个锁

比如当某个synchronized正好有线程在使用,另一个线程尝试进入这个临界区,就会被阻塞。直到另一个线程走完临界区,或者发生相应锁对象的wait操作,它才有机会去争夺进入临界区的权利。

争取到锁的权利(或者说争取到进入临界区的权利之后)BLOCKED状态就会变为RUNNABLE状态,如果在征用锁的过程中没有抢到,那么又要继续等待休息了。

注意BLOCKED状态并非一定显示的存在于synchronized上,也可能是一种嵌套的隐藏方式。

一旦线程出于BLOCKED状态,线程就真的什么都不做了,在java层面始终无法唤醒它。即使用interrupt方法也不行,因为该方法只是在里面做个标记,不会真正唤醒处于阻塞状态的线程。

线程状态WAITING


一个线程拥有对象锁之后,进入相应的代码区域后(进入synchronized),调用相应的“锁对象”的wait方法后产生的一种效果。还有LockSupport的一些操作,比如park等等,还有Thread的join方法。

BLOCKED和WAITING


BLOCKED是虚拟机认为程序还不能进入某个区域(synchronized区域),因为进去会有问题,这是一块临界区。

发生wait操作的先决条件是要进入临界区,也就是线程已经拿到了锁,自己可能进去做些事情,但此时因为某些业务条件,发现还有些其他资源没有准备好,那么自己就再等等再做其他事情,同时把锁释放出来

举例:wait和notify实现生产者消费者模型

如果生产者生产过快,仓库满了,消费者来不及消费,生产者就等待有空位再做事情(wait)。消费者拿走东西然后发有空位的消息(notify),然后生产者又开始工作了。当消费者消费过快发现没有存货时,消费者也会等待(wait),生产者产出内容后发出有存货的通知(notify),消费者就又开始来消费了。

如果某个线程对该锁对象做了notify动作,会从等待池中唤醒一个线程重新恢复RUNNABLE状态。notifyAll会唤醒所有线程。

wait(int timeout)可以设置超时时间,不会死等。如果是带有超时时间的方法被调用了,那么当前线程的状态就是TIMED_WAITING状态了。

线程状态:TIMED_WAITING


当调用Thread.sleep()方法时,相当于使用某个时间资源作为锁对象,达到等待的目的。当时间到达时触发线程回到工作状态。

进入该状态还有刚才上面说的带超时时间的wait,park等方法。

线程状态:TERMINATED


线程结束了就处于这种状态,即run方法走完了。这是java语言级别的一种状态,在操作系统内部可能已经注销了相应的线程,或者将它复用给其他需要使用线程的请求。在java语言级别,只是通过java代码看到的线程状态而已。

wait和notify与synchronized


wait和notify必须要使用synchronized,如果不用就会报错抛异常。

首先wait和notify是基于对象存在的。既然要等,就需要明白等什么,等待的是一个对象发出的信号,所以要基于对象存在,所以它的调用方式是Object.wait()。这样才能和对象挂钩。

为什么要使用锁呢(synchronized)?

既然是基于对象的,因此不得不使用一个数据结构来存放这些等待的线程,这个数据结构是与该对象绑定的(该数据结构是一个双向链表)。在这个对象上同时会有多个线程调用wait/notify方法,所以要加锁。

interrupt操作


对于BLOCKED和RUNNABLE操作都没用,对于WAITING和TIMED_WAITING状态有用,让它们产生实质性的异常抛出。

while(true){Thread.currentThread().isInterrupt()}

JDK1.6增加了Thread.interrupt()方法,可以取代上面代码,就表示当前线程的判断。

suspend/resume vs wait/notify


suspend和resume类似于wait和notify,但是它们不是等待和唤醒线程

suspend后线程出于RUNNNABLE状态,而不是WAITING状态。但是线程本身在这里已经挂起了,线程本身的状态就开始对不上号了。

如果在synchronized内部发生suspend操作,不会像wait那样把锁释放出来,因为它自己还在运行中。当发生resume时,程序正常结束了。代码正常走过synchronized区域,锁也会释放的。

问题:

①首先resume之后,线程状态依然是RUNNABLE,外部线程不知道这个线程已经挂起,需要做resume操作。

它不是基于对象完成这个动作的,因此suspend和resume相关的顺序性不能保证

综上,不推荐使用。同时,这说明了wait和notify为什么要基于对象来做数据结构,因为它需要一个临界区来控制生产者和消费者的平衡。

调度优先级


对优先级level的设置。创建一个线程时,默认的优先级是NORM_PRIORITY,值为5。程序中可以通过setPriority来设定优先级。

JVM还有一种特殊的后台线程,通过对线程调用setDaemon(true)设置为后台线程。它的优先级通常较低,即通常不会跟别人抢CPU,但某些情况下会提升自己的优先级来做一些事情,例如JVM的GC线程就是后台线程,一般不会和业务争夺CPU,而是在资源忙时提升优先级来做事情。

后台线程有一个特点,如果JVM进程中活着的只剩下后台线程,那么意味着就要结束整个进程了。

线程合并join


一个任务划分为多个小任务,多个小任务由多个线程完成。如果程序希望各个线程执行完合并结果的话,就要用到join。

join只是语法层面的线程合并,更像是当前线程出于BLOCKED状态时,去等待其他线程结束的事件,而且是逐个join。join的顺序不一定是线程真正结束的顺序,它无法实现保证线程的顺序性

注意:

①如果运算本身耗时很少,那么多线程的优势体现不出来。

②如果CPU不是多核,那么多线程带来的更多的是上下文切换的开销。多线程操作的共享对象还会有锁瓶颈,否则就是非线程安全的。

举例:任务一耗时1ms,任务二耗时2ms,如果串行执行,耗时3ms。如果用两个线程执行,分配线程的开销和其它比如join操作的开销需要10ms(每个线程分配过程需要执行很多条底层代码指令,join的操作也有各种开销),那么一共需要12ms。

不能一味的为了利用多线程而使用多线程。

线程栈


通过getStackTrace或者getStackByException来获得,得到的是StackTraceElement数组,内部包含相应的class,方法,文件名,行号信息(只是没有异常类型),而已通过他们来追踪代码,监控,定位异常,控制调用源等。

对于调用来源的类,可以用Reflection的getCallerClass(int)来获取。

UncaughtExceptionHandler


如果run方法没有捕获住的异常,通过这个handler可以补救。

一般不依赖这种方式,因为这是线程级别的,业务代码一般不会关心这个层次。即使关心也应该在框架中关心,在run方法中try catch。走到这个位置说明已经脱离了run方法,会立即结束,不能再被线程复用了。




  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值