竞态条件和内存可见性

竞态条件

竞态条件:代码行为取决于各操作的时序。

public class Counting {

    static class Counter {
        private int count = 0;

        public void increment() {
            ++count;
        }

        public int getCount() {
            return count;
        }
    }

    final Counter counter = new Counter();

    class CountingThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                counter.increment();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Counting counting = new Counting();
        CountingThread t1 = counting.new CountingThread();
        CountingThread t2 = counting.new CountingThread();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counting.counter.getCount());
    }
}

运行上述代码多次,结果不是每次都为20000,原因是两个线程使用counter.count对象时发生了竞态条件。

分析++count字节码如下:

getfield #2    //获取count的值
iconst_1       //常数1
iadd           //count+1
putfield #2    //更新的值写回count

上述操作通常称为读-改-写(read-modify-write)模式

假如两个线程同时调用increment(),线程1执行getfield #2获得值42,在线程1执行其他动作之前,线程2也执行了getfield #2获得值42,如此两个线程都将获得值加1写回count中,结果count只被递增了一次,而不是两次。

解决:竞态条件的解决方案是对count进行同步(synchronize)访问,如下:

static class Counter {
//...
        public synchronized void increment() {
            ++count;
        }
//...
    }

线程进入increment()函数时,将获取Counter对象级别的锁,函数返回时释放该锁。某一时间至多有一个线程可以执行函数体,其他线程调用函数时将被阻塞直到锁被释放。

使用java.util.concurrent.atomic包更好

内存可见性

public class Puzzle {

    static boolean answerReady = false;
    static int answer = 0;

    static Thread t1 = new Thread() {
        @Override
        public void run() {
            answer = 42;
            answerReady = true;
        }
    };

    static Thread t2 = new Thread() {
        @Override
        public void run() {
            if (answerReady) {
                System.out.println("The answer is: " + answer);
            } else {
                System.out.println("The answer is not ready");
            }
        }
    };

    public static void main(String[] args) throws InterruptedException {
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

上述可能的结果有The answer is: 42或者The answer is not ready,还有可能The answer is: 0

  • 编译器的静态优化可以打乱代码的执行顺序;
  • JVM的动态优化也可以打乱代码的执行顺序;
  • 硬件可以通过打乱代码的执行来优化其性能;

比乱序执行更糟糕的是,有时候一个线程产生的修改可能对另一个线程不可见,如下:

 public void run() {
            while (!answerReady){
                Thread.sleep(100);
                System.out.println("The answer is: "+answer);
            }
        }

可能answerReady不会变为true,线程2不会退出。

内存可见性基本原则是,如果读线程和写线程不进行同步,就不能保证可见性。

同步的方法有:

  • 获取对象的内置锁;
  • 线程join();
  • 使用java.util.concurrent包提供的工具;

很容易忽略的一个重点是两个线程都需要进行同步,上述竞态条件例子,只在写increment()添加了内置锁,而读getCount()未进行同步,然而例子是线程安全的,因为是在join()之后调用的getCount(),但是为其他调用getCount()的代码埋下了隐患,可能会读取到失效的值,安全科学的做法是getCount()添加同步。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值