Java中的多线程-04

前言

上面我们说到了线程同步的时候需要注意的事项了。学会了使用三种方式——使用synchronized同步代码块,同步代码方法,以后使用jdk5新增的ReentrantLock的方式,手动为同步代码块加锁和解锁。我们还知道了同步代码块的范围是需要注意的。太大了的话,效率会非常的低。如果范围太小了的话,又不能保证多线程的安全性。最后我们也提到了单例模式中的双重验证的方式,不过没有细讲。这里就先说说这个。

单例模式中的双重验证

class Singleton02{
    private static volatile Singleton02 INSTANCE;

    private Singleton02() {
    }

    public static Singleton02 getInstance() {
        if (INSTANCE == null) {
            // 给以下的代码快加上线程锁,只有一个线程可以进入到这个代码块
            synchronized (Singleton02.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Singleton02();
                }
            }
        }
        return INSTANCE;
    }
}

这里如果我们使用得是这样的锁.

public static Singleton02 getInstance() {
        // 给以下的代码快加上线程锁,只有一个线程可以进入到这个代码块
    synchronized (Singleton02.class) {
        if (INSTANCE == null) {
            INSTANCE = new Singleton02();
        }
    }
    return INSTANCE;
}

这个是绝对可以保证线程安全的。但是后面的线程并不会去修改内容,只是读取内容,所以说后面的线程就没有必要在锁外去等待,这种锁的效率是非常的低的。

    public static Singleton02 getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton02.class) {
                INSTANCE = new Singleton02();
            }
        }
        return INSTANCE;
    }

如果使用的是这样的锁,这就是完全错误的,很容易看出来可能有多个线程都进入if中了,从而也就会创建出了多个线程,这就不是所谓的单例模式了。

像上面的这种使用双重验证的方式来增加多线程的效率的方法其实是非常的好用的。

线程死锁

线程的同步虽然很好用,但是不能瞎用。随意的嵌套锁可能会导致线程的死锁。所谓的死锁可以用筷子来举例子。两个人需要吃饭,但是只有一双筷子。按道理来说应该是一个一个来吃。但是一个人获取了筷子A,另一个人获取了筷子B。此时他们都在那儿等待另一个放开筷子,从而自己可以得到两个筷子。这种情况下两种线程都会进入阻塞状态。程序将会失去相应,但是此时不会报任何的错误,也不会抛出任何的异常。

比如如下的代码中就会出现死锁的情况。

public class Demo5 {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        new Thread(()->{
            synchronized (lock1) {
                System.out.println("thread1 get lock1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock2) {
                    System.out.println("thread1 get lock2");
                }
            }
            System.out.println("thread1 end");
        }).start();

        new Thread(() ->{
            synchronized (lock2) {
                System.out.println("thread2 get lock2");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lock1) {
                    System.out.println("thread2 get lock1");
                }
            }
            System.out.println("thread2 end");
        }).start();
    }
}

我使用匿名内部类的形式创建了两个线程,其中还使用了lambda表达式。可以清晰的看到两个线程都存在锁的嵌套的问题。第一个线程是先去获取第一个锁然后获取第二个锁才能做完事情。第二个线程恰好是相反的。当他们都获取一个锁的时候,都想要获取另一个锁,导致了线程的阻塞。这就是多线程中的死锁的问题。死锁问题的原因其实非常的简单就是锁的嵌套。因此在使用的同步的时候我们要尽量不要去使用锁的嵌套。使用锁嵌套进可能会导致死锁问题,而且最为关键的是死锁问题不是每次都能表现出来的。比如说上面的代码中如果我没有使用sleep的话,绝大多数的情况下都是正常运行的。而且基本上都是线程一先结束然后线程二结束。

解决死锁问题的答案很简单就是将嵌套的锁分开。

public class Demo5 {
    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lock1) {
                System.out.println("thread1 get lock1");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (lock2) {
                System.out.println("thread1 get lock2");
            }
            System.out.println("thread1 end");
        }).start();

        new Thread(() -> {
            synchronized (lock2) {
                System.out.println("thread2 get lock2");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (lock1) {
                System.out.println("thread2 get lock1");
            }
            System.out.println("thread2 end");
        }).start();
    }
}

不过问题就是在于,我们是否可以清楚的知道我们写的代码是否会出现死锁的问题。比如说如下的代码。

public class Demo6 {
    public static void main(String[] args) {
        CA a = new CA();
        CB b = new CB();
        new Thread(() -> {
            a.foo(b);
        }).start();
        b.bar(a);
    }
}

class CA {
    public synchronized void foo(CB b) {
        System.out.println("CA.foo");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        b.last();
    }

    public synchronized void last() {
        System.out.println("CA.last");
    }
}

class CB {
    public synchronized void bar(CA a) {
        System.out.println("CB.bar");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        a.last();
    }

    public synchronized void last() {
        System.out.println("CB.last");
    }
}

虽然上面的代码写的是没有任何的实际的意义的。但是也是可以说明很多的问题的。我们只是在两个类中使用了几个同步方法,不过却意外的发生了死锁的问题。仔细看一下代码其实很容易的看出。两个嵌套的锁就是两个类的this。虽然我们没有写出synchronized代码块的嵌套。但是两个不同类的同步的方法就已经相当于是同步的嵌套了。然后主方法中有两个线程,一个占用了一个this,这样就会导致线程的死锁。

总结

上面主要就是说了一个死锁的问题,其实本来要在后面接着写线程通信之间的问题的。但是感觉线程通信这一块应该有蛮多的东西可以吹的,如果接在这个后面应该会蛮长的,那么就下一个里面再写吧。不过这次写的这个确实有点儿少了。


一枚小菜鸡