Thread Safety in Java -- Postscript

foreword

 

In the last article " Thread Safety in Java ", I summarized what thread safety is, and mentioned several common methods of how to ensure thread safety at the end of the article, and emphasized that in order to ensure thread safety, a certain price is generally paid.

 

For example, using thread-safe container ConcurrentHashMap , there will still be segment locks in essence; using volatile to ensure visibility will also have performance consumption: cache data cannot be used in threads and must be obtained from main memory every time; using atomic toolkits The AtomicInteger is essentially through CAS and volatile , and there is also performance consumption; not to mention the locking method, the performance consumption is the largest.

 

The performance consumption mentioned above is actually compared without ensuring thread safety. That is to say, in a multi-threaded environment, if you do not need to access the same data, you do not need to do thread-safe processing, and the performance at this time is the best.

 

Is there a way to allow multiple threads to access non-thread-safe data and achieve thread-safety without locking? It may seem contradictory, but it can be done under certain circumstances. Let's start with this special case.

 

Concurrent Counter Case

 

Now we need to make a concurrent counter to count the number of calls of 4 important methods in the system, which is often seen when working with code tracking tools. This example is to track the number of calls of 4 methods, but it can be any fixed number. . Maybe you think it is very simple, you can use ConcurrentHashMap+AtomicInteger to achieve, the simple implementation process is as follows:

 

/**
 * Created by gantianxing on 2017/12/24.
 */
public class ThreadSafe3 {
    //Non thread safe container
    public static Map<String,AtomicInteger> datas3 = new ConcurrentHashMap<>(4);
    static {
        //The initial value of the 4 counters is 0
        datas3.put("business1",new AtomicInteger(0));
        datas3.put("business2",new AtomicInteger(0));
        datas3.put("business3",new AtomicInteger(0));
        datas3.put("business4",new AtomicInteger(0));
    }
 
    public static void main(String[] args) throws Exception{
        System.out.println("Start time"+System.currentTimeMillis());
        //Create a thread pool with fixed 4 threads
        ExecutorService threadPool = Executors.newFixedThreadPool(4);
 
        //Add 1 to 4 counters respectively, and execute 10000 times in parallel
        Counter counter1 = new Counter("business1");
        for (int i=0;i<10000;i++) {
            threadPool.execute(counter1);
        }
 
        Counter counter2 = new Counter("business2");
        for (int i=0;i<10000;i++){
            threadPool.execute(counter2);
        }
 
        Counter counter3 = new Counter("business3");
        for (int i=0;i<10000;i++){
            threadPool.execute(counter3);
        }
 
        Counter counter4 = new Counter("business4");
        for (int i=0;i<10000;i++){
            threadPool.execute(counter4);
        }
 
        //Print 4 counter statistics
        Thread.sleep(10000);//Sleep for 10 seconds to ensure that all threads are completed
        System.out.println(datas3.get("business1").get());
        System.out.println(datas3.get("business2").get());
        System.out.println(datas3.get("business3").get());
        System.out.println(datas3.get("business4").get());
    }
}
 
//Multi-threaded operation thread safe container ThreadSafe3.datas3
class Counter implements Runnable{
    private String businessId;
 
    public Counter(String businessId) {
        this.businessId = businessId;
    }
 
    @Override
    public void run() {
        AtomicInteger oldNum = ThreadSafe3.datas3.get(businessId);
        oldNum.incrementAndGet();//Counter +1
        if(oldNum.get()==10000){
            System.out.println(businessId+"end time:"+System.currentTimeMillis());
        }
    }
}

 

The implementation process is roughly as follows:

First initialize 4 counters, use AtomicInteger to ensure thread safety counters , and use a ConcurrentHashMap to store these 4 counters and initialize them to 0 ;

Then the main thread creates a thread pool with 4 threads through newFixedThreadPool , and performs 10,000 operations of adding 1 to the 4 counters in parallel , simulating that the 4 business methods are called 10,000 times respectively.

Finally, print the final results of the 4 counters. If the results are all 10000 , the running result is correct.

The thread-safe container ConcurrentHashMap and the concurrent counter AtomicInteger are used here to ensure thread safety. The execution results of the four timers are all 10000 .

 

Use a non-thread-safe container

 

As mentioned at the beginning of the article, using ConcurrentHashMap and AtomicInteger to ensure thread safety will always cost performance. If it is changed to a non-thread-safe container HashMap , and int is used directly as a counter, there is no performance consumption, but thread safety cannot be guaranteed at this time. Let's look at the non-thread-safe implementation:

/**
 * Created by gantianxing on 2017/12/24.
 */
public class ThreadSafe2 {
    //Non thread safe container
    public static Map<String,Integer> datas2 = new HashMap<>(4);
    static {
        //The initial value of the 4 counters is 0
        datas2.put("business1",0);
        datas2.put("business2",0);
        datas2.put("business3",0);
        datas2.put("business4",0);
    }
 
    public static void main(String[] args) throws Exception{
        //创建固定4个线程的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(4);
 
        //分别对4个计数器+1,并行执行10000次
        Counter counter1 = new Counter("business1");
        for (int i=0;i<10000;i++) {
            threadPool.execute(counter1);
        }
 
        Counter counter2 = new Counter("business2");
        for (int i=0;i<10000;i++){
            threadPool.execute(counter2);
        }
 
        Counter counter3 = new Counter("business3");
        for (int i=0;i<10000;i++){
            threadPool.execute(counter3);
        }
 
        Counter counter4 = new Counter("business4");
        for (int i=0;i<10000;i++){
            threadPool.execute(counter4);
        }
 
        //打印4个计数器统计结果
        Thread.sleep(10000);//睡眠10秒保证所有线程执行完成
        System.out.println(datas2.get("business1"));
        System.out.println(datas2.get("business2"));
        System.out.println(datas2.get("business3"));
        System.out.println(datas2.get("business4"));
    }
}
 
//多线程操作线程不安全容器 ThreadSafe2.datas2
class Counter implements Runnable{
    private String businessId;
 
    public Counter(String businessId) {
        this.businessId = businessId;
    }
 
    @Override
    public void run() {
        int oldNum = ThreadSafe2.datas2.get(businessId);
        ThreadSafe2.datas2.put(businessId,oldNum+1);//计数器+1
    }
}

 

跟第一版相比,只是把ConcurrentHashMap改为了HashMapAtomicInteger改为了int。多次执行main方法,每次都会得到不一样的结果,也就出现了线程安全问题(感兴趣的朋友可以直接复制代码运行测试)。这肯定不是期望的结果。

 

有朋友会说既然这时错误的写法,为什么要写出来呢?别急这只是为了引出第三种写法。

 

改进版:使用非线程安全的容器

 

我们知道引起线程安全问题的根本原因就是,多个线程操作了同一份数据。现在有4个计时器,只要我们能保证每个计数器都是由一个固定的线程处理,也就没有线程安全问题了。基于这个原理,第三版实现代码如下:

/**
 * Created by gantianxing on 2017/12/24.
 */
public class ThreadSafe {
    //容量已知 且固定,在这种场景下,可以考虑替换为非线程安全容器
//    Map<String,String> datas = new ConcurrentHashMap<>(4);
    public static Map<String,Integer> datas = new HashMap<>(4);
    static {
        //4个计数器 初始值都是0
        datas.put("business1",0);
        datas.put("business2",0);
        datas.put("business3",0);
        datas.put("business4",0);
    }
 
    public static void main(String[] args) throws Exception{
 
        //为4个不同的类型分别定义独立的单线程化线程池
        ExecutorService business1 = Executors.newSingleThreadExecutor();
        ExecutorService business2 = Executors.newSingleThreadExecutor();
        ExecutorService business3 = Executors.newSingleThreadExecutor();
        ExecutorService business4 = Executors.newSingleThreadExecutor();
        System.out.println("开始时间"+System.currentTimeMillis());
 
        //分别对4个计数器+1,并行执行10000次
        Counter counter1 = new Counter("business1");
        for (int i=0;i<10000;i++){
            business1.execute(counter1);
        }
 
        Counter counter2 = new Counter("business2");
        for (int i=0;i<10000;i++){
            business2.execute(counter2);
        }
 
        Counter counter3 = new Counter("business3");
        for (int i=0;i<10000;i++){
            business3.execute(counter3);
        }
 
        Counter counter4 = new Counter("business4");
        for (int i=0;i<10000;i++){
            business4.execute(counter4);
        }
 
        //打印4个计数器统计结果
        Thread.sleep(10000);//睡眠10秒保证所有线程执行完成
        System.out.println(datas.get("business1"));
        System.out.println(datas.get("business2"));
        System.out.println(datas.get("business3"));
        System.out.println(datas.get("business4"));
    }
}
 
//多线程操作线程不安全容器 ThreadSafe.datas
class Counter implements Runnable{
    private String businessId;
 
    public Counter(String businessId) {
        this.businessId = businessId;
    }
 
    @Override
    public void run() {
        int oldNum = ThreadSafe.datas.get(businessId);
        int newNum = oldNum+1;
        ThreadSafe.datas.put(businessId,newNum);//计数器+1
 
        if(newNum==10000){
            System.out.println(businessId+"结束时间:"+System.currentTimeMillis());
        }
    }
}

 

实现过程 跟第二版只有一点区别,这里使用newSingleThreadExecutor分别创建了4个线程池,每个线程池里只有1个线程,每个线程池只处理一个计数器。

 

多次运行这段程序,4个计数器每次的打印的结果都是10000。这就是我们想要的结果。下面来看下执行数据流程图:



 

可以看到每个计数器都是由一个单独线程处理,无需使用volatile保证可见性;并且每个计数器在高并发下进入各自不同的队列进行排队(newSingleThreadExecutor是无界队列,容易内存溢出,真实场景中可以考虑使用有界队列),保证了各个计数器内部操作是串行执行,同时多个计数器之间是并行执行。

 

第三种实现相对于第一种实现的性能会好一些,本地测试第一种方式需要70ms,第三种需要60ms。可能你会觉得差异不大,在真实的线上环境中需要并发执行的线程会更多。本身计算器只是辅助工具,为了不影响正常业务,能省一点算一点。

 

总结

 

在解决线程安全问题时,能用volatile就不要用加锁;能用线程安全容器,也不要用加锁;当然,能不用线程安全容器处理的,就不要用线程安全容器。但如果你不是很确定的情况下,那还是直接加锁吧(但别以为加锁很简单),毕竟不出bug才是首要任务。

 

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326401404&siteId=291194637