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.CountDownLatch是JDK5.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哪怕是家用级PC的CPU核心频率都非常高,所以完全看不出效果,在和一个朋友的讨论中,他给出了修改的建议,最后改为上面的代码,在这里谢谢Sunny君。run方法中的循环次数越大,i的并发问题就越明显,大家可以动手试下。对于上图的运行结果,和硬件平台有关,也和-server参数有关。
有同学会有疑问了,既然共享变量没加同步处理,那为什么还是会出现10000的结果呢?关于这点我想这可能是JVM优化的结果,对于JVM(HotSpot)大象还没有很深入的研究,不敢随便下结论,请知道的朋友帮忙解答一下。 在Java中,线程是怎么操作共享变量的呢?我们都知道,Java代码在编译后会变成字节码,然后在JVM里面运行,而像实例域(i)这样的变量是存储在堆内存(Heap
Memory)中的,堆内存是内存中的一块区域。线程的执行其实说到底就是CPU的执行,当今的CPU(Intel)基本上都是多核的,因此多线程都是由多核CPU来处理,并且都有L1、L2或L3等CPU缓存,CPU为了提高处理速度,在执行的时候,会从内存中把数据读到缓存后再操作,而每个线程执行add方法操作i++的过程是这样的: 1、线程从堆内存中读取i的值,将它复制到缓存中 2、在缓存中执行i++操作,并将结果赋给变量i 3、再用缓存中的值刷新堆内存中的变量i的值 我上面写的这三步并不是严格按照JVM及CPU指令的步骤来的,但过程就是这么一回事,方便大家理解。通过上面这个过程我们可以看出问题了,如果有多个线程同时要修改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代替int,AtomicLong代替long等等,具体可以参考API文档。如果需求比这更复杂,那还得想其它解决办法。 以上是大象关于共享变量的一些浅薄见解,有什么不对的,还请各位指出来。
本文为菠萝大象原创,如要转载请注明出处。http://www.blogjava.net/bolo
posted on 2014-06-10 16:09
菠萝大象 阅读(11270)
评论(5) 编辑 收藏 所属分类:
Concurrency