在Java并发编程中,如果要保证代码的安全性,则必须保证代码的原子性、可见性和有序性。
在 Java并发12:并发三特性-原子性、可见性和有序性概述及问题示例中,对并发中的三个特性(原子性、可见性和有序性)进行了初步学习。
本章主要就Java中保障可见性的技术进行更加全面的学习。
1.整体回顾
可见性定义:当一个线程修改了共享变量的值,其他线程能够看到修改的值。
2.可见性问题
场景说明:
存在两个线程A、线程B和一个共享变量stop。
如果stop变量的值是false,则线程A会一直运行。如果stop变量的值是true,则线程A会停止运行。
线程B能够将共享变量stop的值修改为ture。
定义普通的共享变量:
//普通情况下,多线程不能保证可见性 private static boolean stop;
下面的代码展示了在多线程环境中,对共享变量的共享:
//普通情况下,多线程不能保证可见性 new Thread(() -> { System.out.println("Ordinary A is running..."); while (!stop) ; System.out.println("Ordinary A is terminated."); }).start(); Thread.sleep(10); new Thread(() -> { System.out.println("Ordinary B is running..."); stop = true; System.out.println("Ordinary B is terminated."); }).start();
某次运行结果:
Ordinary A is running... Ordinary B is running... Ordinary B is terminated.
通过观察结果,发现程序确实存在可见性问题。
3.可见性技术保障
在Java中提供了多种可见性保障措施,这里主要涉及四种:
通过volatile关键字标记内存屏障保证可见性。
通过synchronized关键字定义同步代码块或者同步方法保障可见性。
通过Lock接口保障可见性。
通过Atomic类型保障可见性。
3.1.volatile关键字
使用volatile关键字修饰共享变量stop:
//使用volatile能够保证可见性 private volatile static boolean vStop;
在多线程环境中,对volatile修饰的共享变量stop进行共享测试:
//通过volatile关键字保证可见性 new Thread(() -> { System.out.println("Volatile A is running..."); while (!vStop) ; System.out.println("Volatile A is terminated."); }).start(); new Thread(() -> { System.out.println("Volatile B is running..."); vStop = true; System.out.println("Volatile B is terminated."); }).start();
运行结果(多次):
Volatile A is running... Volatile B is running... Volatile B is terminated. Volatile A is terminated.
通过多次运行,发现运行结果一致,所以可以确定volatile关键字能够保证代码的可见性。
定义一个共享对象用于synchronized关键字进行同步加锁:
//通过synchronized同步代码块保证可见性 private static byte[] obj = new byte[0];
在多线程环境中进行对obj进行加锁、等待和唤醒:
//通过synchronized同步代码块保证可见性 new Thread(() -> { System.out.println("Synchronized A is running..."); synchronized (obj) { try { obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } while (!stop) ; } System.out.println("Synchronized A is terminated."); }).start(); Thread.sleep(10); new Thread(() -> { System.out.println("Synchronized B is running..."); synchronized (obj) { stop = true; obj.notify(); } System.out.println("Synchronized B is terminated."); }).start();
运行结果(多次):
Synchronized A is running... Synchronized B is running... Synchronized B is terminated. Synchronized A is terminated.
通过多次运行,发现运行结果一致,所以可以确定synchronized关键字能够保证代码的可见性。
定义Lock接口和Condition接口:
//通过Lock接口保证可见性 private static ReentrantLock lock = new ReentrantLock(true); private static Condition condition = lock.newCondition();
在多线程环境中进行Lock加锁、condition等待和唤醒:
//通过Lock同步代码块保证可见性 new Thread(() -> { System.out.println("Lock A is running..."); lock.lock(); try { condition.await(); while (!stop) ; } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } System.out.println("Lock A is terminated."); }).start(); new Thread(() -> { System.out.println("Lock B is running..."); lock.lock(); try { stop = true; condition.signal(); } finally { lock.unlock(); } System.out.println("Lock B is terminated."); }).start();
运行结果(多次):
Lock A is running... Lock B is running... Lock B is terminated. Lock A is terminated.
通过多次运行,发现运行结果一致,所以可以确定Lock接口能够保证代码的可见性。
使用AtomicBoolean定义共享变量stop:
//通过Atomic保证可见性 private static AtomicBoolean aStop = new AtomicBoolean(false);
在多线程环境中,对使用AtomicBoolean定义的共享变量stop进行共享测试:
//通过Atomic保证可见性 new Thread(() -> { System.out.println("Atomic A is running..."); while (!aStop.get()) ; System.out.println("Atomic A is terminated."); }).start(); new Thread(() -> { System.out.println("Atomic B is running..."); aStop.set(true); System.out.println("Atomic B is terminated."); }).start();
运行结果(多次):
Atomic A is running... Atomic B is running... Atomic B is terminated. Atomic A is terminated.
通过多次运行,发现运行结果一致,所以可以确定Atomic类型能够保证代码的可见性。
经验证,以下四种措施,可以保证Java代码在运行时的可见性:
并发三特性总结
特性 | volatile关键字 | synchronized关键字 | Lock接口 | Atomic变量 |
原子性 | 无法保障 | 可以保障 | 可以保障 | 可以保障 |
可见性 | 可以保障 | 可以保障 | 可以保障 | 可以保障 |
有序性 | 一定程度保障 | 可以保障 | 可以保障 | 无法保障 |