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){
        /*
         取得两个资源后
         */
        }
}