并发编程,自然就会涉及到多线程;而多线程,那自然是由线程组成的。所以今天就来系统地了解一下线程,了解它的定义、以及几种状态和属性。
1.线程
线程是指程序在执行过程中,能够执行程序代码的一个执行单元。
2. 为何要使用多线程
在操作系统级别上来看主要有以下几个方面:
使用多线程可以减少程序的响应时间,如果某个操作和耗时,或者陷入长时间的等待,此时程序讲不会响应鼠标和键盘等的操作,使用多线程后可以把这个耗时的线程分配到一个单独的线程去执行,从而使程序具备了更好的交互性。
与进程相比,线程创建和切换开销更小,同时多线程在数据共享方面效率非常高。
多CPU或者多核计算机本身就具备执行多线程的能力,如果使用单个进程,将无法重复利用计算机资源,造成资源的巨大浪费。在多CPU计算机使用多线程能提高CPU的利用率。
使用多线程能简化程序的结构,使程序便于理解和维护
3.创建线程
创建线程有三种方式,分别为:
继承Thread类,重写run()方法
实现Runnable接口,并实现该接口的run()方法
实现Callable接口,重写call()方法
注:一般情况下推荐使用第二种方法。
4.线程的六种状态
线程一共具有六种状态,简介版如下图:
复杂版如下图:
(1)New 新建状态:新建的Thread对象,未调用start()
(2)Runnable 可运行状态:可运行状态中包含就绪态与运行态
就绪态:已经获取所有资源,CPU分配执行权就可以执行,所有就绪态线程都在就绪队列中。即在就绪状态的进程除CPU之外,其它的运行所需资源都已全部获得。
就绪状态的线程来源如下:
(2.1)就绪状态只是说你资格运行,调度程序没有挑选到你,你就永远是就绪状态。
(2.2)调用线程的start()方法,此线程进入就绪状态。
(2.3)当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕, 进入就绪状态
(2.4)当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入就绪状态。
(2.5)锁池里的线程拿到对象锁后,进入就绪状态。
运行态:正在执行的线程,一个CPU同一时间只能处理一个线程,因此一个CPU上只有一个运行态程序
(3)Blocked 阻塞状态:线程请求资源失败时会进入该状态。所有阻塞态线程都存储在一个阻塞队列中,阻塞态线程会不断请求资源成功后进入就绪队列。待执行。
阻塞的状态分为三种:
等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒,
同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
(4)Waiting 等待状态(即无限期等待):wait、join等函数会造成进入该状态。同样有一个等待队列存储所有等待线程,线程会等待其他线程指示才能继续执行。等待状态线程主动放弃CPU执行权。
以下方法会让线程陷入无限期等待状态:
没有设置timeout参数的Object.wait()
没有设置timeout参数的Thread.join()
LockSupport.park()
注意:LockSupport.park(Object blocker) 会挂起当前线程,参数blocker是用于设置当前线程的“volatile Object parkBlocker 成员变量”
parkBlocker 是用于记录线程是被谁阻塞的,可以通过LockSupport.getBlocker()获取到阻塞的对象,用于监控和分析线程用的。
“阻塞”与“等待”的区别:
“阻塞”状态是等待着获取到一个排他锁,进入“阻塞”状态都是被动的,离开“阻塞”状态是因为其它线程释放了锁,不阻塞了;
“等待”状态是在等待一段时间 或者 唤醒动作的发生,进入“等待”状态是主动的。如主动调用Object.wait(),如无法获取到ReentraantLock,主动调用LockSupport.park(),如主线程主动调用 subThread.join(),让主线程等待子线程执行完毕再执行。离开“等待”状态是因为其它线程发生了唤醒动作或者到达了等待时间
(5)Timed_Waiting 计时等待:计时等待也是主动放弃CPU执行权的,区别是,超时后会进入阻塞态竞争资源
以下方法会让线程进入TIMED_WAITING限期等待状态:
Thread.sleep()方法
设置了timeout参数的Object.wait()方法
设置了timeout参数的Thread.join()方法
LockSupport.parkNanos()方法
LockSupport.parkUntil()方法
(6)Terminal 结束状态:线程执行结束后的状态。
注:上面第二章图中所言“获取到锁”,即
拿到对象的锁标记,即为获得了对该对象(临界区)的使用权限。即该线程获得了运行所需的资源,进入“就绪状态”,只需获得CPU,就可以运行。因为当调用wait()后,线程会释放掉它所占有的“锁标志”,所以线程只有在此获取资源才能进入就绪状态。
下面对上述流程图的转换作出一些解释:
(1)线程的实现有三种方式,当我们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)当线程刚进入可运行状态(注意,还没运行),发现将要调用的资源被synchronized(同步),获取不到锁标记,将会立即进入锁池状态,等待获取锁标记(这时的锁池里也许已经有了其他线程在等待获取锁标记,这时它们处于队列状态,既先到先得),一旦线程获得锁标记后,就转入就绪状态,等待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数据可被别的线程使用。wait()和notify()因为会对对象的“锁标志”进行操作,所以它们必须在synchronized函数或synchronizedblock中进行调用。如果在non-synchronized函数或non-synchronizedblock中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。
当使用wait()方法之后,线程会进入等待队列,如下图:
图中有一个同步队列,同步队列是干啥的呢?
当前线程想调用对象A的同步方法时,发现对象A的锁被别的线程占有,此时当前线程进入同步队列。简言之,同步队列里面放的都是想争夺对象锁的线程。
当一个线程1被另外一个线程2唤醒时,1线程进入同步队列,去争夺对象锁。
同步队列是在同步的环境下才有的概念,一个对象对应一个同步队列。
————————————————————————————————————————————————————————
以下是wait()和notify()方法和suspend()与resume()方法的比较
OK,看完上面冗长的一段,这里对上述(4.5)和(4.6)中的两个方法(即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()方法的调用都可能产生死锁。使用的时候需要注意。
5.线程的优先级和守护线程
1. 线程优先级
在java中,每一个线程有一个优先级,默认情况下,一个线程继承它父类的优先级。可以用setPriority方法提高或降低任何一个线程优先级。可以将优先级设置在MIN_PRIORITY(在Thread类定义为1)与MAX_PRIORITY(在Thread类定义为10)之间的任何值。线程的默认优先级为NORM_PRIORITY(在Thread类定义为5)。
不过,建议大家尽量不要依赖优先级,如果确实要用,应该避免初学者常犯的一个错误:饿死,即如果有几个高优先级的线程没有进入非活动状态,低优先级线程可能永远也不能执行。
2. 守护线程
调用setDaemon(true);将线程转换为守护线程。守护线程唯一的用途就是为其他线程提供服务。计时线程就是一个例子,他定时发送信号给其他线程或者清空过时的告诉缓存项的线程。当只剩下守护线程时,虚拟机就退出了,由于如果只剩下守护线程,就没必要继续运行程序了。
另外JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。
好啦,以上就是关于线程的定义以及线程的六种状态的转换(重点)与相关方法与属性的相关知识总结啦。
领取专属 10元无门槛券
私享最新 技术干货