[Java多线程编程之九] 线程安全

  并发编程的东西理论太多不太好理解,直接上代码。

一、线程安全的原子性问题

  在多线程环境中,很多时候我们希望多个步骤可以看成一个整体原子地被执行,如果多个线程操作了同一个数据,且不做任何特别处理,通常会出现问题,如下面的代码所示:

//  非线程安全的数值序列生成器
public class UnsafeSequence {
    private int value;
    
    public int getNext() {
        return value++;
    }
    
    public int getValue() {
        return value;
    }
}

  如果多个线程调用同个 UnsafeSequence 对象生成数值序号,可能会产生生成的序号重复的问题,测试代码如下所示:

    public static void main(String[] args) throws InterruptedException {
        UnsafeSequence sequence = new UnsafeSequence();
        new Thread(new Runnable() {         
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++)
                    sequence.getNext();
                System.out.println("over");
            }
        }).start();
        
        for (int i = 0; i < 10000; i++)
            sequence.getNext();
        System.out.println("main over");
        Thread.sleep(2000);
        System.out.println(sequence.getValue());
    }

  我们启了一个子线程、一个主线程,分别获取10000次序号,休眠两秒时间给两个线程充足的执行时间,然后打印 value 的值。正常情况下,经过不同线程一共20000次获取序号的操作,value 的值应该是20000,但实际执行效果如下:


  如果多次执行,每次都会打印出不同的值,出现这种情况是因为运行时是因为 value++ 看起来是单个操作,实际上包含了三个独立操作:读取 value,将 value 加1,并将计算结果回写 value。如果在执行加1操作前,不同线程读取了 value 的同一值 A,不管后面两步线程间执行的顺序如何,最终结果都是 A+1,这就导致吞掉了一次加1的操作,这还仅仅只是两个线程的一次并行加1操作,所以从上面测试结果来看,发生了2115次这种并行。如果有更多的线程,更多次并行,吞掉的加1操作更多,程序无法得到正确的结果。

  上面的示例演示了线程安全的原子性问题

  如何解决原子性问题?使用同步保证多个操作原子地被执行,同步可以使用 sychronized(JVM内置监视器锁)、Lock(JDK API实现的锁)、线程安全类(JUC包)。

1、使用sychronized解决原子性问题

public class SafeSequence {
    private int value;
    
    public synchronized int getNext() {
        return value++;
    }
    
    public synchronized int getValue() {
        return value;
    }
    
    public static void main(String[] args) throws InterruptedException {
        SafeSequence sequence = new SafeSequence();
        new Thread(new Runnable() {         
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++)
                    sequence.getNext();
                System.out.println("over");
            }
        }).start();
        
        for (int i = 0; i < 10000; i++)
            sequence.getNext();
        System.out.println("main over");
        Thread.sleep(2000);
        System.out.println(sequence.getValue());
    }
}

  getNext() 方法加了 sychronized 标识,意味着多线程环境下对这个方法的调用会同步,如果当前A线程正在调用该方法,其余线程必须等待A线程执行完之后才有机会执行。从同步的实现上,用到了锁的原语,JVM会为每个对象创建一个监视器锁,sychronized 默认会用到对象的监视器锁,当线程要调用方法时,需要先抢到锁,然后才能执行方法体,否则会阻塞。不管执行多少次,执行的结果都是20000。



2、使用Lock解决原子性问题

public class LockSafeSequence {
    private int value;
    private Lock lock = new ReentrantLock();
    
    public int getNext() {
        try {
            lock.lock();
            return value++;
        } finally {
            lock.unlock();
        }
    }
    
    public int getValue() {
        return value;
    }
    
    public static void main(String[] args) throws InterruptedException {
        LockSafeSequence sequence = new LockSafeSequence();
        new Thread(new Runnable() {         
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++)
                    sequence.getNext();
                System.out.println("over");
            }
        }).start();
        
        for (int i = 0; i < 10000; i++)
            sequence.getNext();
        System.out.println("main over");
        
        Thread.sleep(2000);
        System.out.println(sequence.getValue());
    }
}

  使用JDK API实现的 Lock 也能达到 sychronized 的效果,不管执行多少次执行结果同样是20000不变。

3、使用线程安全类解决原子性问题

public class AtomSafeSequence {
    private AtomicInteger value = new AtomicInteger();
    
    public int getNext() {
        return value.incrementAndGet();
    }
    
    public int getValue() {
        return value.get();
    }
    
    public static void main(String[] args) throws InterruptedException {
        AtomSafeSequence sequence = new AtomSafeSequence();
        new Thread(new Runnable() {         
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++)
                    sequence.getNext();
                System.out.println("over");
            }
        }).start();
        
        for (int i = 0; i < 10000; i++)
            sequence.getNext();
        System.out.println("main over");
        
        Thread.sleep(2000);
        System.out.println(sequence.getValue());        
    }
}

  对于线程安全类来说,如果程序要实现并发控制的功能已经封装在里面了,那么可以放心地使用线程安全类,而无需担心并发问题,在实际开发中,在满足需求的情况下,优先使用Java类库中现成的线程安全类来实现功能。



二、线程安全的可见性问题

  在多线程环境中,每个线程都有它私有的内存,如果不做特别的处理,线程会将数据缓存到其私有内存中,可能会导致程序错误,如下面的代码所示:

public class UnsafeSwitch {
    private boolean flag = true;

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
    
    public static void main(String[] args) throws InterruptedException {
        UnsafeSwitch sw = new UnsafeSwitch();
        
        new Thread(new Runnable() {         
            @Override
            public void run() {                 
                while (sw.flag) {
                    Thread.yield();                     
                }
                System.out.println(Thread.currentThread().getName() + " is over!");
            }               
        }).start();
        
        sw.setFlag(false);
        Thread.currentThread().join();
    }
}

  在JDK1.7中,除了在32位下使用 -client 模式运行匿名线程能打印执行结束的信息,其他情况都线程都无法跳出循环,因为其读取到的 sw.flag 的值一直都是 true,当然在高版本的JDK中对这一问题已经进行了修复,但这说明JDK的版本和设置的参数会对程序运行结果造成影响。

  为了提高执行效率,Java可能会对代码进行优化,包括对数据进行缓存、指令重排序等,并且这种优化可能发生在编译器编译、JVM指令处理、内存缓存等各个阶段,由于不同版本的JDK优化的策略不一致,导致了某些版本JDK出现上面这种问题。

  上面的例子里匿名线程无法看到主线程对 sw.flag 修改后的值,这就是线程安全的可见性问题。再来个不管哪个版本的JDK中一定会出现的可见性问题的例子:

public class Service {
    public boolean isContinueRun = true;
    
    public void runMethod() {
        System.out.println("开始执行");
        while (isContinueRun) {}
        System.out.println("停止执行");
    }
    
    public void stopMethod() {
        isContinueRun = false;
    }
    
    public static void main(String[] args) throws InterruptedException {
        Service service = new Service();
        Thread threadA = new Thread(new Runnable() {            
            @Override
            public void run() {
                service.runMethod();
            }
        });
        
        Thread threadB = new Thread(new Runnable() {            
            @Override
            public void run() {
                service.stopMethod();
                System.out.println(service.isContinueRun);
            }
        });
        threadA.start();
        Thread.sleep(3000);
        threadB.start();
    }
}

  线程A先启动执行 runMethod() 方法,如果 isContinueRun 为true,则一直while循环;休眠几秒后,线程B启动执行 stopMethod() 方法,在该方法内部会修改 isContinueRun 的值为false。正常情况下,在线程B执行之后,线程A应该跳出循环并且输出信息,但实际上并没有,执行结果如下所示:


  证明线程A无法看到 isContinueRun 的变化,产生了可见性问题。解决原子性问题的方法也适用于解决可见性问题,因为同步不单意味这同步代码块只有一个线程能访问,也意味这某个线程在同步代码块中执行的操作对其他所有线程可见。除此之外还可以使用 volatile 关键字,被其修饰的变量具有可见性(即某个线程对变量的修改对其他线程可见)

1、使用sychronized解决可见性问题

public class SychronizedService {
    public boolean isContinueRun = true;
    
    public void runMethod() throws InterruptedException {
        System.out.println("开始执行");
        synchronized (this) {           
            while (isContinueRun) {
                wait();
            }
        }       
        System.out.println("停止执行");
    }
    
    public synchronized void stopMethod() {
        isContinueRun = false;
        notify();
    }
    
    public static void main(String[] args) throws InterruptedException {
        SychronizedService service = new SychronizedService();
        Thread threadA = new Thread(new Runnable() {            
            @Override
            public void run() {
                try {
                    service.runMethod();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        
        Thread threadB = new Thread(new Runnable() {            
            @Override
            public void run() {
                service.stopMethod();
                System.out.println(service.isContinueRun);
            }
        });
        
        threadA.start();
        Thread.sleep(1000);
        threadB.start();        
    }
}

  为了使用同步,对while循环外围加了一个 sychronized 块,为了让线程A释放掉锁给线程B执行修改变量的操作,使用 wait/notify 来做控制,最终执行效果如下:



2、使用线程安全类解决可见性问题

public class AtomService {
    public AtomicBoolean isContinueRun = new AtomicBoolean(true);
    
    public void runMethod() {
        System.out.println("开始执行");
        while (isContinueRun.get()) {}
        System.out.println("停止执行");
    }
    
    public void stopMethod() {
        isContinueRun.set(false);
    }
    
    public static void main(String[] args) throws InterruptedException {
        AtomService service = new AtomService();
        Thread threadA = new Thread(new Runnable() {            
            @Override
            public void run() {             
                service.runMethod();
            }
        });
        
        Thread threadB = new Thread(new Runnable() {            
            @Override
            public void run() {
                service.stopMethod();
                System.out.println(service.isContinueRun);
            }
        });
        
        threadA.start();
        Thread.sleep(1000);
        threadB.start();        
    }   
}

  无需额外通过同步保障可见性,AtomicBoolean 线程安全原子类封装了该功能,执行结果:



3、使用volatile解决可见性问题

public class VolatileService {
    public volatile boolean isContinueRun = true;
    
    public void runMethod() {
        System.out.println("开始执行");
        while (isContinueRun) {}
        System.out.println("停止执行");
    }
    
    public void stopMethod() {
        isContinueRun = false;
    }
    
    public static void main(String[] args) throws InterruptedException {
        VolatileService service = new VolatileService();
        Thread threadA = new Thread(new Runnable() {            
            @Override
            public void run() {             
                service.runMethod();
            }
        });
        
        Thread threadB = new Thread(new Runnable() {            
            @Override
            public void run() {
                service.stopMethod();
                System.out.println(service.isContinueRun);
            }
        });
        
        threadA.start();
        Thread.sleep(1000);
        threadB.start();        
    }       
}

  volatile 能禁止指令重排序,禁止为了优化而将变量值缓存到线程私有内存中,因此一个线程对声明了 volatile 变量的修改,总是与其他线程看到变量状态的变化同步,执行结果如下:



三、线程安全

1、什么是线程安全?

  当多线程访问某个类,不管运行环境的调度方式或线程间如何交替执行,并且在主调代码不需要任何额外的同步或协同,这个类都能变成出正确的行为,那么就称这个类是线程安全的。

  上面的例子,在没有采用合适的并发控制之前,程序运行时就没有表现出正常的行为,结果是不可预估的,因此是线程不安全的。

2、线程安全问题是如何产生的?

  多个线程对共享(shared)可变(Mutable)变量进行访问时,没有采用同步机制来协同不同线程对变量的访问,导致多个线程交叉执行产生原子性问题、多个线程访问数据隔离的可见性问题。

3、如何避免线程安全问题?

  如果一个对象没有被多线程访问,那么它是线程安全的。线程安全问题发生的原因是多线程对存储状态的共享变量的访问操作没有进行正确管理,导致上面的原子性和可见性问题。有四种方式可以修复这个问题:

  • 不在线程之间共享该状态变量(线程封闭)
  • 将状态变量修改为不可变的变量(final)
  • 在访问状态变量时使用同步(sychronized、Lock、线程安全类)
  • 被多线程执行的程序是无状态的(无状态意味着没有需要共享数据)


4、使用了线程安全类的程序是否就一定是线程安全程序?

  不一定。如下面的程序,两个变量 ab 的类型都是线程安全类,我们希望 ab 同时加1,正常情况下输出的 a 应该等于 b

public class UnsafeProgram {
    private AtomicInteger a = new AtomicInteger(0);
    private AtomicInteger b = new AtomicInteger(0);
    
    public void incr() {
        a.incrementAndGet();
        b.incrementAndGet();
    }
    
    public void print() {
        System.out.println("a = " + a.get() + ", b = " + b.get());
    }
    
    public static void main(String[] args) {
        UnsafeProgram program = new UnsafeProgram();
        new Thread(new Runnable() {         
            @Override
            public void run() {
                while (true) {
                    program.incr();
                }
            }
        }).start();
        
        while (true) {
            program.print();
        }
    }
}

  程序执行结果:


  线程安全类指它封装和实现的功能在多线程环境下能正确执行,不会产生不可预测的安全性问题。但这种安全无法延伸至使用线程安全类的程序,因为线程安全类和使用它的程序面对的多线程下要保障程序正确执行的不变性条件不同。

5、原子性

(1)竞态条件

  在并发程序中,如果某些应该原子性执行的多个操作,在多线程环境下,被不同线程交替执行时由于不恰当的执行时序而出现不正确的结果,我们称之为竞态条件

  以一开始的程序为例,value++ 操作实际上包含了三个独立操作:读取 value,将 value 加1,并将计算结果回写 value。如果不同线程出现下图所示的情况,就会导致有些线程的+1操作被覆盖掉,产生线程安全问题。

  线程B在一开始读取到的value的值是有效的,但是当线程A执行运算时,value为9这个状态就失效了。采用加锁的方式同步,时间上就是将同步的代码快的执行变成串行的,如下所示:


  从本质上看,竞态条件就是基于一种可能失效的观察结果来做出判断或者执行某个运算。

  在程序设计中,当我们不想让客户端程序使用某个类时自由地创建对象,并且控制这个类只能有一个实例对象时,通常会使用工厂模式,代码如下:

public class Application {
    private static Application application = null;
    // 禁止调用构造方法创建类
    private Application() {
        System.out.println("构造方法被调用了");
    }
    
    public static Application getInstance() {
        if (application == null) {
            application = new Application();
        }
        return application;
    }

  上面这种 “先检查后执行” 的写法通常用于在程序中用到某个资源才进行懒加载。

  在 getInstance() 方法中,当线程判断到application是空时,就去创建对象,否则返回application,这里存在竞态条件,因此不是线程安全的。假设线程A和线程B同时执行该方法,线程A判断application为空,然后准备创建对象,在创建对象并将application指向新创建的对象之前,如果线程B也判断到application为空,然后准备创建对象,这时线程A创建好了对象,application不再是空的了,线程B之前观察到的结果失效了,接着创建对象并将原来线程A创建的对象覆盖掉,引发线程安全问题。

  下面的代码演示了这种线程安全问题:

    public static void main(String[] args) {            
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {             
                @Override
                public void run() {
                    System.out.println(Application.getInstance());                  
                }
            }).start();         
        }
    }

  执行结果如下,可以看到构造函数被调用了四次,最后 application 指向的对象是哪个取决于哪个线程是最后创建对象的。




(2)复合操作

  要避免竞态条件,就需要在某个线程修改该变量时,通过某种方式防止其他线程使用这个变量,从而确保其他线程只能在修改操作完成之前或之后读取和修改状态,而不是在修改状态的过程中。

  要解决上面的问题,可以将对application是否为空的判断与对象的创建两个操作当成一个原子操作,用同步的手段实现:

    public static Application getInstance() {
        synchronized (Application.class) {
            if (application == null) {          
                application = new Application();
            }
        }
        return application;
    }

  但是这样会导致每次来获取对象的时候都需要获取锁,导致创建完对象后获取还是会阻塞,因此将代码改成下图:

    public static Application getInstance() {
        if (application == null) {
            synchronized (Application.class) {
                if (application == null) {          
                    application = new Application();
                }
            }
        }
        return application;
    }

  上面的写法叫 双层重复检查,首先为了避免对象创建后获取还需要获得锁,将同步代码块放在 if 里面,为了避免线程B在线程A执行完同步代码块后获得锁执行时同步创建,所以在同步代码快里要再做一次检查,执行结果如下,可以看到构造方法只被调用到一次。


  更简洁的写法,可以利用Java自带的类加载初始化机制,将对象的初始化放在 static 块中,但是这种算 “饿汉模式” (预加载),不算懒加载了,代码如下:

public class Application {
    private static Application application = null;
    static {
        application = new Application();
    }
    
    // 禁止调用构造方法创建类
    private Application() {
        System.out.println("构造方法被调用了");
    }
    
    public static Application getInstance() {
        return application;
    }
}

  为了测试出懒加载还是预加载,测试代码修改成:

    public static void main(String[] args) {        
        System.out.println("开始执行");
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {             
                @Override
                public void run() {
                    System.out.println(Application.getInstance());                  
                }
            }).start();         
        }
    }

  执行结果如下,可以看出是预加载:



6、可见性

  从线程可见性问题的例子可以看出,当一个线程修改了共享可变变量,而其他线程察觉不到变化时,也会引发线程安全问题。因此,为了保证线程安全,我们不仅希望防止某个线程正在使用对象状态而另一个线程在同时修改该状态,而且希望确保当一个线程修改了对象状态后,其他线程能够看到发生的状态变化。

  同步代码块和同步方法都能保证以原子的方式执行操作,但一种常见的误解是:关键字 sychronizedLock只能实现原子性。实际上它们也能保证可见性。

(1)什么情况会导致可见性?

  先来看看下面的代码,存在可见性问题:

public class SychronizedVisibility {    
    private int num = 0;
    
    public static void main(String[] args) throws InterruptedException {
        SychronizedVisibility obj = new SychronizedVisibility();
        Thread threadA = new Thread() {
            public void run() {
                System.out.println("num = " + obj.num); 
                while (obj.num == 0) {}
                System.out.println("num = " + obj.num);
            }
        };
        
        Thread threadB = new Thread() {
            public void run() {
                obj.num = 1;            
            }
        };      
        threadA.start();    
        Thread.sleep(100);
        threadB.start();
        threadA.join();
        threadB.join();
    }
}

  线程A先启动,然后判断到obj.num为0则进入while循环,100毫秒后,线程B启动,将obj.num的值修改为1,正常情况下线程A应该看到这个变化,但实际运行结果是线程A无法跳出循环,产生了可见性问题,如下图所示:


  一时兴起,想统计下线程A的while循环了多少次,加了几行代码,变成这样:

...
                int count = 0;
                while (obj.num == 0) {                              
                    System.out.println("count = " + (++count)); 
                }
...

  然后执行结果显示,线程A又能看到线程B对共享状态的修改了:


  突然有点懵逼,这什么情况?看到count最后打印的数字为5245,100ms线程Awhile循环只执行了5245次,这个次数有点低,原因是 System.out.println 严重拖了后腿,给num加个volatile,保证可见性,在while循环体内只执行count的加一操作,不打印,看看循环了多少次

...
    private volatile int num = 0;
...
                while (obj.num == 0) {                      
                    ++count;
                }
                System.out.println("count = " + count);
...

  从执行结果看,100ms内while循环了上亿次,充分说明了 System.out.println 性能有多菜,看来while循环体执行的次数对可见性存在影响。


  将线程A启动后主线程休眠时间缩短为10ms,测试下循环体走了多少次

        threadA.start();    
        Thread.sleep(10);
        threadB.start();

  执行结果显示执行了144万次


  将前面加的volatile去掉,再次执行,依然不存在可见性问题

    private int num = 0;

  将主线程休眠的时间改回100ms,又重现了可见性问题


  上面情况出现的原因是while循环体被执行达到一定次数后,会成为热点代码,触发Java的 JIT 机制,将热点代码从解释执行切换到编译执行时,JIT 复杂的优化机制可能还会对数据进行缓存,对指令进行重排序,在多线程中,可能会引发线程安全问题。

  从上面的实验,可以得出几个结论:

  • (1)对共享变量不做同步或使用volatile,执行时不一定会出现可见性问题;
  • (2)热点代码触发的优化可能导致线程安全问题;
  • (3)为了避免可见性问题,应该使用同步或 volatile ,让热点代码优化时不对数据做缓存(即在线程私有内存中备份数据),同步可以避免热点代码对应的指令重排序

《Java并发编程实战》中有这么一段话

  “在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行以下意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得出正确的结果。”

  从上面的实验可以看出当代码被执行达到一定次数,就会触发上面说的 意想不到的调整,这也就解释了,为什么一些在生产上久经考验的程序,在某一天发生了生产问题才发现存在线程安全问题。我们无法预知代码指令会被如何优化,也不知道什么时候会出现可见性问题,所以在编写并发程序时,要对关键地方做好足够的同步,特别是 while 循环、for 循环。


(2)加锁与可见性

  加锁除了有保证多个操作原子地被执行之外,还能确保某个线程以一种可预测的方式来查看另一个线程的执行结果,这里的锁除了使用 sychronized 代表的内置监视器锁,还可以是JDK API中的 Lock,下面演示使用锁解决上面的可见性问题:

  • 使用内置监视器锁
public class SychronizedVisibility {    
    private int num = 0;
        
    public synchronized int getNum() {
        return num;
    }

    public synchronized void setNum(int num) {
        this.num = num;
    }

    public void test1() {       
        System.out.println("num = " + num);             
        int count = 0;      
        while (getNum() == 0) {                 
            ++count;
        }           
        System.out.println("count = " + count);     
        System.out.println("num = " + num);
    }
    
    public static void main(String[] args) throws InterruptedException {
        SychronizedVisibility obj = new SychronizedVisibility();
        
        Thread threadA = new Thread() {
            public void run() {
                obj.test1();            
            }
        };
        
        Thread threadB = new Thread() {
            public void run() {
                obj.setNum(1);          
            }
        };
            
        threadA.start();
        Thread.sleep(100);
        threadB.start();
        threadA.join();
        threadB.join();
    }
}
  • 使用JDK API实现的锁
public class SychronizedVisibility {    
    private int num = 0;
    private Lock lock = new ReentrantLock();
        
    public int getNum() {
        try {
            lock.lock();
            return num;
        } finally {
            lock.unlock();
        }
    }

    public void setNum(int num) {
        lock.lock();
        this.num = num;
        lock.unlock();
    }

    public void test1() {       
        System.out.println("num = " + num);             
        int count = 0;      
        while (getNum() == 0) {                 
            ++count;
        }           
        System.out.println("count = " + count);     
        System.out.println("num = " + num);
    }
    
    public static void main(String[] args) throws InterruptedException {
        SychronizedVisibility obj = new SychronizedVisibility();
        
        Thread threadA = new Thread() {
            public void run() {
                obj.test1();            
            }
        };
        
        Thread threadB = new Thread() {
            public void run() {
                obj.setNum(1);          
            }
        };
            
        threadA.start();
        Thread.sleep(100);
        threadB.start();
        threadA.join();
        threadB.join();
    }
}

  所以之所以要求在访问某个共享且可变的变量时要求所有线程在同一个锁上同步,就是为了确保某个线程写入该变量的值对其他线程来说都是可见的,否则,如果一个线程在未持有正确锁的情况读取某个变量,那么读到的可能是一个失效值。

《Java并发编程实战》中有这么一段话

  “加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。”



(3)long和double非原子的64位操作

  当线程在没有同步的情况下读取变量,可能会读取到一个失效的值,可能是初始值,也可能是之前某个线程设置的值,但不是一个随机值,这种安全性保证叫最低安全性

  但是最低安全性不适用于64位的 longdouble 类型。虽然Java内存模型要求,变量的读取和写入操作都必须是原子操作,但是对于非volatile的long和double变量,JVM允许将64位的读写操作分解成为两个32位的操作。这就导致了有可能存在这样的竞态条件:读写线程并发,由于写线程不是一次性对64位写入的,所以读线程可能读到某个数据的高32位,读到另外一个数据的低32位,拼接起来读到的最终数据连最低安全性都无法保证。

  所以,在并发程序中,应保证:

  即使不考虑数据失效问题,在多线程程序中使用共享且可变的long和double等类型的变量也是不安全的,应该用 volatile 来声明它们,或者用锁保护起来。


7、volatile

  volatile 是Java提供的一种比锁稍弱的同步机制,volatile可以禁止编译运行时对声明了volatile的变量的操作与其他内存操作进行指令重排序,当一个变量被声明为volatile时,它就不会被缓存在寄存器或者对其他处理器不可见的地方,而是放在共享内存中,所有线程都能实时修改和读取变量的值。

  当我们使用 volatile 时,总是将目光聚焦在被声明了 volatile 的变量上,实际上 volatile 对可见性的影响有时也可以达到类似锁的效果,如下代码所示:

public class VolatileVisibility {
    private int num = 0;
    private boolean run = true;
    
    public static void main(String[] args) throws InterruptedException {
        VolatileVisibility obj = new VolatileVisibility();
        
        Thread threadA = new Thread() {
            public void run() {
                System.out.println("num = " + obj.num);                             
                while (obj.num == 0) {
                    if (obj.run) {}
                }               
                System.out.println("threadA: num = " + obj.num + ", run = " + obj.run);         
            }
        };      
        Thread threadB = new Thread() {
            public void run() {
                obj.num = 1;    
                obj.run = false;
                System.out.println("threadB: num = " + obj.num + ", run = " + obj.run);
            }
        };
            
        threadA.start();
        Thread.sleep(100);
        threadB.start();
        threadA.join();
        threadB.join();
    }   
}

  先重现下可见性问题,执行结果如下:


  现在将 run 成员声明加上 volatile

private volatile boolean run = true;

  重新执行下代码:


  原来 num 是有可见性问题的,也没有用 volatile 声明或用锁的手段同步来保证可见性,但是现在 run 对它的可见性产生了影响,因为线程B在写入volatile变量之前,修改了 num,当线程A读取到volatile变量之后,线程B对 num 的修改线程A也能看见了,总结一下:

  当线程A首先写入一个 volatile 变量并且线程B随后读取该变量时,在写入 volatile 变量之前对A可见的所有变量的值,在B读取了 volatile 变量后,对B也是可见的。

  从某种程度上看,写入 volatile 变量相当于退出同步代码块,而读取 volatile 相当于进入同步代码块,读写 volatile 之前执行的代码是同步代码块,实际上上面的例子中,就算线程B不去读写 volatile 变量,只要线程A在while循环中读取了 volatile 变量,依然能保证对 num 的可见性。

        Thread threadB = new Thread() {
            public void run() {
                obj.num = 1;    
            }
        };

  虽然 volatile 变量能对其他变量的可见性产生影响,但更多时候建议只用它来控制被声明的变量本身的可见性,因为对其他变量的可见性影响很脆弱,也比较难以理解,代码结构一变可能又不行了。

  volatile 只能保证可见性,而不能保证原子性,特别是 voaltile 变量被纳入某个不变形条件中,如下面的例子:

public class UnsafeVolatile {
    private volatile AtomicInteger a = new AtomicInteger(0);
    private volatile AtomicInteger b = new AtomicInteger(0);
    
    public void incr() {
        a.incrementAndGet();
        b.incrementAndGet();
    }
    
    public void print() {
        System.out.println("a = " + a.get() + ", b = " + b.get());
    }   
    
    public static void main(String[] args) {
        UnsafeVolatile program = new UnsafeVolatile();
        new Thread(new Runnable() {         
            @Override
            public void run() {
                while (true) {
                    program.incr();
                }
            }
        }).start();
        
        while (true) {
            program.print();
        }
    }   
}

  不变性条件是a、b相等,因为初始值一样,每次也都是执行加1操作,但是打印a、b的值时,a、b不一定一致。即使a、b是被声明为 volatile 的原子类,也不满足该不变性条件,非线程安全,执行结果如下:


  要满足该不变性条件,必须使用加锁机制同步,可以得出结论:

  加锁机制既确保可见性又可以确保原子性,而 volatile 变量只能确保可见性。

当且仅当满足以下所有条件时,才应用使用 volatile 变量:

  • 对变量的写入不依赖变量的当前值,或者保证只有单个线程更新变量的值。
  • 该变量不会与其他状态变量一起纳入不变性条件中。
  • 访问该变量时不需要加锁(加锁也就没有必要使用volatile了)。

  当使用 volatile 声明的变量是引用类型时,意味着不会缓存对象成员,相当于给对象成员也加了 volatile 声明。

如下面代码:

public class VolatileObjVisibility {
    public Bean bean = new Bean(true);
    
    public static void main(String[] args) throws InterruptedException {
        VolatileObjVisibility obj = new VolatileObjVisibility();
        
        Thread threadA = new Thread() {
            @Override           
            public void run() {
                System.out.println("threadA start, obj = " + obj);
                while (obj.bean.run) {}
                System.out.println("threadA end");
            }
        };
        
        Thread threadB = new Thread() {
            @Override           
            public void run() {
                obj.bean.run = false;
                System.out.println("threadB end, obj = " + obj);
            }
        };
        
        threadA.start();
        Thread.sleep(100);
        threadB.start();
        threadA.join();
        threadB.join();
    }
}

class Bean {
    public boolean run;
    
    public Bean(boolean run) {
        this.run = run;
    }
}

  在没用 volatile 声明bean变量之前,存在可见性问题,执行结果如下:


  加了声明之后

public volatile Bean bean = new Bean(true);

  线程A能看到 obj.bean.run 被线程B修改后的值,执行结果如下:


8、static

  一个类中定义的static成员,不管是属性还是方法,对所有类的实例化对象共享,在学习并发编程时,很多人都会有疑问,既然static是所有对象共享的,那它是不是对使用不同对象的不同线程共享?意味着可见性?

public class StaticVisibility {
    public static int num = 0;
    
    public static void main(String[] args) throws InterruptedException {
        StaticVisibility obj1 = new StaticVisibility();
        StaticVisibility obj2 = new StaticVisibility();
        
        Thread threadA = new Thread() {
            @Override           
            public void run() {
                while (obj1.num < 10) {}
                System.out.println("treadA: static num = " + obj1.num);
            }
        };
        
        Thread threadB = new Thread() {
            @Override           
            public void run() {
                while (obj2.num < 10) {
                    obj2.num++;
                    System.out.println("threadB: static num = " + obj2.num);
                }
            }
        };
        
        threadA.start();
        Thread.sleep(100);
        threadB.start();
        
        threadA.join();
        threadB.join();
    }
}

  从直接结果上看,声明为 static 的变量不能保证线程安全:


  需要将变量声明为 volatile 或使用同步访问才能保证可见性。

public static volatile int num = 0;

你可能感兴趣的:([Java多线程编程之九] 线程安全)