15.3 同步和死锁
15.2里面的例子里,静态变量num是共享的,一般来说,多线程任务中,有一些资源是关键的,必须获得这些资源才能开始任务,但是这些资源又只有一个,例如一个多线程任务需要使用打印机,那么只能等上一个使用完。在执行代码的时候也有这样的情形,例如有一个函数或者变量(你可以把这个函数看成是打印机),所有线程都要用,但原则上必须一个个来,否则就出问题(一张纸这个线程打印一部分那个线程打印一部分),就好像15.2的例子里面的变量num。
那怎么确保变量或者函数的独占呢?换句话说,怎么确保线程执行某个方法或者使用某个变量的时候,别的线程无法使用呢?
使用同步,关键字是synchronized,使用这个关键字修饰的函数,运行时只能有一个线程能执行。
那么死锁是什么原因呢,当出现多个关键资源,都要同步的时候,例如得到了AB,才可以执行,而有些机制允许线程先占有其中的一个资源等待另一个资源,如果出现这样的情形:线程1持有A等待B释放,线程2持有B等待A释放。这就是死锁了。操作系统发生死锁的话,很可能就以死机收场了。解决死锁的话有多种算法,这里不讨论细节。一种是按顺序获取资源,没有A资源不能获取B资源;一种是时间等待,如果经过了某个时间段还没等来资源,那么部分线程释放资源以解锁。
于是某些关键资源(例如某个变量)只能由一个线程独占,用完后才能释放。在具体的Java语言中,这可能是数行代码、或者是一个函数。这里有两种方法:
1.使用Lock锁
Lock是指java.util.concurrent.locks.Lock,它是一个接口,一个常用的实现是ReentranLock。
修改上一节的例子,增加锁(这里使用lambda版本的):
public class Test{ private static int num=0; private Lock lock=new ReentrantLock(); public static void main(String args[]){ Test t=new Test(); for(int i=0;i < 100;i++){ Runnable r=()-> { t.add(); }; Thread th=new Thread(r); th.start(); } } public void add(){ lock.lock(); try{ System.out.println(num); num++; } finally{ lock.unlock(); } }}
增加了一个Lock类型的变量,然后在add方法里,使用关键资源(变量num)之前,先用锁住(lock.lock()),然后使用语法try尝试获取,如果此时有另外一个线程正使用i,那么当前线程会阻塞等待i的释放。至于为啥要在finally释放锁(lock.unlock()),是因为不管try里面是否有异常,都能执行到unlock。注意,这里并不是捕获异常的,并不需要catch。
运行这个类,就能获得正确的数字顺序。
2.使用synchronized关键字
可以直接用这个关键字来修饰方法add:
public class Test{ private static int num=0; public static void main(String args[]){ Test t=new Test(); for(int i=0;i < 100;i++){ Runnable r=()-> { t.add(); }; Thread th=new Thread(r); th.start(); } } public synchronized void add(){ System.out.println(num); num++; }}
synchronized要写在返回类型之前,可用来修饰函数,也可以用来获取某个对象,这个对象就是前面说的关键资源、独占资源。
synchronized还有一种形式,直接请求某个对象:
public class Test{ private static Integer num=0; public static void main(String args[]){ Test t=new Test(); for(int i=0;i < 100;i++){ Runnable r=()-> { t.add(); }; Thread th=new Thread(r); th.start(); } } public void add(){ synchronized(num){ System.out.println(num); num++; } }}
由于synchronized只能请求对象,而之前的num是int类型的,不是对象,所以需要把num的类型改成Integer。synchronized还可以嵌套,例如:
synchronized(num1){ /* 一些代码 */ synchronized(num2){ /* 取得两个资源后 */ }}