0%

变量的异步定时加载之线程安全

在变量的异步定时加载场景中,存在线程安全问题,接下来根据“单变量”和“多变量”这两种类型进行分别讨论。

一、单变量

示例代码如下:

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

public class SingleVariableAsyncLoadExample {

    private static ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10, new ThreadFactory() {

        public Thread newThread(Runnable r) {
            return new Thread(r);
        }
    });

    private long data;

    {
        asyncLoad();
    }

    private void asyncLoad() {
        threadPool.scheduleAtFixedRate(new Runnable() {

            //异步加载线程加载
            @Override
            public void run() {
                data = System.currentTimeMillis() % 100;
            }
        }, 3, 3, TimeUnit.MINUTES);
    }

    /**
     * 方法会被工作线程持续调用
     */
    public void print() {
        System.out.println(data);
    }
}

单变量场景又可细分为“基本类型”和“引用类型”两种子类型。

1.1、基本类型

对于基本类型,只需考虑赋值操作的线程安全问题,无需探讨其他形式的线程安全问题。在8种基本类型中,“boolean,byte,char,short,int,float”这6种基本类型变量的赋值操作是原子的,故不存在赋值操作线程安全问题;而对于“long”和“double”这两种基本类型变量的赋值操作,不一定是原子的,故存在赋值操作的线程安全问题,可通过加上volatile修饰符加以解决。

1.2、引用类型

引用类型的赋值操作是原子的,但存在另外一种形式的线程安全问题,即“重排序导致的实例构造未完成问题”。
示例代码如下:

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

public class SingleVariableAsyncLoadExample {

    private static ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10, new ThreadFactory() {

        public Thread newThread(Runnable r) {
            return new Thread(r);
        }
    });

    private Person person;

    {
        asyncLoad();
    }


    private void asyncLoad() {
        threadPool.scheduleAtFixedRate(new Runnable() {

            //异步加载线程加载
            @Override
            public void run() {
                person = new Person((int) (System.currentTimeMillis() % 100), 
                        "dslztx" + (System.currentTimeMillis() % 10));
            }
        }, 3, 3, TimeUnit.MINUTES);
    }


    /**
     * 方法会被工作线程持续调用
     */
    public void print() {
        Person localPerson = person;                       //4 

        System.out.println("[Name]:" + localPerson.name);  //5
        System.out.println("[Age]:" + localPerson.age);    //6
    }
}

class Person {
    int age;
    String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }
}

具体存在的线程安全问题是:由于重排序,当“//4”处的localPerson变量指向新实例对象时,该新实例对象的成员变量可能仍未完成构造,故此时“//5”和“//6”打印成员变量的默认值。
可通过加上volatile修饰符加以解决,修改后代码如下:

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

public class SingleVariableAsyncLoadExample {

    private static ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10, new ThreadFactory() {

        public Thread newThread(Runnable r) {
            return new Thread(r);
        }
    });

    private volatile Person person;

    {
        asyncLoad();
    }

    private void asyncLoad() {
        threadPool.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                person = new Person((int) (System.currentTimeMillis() % 100),
                        "dslztx" + (System.currentTimeMillis() % 10));   //3
            }
        }, 3, 3, TimeUnit.MINUTES);
    }

    /**
     * 方法会被持续调用
     */
    public void print() {
        Person localPerson = person;                        //4

        System.out.println("[Name]:" + localPerson.name);   //5
        System.out.println("[Age]:" + localPerson.age);     //6
    }

}

class Person {
    int age;

    String name;

    public Person(int age, String name) {
        this.age = age;      //1
        this.name = name;    //2
    }
}

根据happens-before规则之程序顺序规则,有“1 happens-before 2,2 happens-before 3”和“4 happens-before 5,5 happens-before 6”。当print()方法中localPerson未指向新实例对象时,使用旧的成员变量值;否则指向新实例对象,根据happens-before规则之volatile变量规则,有“3 hasppens-before 4”,然后再根据happens-before规则之传递性规则,有“1 happens-before 5,2 happens-before 5,1 happens-before 6,2 happens-before 6”,不会出现上述“重排序导致的实例构造未完成问题”。

二、多变量

示例代码如下:

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

public class MultiVariableAsyncLoadExample {

    private static ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10, new ThreadFactory() {

        public Thread newThread(Runnable r) {
            return new Thread(r);
        }
    });

    private int age;

    private String name;

    {
        asyncLoad();
    }

    private void asyncLoad() {
        threadPool.scheduleAtFixedRate(new Runnable() {

            //异步加载线程加载
            @Override
            public void run() {
                age = (int) (System.currentTimeMillis() % 100);
                name = "dslztx" + (System.currentTimeMillis() % 10);
            }
        }, 3, 3, TimeUnit.MINUTES);
    }

    /**
     * 方法会被工作线程持续调用
     */
    public void print() {
        System.out.println("[Age]:" + age);       //1
        System.out.println("[Name]:" + name);     //2
    }

}

在多变量场景中,除了上述单变量导致的线程安全性问题之外,还存在另外一种形式的线程安全问题,即“多变量异步加载进度不一致问题”,比如“1处读取到新值,2处读取到旧值”。

解决方案是创建一个临时类,将相关变量作为该临时类的成员变量,最后化“多变量问题”为“单变量问题”。
改进后代码如下:

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

public class MultiVariableAsyncLoadExample {

    private static ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10, new ThreadFactory() {

        public Thread newThread(Runnable r) {
            return new Thread(r);
        }
    });

    private volatile Person person;

    {
        asyncLoad();
    }

    private void asyncLoad() {
        threadPool.scheduleAtFixedRate(new Runnable() {

            //异步加载线程加载
            @Override
            public void run() {
                person = new Person((int) (System.currentTimeMillis() % 100), 
                        "dslztx" + (System.currentTimeMillis() % 10));
            }
        }, 3, 3, TimeUnit.MINUTES);
    }

    /**
     * 方法会被工作线程持续调用
     */
    public void print() {
        Person localPerson = person;                        

        System.out.println("[Name]:" + localPerson.name);   
        System.out.println("[Age]:" + localPerson.age);     
    }

}

class Person {
    int age;

    String name;

    public Person(int age, String name) {
        this.age = age;
        this.name = name;
    }
}
您的支持将鼓励我继续分享!