蚩尤后裔

芝兰生于深林,不以无人而不芳

volatile 关键字

volatile 简述

  • 学习volatile同步关键字之前,必须掌握Java内存模型,可参考《JVM 详解》中的Java内存模型部分
  • volatile是Java提供的一种轻量级的同步机制,同synchronized相比(synchronized通常称为重量级锁),volatile更轻量级
  • 与synchronized同步关键字相比,volatile运行时开销较少,但是它所能实现的功能也仅是 synchronized 的一部分。
  • 锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。
  1. 互斥:即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问,这样一次就只有一个线程能够使用该共享数据。
  2. 可见性:它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 。如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。 
  • Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性(和数据库事务同理)。这就是说线程能够自动发现 volatile 变量的最新值。

volatile 特性

  • 将一个共享变量声明为volatile后,会有以下效应:
  1. 当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去,这个写操作会导致其他线程中的缓存无效,从而保证可见性
  2. 禁止指令重排序优化。重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。但是重排序也需要遵守一定规则:
            1)重排序操作不会对存在数据依赖关系的操作进行重排序。
    比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
      2)重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变

经典错误

错误情景

/**
 * Created by Administrator on 2018/6/5 0005.
 * 继承Thread方式
 */
public class ThreadByThread extends Thread {
    /**
     * 静态成员变量用来计数
     * 模拟一千张机票进行出售
     */
    public static int count = 1000;
    private CountDownLatch countDownLatch;

    public ThreadByThread(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName() + " start..." + new Date());
            for (int i = 0; i < 100; i++) {
                count--;
                Thread.sleep(100 + new Random().nextInt(100));
            }
            /**倒计数锁存器计数减一,此方法是没有阻塞的,不要与CyclicBarrier的混淆*/
            countDownLatch.countDown();
            System.out.println(Thread.currentThread().getName() + " end..." + new Date());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
/**
 * Created by Administrator on 2018/6/13 0013.
 * 测试
 */
public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        /**使用线程池性+倒计数锁存器
         * */
        CountDownLatch countDownLatch = new CountDownLatch(10);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            /**新开线程去买票,每个线程卖100*/
            executorService.execute(new ThreadByThread(countDownLatch));
            Thread.sleep(500+new Random().nextInt(500));
        }
        /**主线程阻塞等待三个卖票的线程执行完毕*/
        countDownLatch.await();
        /**如果程序执行正确,那么应该输出剩余的正确票数*/
        System.out.println(ThreadByThread.count + " : " + new Date());
        /**关闭下线程池*/
        executorService.shutdown();
    }
}

--------------结果输出------------------------

pool-1-thread-1 start...Thu Jun 14 14:00:19 CST 2018
pool-1-thread-2 start...Thu Jun 14 14:00:20 CST 2018
pool-1-thread-3 start...Thu Jun 14 14:00:20 CST 2018
pool-1-thread-4 start...Thu Jun 14 14:00:21 CST 2018
pool-1-thread-5 start...Thu Jun 14 14:00:22 CST 2018
pool-1-thread-6 start...Thu Jun 14 14:00:22 CST 2018
pool-1-thread-7 start...Thu Jun 14 14:00:23 CST 2018
pool-1-thread-8 start...Thu Jun 14 14:00:24 CST 2018
pool-1-thread-9 start...Thu Jun 14 14:00:24 CST 2018
pool-1-thread-10 start...Thu Jun 14 14:00:25 CST 2018
pool-1-thread-1 end...Thu Jun 14 14:00:34 CST 2018
pool-1-thread-2 end...Thu Jun 14 14:00:35 CST 2018
pool-1-thread-3 end...Thu Jun 14 14:00:35 CST 2018
pool-1-thread-4 end...Thu Jun 14 14:00:36 CST 2018
pool-1-thread-5 end...Thu Jun 14 14:00:37 CST 2018
pool-1-thread-6 end...Thu Jun 14 14:00:38 CST 2018
pool-1-thread-7 end...Thu Jun 14 14:00:38 CST 2018
pool-1-thread-8 end...Thu Jun 14 14:00:39 CST 2018
pool-1-thread-9 end...Thu Jun 14 14:00:39 CST 2018
pool-1-thread-10 end...Thu Jun 14 14:00:40 CST 2018
6 : Thu Jun 14 14:00:40 CST 2018
Process finished with exit code 0

错误原因

  • 从上面可以发现一共1000张票,10个线程每个卖出100张,最后应该余票为0才对,可是却输出还有6张票。而且随着线程数量越多,操作时间越久,结果的偏差就会越大
  • 这是因为"count--"看上去类似一个单独操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,类似的还有x++、++x、--x等
  • 所以,在多线程环境下,有可能线程5将count读取到本地内存时,其他线程已经将count减小了很多,而线程5却对过期的count在进行操作(自减),重新写到主存中,最终导致了num的结果不合预期,大于了0。类似于数据库的“脏读”
  • public static int count = 1000;在本例中有没有使用volatile修饰结果都是一样的,因为volatile只保证可见性,却不保证原子性。而count--操作也不具有原子性!

原子操作类解决

  • 可以使用java.util.concurrent.atomic包下的AtomicInteger类来解决这个问题,atomic包时JDK1.5开始引进的,专门用于处理高并发的情况
  • AtomicInteger类可以以原子的方式来更新int值,即保持原子性
/**
 * Created by Administrator on 2018/6/5 0005.
 * 继承Thread方式
 */
public class ThreadByThread extends Thread {
    /**
     * 静态成员变量用来计数
     * 模拟一千张机票进行出售
     */
    public static AtomicInteger atomicInteger = new AtomicInteger(1000);
    private CountDownLatch countDownLatch;

    public ThreadByThread(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        try {
            System.out.println(Thread.currentThread().getName() + " start..." + new Date());
            for (int i = 0; i < 100; i++) {
                /**减一*/
                atomicInteger.decrementAndGet();
                Thread.sleep(100 + new Random().nextInt(100));
            }
            /**倒计数锁存器计数减一,此方法是没有阻塞的,不要与CyclicBarrier的混淆*/
            countDownLatch.countDown();
            System.out.println(Thread.currentThread().getName() + " end..." + new Date());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

synchronized加锁同步

  • 虽然使用synchronized处理这个例子有点大材小用,但至少是个思路

/**
 * Created by Administrator on 2018/6/5 0005.
 * 继承Thread方式
 */
public class ThreadByThread extends Thread {
    /**
     * 静态成员变量用来计数
     * 模拟一千张机票进行出售
     */
    public static int count = 1000;
    private CountDownLatch countDownLatch;

    public ThreadByThread(CountDownLatch countDownLatch) {
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
        /**使用类锁(对本类的所有对象有效),在当前线程没有执行完之前,其余线程都会处于等待状态
         * 通俗的将是:线程1在卖票的时候,其余线程必须得等待
         * */
        synchronized (ThreadByThread.class){
            try {
                System.out.println(Thread.currentThread().getName() + " start..." + new Date());
                for (int i = 0; i < 100; i++) {
                    /**减一*/
                    count--;
                    Thread.sleep(100 + new Random().nextInt(100));
                }
                /**倒计数锁存器计数减一,此方法是没有阻塞的,不要与CyclicBarrier的混淆*/
                countDownLatch.countDown();
                System.out.println(Thread.currentThread().getName() + " end..." + new Date());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


阅读更多
个人分类: 线程
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭