上一篇文章已经介绍了,线程是对CPU的模拟和抽象,因为一台机器只有一个CPU,又要执行多个应用的代码,为了让上层应用不考虑这些细节,而使用线程这么个东西抽象一下,这样让上层应用觉得整个CPU都是它的。但CPU毕竟只有一个(或是有限的),那么就必定存在线程的切换。这也就涉及线程的状态转换:
主意这里的箭头有双向的,有单向的。
线程在运行时可能由于需要某种资源而暂时停止运行,这称之为阻塞,当需要的资源得到满足时线程的状态会变成就绪,如果这个时候正好有CPU空闲或者正好线程调度到这个线程上,那么该线程就会马上执行。线程在运行时还有可能因为自己的时间片用完了,线程调度程序安排别的线程执行(上下文切换),而从运行状态变成就绪状态。就绪状态也可以稍后调度为运行状态。线程调度的算法有很多种,这在操作系统领域有很多文献专门来描述这个。
死锁(deadlock)
在写多线程相关的书籍或者文章中,恐怕死锁这个词出现的频率最高。那死锁到底是一个什么意思呢?
为了解释这个概念,我们假设有两个线程T1和T2,有两个资源R1和R2。在某一时刻,T1拥有R1,T2拥有R2。但是这两个线程贪得无厌,T1还需要R2才能运行所以阻塞了,而T2需要R1才能运行所以也阻塞了。所以这两个线程总是不断地尝试获取永远也得不到的东西互不相让,这种状态除非有外力的介入,不然不会打破。这就是死锁。死锁的危害性非常大,有可能造成整个系统的宕机。
我们来看下面这段程序,来演示一下如何弄出个死锁出来(该程序仅仅为了演示目的,实际中不要如此编码):
1: using System;
2: using System.Threading;
3: namespace DeadLock
4: {
5: class Program
6: {
7: static void Main(string[] args)
8: {
9: Test t = new Test();
10: Thread t1 = new Thread(t.test1);
11: Thread t2 = new Thread(t.test2);
12:
13: t1.Start();
14: t2.Start();
15:
16: Console.ReadLine();
17:
18: }
19: }
20:
21: public class Test
22: {
23: private object resource1 = new object();
24: private object resource2 = new object();
25:
26: public void test1()
27: {
28:
29: Console.WriteLine(string.Format("test1:Thread{0} try to get resouce1", Thread.CurrentThread.ManagedThreadId.ToString()));
30: lock (resource1)
31: {
32: Console.WriteLine(string.Format("test1:Thread{0} got resouce1", Thread.CurrentThread.ManagedThreadId.ToString()));
33: Thread.Sleep(500);
34: Console.WriteLine(string.Format("test1:Thread{0} try to get resouce2", Thread.CurrentThread.ManagedThreadId.ToString()));
35: lock (resource2)
36: {
37: Console.WriteLine(string.Format("test1:Thread{0} got resouce2", Thread.CurrentThread.ManagedThreadId.ToString()));
38: }
39: }
40: }
41:
42: public void test2()
43: {
44: Console.WriteLine(string.Format("test2:Thread{0} try to get resouce2", Thread.CurrentThread.ManagedThreadId.ToString()));
45: lock (resource2)
46: {
47: Console.WriteLine(string.Format("test2:Thread{0} got resouce2", Thread.CurrentThread.ManagedThreadId.ToString()));
48: Thread.Sleep(500);
49: Console.WriteLine(string.Format("test2:Thread{0} try to get resouce1", Thread.CurrentThread.ManagedThreadId.ToString()));
50: lock (resource1)
51: {
52: Console.WriteLine(string.Format("test2:Thread{0} got resouce1", Thread.CurrentThread.ManagedThreadId.ToString()));
53: }
54: }
55: }
56: }
57: }
thread1获取resource1后等待一会儿,让thread2获取到resource2,然后thread1“咬住”resource1死死不放,还去获取resource2,而thread2“咬住”resource2死死不放,去获取resource1,这样thread1和thread就像抱团一样了。
死锁形成的必要条件:
互斥条件:resource1和resource2只能被一个线程拥有
占有和等待条件:已经得到resource1的thread1可以再请求resource2,thread2也可以在获取到resource2后再获取resource1
不可抢占条件:已经分配给一个线程的资源不能强制性地被抢占(thread1已经得到了resource1,不能强制地剥夺它,除非thread1自己释放resource1,在这里就是退出lock块)
循环等待条件:死锁发生时,系统中肯定有两个或两个以上的线程组成一条环路,该环路中的每个线程都等待着下一个线程已经占有的资源。
与死锁相近的一个概念是活锁,或称之为饥饿
活锁(livelock)
说的就是某个线程或进程因为某些原因总是得不到自己需要的资源。假如现在排队买饭,饭堂规定年龄小的和年龄老的可以优先买饭,站在队伍的前面,如果总是有源源不断的人加进来,且总是 比你年龄小或年龄老,你就总是排在队伍的后头,最后饥饿而死~~
还有一个常见的情况是,编程时为了提高效率,常常将写操作和读操作分开,比如多个线程可以同时读某一资源,但只要有一个线程写那么其他线程就不能读也不能写了,那么如果现在有很多线程在读资源A,而有一个线程来写,这样就会造成总是有线程在读,而写线程却插不上去。
线程安全(thread safety)
在一些框架API文档中总是会出现这么一个句子:该接口是线程安全的。那么线程安全到底是什么意思呢?
意思就是说这个接口是否能在多线程环境下安全的调用。比如该接口可能会修改一些共享的数据,而又没有对这些共享数据加锁,那么就不能安全地在多线程环境下使用了。