JUC之volatile关键字详解
程序员文章站
2022-07-13 14:55:03
...
一、JUC简介
- 在Java5.0提供了java.util.concurrent(简称JUC包),在此包中增加了在并发编程中很常用的工具类,在用于定义类似于线程的自定义子系统,包括线程池,异步IO和轻量级任务框架;还提供了设计用于多线程上下文中的Collection实现等。
二、volatile关键字
- volatile关键字:当多个线程进行共享数据时,可以保证内存中的数据时可见的;相比较于syschronized是一种较为轻量级的同步策略。
- volatile不具备“互斥性”。
- volatile不能保证变量的“原子性”。
package com.itszt;
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
System.out.println("开启子线程");
myThread.start();
System.out.println("子线程开启完毕");
Thread.sleep(2000);
System.out.println("在主线程中终止子线程");
myThread.setCanRun(false);
System.out.println("main里面的----"+myThread.canRun);
}
public static class MyThread extends Thread{
private boolean canRun = true;
public boolean isCanRun() {
return canRun;
}
public void setCanRun(boolean canRun) {
this.canRun = canRun;
}
@Override
public void run() {
super.run();
while (canRun) {
}
System.out.println("子线程里面的:"+canRun);
System.out.println("子线程终止。。。。");
}
}
}
运行结果:
上面例子中,我们的main方法的主线程与子线程MyThread同时对变量canRun变量进行操作,canRun就是多线程的共享数据,通过运行结果可以看出,主线程更改了canRun的值为false,按理说根据循环条件,子线程应该会终止,但是结果并没有终止,而是仍然处在死循环中,可见子线程中的canRun并没有被更改,仍然是true。
为什么会出现这样的问题呢?我们明明已经更改了变量的值啊?下面,我们来看一下这个变量的值是如何的一个存在,和两个线程是如何取值的。
可以看出,多个线程在共享这个数据时,每个线程会自己备份这个数据,并单独进行对数据的操作,这样,线程之间对应这个共享数据时不可见的。
- 解决办法一:
当我们用关键字volatile修饰canRun后:
package com.itszt;
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
System.out.println("开启子线程");
myThread.start();
System.out.println("子线程开启完毕");
Thread.sleep(2000);
System.out.println("在主线程中终止子线程");
myThread.setCanRun(false);
System.out.println("main里面的----"+myThread.canRun);
}
public static class MyThread extends Thread{
private volatile boolean canRun = true;
public boolean isCanRun() {
return canRun;
}
public void setCanRun(boolean canRun) {
this.canRun = canRun;
}
@Override
public void run() {
super.run();
while (canRun) {
}
System.out.println("子线程里面的:"+canRun);
System.out.println("子线程终止。。。。");
}
}
}
运行结果:
- 解决办法二:
使用同步锁sychronized
package com.itszt;
public class Test {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
System.out.println("开启子线程");
myThread.start();
System.out.println("子线程开启完毕");
Thread.sleep(2000);
System.out.println("在主线程中终止子线程");
myThread.setCanRun(false);
System.out.println("main里面的----"+myThread.canRun);
}
public static class MyThread extends Thread{
private boolean canRun = true;
public boolean isCanRun() {
return canRun;
}
public void setCanRun(boolean canRun) {
this.canRun = canRun;
}
@Override
public void run() {
super.run();
while (true){
synchronized (Test.class){
if (!canRun){
break;
}
}
}
System.out.println("子线程里面的:"+canRun);
System.out.println("子线程终止。。。。");
}
}
}
运行结果:
三、i++原子性问题
- i++实际分为三步:读取--->修改--->写入
- 原子性:“读-改-写”三步为一体的,是不能被拆分的
- 原子变量:JDK5以后,java.util.concurrent.atomic包下,提供了常用的原子变量
- 原子变量的值用volatile修饰,确保变量内存可见性
- CAS(Compare-And-Swap)算法保证数据的原子性
package com.itszt;
/**
*
*
*
* 关键字:volatile:保证该数据是被所有子线程共享的
*
* 与static的区别:所有的对象共享同一个数据(volatile是作用于多线程的,static是作用于多对象的)
*
*
*/
public class Test1 {
private static volatile int i=0;
private static long timeBegin;
public static void main(String[] args){
timeBegin = System.currentTimeMillis();
for (int a = 0; a < 50; a++) {
new MyThread().start();
}
}
private static class MyThread extends Thread{
@Override
public void run() {
super.run();
for (int b = 0; b < 10000; b++) {
i++;
}
System.out.println("最后的值: "+i);
if (i==500000) {
System.out.println("结束的时间为:"+(System.currentTimeMillis()-timeBegin));
}
}
}
}
运行结果:
可以看出,使用volatile关键字修饰的i,经过50个线程处理,并没有自加到500000,出现这种情况的原因正是因为volatile不能保证操作的原子性,当一个线程读取到当前的i值,在进行读-改-写操作的过程中,已有其他线程更改过了i的值,这样导致更改数据的不一致性。
当我们用原子变量修饰共享数据,就能很好的保证原子性。
package com.itszt;
import java.util.concurrent.atomic.AtomicInteger;
/**
*
*
* i++ 的原子性 读-改-写
*
* 锁:
*
* 悲观锁:
*
* 我们使用的时候别人是不能用的,只有当我们使用完释放了资源,别人才能用
* synchronized、lock:可以解决原子性问题,但是运行效率都不高
*
* 乐观锁:
*
* 多个线程可以对同一个数据同时操作,或者同一段代码进行操作,只不过操作结果是否生效就不一定了。
*
* JUC是如何帮我们解决这个问题的呢?
*
* JUC基于CAS算法来做
*
* 多个线程对同一个数据进行操作时会留一个版本号,每个线程每操作一次,就会更新版本号,将版本号自增1
* 比如:A操作之前的版本号为0,那么当A操作完之后,A将更改这个版本号为1
* 加入A操作完成之后,在A需要更新版本之前,会检查当前的版本号,发现版本号发生变化不在是0的时候,
* 或者已经为1,那么肯定有另外的线程,比如B对该数据进行了更改,并且生成了新的版本号,此时A就需要重新执行操作了(读改写)
*
*
*
*/
public class Test2 {
//使用原子变量进行改进
private static volatile AtomicInteger i = new AtomicInteger(0);
//声明一个时间戳
private static long timeBegin;
public static void main(String[] args){
timeBegin = System.currentTimeMillis();
//模拟多线程去操作共享数据i
for (int b = 0; b < 50; b++) {
new MyThread().start();
}
}
//定义个线程
public static class MyThread extends Thread{
@Override
public void run() {
super.run();
//循环执行i++
for (int a = 0; a < 10000; a++) {
i.incrementAndGet();//等同于i++
}
System.out.println("输出i的值:"+i);
//计算执行完毕的时间
if (i.get()==500000) {
System.out.println("完成时间: "+(System.currentTimeMillis()-timeBegin));
}
}
}
}
运行结果:解决volatile关键字存在不能保证原子性的不足,另一种解决方案是使用同步锁synchronized,但是执行效率远远不及乐观锁的执行效率
package com.itszt;
/**
*
*
*
* 关键字:volatile:保证该数据是被所有子线程共享的
*
* 与static的区别:所有的对象共享同一个数据(volatile是作用于多线程的,static是作用于多对象的)
*
*
*/
public class Test1 {
private static volatile int i=0;
private static long timeBegin;
public static void main(String[] args){
timeBegin = System.currentTimeMillis();
for (int a = 0; a < 50; a++) {
new MyThread().start();
}
}
private static class MyThread extends Thread{
@Override
public void run() {
super.run();
for (int b = 0; b < 10000; b++) {
//使用同步锁
synchronized (Test1.class){
i++;
}
}
System.out.println("最后的值: "+i);
if (i==500000) {
System.out.println("结束的时间为:"+(System.currentTimeMillis()-timeBegin));
}
}
}
}
运行结果:
四、CAS算法
- CAS(Compare-And-Swap)算法是硬件对于并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问;
- CAS是一种无锁的非阻塞算法实现
-
CAS包含三个操作数:
- 需要读写的内存值:V
- 进行比较的预估值:A
- 拟写入的更新值:B
- 当且仅当V==A时,V=B,否则,将重新操作。
上一篇: volatile关键字
下一篇: WAS常见问题