Java虚拟机并发编程-1.并发的威力与风险

1.1 线程:程序的执行流程

    线程可以看成是进程中的一个执行流程,当我们运行一个程序的时候,其所属进程中至少存在一个执行线程。当多个线程在同一个应用程序或JVM实例下运行的时候,实际意味着此时有多个人物或操作在并发运行。我们所说的并发程序通常是指那些使用了多线程或多个并发执行流程的应用程序。

    单核处理器将会不断的在多个执行流程中进行上下文切换,但任意时刻有且只有一个线程能够被执行。多核处理器中,在任意时刻可以有多个执行流程被执行。

 1.2 并发的威力

     并发可以帮助应用程序提高响应速度、减少等待时间并增加吞吐量。

1.3并发的风险

     饥饿:当一个线程等待某个需要运行很长世间或永远无法完成的事件发生时,该线程就会陷入饥饿,饥饿可能发生在线程等待用户输入时、等待某些外部事件发生时或等待其他线程释放某个锁时,为了避免线程陷入饥饿,我们可以为其设计一个等待超市的策略,让线程等待有限的时间。

     死锁:两个或者多个线程相互等待对方释放所占用的资源或执行某些动作,解决死锁的方案莫过于避免显示加锁以及避免使用可变状态。

     竞争条件:如果两个线程使用相同的资源或数据,那么我们就将这种情况称为竞争条件,竞争条件不仅仅会发生在两个线程同时更改相同数据的场景中,还可能发生在一个线程在修改某数据而另一个线程同时在读这个数据的时候,竞争条件主要是由下面两大原因造成的:即时下流行的Just-In-Time(JIT)编译器的优化以及Java内存模型。请看以下实例

public class RaceCondition{
   
     private static boolean done;

     public static void main(final String[] args) throws InterruptedException {
         new Thread(
            new Runnable(){
               public void run(){
                   int i = 0;
                   while(!done){
                        i++;
                   }
                   System.out.println("Done!");
               }
            }
         ).start();
     }
     
     System.out.println("OS:"+System.getProperty("os.name"));
     Thread.sleep(2000);
     done = true;
     System.out.println("flag done set to ture");
}

    默认情况下,Java在32位Windows平台上是以client模式运行的,而在Mac平台上是以server模式运行的。当程序以server模式运行时,即主线程将done变量的值设为ture,第二个线程也无法看到该变量值的变化。这种现象是由于Java server JIT编译器优化所导致的。

    了解可见性:理解内存栅栏

     上个例子中是由于JIT编译器可能对新线程代码里的while循环进行了优化,导致新线程在线程上下文中无法看到变量done的变化。此外,新线程可能只会从其寄存器或本地cache中读取标记变量done的变化,而不是每次都跑去速度更慢的内存里进行操作。基于上述原因新线程就无法看到主线程对其标记变量值的变更了。如果想要快速修复此问题,只需要将变量done标记为volatile就可以了。volatile的作用是告知JIT编译器不要对被标记的变量执行任何可能影响其访问顺序的优化,任何对该变量的读写访问都要忽略本地cache并直接对内存进行操作。修改如下:

private static boolean done;
修改为
private static volatile boolean done;

    虽然将变量标记为volatile可以完全规避此类问题,但却使每次变量访问都需要跨越内存栅栏并导致应用程序性能下降,而且在多线程并发访问的时候,volatile无法保证整体操作的原子性,线程很可能取得该变量的中间结果,为了解决这个问题,我们可以屏蔽对变量的直接访问,并将所有的访问都引导为通过同步的getter和setter函数来进行,具体代码如下:

private static boolean done;
public static synchronized boolean getFlag(){return done;}
public static synchronized void setFlag(boolean flag){done = flag;}

    内存栅栏就是从本地或工作内存到主存之间的拷贝动作,仅当写操作线程先跨越内存栅栏而读操作线程后跨越内存栅栏的情况下,写操作线程所做的变更才对其他线程可见。关键字synchronized 和volatile都强制规定了所有变量都全局可见。在程序运行过程中,所有的变更会先在寄存器或本地cache中完成,然后才会拷贝到主存以跨越内存栅栏。此种跨越顺序称为happengs-before。Java并发API中很多操作都隐含有跨越内存栅栏的含义:volatile、synchronized、Thread中的函数如start()和interrupt()、ExecutorService中的函数以及像CountDownLatch这样的同步工具类等。

   

    规避共享可变性:假设我们在程序中定义了一个非final的字段,每当一个线程更改了该字段的值,我们都需要考虑是应该将变更后的值写回内存还是应该将其保留在寄存器cache中以便其他线程读取。如果我们定义了一个指向某不可变的final字段,并让多个线程同时访问该字段,则这种形式的共享不会有任何隐患。

    

猜你喜欢

转载自renhanxiang.iteye.com/blog/2167260