Volatile关键字
- 前言
- 前置知识
- 程序、进程、线程
- 程序
- 进程
- 线程
- 并发所涉及的一些特性
- 线程安全
- 原子性
- 可见性
- Volatile
- 案例
- 环境
- 代码展示
- 可见性测试
- 原子性测试
前言
最近在看《Java并发编程实战》,期望对一些并发的知识点做一些总结。最好有一定的Java基础、并发的基础。
前置知识
程序、进程、线程
程序
程序是静态的代码块。
进程
进程是动态的,是程序运行之后的产物,同时进程是资源分配的最小单元。
线程
线程是对进程进一步细化,因为CPU的数量限制了进程的数目,为了适应更大的吞吐量,线程就产生了。同属于一个进程的线程共享该进程的资源,是操作系统进行调度的最小单元。
并发所涉及的一些特性
线程安全
多个线程同时访问某个资源的时候,不需要额外同步,都能保证结果的正确性。
原子性
代码块运行期间不会受到外部的影响,即该代码块是不可分割的最小的操作单元。
可见性
某个线程修改了共享的数据单元之后,能够立即通知到其他线程。
Volatile
相比synchronized(重量级同步),Volatile是轻量级同步锁,方便了并发编程,常用在信号量来控制程序执行逻辑,但是过度依赖volatile变量会使得使用锁的代码更脆弱,难以理解。
相比于synchronized,Volatile读取的时候不会加锁,所以不能保证线程安全。
Volatile能够保证可见性,无法保证原子性,参见下面的例子。
案例
环境
maven3.X、JDK17
代码展示
可见性测试
没有volatile,这时候如果自增发生在while循环取得num之后,就会导致死循环。
// 可能死循环
static int num1 = 1;
/**
* 可能死循环
*/
@Test
public void atomTest1() {
new Thread (VolatileTest::addNumByOne1).start ( );
while (num1 < 2) {
}
System.out.println (num1);
}
static void addNumByOne1() {
try {
TimeUnit.SECONDS.sleep (1);
} catch (InterruptedException e) {
throw new RuntimeException (e);
}
num1++;
}
通过Volatile保证了无论何时在其他线程修改num之后,主线程能够正确的读取修改之后的num值,以跳出循环
// 可见性,一旦修改 就通知
static volatile int num2 = 1;
/**
* 可见性体现
*/
@Test
public void atomTest2() {
new Thread (VolatileTest::addNumByOne2).start ( );
while (num2 < 2) {
}
System.out.println (num2);
}
static void addNumByOne2() {
try {
TimeUnit.SECONDS.sleep (1);
} catch (InterruptedException e) {
throw new RuntimeException (e);
}
num2++;
}
原子性测试
这里很容易以为最好打印出的结果是20000,因为volatile保证了可见性,那么每个线程都能获取到其他县城修改之后的num,进行+1。但是结果大多数都是小于20000的。
// 原子性
static volatile int num3 = 1;
/**
* 不能保证原子性
*/
@Test
public void atomTest3() throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool (10);
for (int i = 0; i < 10; i++) {
pool.submit (new Thread (() -> {
for (int j = 0; j < 2000; j++) {
addNumByOne3();
}
},"t" + i));
}
pool.shutdown ();
pool.awaitTermination (1,TimeUnit.SECONDS);
System.out.println (num3);
}
static void addNumByOne3() {
num3++;
}
运行结果:
产生这个结果的原因就是,当某个线程获取到了num3,准备自增的时候,别的线程也读取到了这个num3, 也发生了自增,最后写入内存,那么本来应该增加了2,现在可能只增加了1。具体点说当前num3更新到了3,这时候t1读取到了num3准备更新的时候并不会阻止t2来读取num3,这时候两个num3处于相同的数值3,之后t1,t2各自执行num3+1,写回内存,那么自然最后更新的结果就出现了问题了。