【Java学习】解决内存可见性问题和CAS算法解决原子性问题(25)

内存可见性

看下面的例子:
子线程中有一个成员变量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问题。


谢谢!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值