Java并发编程:Volatile关键字和Atomic类-知了汇智

Java并发编程:Volatile关键字和Atomic类

  在接触并发编程之前我对volatile关键字是没有什么映像的,这个关键字解决了什么问题呢?让我们先来看一个示例:

public class UseVolatitle extends Thread {
    private boolean isrunning = true;

    public void setIsrunning(boolean isrunning) {
        this.isrunning = isrunning;
    }

    @Override
    public void run() {
        System.out.println("开始启动线程");
        while(isrunning) {
        }
        System.out.println("线程结束");
    }

    public static void main(String[] args) throws Exception {
        UseVolatitle useVolatitle = new UseVolatitle();
        useVolatitle.start();
        Thread.sleep(2000);

        useVolatitle.setIsrunning(false);
        System.out.println("主线线程将running设置为false");
    }
}

  该案例中有一个boolean类型的isrunning变量,子线程的while一直尝试去获取isrunning的值,当变量值为false的时候,则跳出while循环。从代码中看到,主线程在休眠了2秒钟之后即刻将公共变量isrunning设置为false,如果不出意外,子线程已经拿到了值为false,则中断循环。这是我们预期的结果,可真实的情况是这样子的么?该程序运行结果如下:

开始启动线程
主线线程将running设置为false

  遗憾的是虽然打印出了两个两条语句,但是控制台并没有停止,死循环依旧在那卡着。说明咱们的子线程并没有结束且退出。试图把我们的变量加上volatile关键字。

public class UseVolatitle extends Thread {
    private volatile boolean isrunning = true;

    public void setIsrunning(boolean isrunning) {
        this.isrunning = isrunning;
    }

    @Override
    public void run() {
        System.out.println("开始启动线程");
        while(isrunning) {
        }
        System.out.println("线程结束");
    }

    public static void main(String[] args) throws Exception {
        UseVolatitle useVolatitle = new UseVolatitle();
        useVolatitle.start();
        Thread.sleep(2000);

        useVolatitle.setIsrunning(false);
        System.out.println("主线线程将running设置为false");
    }
}

  再次运行,虽然卡着几秒钟,但最终程序能够正常终止。是什么原因导致了第一个案例中的代码无法正常终止程序呢?这涉及到多线程可见性的问题。

  可见性

  可见性是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:第一个案例中的主线程修改了变量isrunning的值之后,对子线程具有不可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。

  我们用Java内存模型来看volatile关键字是如何做到线程之间可见性的。
Java并发编程:Volatile关键字和Atomic类

  一个线程可以执行的操作有使用(use),赋值(assign),装载(load),存储(store),锁定(lock),解锁(unlock)。而主存可以执行的操作有读(read),写(write),锁定(lock),解锁(unlock),每个操作都是原子的。这里的线程执行器相当于调度的作用,当有线程对isRunning变量进行操作的时候,线程执行引擎先从主内存空间里读变量值进来,然后再对其进行赋值操作,当赋值结束之后,线程执行引擎又线程工作内存中将已经修改后的变量值写进主内存中。volatile的作用就是强制线程到主内存里去读取变量,而不是去线程工作空间内存区读取,从而实现了多个线程间变量可见性,也就是满足了线程安全的可见性。JVM这样嗯设计的思路是以空间换时间为代码,解决其并发性。在Java中,除了volatile关键字能够保证变量的可见性之外,final和synchronized同样具有此功能。

  原子性

  原子性操作的最小单位,例如转账业务:从农业银行转1块钱到工商银行。首先从用户的农业银行A帐号扣除1块钱,然后再向用户所在的工商银行帐号B新增1块钱。这两个操作要么同时成功,要么同时失败。

  那么在Java并发编程中会遇到什么样的原子性问题呢?先来看一个案例:

public class NoUseAtomic {

    static int count = 0;

    public int add() {
        count = count+10;
        return count;
    }

    public static void main(String[] args) {
        NoUseAtomic noUseAtomic = new NoUseAtomic();
        List<Thread> list = new ArrayList<Thread>();
        for(int i = 0; i < 100; i++) {
            list.add(new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(noUseAtomic.add());
                }
            }));
        }
        for(Thread t : list) {
            t.start();
        }
    }
}

  该案例启动了100个线程,每个线程向count变量做+10操作,控制台的输出如下:

10
20
30
40
......
960

  如果你仔细观察,你会发现那个该死1000没有出现,虽然这100个线程执行先后顺序我们是不可预测的,但是我总是希望在某个线程的某个时刻输出期待已久的1000,但现实总是令人悲伤的。现在让我们来给我们的count变量替换Atomic类,使之能出现我们预期的结果。

public class UseAtomic {

    static AtomicInteger count = new AtomicInteger(0);

    public int add() {
        count.addAndGet(10);
        return count.get();
    }

    public static void main(String[] args) {
        UseAtomic noUseAtomic = new UseAtomic();
        List<Thread> list = new ArrayList<Thread>();
        for(int i = 0; i < 100; i++) {
            list.add(new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(noUseAtomic.add());
                }
            }));
        }
        for(Thread t : list) {
            t.start();
        }
    }
}

  运行上诉代码,虽然输出的先后顺序也同样是不可预期的,但是在众多的输出中,你会发现1000被打印出来。Atomic系列类封装了一系列的基础类型和对象操作,其主要目的就是为了现实原子性,主要核心类如下:

  AtomicInteger

  AtomicLong

  AtomicBoolean

  AtomicIntegerArray

  AtomicLongArray

  AtomicReference

  需要注意的是

  1.并非所有的Java基本数据类型都具有其原子类,比如并不存在AtomicChar,AtomicFloat和AtomicDouble等。

  2.虽然Atomic是原子性的,但是add方法并发原子性的。例如下面这个案例就不能包装add每次都输出10的整数倍。

public class WarmingInAtomic {

    static AtomicInteger count = new AtomicInteger(0);

    public int add() {
        count.addAndGet(1);
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        return count.get();
    }

    public static void main(String[] args) {
        WarmingInAtomic warmingInAtomic = new WarmingInAtomic();
        List<Thread> list = new ArrayList<Thread>();
        for(int i = 0; i < 100; i++) {
            list.add(new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(warmingInAtomic.add());
                }
            }));
        }
        for(Thread t : list) {
            t.start();
        }
    }
}

  这个例子和上个例子一样,add()方法的目的都是使count增10,然后该案例的与上一个案例的区别是该案例中加10分为10步来完成。我截取部分控制台输出来就行了。

109
118
127
136
145
154
163
172

  相必你看到这就明白了,那个add()方法操作还没结束,就被主线程给打印输出了,add()方法并不是一个原子性的整体。要想让我们的add方法保持其原子性,还得给他加上关键字synchronized。

public class WarmingInAtomic {

    static AtomicInteger count = new AtomicInteger(0);

    public synchronized int add() {
        count.addAndGet(1);
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        count.addAndGet(1);
        return count.get();
    }

    public static void main(String[] args) {
        WarmingInAtomic warmingInAtomic = new WarmingInAtomic();
        List<Thread> list = new ArrayList<Thread>();
        for(int i = 0; i < 100; i++) {
            list.add(new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(warmingInAtomic.add());
                }
            }));
        }
        for(Thread t : list) {
            t.start();
        }
    }
}

  同样的,这里截取控制台部分输出结果来看:

10
20
30
40
50
60
70
80
90
100

  这次输出与上次不一样,add方法只有执行完return语句,并做完+10操作,才能被主线程打印出来的,这样才能保证add方法的原子性。

  最后说一说AtomicReference类的使用。

public class Person {
    private String name;
    private int age;

    public Person() {
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + ''' +
                ", age=" + age +
                '}';
    }
}
public class UseAtomicRefrence1 {
    static Person person;

    public static void main(String[] args) throws InterruptedException {
        UseAtomicRefrence1 uar = new UseAtomicRefrence1();
        person = new Person();
        person.setName("root");
        person.setAge(18);
        Thread t1 = new Thread(new Task1());
        Thread t2 = new Thread(new Task2());
        t1.start();
        t2.start();

        t1.join();
        t2.join();

        Thread.sleep(1000);
        System.out.println("now value : " + person.toString());
    }

    static class Task1 implements Runnable {
        @Override
        public void run() {
            person.setAge(19);
            person.setName("admin");

            System.out.println("thread1 value : " + person.toString());
        }
    }

    static class Task2 implements Runnable {
        @Override
        public void run() {
            person.setAge(20);
            person.setName("super user");

            System.out.println("thread2 value : " + person.toString());
        }
    }
}

  这样做是不能保证数据的一致性的,控制台输出如下:

thread1 value : Person{name='admin', age=19}
thread2 value : Person{name='super user', age=20}
now value : Person{name='super user', age=20}

  三个线程分别设置的person对象的属性,并不能独立的输出,数据已经被篡改,把代码改为:

public class UseAtomicRefrence2 {

    //普通引用
    static Person person;

    //原子性引用
    static AtomicReference<Person> aPerson;

    public static void main(String[] args) throws InterruptedException {
        UseAtomicRefrence2 uar = new UseAtomicRefrence2();
        //new一个person类
        person = new Person();
        person.setName("root");
        person.setAge(18);
        //new一个AtomicReference原子性引用
        aPerson = new AtomicReference<>(person);
        System.out.println("启动其他线程之前主线程的值:" + aPerson.get());

        Thread t1 = new Thread(new Task1());
        Thread t2 = new Thread(new Task2());
        t1.start();
        t2.start();

        t1.join();
        t2.join();

        Thread.sleep(1000);
        System.out.println("主线程当前值 : " + person.toString());
    }

    static class Task1 implements Runnable {
        @Override
        public void run() {
            Person expect = aPerson.get();
            Person update = new Person("admin", 19);
            //cas原子性操作
            aPerson.compareAndSet(expect, update);

            System.out.println("thread1 value : " + aPerson.get());
        }
    }

    static class Task2 implements Runnable {
        @Override
        public void run() {
            Person expect = aPerson.get();
            Person update = new Person("super user", 20);
            //cas原子性操作
            aPerson.compareAndSet(expect, update);

            System.out.println("thread2 value : " + aPerson.get());
        }
    }
}

  控制台输出如下:

启动其他线程之前主线程的值:Person{name='root', age=18}
thread1 value : Person{name='admin', age=19}
thread2 value : Person{name='super user', age=20}
主线程当前值 : Person{name='root', age=18}

  从上面的控制题看出,主线程,thread1,thread2线程分别对person对象的设置能在各自的线程中打印出来,并且thread1,thread2对静态变量的修改对主线程的对象属性值没有影响,保证了线程的安全。
 

  版权声明:本文来源于网络,由知了堂搜集整理,仅供大家学习Java时使用

项目教学·项目驱动

132 2811 3191
预约免费试学
点击咨询
预约试学