▷ 进程、线程和多线程
进程:
定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
- 第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
- 第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,我们称其为进程。
状态:就绪、运行和阻塞
就绪状态其实就是获取了出cpu外的所有资源,在队列中等待;运行就是获得了处理器分配的资源,程序开始执行;阻塞态,当程序条件不够时候,需要等待条件满足时候才能执行。
线程:
一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统多个程序间并发执行的程度。
java创建线程三种方式:
- 继承Thread类创建线程
- 实现Runnable接口创建线程
- 使用Callable和Future创建线程
1.创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法没有返回值,再创建Callable实现类的实例。(从java8开始,可以直接使用Lambda表达式创建Callable对象)。
2.使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
3.使用FutureTask作为Thread对象的target创建并启动新线程。
4.调用FutureTask对象的get方法来获得子线程执行结束后的返回值。
多线程:
定义:指的是这个程序(一个进程)运行时产生了不止一个线程。
- 并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。
- 并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。
- 线程安全:经常用来描绘一段代码。指在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存,cpu是不是够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果,如不加事务的转账代码。
- 同步:Java中的同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,来保证结果的准确。如上面的代码简单加入@synchronized关键字。在保证结果准确的同时,提高性能,才是优秀的程序。线程安全的优先级高于性能。
线程的状态:
- 新建状态(New) : 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
- 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
- 运行状态(Running) : 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
- 阻塞状态(Blocked) : 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
(01) 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。
(02) 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
(03) 其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。 - 死亡状态(Dead) : 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
高级多线程控制类:
- ThreadLocal类
用处:保存线程的独立变量。对一个线程类(继承自Thread)
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。常用于用户登录控制,如记录session信息。
实现:每个Thread都持有一个TreadLocalMap类型的变量(该类是一个轻量级的Map,功能与map一样,区别是桶里放的是entry而不是entry的链表。功能还是一个map。)以本身为key,以目标为value。
主要方法是get()和set(T a),set之后在map里维护一个threadLocal -> a,get时将a返回。ThreadLocal是一个特殊的容器。 - Lock类
三个实现:
ReentrantLock:
可重入的意义在于持有锁的线程可以继续持有,并且要释放对等的次数后才真正释放该锁。
ReentrantReadWriteLock.ReadLock
ReentrantReadWriteLock.WriteLock
lock更灵活,可以自由定义多把锁的加锁解锁顺序(synchronized要按照先加的后解顺序)
提供多种加锁方案,lock 阻塞式, trylock 无阻塞式, lockInterruptily 可打断式, 还有trylock的带超时时间版本。和Condition类的结合。 - BlockingQueue类
阻塞队列。该类是java.util.concurrent包下的重要类,通过对Queue的学习可以得知,这个queue是单向队列,可以在队列头添加元素和在队尾删除或取出元素。类似于一个管 道,特别适用于先进先出策略的一些应用场景。普通的queue接口主要实现有PriorityQueue(优先队列)。
获取线程异常:
线程无法通过try/catch获取到异常。
写的时候最好要设置线程名称 Thread.name,并设置线程组 ThreadGroup,目的是方便管理。在出现问题的时候,打印线程栈 (jstack -pid) 一眼就可以看出是哪个线程出的问题,这个线程是干什么的。
线程池:
线程池作用就是限制系统中执行线程的数量。
ThreadPoolExecutor:
常用的有四种:
- newSingleThreadExecutor (创建一个单线程的线程池。)
- newFixedThreadPool (创建固定大小的线程池。)
- newCachedThreadPool (创建一个可缓存的线程池。)
- newScheduledThreadPool (创建一个支持定时以及周期性执行任务的线程池。)
设置线程池:限制最大线程,如果是CPU密集型的任务,核心线程就用核数,最大线程就用核数的N倍
什么时候出现僵死进程
当一个进程创建了一个子进程时,他们的运行时异步的。即父进程无法预知子进程会在什么时候结束,那么如果父进程很繁忙来不及wait 子进程时,那么当子进程结束时,会不会丢失子进程的结束时的状态信息呢?处于这种考虑unix提供了一种机制可以保证只要父进程想知道子进程结束时的信息,它就可以得到。
这种机制是:在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存。但是仍然保留了一些信息(如进程号pid 退出状态 运行时间等)。这些保留的信息直到进程通过调用wait/waitpid时才会释放。这样就导致了一个问题,如果没有调用wait/waitpid的话,那么保留的信息就不会释放。比如进程号就会被一直占用了。但系统所能使用的进程号的有限的,如果产生大量的僵尸进程,将导致系统没有可用的进程号而导致系统不能创建进程。
如果父进程先结束,而子进程后结束,且没有调用wait/waitpid来等待子进程的结束,每个进程结束时,系统都会扫描当前系统中运行的所有进程,看看有没有哪个进程时刚刚结束的这个进程的子进程,如果有,就有init来接管它,成为它的父进程。
- 即:如果子进程先结束而父进程后结束,即子进程结束后,父进程还在继续运行但是并未调用wait/waitpid那子进程就会成为僵尸进程。
如何实现线程安全:
基本上所有的并发模式在解决线程安全问题上,都采用“序列化访问临界资源”的方案,即在同一时刻,只能有一个线程访问临界资源,也称同步互斥访问。通常来说,是在访问临界资源的代码前面加上一个锁,当访问完临界资源后释放锁,让其他线程继续访问。
用Synchronization或者Lock
CAS原子锁:
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
TODO:具体还需要研究
ThreadLocal什么时候会出现OOM的情况?为什么?
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
- 使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致内存泄漏。
- 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏,因为这块内存一直存在。