线程安全问题
线程安全问题
本篇主要讲解 线程安全问题,演示什么情况下会出现线程安全问题,以及介绍了 Java内存模型 、volatile关键字 、CAS 等 ,最后感谢吴恒同学的投稿! 一起来了解吧!!
1. 如何会发生线程安全
运行如下程序:
/**
* @program:
* @description: 多线程操作的对象
* @author:
* @create:
**/
public class MyCount {
private int myCount = 0 ;
public int getMyCount() {
return myCount;
}
public void setMyCount(int myCount) {
this.myCount = myCount;
}
@Override
public String toString() {
return "MyCount{" +
"myCount=" + myCount +
'}';
}
}
创建线程
public class CountThread1 extends Thread{
private MyCount myCount ;
private static Object synch = new Object();
public CountThread1( MyCount myCount) {
this.myCount = myCount;
}
@Override
public void run() {
//myCount 加到100
while (true) {
if(myCount.getMyCount()<100) {
myCount.setMyCount(myCount.getMyCount() + 1);
System.out.println(Thread.currentThread().getName() + " set myCount值:" + myCount.getMyCount());
}else{
break;
}
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行下列线程
public static void main(String[] args) {
MyCount myCount = new MyCount();
CountThread1 c1 = new CountThread1(myCount);
CountThread1 c2 = new CountThread1(myCount);
CountThread1 c3 = new CountThread1(myCount);
c1.setName("c1");
c2.setName("c2");
c3.setName("c3");
c1.start();
c2.start();
c3.start();
}
测试结果:
以上是多线程同时对同一变量进行操作时,发生的非线程安全问题。换句话说只用共享资源的读写访问才需要同步化,如果不是共享资源,那么根本没有同步的必要。
2.线程内存模型
2.1 线程内存模型如下:
某些JVM运行中,有两块主要的内存,一个是主内存,另外一个是每个线程都具有的工作内存。
2.2 线程运行的流程如下:
1. 从主内存中copy要操作的数据到自己的工作内存中去。
2. 线程主体从自己的工作内存中读取数据进行操作。
3. 操作完成后,在同步到主内存中去。
结合线程运行的流程,上述多线程可能会出现以下执行流程:
1. c3线程获得cpu资源,执行+1操作,在c3想同步count值1到主内存中去时。
2. c2线程得到了cpu的资源,也同样执行+1操作,但没+1前count的值是0,而不是1,c2执行完后,打印count=1的值,并且把数据同步到主内存中。
3. 此时c3又得到了cpu的资源,于所执行刚才没有完成的同步操作,同时又打印count=1的值。
这就导致出现上图结果的原因。
解决上述问题最常见的方法就是在线程的run方法上添加synchronized关键字
while (true) {
synchronized (synch) {
if(myCount.getMyCount()<100) {
myCount.setMyCount(myCount.getMyCount() + 1);
System.out.println(Thread.currentThread().getName() + " set myCount值:" + myCount.getMyCount());
}else{
break;
}
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3.volatile关键字
如下线程:
public class CountThread2 extends Thread{
private boolean isRunning = true;
@Override
public void run() {
System.out.println("进入到run方法了");
while (isRunning) {
}
System.out.println("run方法结束");
}
public void setNotRunning() {
System.out.println("isRunning为false");
isRunning = false;
}
}
执行如下方法
CountThread2 ct2 = new CountThread2();
ct2.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ct2.setNotRunning();
结果如下:
明明isRunning被设置为false,为什么线程还没有结束?
还要把这个图拿过来
当主线程把isRunning设置为false,并同步到主线程后,当 ct2 线程执行while(isRunning)时,isRunning的值是从工作内存中获取的,也就是多线程同时操作同一属性时,当主内存中变量被修改时,其它线程并没有感知到该变量被修改。
代码修改
//变量添加 volatile关键字
private volatile boolean isRunning = true;
执行结果如下:
发现当isRunning被修改为false时,ct2线程就结束了。这是为什么了?
当线程修改volatile变量后,会立刻同步到主内存中,同时迫使在使用该变量的其它线程去主内存中同步该变量到各自的工作内存中。但遗憾的是volatile不具有原子性
4.原子性和可见性
4.1 线程安全包含原子性和可见性两个方面, Java的同步就在都是围绕这两个方面来确保线程安全的
4.1.1 什么是可见性?可见性就是volatile修饰变量表现出来的性质,使变量在多个线程中可见。
4.1.2 什么是原子性操作?原子性操作就是不可分的操作,int i = a;就是原子性操作,而上述count++就不是原子性操作
上图是对线程内存模型进一步的描述,一线程在执行use操作时,突然时去了cpu执行权限(cpu执行任何原子性操作时,是不可能出现中断的),也就出现上述非线程安全的问题了。
5.synchronized
多线程在访问synchronized同步区域时,如果一线程获取到同步锁,其它线程就会被阻塞,就会形成线程安全的机制。
既然线程安全包含原子性和可见性 ,synchronized具有线程安全的功能,那么synchronized具有原子性和可见性?
这种等比性思想对吗?为什么synchronized具有原子性和可见性
方法一:
private int i= 1;
synchronized public void run() {
System.out.println("i++:"+i);
}
方法二:
private volatile int i= 1;
public void run() {
System.out.println("i++:"+i);
}
当多线程访问方法一,多个线程依次访问方法二,我想两种类型都是线程安全,且结果一致的。那你对synchronized又有什么新的想法了?synchronized = volatile+非当前线程阻塞
6.悲观锁 VS 乐观锁
修改MyCount代码
public class MyCount {
private AtomicInteger myCount = new AtomicInteger(0);
public int getMyCount() {
return myCount.get();
}
public int setMyCount() {
return myCount.incrementAndGet();
}
}
继续执行如下操作
MyCount myCount = new MyCount();
CountThread1 c1 = new CountThread1(myCount);
CountThread1 c2 = new CountThread1(myCount);
CountThread1 c3 = new CountThread1(myCount);
c1.setName("c1");
c2.setName("c2");
c3.setName("c3");
c1.start();
c2.start();
c3.start(
你会发现线程是安全的,这又是为什么了?这种线程安全的原因和synchronized又有什么联系和区别?
synchronized是一种悲观锁机制,有一线程获取锁后,其它线程就被阻塞,通过这种方法可以到达线程全的效果。多线程竞争的情况下会出现阻塞和唤醒的性能问题。
CAS compare and swap ,先比较然后交换
上述代码 myCount.incrementAndGet();源码如下
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
getAndAddInt(this, valueOffset, 1) + 1;源码如下
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
现描述:getAndAddInt()方法中个参数的意思
①: var5 = this.getIntVolatile(var1, var2);
var1 : myCount值在内存中的首地址
var2:myCount值在内存中的偏移量
var5 = this.getIntVolatile(var1, var2); 也就是获取当前myCount的值
②:!this.compareAndSwapInt(var1, var2, var5, var5 + var4)
var1,和 var2 获取内存中myCount最新的值,
var4: var4就是1,
var5是 this.getIntVolatile(var1, var2)获取的旧值
var5 + var4是myCount将要得到的值
判断这两个值是否相等,如果不相等,就表明myCount的值被其它线程操作,myCount值不是最新的,需要从新获取,也就是从新执行 ① 和②,直到值新旧值相等时,表明没有其它线程在操作此变量,然后
就把var5 + var4 赋值给 var5,从而达到线程安全。
上述的流程,就是CAS想要表达的思想,多线程访问同一临界区域时,都认为没有上锁,通过先比较来确认是否发生冲突,直到没有冲突时执行交换操作。这也同时解决了synchronized 多线程竞争的情况下会出现阻塞和唤醒的性能问题。
备注:以上线程内存模型相关图片来自于高洪岩《Java 多线程编程核心技术》
7.总结
本篇主要介绍了 线程安全问题,Java内存模型,volatile关键字 synchronized关键字 CAS 等
最后 感谢吴恒同学的投稿 !!!
个人博客地址: https://www.askajohnny.com 欢迎访问!
本文由博客一文多发平台 OpenWrite 发布!