单例,多线程的一些验证

作为一个基础知识,java 下的一种单例实现是这样的

package me.zrj.test.test20170607;

public class Singleton {
    private Singleton() {}
    
    private static Singleton instance;
    
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

这种单例的写法的明显问题就是线程安全,说都知道这么说,那么怎么来复现这个线程安全问题呢

一个最直接的写法就是这样

package me.zrj.test.test20170607;

public class MyThread implements Runnable {
    
    @Override
    public void run() {        
        System.out.println(Singleton.getInstance());
    }
    
}
package me.zrj.test.test20170607;

public class App 
{
    public static void main( String[] args ) throws InterruptedException
    {
        System.out.println( "Hello World!" );        
        
        new Thread(new MyThread()).start();
        new Thread(new MyThread()).start();        

    }
    
}

但是这个复现的验证代码本身也是能看出来是有问题的,就是 new Thread 的先后 start 并不能保证他们在“同一时间”开始跑,而如果他们先后开始跑的时刻相差比较大,那么就无法复现那个单例的实现的 bug 了

但是。。。实际上验证的时候发现,这种写法几乎每次都能复现单例实现的 bug,由此可见,相比于 new 一个对象的耗时来说,new 一个线程的耗时还是更短的

那如果我们把这个验证的代码写成这样子呢

package me.zrj.test.test20170607;

public class App 
{
    public static void main( String[] args ) throws InterruptedException
    {
        System.out.println( "Hello World!" );        
        
        new Thread(new MyThread()).start();
        Thread.sleep(100);
        new Thread(new MyThread()).start();        

    }
    
}

这样的话,就可以看到两次拿回来的单例都是指向同一个地址了

那么,如果在这种情况下,我们还想能够复现那个单例实现的 bug,应该怎么来做呢,或者说,怎么来实现一个 countdownlatch 呢

按照理解去写,可以写成这样

package me.zrj.test.test20170607;

public class MyThread implements Runnable {
    
    synchronized void print() {
        try {
            wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "\t" + Singleton.getInstance());
    }
    
    synchronized void go() {
        notifyAll();
    }
    
    @Override
    public void run() {       
        print();        
    }
    
}
package me.zrj.test.test20170607;

import java.util.Date;

public class App 
{
    public static void main( String[] args ) throws InterruptedException
    {
        System.out.println( "Hello World!" );        
        
        MyThread myThread = new MyThread();
        
        new Thread(myThread).start();
        Thread.sleep(100);
        new Thread(myThread).start();

        myThread.go();
        
        while (true) {
            Thread.sleep(1000);
            System.out.println(new Date());
        }
    }
    
}

但是发现这种写法会出现一个问题,就是虽然 notifyAll() 了,但是依然只有一个线程打印出来东西

看到这里, https://www.zhihu.com/question… 说 notifyAll() 只是让所有线程去抢锁,但是其实还是只有一个线程能跑 synchronized 中的代码

所以真正的 countdownlatch 应该是这么来写

package me.zrj.test.test20170607;

import java.util.concurrent.CountDownLatch;

public class MyThread implements Runnable {
    
    CountDownLatch countDownLatch;
    
    public MyThread(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }
    
    synchronized void print() {
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " " + Singleton.getInstance());
    }
    
    @Override
    public void run() {       
        print();        
    }
    
}
package me.zrj.test.test20170607;

import java.util.Date;
import java.util.concurrent.CountDownLatch;

public class App 
{
    public static void main( String[] args ) throws InterruptedException
    {
        System.out.println( "Hello World!" );        
        
        CountDownLatch countDownLatch = new CountDownLatch(1);
        
        MyThread myThread = new MyThread(countDownLatch);
        
        new Thread(myThread).start();
        Thread.sleep(100);
        new Thread(myThread).start();

        countDownLatch.countDown();
        
        while (true) {
            Thread.sleep(1000);
            System.out.println(new Date());
        }
    }
    
}

Leave a Reply

Your email address will not be published. Required fields are marked *