线程
线程的状态转换
线程在一定条件下,状态会发生变化。线程一共有以下几种状态:
- 新建状态(New):新创建了一个线程对象。
- 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于“可运行线程池”中,变得可运行,
只等待获取CPU
的使用权。即在就绪状态的进程除CPU之外,其它的运行所需资源都已全部获得。 - 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
- 阻塞状态(Blocked):阻塞状态是线程因为某种原因
放弃CPU使用权
,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:- 等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒。
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
- 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
——当线程试图获取某个对象的同步锁时,如果该锁被其他线程所持有,则当前线程进入阻塞状态,如果想从阻塞状态进入就绪状态必须得获取到其他线程所持有的锁。
——当线程调用了一个阻塞式的IO方法时,该线程就会进入阻塞状态,如果想进入就绪状态就必须要等到这个阻塞的IO方法返回。
——当线程调用了某个对象的wait()方法时,也会使线程进入阻塞状态,notify()方法唤醒。
——调用了Thread的sleep(long millis)。线程睡眠时间到了会自动进入阻塞状态。
——一个线程调用了另一个线程的join()方法时,当前线程进入阻塞状态。等新加入的线程运行结束后会结束阻塞状态,进入就绪状态。
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
线程变化的状态转换图如下:
image.png
注:拿到对象的锁标记,即为获得了对该对象(临界区)的使用权限。即该线程获得了运行所需的资源,进入“就绪状态”,只需获得CPU,就可以运行。因为当调用wait()后,线程会释放掉它所占有的“锁标志”,所以线程只有在此获取资源才能进入就绪状态。
下面小小的作下解释:
1、线程的实现有两种方式,一是继承Thread类,二是实现Runnable接口,但不管怎样, 当我们new了这个对象后,线程就进入了初始状态;
2、当该对象调用了start()方法,就进入就绪状态;
3、进入就绪后,当该对象被操作系统选中,获得CPU时间片就会进入运行状态;
4、进入运行状态后情况就比较复杂了
4.1、run()方法或main()方法结束后,线程就进入终止状态;
4.2、当线程调用了自身的sleep()方法或其他线程的join()方法,进程让出CPU,然后就会进入阻塞状态(该状态既停止当前线程,但并不释放所占有的资源即调用sleep ()函数后,线程不会释放它的“锁标志”。)。当sleep()结束或join()结束后,该线程进入可运行状态,继续等待OS分配CPU时间片。典型地,sleep()被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止。
4.3、线程调用了yield()方法,意思是放弃当前获得的CPU时间片,回到就绪状态,这时与其他进程处于同等竞争状态,OS有可能会接着又让这个进程进入运行状态; 调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间片从而需要转到另一个线程。yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。
4.4、当线程刚进入可运行状态(注意,还没运行),发现将要调用的资源被synchroniza(同步),获取不到锁标记,将会立即进入锁池状态,等待获取锁标记(这时的锁池里也许已经有了其他线程在等待获取锁标记,这时它们处于队列状态,既先到先得),一旦线程获得锁标记后,就转入就绪状态,等待OS分配CPU时间片;
4.5.suspend() 和 resume()方法:两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume()被调用,才能使得线程重新进入可执行状态。典型地,suspend()和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume()使其恢复。
4.6、wait()和 notify() 方法:当线程调用wait()方法后会进入等待队列(进入这个状态会释放所占有的所有资源,与阻塞状态不同),进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒(由于notify()只是唤醒一个线程,但我们由不能确定具体唤醒的是哪一个线程,也许我们需要唤醒的线程不能够被唤醒,因此在实际使用时,一般都用notifyAll()方法,唤醒有所线程),线程被唤醒后会进入锁池,等待获取锁标记。
wait() 使得线程进入阻塞状态,它有两种形式:
一种允许指定以毫秒为单位的一段时间作为参数;另一种没有参数。前者当对应的 notify()被调用或者超出指定时间时线程重新进入可执行状态即就绪状态,后者则必须对应的 notify()被调用。当调用wait()后,线程会释放掉它所占有的“锁标志”,从而使线程所在对象中的其它synchronized数据可被别的线程使用。waite()和notify()因为会对对象的“锁标志”进行操作,所以它们必须在synchronized函数或synchronizedblock中进行调用。如果在non-synchronized函数或non-synchronizedblock中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。
注意区别:初看起来wait() 和 notify() 方法与suspend()和 resume() 方法对没有什么分别,但是事实上它们是截然不同的。区别的核心在于,前面叙述的suspend()及其它所有方法在线程阻塞时都不会释放占用的锁(如果占用了的话),而wait() 和 notify() 这一对方法则相反。
上述的核心区别导致了一系列的细节上的区别
首先,前面叙述的所有方法都隶属于 Thread类,但是wait() 和 notify() 方法这一对却直接隶属于 Object 类,也就是说,所有对象都拥有这一对方法。初看起来这十分不可思议,但是实际上却是很自然的,因为这一对方法阻塞时要释放占用的锁,而锁是任何对象都具有的,调用任意对象的 wait() 方法导致线程阻塞,并且该对象上的锁被释放。而调用任意对象的notify()方法则导致因调用该对象的 wait()方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。
其次,前面叙述的所有方法都可在任何位置调用,但是wait() 和 notify() 方法这一对方法却必须在 synchronized 方法或块中调用,理由也很简单,只有在synchronized方法或块中当前线程才占有锁,才有锁可以释放。同样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。因此,这一对方法调用必须放置在这样的 synchronized方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现IllegalMonitorStateException异常。
wait() 和 notify()方法的上述特性决定了它们经常和synchronized方法或块一起使用,将它们和操作系统的进程间通信机制作一个比较就会发现它们的相似性:synchronized方法或块提供了类似于操作系统原语的功能,它们的执行不会受到多线程机制的干扰,而这一对方法则相当于 block和wake up 原语(这一对方法均声明为 synchronized)。它们的结合使得我们可以实现操作系统上一系列精妙的进程间通信的算法(如信号量算法),并用于解决各种复杂的线程间通信问题。
关于 wait() 和 notify() 方法最后再说明两点:
第一:调用notify() 方法导致解除阻塞的线程是从因调用该对象的 wait()方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。
第二:除了notify(),还有一个方法 notifyAll()也可起到类似作用,唯一的区别在于,调用 notifyAll()方法将把因调用该对象的 wait()方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。
谈到阻塞,就不能不谈一谈死锁,略一分析就能发现,suspend()方法和不指定超时期限的wait()方法的调用都可能产生死锁。遗憾的是,Java并不在语言级别上支持死锁的避免,我们在编程中必须小心地避免死锁。
为什么线程切换比进程快?
首先我们需要了解一个概念:上下文切换
直接说概念可能有点看不懂,我们来举个例子:比如我们正在洗衣服,这时候突然有人打来电话,那么我们就需要放下手中的衣服去接电话,这就好比是进程切换,从洗衣服的进程换到打电话的进程,打完电话,我们还需要接着洗衣服,那么此时我们有什么呢?有洗了一半的衣服,有打开的洗衣液,有水,衣服还处在刚刚洗了一半的状态,这一系列的状态就可以理解为上下文。
回到计算机的世界里,内核为每一个进程维持一个上下文,上下文就是重新启动一个进程所需的状态,包含以下内容:
- 通用目的寄存器
- 浮点寄存器
- 程序计数器
- 用户栈
- 状态寄存器
- 内核栈
- 各种内核数据结构:比如描绘地址空间的页表,包含有关当前进程信息的进程表,包含进程已打开文件信息的文件表
进程的切换实际上就是上下文切换
那么为什么线程切换比进程快呢?这里我们还需要理解一个概念:虚拟内存
虚拟内存是操作系统为每个进程提供的一种抽象的,私有的,连续地址的虚拟内存空间,但是我们都知道实际上进程的数据以及代码必然要放到物理内存上,那么我们怎么知道虚拟空间中的数组实际上存放的具体位置呢?答案就是页表
每个进程都有属于自己的虚拟内存空间,进程中的所有线程共享进程的虚拟内存空间,所以进程之间互不影响,线程之间可能会相互影响
现在我们可以来回答这个问题了
线程切换比进程块的主要原因就是进程切换涉及虚拟内存地址空间的切换而线程不会
。因为每个进程都有自己的虚拟内存地址空间,而线程之间的虚拟地址空间是共享的,因此同一个进程之中的线程切换不涉及虚拟地址空间的转换,然而将虚拟地址转化为物理地址需要查找页表,查找页表是一个很慢的过程,所以线程切换自然就比进程快了。
死锁
死锁是指在并发系统中,两个或多个进程因为互相等待对方释放资源而无法继续执行的状态。
死锁发生的条件通常包括以下四个条件:
- 互斥条件(Mutual Exclusion):
至少有一个资源
被标记为只能被一个进程
占用,即一次只能有一个进程使用该资源。 - 请求与保持条件(Hold and Wait):一个进程在
持有至少一个资源
的同时,又请求其他进程
占用的资源。 - 不可剥夺条件(No Preemption):已经分配给一个进程的资源
不能被强制性地剥夺
,只能由持有该资源的进程主动释放 - 循环等待条件(Circular Wait):存在一个进程资源的循环链,每个进程都在等待下一个进程所占用的资源。
当这四个条件同时满足时,就可能发生死锁。为了避免死锁的发生,可以采取一些策略,如资源预分配、避免循环等待、引入资源剥夺等。
标签:状态,调用,操作系统,线程,进程,方法,wait From: https://www.cnblogs.com/DCFV/p/18302056