随笔-67  评论-522  文章-0  trackbacks-0
    Java并发一直都是开发中比较难也比较有挑战性的技术,对于很多新手来说是很容易掉进这个并发陷阱的,其中尤以共享变量最具代表性,其实关于讲这个知识点网上也不少,但大象想讲讲自己对这个概念的理解。
    共享变量比较典型的就是指类的成员变量,在类中定义了很多方法对成员变量的使用,如果是单实例,当有多个线程同时来调用这些方法,方法又没加控制,那么这些方法对成员变量的操作就会使得该成员变量的值变得不准确了。
    大象用一个最典型的i++例子来说明:
    public class Test {
        private int i = 0;
        private final CountDownLatch mainLatch = new CountDownLatch(1);

        public void add(){
            i++;
        }

        private class Work extends Thread{
            private CountDownLatch threadLatch;

            public Work(CountDownLatch latch){
                threadLatch = latch;
            }

            @Override
            public void run() {
                try {
                    mainLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int j = 0; j < 1000; j++) {
                    add();
                }
                threadLatch.countDown();
            }
        }

        public static void main(String[] args) throws InterruptedException {
            for(int k = 0; k < 10; k++){
                Test test = new Test();
                CountDownLatch threadLatch = new CountDownLatch(10);
                for (int i = 0; i < 10; i++) {
                    test.new Work(threadLatch).start();
                }
                test.mainLatch.countDown();
                threadLatch.await();
                System.out.println(test.i);
            }
        }
    }
    java.util.concurrent.CountDownLatchJDK5.0提供的关于并发的一个新API,它的作用就像一个门闩或是闸门那样。上面这段代码一共执行10次,每次启动10个线程同时执行。mainLatch.await()相当于门闩挡着线程,让准备好的线程处于等待状态,当所有的线程都准备好时再调用mainLatch.countDown()方法,打开门闩让线程同时执行。我在这里用这个类的原因,是想让我创建的10个线程都准备好后再一起并发执行,这样才能很明显的看出add方法里面的i++效果。如果不引入CountDownLatch,只执行test.new Work(threadLatch).start(),则获得的结果可能看不出来线程竞争共享变量产生的错误情况。threadLatch这个CountDownLatch的作用是让10个线程都执行完run方法的for循环后通知主线程的threadLatch.await()停止等待打印出当前i的值。
    这段代码我加了-server参数运行了多次,每次结果都不一样,我取了几个比较明显的结果。当然,你也可以多运行几次看看效果。
    
    
    
    共享变量i没做任何同步操作,当有多个线程都要读取并修改它时,问题就产生了。正确的结果应该是10000,但是我们看到了,不是每次结果都是10000。我这段代码最初的版本不是这样的,因为现在的CPU哪怕是家用级PCCPU核心频率都非常高,所以完全看不出效果,在和一个朋友的讨论中,他给出了修改的建议,最后改为上面的代码,在这里谢谢Sunny君。run方法中的循环次数越大,i的并发问题就越明显,大家可以动手试下。对于上图的运行结果,和硬件平台有关,也和-server参数有关。
    有同学会有疑问了,既然共享变量没加同步处理,那为什么还是会出现10000的结果呢?关于这点我想这可能是JVM优化的结果,对于JVM(HotSpot)大象还没有很深入的研究,不敢随便下结论,请知道的朋友帮忙解答一下。
    Java中,线程是怎么操作共享变量的呢?我们都知道,Java代码在编译后会变成字节码,然后在JVM里面运行,而像实例域(i)这样的变量是存储在堆内存(Heap Memory)中的,堆内存是内存中的一块区域。线程的执行其实说到底就是CPU的执行,当今的CPU(Intel)基本上都是多核的,因此多线程都是由多核CPU来处理,并且都有L1L2L3CPU缓存,CPU为了提高处理速度,在执行的时候,会从内存中把数据读到缓存后再操作,而每个线程执行add方法操作i++的过程是这样的:
        1、线程从堆内存中读取i的值,将它复制到缓存中
        2、在缓存中执行i++操作,并将结果赋给变量i
        3、再用缓存中的值刷新堆内存中的变量i的值
    我上面写的这三步并不是严格按照JVMCPU指令的步骤来的,但过程就是这么一回事,方便大家理解。通过上面这个过程我们可以看出问题了,如果有多个线程同时要修改i,那么都需要先读取堆内存中的变量i值,然后把它复制到缓存后执行i++操作,再将结果写回到堆内存的变量i中。这个执行的时间非常短,可能只有零点几纳秒(主要还是跟硬件平台有关),但还是出现了错误。产生这种错误的原因是共享变量的可见性,线程1在读取变量i的值的时候,线程2正在更新变量i的值,而线程1这时看不到线程2修改的值。这种现象就是常说的共享变量可见性。
    下图是线程执行的抽象图,也可以说是Java内存模型的抽象示意图,可能不严谨,但大意是这样的。
    

    现在选用开发框架一般都会选择Spring,或是类似Spring这样的东西,而代码中经常用到的依赖注入的Bean如果没做处理一般都会是单例模式。试想一下,按下面这个方式引用Service或其它类似的Bean,在UserService中又不小心用到了共享变量,同时没有处理它的共享可见性,即同步,那将会产生意想不到的结果。不光Service是单例的,Spring MVC中的Controller也是单例的,所以编写代码的时候一定要注意共享变量的问题。 
    @Autowired
    private UserService userService;
    所以我们要尽可能的不使用共享变量,避开它,因为处理好共享变量可见性不是一个很简单的问题。如果有非用不可的理由,请使用java.util.concurrent.atomic包下面的原子类来代替常用变量类型。比如用AtomicInteger代替intAtomicLong代替long等等,具体可以参考API文档。如果需求比这更复杂,那还得想其它解决办法。
    以上是大象关于共享变量的一些浅薄见解,有什么不对的,还请各位指出来。
    本文为菠萝大象原创,如要转载请注明出处。http://www.blogjava.net/bolo
posted on 2014-06-10 16:09 菠萝大象 阅读(5726) 评论(5)  编辑  收藏 所属分类: Concurrency

评论:
# re: 浅谈Java共享变量[未登录] 2014-06-17 21:33 | Gospel
大象可以试一下在i前面加上关键字volatile试一下  回复  更多评论
  
# re: 浅谈Java共享变量 2014-06-18 16:39 | 菠萝大象
@Gospel
volatile的变量可不是什么情况都适用的呦,不要乱用呦。我下一篇正准备谈谈volatile  回复  更多评论
  
# re: 浅谈Java共享变量[未登录] 2014-06-19 23:26 | Gospel
@菠萝大象
是的 感觉在用多线程的时候就像在走钢丝   回复  更多评论
  
# re: 浅谈Java共享变量 2015-01-25 22:46 | Yaya
参考你的博客 收益颇多 不过有一点不是很清楚 你说的 多核CPU情况下的共享变量那个图 不是很理解 CPU多核 的情况下 每个CPU都有自己的缓存Cache CPU将计算的数据交给Cache之后刷新给内存 如果是多核多线程 那么针对某一个核CPU的所有线程 是共享变量 那么其他线程 结果可能会不如预期 这个显然和我们所接受的共享变量不太一样 不清楚这个地方 你是如何证明的(刚入门Java一年 想法有点幼稚 莫怪)   回复  更多评论
  
# re: 浅谈Java共享变量 2015-01-26 09:04 | 菠萝大象
@Yaya
你可以看看我另一篇文章"浅谈volatile变量的理解"  回复  更多评论
  

只有注册用户登录后才能发表评论。


网站导航: