进程和线程
1. 线程和进程的概念、并行和并发的概念
进程虽然包含程序,但它的作用不是去执行程序,而是负责资源分配。进程是一个静态概念,例如一个class文件,一个exe文件。
尽管通常我们认为一个进程只有单一的控制流,但是在现代系统中,一个进程实际上可以由多个称为线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据。由于网络服务器中对并行处理的要求,线程称为越来越重要的编程模型,因为多线程之间比多进程之间更容易共享数据,也因为线程一般都比进程更高效。
线程也别称为轻量级进程,是程序执行流的最小单元。线程是程序中一个单一的顺序控制流程。进程内一个相对独立的,可调度的执行单元,是系统独立调度和分派CPU的基本单位。
一个进程可以没有线程,作为空进程存在。但由于没有线程,CPU无法在这里得到执行,CPU的执行依赖于线程。
而线程无法脱离进程存在。
线程是CPU的持有者,但CPU实际上是先给到进程的。在不同的进程之间时间片进行一个切换。
线程关注与CPU,但也有自己独立的内存。进程的资源线程是可以共享的,然后线程自身也有栈,寄存器等。这里可以从Java的内存模型中看:线程栈中拥有外部变量的拷贝,线程对这个拷贝进行处理后,再把这个修改后的拷贝刷新回主内存。
如果某个系统支持两个或者多个动作(Action)同时存在,name这个系统就是一个并发系统;如果某个系统支持两个或多个动作同时执行,name这个系统就是一个并行系统。
并行是并发概念的一个子集。也就是说,可以编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么久不能以并行方式来运行代码。因此,凡是在求解单个问题时涉及多个执行流程的编程模式或者执行行为,都属于并发编程的范畴。
参考:
《Java多线程编程-(1)-线程安全和锁Synchronized概念》
《线程和进程的区别是什么?》
《并发与并行的区别?》
2. 创建线程的方式及实现
总体而言,创建Java线程有三种方式
2.1 继承Thread类
继承Thread类并重写run()方法;获取线程名可以直接使用getName()方法
2.2 实现Runnable接口
实现Runnable接口并实现run()方法,获取线程名可以使用Thread.currentThread().getName();
2.3 通过Callable和FutureTask创建线程
- 创建callable接口的实现类,并实现call()方法
- 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callback对象的call()方法的返回值。
- 使用FutrureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法获得子线程执行结束后的返回值。
除过以上三种直接创建线程的方法,也可以通过连接池直接获取线程。
参考:
《java创建线程的四种方式》
3. 进程间通信的方式(此题存疑)
Java多线程中线程之间的通信方式有以下几种:
- 同步
多个线程通过synchronized关键字来实现线程间的通信。这种方式本质是“共享内存”式通信:多个线程需要访问同一个共享变量,谁拿到了锁,谁就可以执行。
例如线程A和线程B持有同一个MyObject类的对象object,尽管两个线程需要调用不同的方法,但它们是同步执行的。比如线程B需要等待线程A执行完了methodA()方法后,才能执行mehtodB()方法,这样就实现了A与B之间的通信。 - while轮询方式
线程A不断地改变条件,线程B不停地通过while语句来检测(list.size() == 5)是否成立,从而实现了线程间的通信。但这种方式会造成CPU的浪费,浪费在不断查询条件是否成立上。而且如果线程B每次都在读取本地的条件变量,name尽管另一个线程已经改变了轮询的条件,B也无法察觉从而造成死循环。 - wait/notify机制
还是上面的例子,当发现条件不满足时,B调用wait()放弃CPU,并进入阻塞状态,而不是像轮询那样占用CPU;当条件满足是,A调用notify()通知A。但是如果通知过早,也会打乱程序的执行逻辑。 - 管道通信
使用io包下的PipedInputStream和PipedOutputStream进行通信。管道通信更像消息传递机制,通过管道将一个线程中的消息发送给另一个。
除去以上几种方法,socket套接字也可以用于线程间通信。
4. 说说 CountDownLatch、CyclicBarrier 原理和区别
倒计时CountDownLath是一个线程(或多个),等待另外N个线程完成某个事情之后才能执行。
循环屏障CyclicBarrier是N个线程互相等待,任何一个线程完成之前,所有的线程都必须等待。
对于CountDownLatch来说,重点是那是“一个线程”,是它在等待,而另外那N的线程在把某个事情做完之后可以继续等待,可以终止。而对于CyclicBarrier来说,重点是“N个线程”,它们之间任何一个没有完成,所有的线程都必须等待。
CountDownLactch是计数器,线程完成一个就记一个,就像报数一样,只不过是递减的。
CyclicBarrier更像一个水闸,线程执行就像水流,在水闸处都会堵住,等到水满(线程到齐),才所有线程开始泄流。
除此之外,CountDownLatch不可重复利用,当计数为0时,无法重置;而CyclicBarrier可以重复利用,当计数到达指定值时,计数置为0则重新开始。
参考:
《CyclicBarrier和CountDownLatch区别》
《CountDownLatch和CyclicBarrier的区别》
5. 说说 Semaphore 原理
信号量Semaphore在JDK1.5被引入,可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。信号量是一个非负整数,表示了当前公共资源的可用数目。当一个线程要使用公共资源时,首先要查看信号量,如果信号量的值大于1,则将其减一,然后去战友公共资源。而如果信号量的值为0,则线程会将自己阻塞,直到有其他线程释放公有资源。
Semaphore内部维护了一组虚拟的许可,许可的数量可以通过构造函数的参数指定。
- 访问特定资源前,必须使用acquire()方法获得许可,如果许可数量为0.则该线程一直阻塞,直到有线程释放信号量,或超时。
- 当线程访问资源结束并释放后,使用release()方法释放一个许可,将信号量的值加1,然后唤醒等待的线程。
Semaphore和ReentrantLock类似,获取许可有公平策略和非公平许可策略,通过在创建Semaphore对象的时候指定参数。默认为非公平许可策略。
Semaphore可以用来做流量分流,特别是对公共资源有限的场景,比如数据库连接。Semaphore的实现则主要基于Java同步器AQS(AbstractQueuedSynchronizer)来实现线程的管理。Semaphore有两个构造函数,参数permits标识许可数,最后传递给了AQS的state值。
对于acquire()方法,许可策略会造成一定程度的不同。
- 对于非公平许可,acquires值默认为1,表示尝试获取一个许可,available表示当前可用的许可数,而remaining则代表如果发放本次许可数后剩余的许可数。如果remaining(remaining= available - acquires)< 0 ,比其实目前没有剩余的许可,则当前线程进入AQS中的doAcquireSharedInterruptibly方法等待可用许可并挂起,直到被唤醒。
- 而对于公平许可,则当没有剩余许可时,多了一个队阻塞队列的检查。如果阻塞队列没有等待的线程,则参与许可的竞争。否则直接插入到阻塞队列尾节点并挂起,等待被唤醒。
对于release()方法,releases的值默认为1,表示尝试释放1个许可,next代表如果许可释放成功,可用许可的数量。通过unsafe.compareAndSwapInt修改state的值,确保同一时刻只有一个线程可以释放成功。许可释放成功后,当前线程进入到AQS的doReleaseShared方法,唤醒等待队列中等待许可的线程。
参考:
《深入浅出java Semaphore》
《Java并发包中Semaphore的工作原理、源码分析及使用示例》
6. 说说 Exchanger 原理
Exchanger用于线程间数据的交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange()方法交换数据,如果第一个线程先执行exchange()方法,它会一直等待第二个线程也执行exchange()方法。当两个线程都达到同步点是,这两个线程就可以交换数据,将本线程生产出阿里的数据传递给对方。
Exchanger可被视为SynchronousQueue的双向形式。Exchanger在遗传算法和管道设计等应用中很有用。
内存一致性:对于通过Exchanger成功交换对象的每对线程,每个线程中在exchange()之前的操作happen-before从另一线程中相应的exchange()返回的后续操作。(?)
-每个要进行数据交换的线程在内部都会用一个Node来表示。
- 首先判断Slot是否为null,如果为null则在当前index上创建一个slot,创建slot时为了提供效率使用了双重检查锁定模型。
- 如果slot上没有Node,说明是当前线程先到达slot。当index = 0时,当前线程阻塞等待另一个线程前来交换数据;当index !=0时,当前线程自旋等待其他线程前来交换数据。其中阻塞等待有await和awaitNanos两种。
- 当index = 0时,当前线程通过spinWait()方法自旋等待其他线程。如果超过自旋次数,则取消Node,然后重新建一个Node,减小index且有可能减小max,继续循环。如果经过多次自旋等待还是没有其他线程交换数据,index会一直右移直到变为0。当index = 0时,就会阻塞等待其他线程交换数据。
- 如果走到最后一个分支,说明竞争激烈。如果在第一个slot上竞争失败2次,说明该slot竞争激烈,index递减,换个slot继续;如果累计竞争失败超过3次,说明存在多个slot竞争激烈,尝试递增max,增加slot的个数,并将index置为m+1,换个slot继续。
参考:
《Java并发编程--Exchanger》
《Java Thread&Concurrency(4): 深入理解Exchanger实现原理》
《Jdk1.6 JUC源码解析(27)-Exchanger》
7. ThreadLocal 原理分析,ThreadLocal为什么会出现OOM,出现的深层次原理
ThreadLocal并不是一个Thread,而是Thread的一个局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。
ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
ThreadLocal类中有一个static声明的Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。
线程局部变量时存储在Thread对象的threadLocals属性中,而threadLocals属性是一个ThreadLocal.ThreadLocalMap对象。ThreadLocalMap时ThreadLocal的静态内部类。
一个Thread中只有一个ThreadLocalMap,一个ThreadLocalMap中可以有多个ThreadLocal对象,其中一个ThreadLocal对象对应一个ThreadLocalMap中的一个Entry。不管一个线程拥有多少个局部变量,都是使用同一个ThreadLocalMap来保存的。ThreadLocal的set(T value)方法为每个Thread对象都创建了一个ThreadLocalMap,并且将value放入ThreadLocalMap中,ThreadLocalMap作为Thread对象的成员变量保存。
ThreadLocal涉及了两个层面的内存自动回收:
- 在ThreadLocal层面的内存回收:
当线程死亡时,所有保存的线程局部变量都会被回收。 - ThreadLocalMap层面的内存回收
如果线程可以活很长时间,并且该线程保存的局部变量有很多(也就是很多Entry对象),就会涉及到在线程的生命期内回收ThreadLocalMap内存了.
Entry对象的key是弱引用的包装。ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统GC的时候ThreadLocal势必会被回收。这样一来,ThreadLocalMap中就会出现key为null的entry,就没有办法访问这些key为null的Entry的value;而如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链: Thread Ref -> Thread -> ThreadLocalMap -> Entry ->value,从而永远无法回收,造成内存泄漏。
参考:
《 Java多线程编程-(9)-ThreadLocal造成OOM内存溢出案例演示与原理分析》
《 Java多线程编程-(8)-多图深入分析ThreadLocal原理》
8. 讲讲线程池的实现原理
当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果阻塞队列满了,就创建新的线程自行当前任务,直到线程池中的线程数达到maxPoolSize;这时再有任务来,就只能执行reject()处理该任务。
对于keepAliveTime,只在当前线程池中的线程数大于corePoolSize时才会起作用。当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。
但如果调用了allowCoreThreadTimeOut(boolean)方法,线程池中的线程数不大于corePoolSize时,keepAliveTime也会起作用,直到线程池中的线程数为0。
如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
参考:
《Java中线程池的实现原理-求职必备
》
《深入理解Java之线程池》
9. 线程池的几种实现方式
9.1 newFIxedThreadPool(int nThreads)
创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数量,这是线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的线程。
FixedThreadPool是一个典型且优秀的线程池,具有线程池提高效率和节省创建线程时所耗的开销的优点。但在线程池空闲时,它不会释放工作线程,会继续占用系统资源。
9.2 newCachedThreadPool()
创建一个可缓存的线程池,如果线程池的规模超过了处理需求,将自动回收空闲线程,而当需求增加时,则可以自动添加新线程,县城迟池的规模不存在任何限制。
CachedThreadPool的特点是在线程池空闲时,会释放工作线程,从而释放工作线程所占用的资源。但当出现新任务时,又要创建新的工作线程从而需要额外的系统开销。并且在使用时,因为对线程池规模没有限制,一定要注意控制任务的数量,否则由于大量线程同时运行,很有可能会造成系统瘫痪。
9.3 newSingleThreadExecutor()
一个单线程的Executor,它创建单个工作线程来执行任务,如果这个线程异常结束,会创建一个新的来替代它;它的特点是能确保依照任务在队列中的顺序来串行执行。
9.4 newScheduledThreadPool(int corePoolSize)
创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer。
参考:
《JAVA线程池原理以及几种线程池类型介绍》
《创建线程池的几种方式》
10. 线程的生命周期,状态是如何转移的
线程的生命周期开始于Thread对象创建完成时(new操作完毕后),这时线程处于新建状态。
生命周期结束于当run()方法中代码正常执行完毕或线程抛出一个未捕获的异常或错误时,这时线程处于死亡状态。
线程整个生命周期有五种状态:
- 新建状态New
用new Thread()建立一个线程对象后,该线程对象就处于了新建状态。 - 就绪状态Runnable
通过调用线程的start()方法就会使线程进入就绪状态。(不能对已经启动的线程再次调用start()方法,否则将会抛出异常)
处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(就绪池),等待系统为其分配CPU。 - 运行状态Running
已经处于就绪状态的线程,如果获得了CPU的调度,就会从就绪状态转变为运行状态,执行run()方法中的任务。
处于此态的线程最为复杂,因为它可以变成阻塞,就绪,和死亡状态。
如果该线程在执行完run()方法前失去了CPU资源,就会从运行状态变为就绪状态,等待系统重新分配资源。 - 阻塞状态Blocked
当发生如下情况时,线程会从运行状态变为阻塞状态:
线程调用sleep()方法主动释放CPU控制权
线程调用一个阻塞式IO方法,在该方法返回前,该线程被阻塞
线程试图获得一个同步监视器,但该同步监视器正被其他线程所持有
线程在等待某个通知(notify)
程序调用了线程的suspend()方法将线程挂起。
阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,线程就转换为就绪状态,重新到就绪队列中排队等待CPU。 - 死亡状态Terminated
当线程的run()方法执行完,或者被强制性地终止(异常,调用stop(),destory()方法等),就会从运行状态转变为死亡状态。线程一旦死亡,无法复生。如果试图在一个死亡的线程上调用start()方法,会抛出IllegalThreadStateExecption异常。
11. wait方法能不能被重写,wait能不能被中断
wait()、notify()和notifyAll()方法是本地方法,并且为final方法,无法被重写。
如果在当前线程等待通知之前或者正在等待通知时,任何线程中断了当前线程。会抛出InterruptedException ,当前线程的中断状态 被清除。