您的位置:首页 > 编程语言 > Java开发

java多线程基础总结【一】volatile关键字与原子性问题

2018-04-01 21:55 561 查看
最近看尚硅谷JUC视频,看的很过瘾,就怕两天后就忘了,于是做个总结。
I hear and I forget,I see and I remember,I do and I understand。
为什么使用多线程?为了尽可能的使用CPU资源,也就是系统资源来提高效率,但是如果我们多线程使用不当可能会使效率更低,因为多线程会加大系统开销,而线程与线程之间又涉及到系统资源的调度,上下文的切换以及线程的创建销毁等。所以我们应该合理的来使用多线程。
内存可见性:指当某个线程正在使用对象状态而另一个线程同时在修改该状态,需要确保当一个对象修改了对象状态后,其他线程能够看到发生的状态变化。
可见性错误:指当读操作与写操作在不同的线程中执行时,我们无法确保读操作的线程能适时的看到其他线程写入的值,有时甚至是不可能的事。
我们可以使用同步来使对象或变量被安全的发布,除此之外我们可以使用更加轻量级的volatile关键字。
先看下代码package com.buerc.thread;

public class TestVolatile {
public static void main(String[] args) {
MyThread mt=new MyThread();
new Thread(mt).start();//子线程启动会进行修改操作

while(true) {
if(mt.isFlag()) {//主线程执行到这里进行读操作
System.out.println("===========");break;
}
}
/*
* 此时打印flag=true
* 但是控制台并没有停止,说明while语句还在进行循环,却不满足if条件,很奇怪的现象
*/
}
}

class MyThread implements Runnable{
private boolean flag=false;
@Override
public void run() {
try {
Thread.sleep(200);//为了放大问题先睡200毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
flag=true;//修改操作
System.out.println("flag="+isFlag());
}
public boolean isFlag() {
return flag;
}
}
MyThread的flag虽然为子线程与主线程的共享数据,但是却由于内存不可见性(每个线程的创建会产生一个相应的缓存区域,然后会在缓存区域操作数据最后同步到主存),尽管子线程已经修改了(最后也同步的主存了),主线程却一直在自己缓存区域读取数据,从而导致子线程打印flag=true主线程却不执行打印语句。改进措施加synchronized,如下package com.buerc.thread;

public class TestVolatile {
public static void main(String[] args) {
MyThread mt=new MyThread();
new Thread(mt).start();//子线程启动会进行修改操作

while(true) {
synchronized(mt) {//加同步代码块,让主线程每次都去主存中读取数据,从而读取的是新的值
if(mt.isFlag()) {//主线程执行到这里进行读操作
System.out.println("===========");break;
}
}
}
/*
* 此时打印flag=true
* 但是控制台并没有停止,说明while语句还在进行循环,却不满足if条件,很奇怪的现象
*/
}
}

class MyThread implements Runnable{
private boolean flag=false;
@Override
public void run() {
try {
Thread.sleep(200);//为了放大问题先睡200毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
flag=true;//修改操作
System.out.println("flag="+isFlag());
}
public boolean isFlag() {
return flag;
}
}
这样加了synchronized关键字后,主线程每次读取数据就不会从自己缓存中读取数据了,而是每次都从主存中读取数据,从而会执行打印进而跳出循环。但是由于加了锁,所以会导致效率降低。所以我们这里使用volatile关键字。如下
package com.buerc.thread;

public class TestVolatile {
public static void main(String[] args) {
MyThread mt=new MyThread();
new Thread(mt).start();//子线程启动会进行修改操作

while(true) {
if(mt.isFlag()) {//主线程执行到这里进行读操作
System.out.println("===========");break;
}
}
/*
* 此时打印flag=true
* 但是控制台并没有停止,说明while语句还在进行循环,却不满足if条件,很奇怪的现象
*/
}
}

class MyThread implements Runnable{
private volatile boolean flag=false;//这里的共享数据使用了volatile关键字进行申明
@Override
public void run() {
try {
Thread.sleep(200);//为了放大问题先睡200毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
flag=true;//修改操作
System.out.println("flag="+isFlag());
}
public boolean isFlag() {
return flag;
}
}
我们比较一下:刚开始既没有synchronized也没有volatile关键字会由于内存可见性问题,出现奇怪的现象,当我们加了synchronized同步代码块之后,每次访问到的都是新数据,解决了这个问题,但由于synchronized会导致性能低下,所以我们去掉了synchronized同步,并将共享数据flag加以volatile修饰。这样做的好处是,解决了flag内存可见性问题,同时由于没有加锁,不会存在其他线程访问时会产生阻塞的问题,性能相比synchronized来说有所提高。
volatile关键字:一种稍弱的同步机制,用来确保将变量的更新操作通知到其他线程,可以将volatile看做是一种轻量级的锁,但是又与锁有所不同

对于多线程,不是一种互斥关系,即我访问不会导致你阻塞
不能保证变量状态的原子性操作。
虽然volatile是稍弱的同步机制,能保证共享数据的内存可见性。但是由于它不能保证原子性问题,因而有些情况下也会发生线程安全问题。例如下面的例子。
package com.buerc.thread;

public class TestAtomic {
public static void main(String[] args) {
Atomic atomic=new Atomic();
for (int i = 0; i < 10; i++) {
new Thread(atomic).start();
}
}
}

class Atomic implements Runnable{
private volatile int id=0;
public int getId() {
return id++;
}
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getId());
}
}
打印结果:0 1 1 2 3 4 5 6 8 7
按理来说应该不会出现重复打印的情况,但是结果却出现了。事实上i++操作分为三步--读改写,这三步操作是不可分割的,必须是一个整体(原子性操作)。因而由于volatile不能保证原子性操作,所以会出现打印重复的情况。改进如下:package com.buerc.thread;

import java.util.concurrent.atomic.AtomicInteger;

public class TestAtomic {
public static void main(String[] args) {
Atomic atomic=new Atomic();
for (int i = 0; i < 10; i++) {
new Thread(atomic).start();
}
}
}

class Atomic implements Runnable{
// private volatile int id=0;
private AtomicInteger id=new AtomicInteger(0);
public int getId() {
// return id++;
return id.getAndIncrement();
}
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(getId()+"\t");
}
}
这里申明换成了AtomicInteger。AtomicInteger底层使用了CAS算法来保证原子性问题。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息