volatile 关键字理解一(保证可见性)

一、概述

        volatile是Java中的关键字,用来修饰会被不同线程访问和修改的变量。

        volatile是Java虚拟机提供的轻量级的同步机制,它有三个特性:

        (1)保证可见性

        (2)不保证原子性

        (3)禁止指令重排

二、特性详解

        volatile保证可见性

        Java内存模型(JMM)定义了一组规则、规范,规定了程序中各个变量的访问方法。JMM关于同步的规定:

        (1)线程解锁前,必须把共享变量的值刷新回主内存;

        (2)线程加锁前,必须读取主内存的最新值同步到自己的工作内存;

        (3)加锁解锁必须是同一把锁;

        说明:由于JVM运行程序的实体是线程,创建每个线程时,JMM会为其创建一个工作内存(也称栈空间),工作内存是每个线程的私有数据区域。

        Java内存模型规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问。

        但是线程对变量的操作(读取、赋值等)必须在工作内存中进行。因此首先要将变量从主内存拷贝到自己的工作内存,然后对变量进行操作,操作完成后再将变量写会主内存中。

        举例说明:

        (1)火车票卖票系统还剩下一张票,并已经刷入到主内存中:ticketNum = 1;

        (2)此时有3个用户在同时购买票,3个线程都读入了目前的票数,ticketNum=1,那么线程就会继续进入购买流程。

        (3)假设其中一个线程先抢占了CPU资源,先买到票,并将自己的工作内存中的ticketNum值改为0,ticketNum=0,然后再写回到主内存。

        这时,由于一个线程的用户已经买到了票,那么其他用户的线程应该不能再继续进入购买票的流程了,因此需要系统通知到其他线程 ticketNum=0 这个消息。如果可以达到这样的效果,可以理解为 具有可见性

        无可见性代码演示:

@Test
public void test1() {
    DataDemo dataDemo = new DataDemo();
    RunThread runThread = new RunThread(dataDemo);
    runThread.start();
    while (dataDemo.getNumber() == 0) {

    }
    System.out.println("具有可见性验证通过");
}

public class DataDemo {

    private int number = 0;

    public void add() {
        this.number = this.number + 10;
    }

    public int getNumber() {
        return number;
    }
}

public class RunThread extends Thread {

    private DataDemo dataDemo;

    public RunThread(DataDemo dataDemo) {
        this.dataDemo = dataDemo;
    }

    @Override
    public void run() {
        System.out.println("线程[" + Thread.currentThread().getName() + "] 正在执行");
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        dataDemo.add();
        System.out.println("线程[" + Thread.currentThread().getName() + "]更新后,number值为:" + dataDemo.getNumber());
    }
}


执行结果:
线程[Thread-0] 正在执行
线程[Thread-0]更新后,number值为:10

                结果分析:

        可以看出子线程启动后将number值改为了10,虽然已经改为了非0,但是主线程仍然一直处于while循环中,因此此时number不具有可见性,系统不会主动通知主线程number值修改。

        原理说明:

        这个问题其实就是私有堆栈中的值和公共堆栈中的值不同步造成的。解决这样的问题就要使用 volatile 关键字了,它主要的作用就是当线程访问number这个变量时,强制性从公共堆栈中进行取值。

                                        

        

        可见性代码演示:

@Test
public void test1() {
    DataDemo dataDemo = new DataDemo();
    RunThread runThread = new RunThread(dataDemo);
    runThread.start();
    while (dataDemo.getNumber() == 0) {

    }
    System.out.println("具有可见性验证通过");
}

public class DataDemo {

    // 给变量 number 添加 volatile 关键字修饰
    volatile private int number = 0;

    public void add() {
        this.number = this.number + 10;
    }

    public int getNumber() {
        return number;
    }
}

public class RunThread extends Thread {

    private DataDemo dataDemo;

    public RunThread(DataDemo dataDemo) {
        this.dataDemo = dataDemo;
    }

    @Override
    public void run() {
        System.out.println("线程[" + Thread.currentThread().getName() + "] 正在执行");
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        dataDemo.add();
        System.out.println("线程[" + Thread.currentThread().getName() + "]更新后,number值为:" + dataDemo.getNumber());
    }
}

执行结果:
线程[Thread-0] 正在执行
线程[Thread-0]更新后,number值为:10
具有可见性验证通过

        结果分析:

        通过对变量number变量添加了volatile关键字修饰,可以看出子线程启动后将number值改为了10,随后主线程跳出了while循环,输出了“具有可见性验证通过”,说明此时number具有可见性。

        原理说明:

        通过使用 volatile 关键字,强制从公共内存中读取变量的值,内存结构如图:

                

        由于本人水平有限,本博客可能存在不足和错误在所难免,还请大家多多指正。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
-多个线程的几种实现方式包括:承Thread类,实现Runnable接口,实Callable接口,使用线程池。 - Java中的线程池是通过ThreadPoolExecutor类实现的。线程池维护了一个线程队列,可以复用线程,减少线程的创建和销毁开销,提高了能。 - 不建议直接使用Executors工具类创建线程池是因为它使用的是默认的线程池配置,可能导致线程数量过多,耗尽系统资源。OOM(Out of Memory)是由于创建过多的线程导致内存不足而发生的错误。 - Java内存模型(JMM)是一种规范,定义了多线程程序中各个变量的访问方式。它包括主内存和工作内存,通过控制变量的可见和原子保证线程间的通信与同步。 - 并发编程可能会发生的问题包括:竞态条件、死锁、活锁、饥饿等。可见问题指一个线程对共享变量的修改对其他线程是否可见,原子问题指一个操作是否可以被中断或者同时执行。 - 并发编程下会出现原子问题是因为多个线程同时修改同一个共享变量时,可能会导致不一致的结果。有序问题是指程序执行的顺序与预期不符。可以使用synchronized关键字、Lock锁等来解决原子和有序问题。加上volatile关键字可以保证可见,禁止指令重排序。 - 内存屏障是通过编译器和处理器来实现的,用于控制指令的执行顺序和内存的可见。synchronized关键字会在进入和退出临界区时加上内存屏障。 - 单线程指令重排在不影响单线程执行结果的前提下进行优化,但可能会影响多线程的正确。双重校验锁中使用volatile是为了禁止指令重排,确保多线程环境下的正确。 - InnoDB的索引是通过B+树实现的。B+树具有树高度低、查询效率高、支持范围查询等优势。 - 聚簇索引与非聚簇索引的区别在于数据的存储方式。聚簇索引将数据行存储在叶子节点中,非聚簇索引则将叶子节点指向数据行。不是所有情况都需要取回表的数据,可以通过覆盖索引来避免回表操作。 - 最左前缀匹配指在使用联合索引时,只有从左到右使用索引的前缀部分才能发挥索引的作用。将区分度高的字段放在最左边可以提高索引的效率。唯一索引与普通索引的区别在于是否允许重复值。 - 排查慢SQL可以通过查看慢查询日志、使用能分析工具(如EXPLAIN、SHOW PROFILE)、优化查询语句等方法。 - MySQL的锁包括行锁和表锁。行锁在并发能上更好,但需要更多的系统资源,适合处理并发访问较高的场景。表锁在资源消耗上较少,但并发能相对较差,适合处理并发访问较低的场景。 - FOR UPDATE语句会对查询到的行加上行锁。 - 悲观锁是指在操作数据时始终假设会发生并发冲突,因此会将数据加锁以阻止其他事务的访问。乐观锁是指不加锁,而是通过版本号或时间戳等机制来判断是否发生冲突,减少了加锁的开销。悲观锁适用于并发冲突较多的场景,乐观锁适用于并发冲突较少的场景。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值