内存可见性
看下面的例子:
子线程中有一个成员变量flag,默认值为false,
子线程开启后,将flag的值改为true,
主线程中拿到flag的值,如果为true,即子线程成功修改flag,
则运行 System.out.println(“子线程修改了flag的值!”);
代码示例
子线程实现runnable接口
package csdn;
public class Mythread1 implements Runnable {
boolean flag;
@Override
public void run() {
this.flag = true;
}
public boolean getFlag() {
return flag;
}
}
主线程
package csdn;
public class Demo1 {
public static void main(String[] args) {
Mythread1 mythread1 = new Mythread1();
Thread thread = new Thread(mythread1);
thread.start();
while (true) {
if (mythread1.getFlag()) {
System.out.println("子线程修改了flag的值!");
}
}
}
}
进入死循环后,发现flag的值一直为false.
并没有成功修改.
出现的原因:
Java中的内存模型
Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,
线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。
线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。
不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
所以在上述代码中,虽然子线程在工作内存中将flag的值改了,但是并没有立即覆盖掉主内存的flag的值,具体什么时候覆盖,谁也不知道.所以出现了上述的内存可见性问题
普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,
当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
解决方法:
Java提供了volatile关键字来保证可见性
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,
并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
以下代码加了volatile关键字,保证了内存可见
package csdn;
public class Mythread1 implements Runnable {
volatile boolean flag;
@Override
public void run() {
this.flag = true;
}
public boolean getFlag() {
return flag;
}
}
package csdn;
public class Demo1 {
public static void main(String[] args) {
Mythread1 mythread1 = new Mythread1();
Thread thread = new Thread(mythread1);
thread.start();
while (true) {
if (mythread1.getFlag()) {
System.out.println("子线程修改了flag的值!");
}
}
}
}
volatile 这个关键字的特点,解决了内存可见性问题, 这个关键字没有互斥性
volatile 不能保证原子性
原子性
原子性:即不可在分割i++,i–这种的就不属于原子性操作
i++它可以分为,先将i的值赋给一个变量,再加一赋值给自己
案例,开启两个线程获取共享变量i的值
package com.westos.morning;
public class Demo {
public static void main(String[] args) {
//CAS博客
new MyThread().start();
new MyThread().start();
}
}
class MyThread extends Thread{
public static int i=0;
public MyThread() {
}
public int getI() {
return i++;
}
@Override
public void run() {
while (true){
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.getName()+" "+getI());
}
}
}
以上代码出现了原子性问题.
案例售票厅卖票,3个窗口,共享100张票
package com.westos.demo2;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class CellRunable implements Runnable {
static int piao = 100;
static Object obj = new Object();
static Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
//ReentrantLock 一个可重入的互斥锁 Lock,它具有与使用
//synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
//加锁
//lock.lock();
if (piao > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售 " + (piao--) + " 张票");
}
//出来之后,才会释放锁
//lock.unlock();
}
}
}
package com.westos.demo2;
/**
* @Author: Administrator
* @CreateTime: 2019-01-19 16:24
* @Description todo
*/
public class MyTest {
public static void main(String[] args) {
CellRunable cellRunable = new CellRunable();
Thread th1 = new Thread(cellRunable);
Thread th2 = new Thread(cellRunable);
Thread th3 = new Thread(cellRunable);
th1.setName("窗口1");
th2.setName("窗口2");
th3.setName("窗口3");
th1.start();
th2.start();
th3.start();
}
}
出现相同的数:由于原子性导致的,原子性:不可再分割 i++ 或 i-- 他不是一个原子性操作
出现负数票:由于线程的随机性所导致的
负票原因是,当减到为1张票时候,假如线程1进来了,这时候,线程2抢占cpu也进来了循环,此时线程1减完,线程2还要减.所以要顺利实现3个窗口共同卖票.必须是加锁,才能既实现没有重复票,也没有负数票.
出现了数据安全问题,
我们把有可能出现问题的代码用一个同步代码块包裹起来
解决方法 加锁
但是加锁是个重量级的,效率低,耗费底层资源
所以,如果把i++,i–,变为CAS算法,原子性操作,就不会出现上述的 重复值 问题
CAS算法
CAS:Compare and Swap, 翻译成比较并交换。硬件支持
java.util.concurrent包中借助CAS实现了区别于synchronouse同步锁的一种乐观锁。
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
java.util.concurrent.atomic这个包下提供了一些原子变量,可以实现CAS算法.
AtomicInteger()
创建一个新的AtomicInteger 0初始值。
AtomicInteger(int initialValue)
创建具有给定的初始值的一种新的AtomicInteger。
int getAndDecrement()
原子由一个电流值递减。
int getAndIncrement()
原子逐个增加电流值。
解决上述的获取i的值,出现重复值的修改代码----CAS算法
package com.westos.morning;
import java.util.concurrent.atomic.AtomicInteger;
public class Demo {
public static void main(String[] args) {
//CAS博客
new MyThread().start();
new MyThread().start();
}
}
class MyThread extends Thread{
//new 一个原子变量
public static AtomicInteger i=new AtomicInteger(1);
public MyThread() {
}
public int getI() {
return i.getAndIncrement();
}
@Override
public void run() {
while (true){
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.getName()+" "+getI());
}
}
}
但是CAS虽然能解决原子性问题,但它是个无锁机制,不能保证线程的随机性,所以,买票的代码如果将i–,换成原子操作,也不能完全解决问题,没有了重复票,但是还有负票,所以,这种问题必须加锁.
加锁有好几种,
法一,同步synchronized
法二,Lock类
怎么加锁
加锁改进代码:
package com.westos.demo2;
/**
* @Author: Administrator
* @CreateTime: 2019-01-19 16:24
* @Description todo
*/
public class MyTest {
public static void main(String[] args) {
CellRunable cellRunable = new CellRunable();
Thread th1 = new Thread(cellRunable);
Thread th2 = new Thread(cellRunable);
Thread th3 = new Thread(cellRunable);
th1.setName("窗口1");
th2.setName("窗口2");
th3.setName("窗口3");
th1.start();
th2.start();
th3.start();
}
}
CAS算法atomicInteger.getAndDecrement()替换i–操作
package com.westos.demo2;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class CellRunable implements Runnable {
//static int piao = 100;
static Object obj = new Object();
static Lock lock = new ReentrantLock();
static AtomicInteger atomicInteger = new AtomicInteger(100);
@Override
public void run() {
while (true) {
//ReentrantLock 一个可重入的互斥锁 Lock,它具有与使用
//synchronized 方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
//加锁
lock.lock();
if (atomicInteger.get() > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售 " + (atomicInteger.getAndDecrement()) + " 张票");
}
//出来之后,才会释放锁
lock.unlock();
}
}
}
加锁就可以既保证原子性问题----不会出现重复值,也保证了线程随机性----不会出现负值.
CAS弊端:
【1】CAS长时间自旋不成功,给CPU带来很大的性能开销。解决方法:JVM能支持pause指令,效率会有一定的提升。
【2】只能保证一个共享变量的原子操作。对多个共享变量操作时,不能保证原子性。
解决方法:加锁;共享变量合并成一个共享变量
【3】ABA的问题。解决方法就是:增加版本号,每次使用的时候版本号+1,每次变量更新的时候版本号+1。java提供atomicstampzedreference来解决ABA问题。
谢谢!