volatile与synchronize

68 阅读6分钟

文章目录


前言

volatile与synchronized 都是java的关键字

  • volatile一般修饰变量,被修饰的变量会及时将计算值刷新回主内存;
  • synchronized 一般作用于 方法, 代码块等,一般分为 对象锁 和 类锁;

一、简介

volatile

  • 能够保证数据可见性
  • 禁止指令重排,在一定程度上保证了多线程情况下的执行顺序,从而保证结果执行正确
  • 不能完全保证多线程情况下的结果正确: 非原子性操作无法保证

Q: volatile

如何保证数据可见性?

1)将当前处理器缓存行的数据写回到系统内存。(会引起处理器缓存回写到内存)
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。(一个处理器的缓存回写到内存会导致其他处理器的缓存无效)

synchronized

  • 被synchronized 修饰后,大体分为两种,一种是对象锁,一种是类锁
  • synchronized 可以保证被修饰的部分,一个时间点只有一个线程执行
  • 在多线程情况下,相当于将多线程转为了串行执行,不能完全达到多线程的性能,但是可以通过控制锁的粒度,尽量减少锁的时间;
  • 对象锁,锁住的是实例对象,类锁,锁住的是字节码

二、名词解释

可见性

  • 当写一个 volatile 变量时,JMM 会把该线程本地内存中的变量强制刷新到主内存中去;这个写操作会导致其他线程中的 volatile 变量缓存无效,无效后也就是强制让其他线程再次读取主内存数据,达到了数据被修改后能被其他线程立马感知到,即可见性;

原子性

  • 没错,与事务中的原子性可以认为是等价的;

指令重排

  • jvm为了优化代码运行的一种手段,编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序

  • 举个例子 ABC 三步

    int a = 1; // A操作 int b = 2; // B操作 int sum = a + b;// C 操作 System.out.println(sum);

多线程情况下, 可能是ABC 也可能是 BAC,这种指令重排不会影响结果,所以是允许的;

临界区

  • 所谓“临界区”,指的是某一块代码区域,它同一时刻只能由一个线程执行。说人话就是: 被锁 锁住的代码部分;

对象锁

当synchronized 作用于普通方法,代码块,this 的时候,都是对象锁,每个对象实例一把锁,多个对象实例多个锁,多个锁之间没有影响, 那么多个对象实例之间的数据就不应该用对象锁,只适合单个对象;

类锁

当synchronized 作用于静态方法,this.getClass() 的时候,因为每个对象只有一份class,也就是唯一的,所以这种锁,无论你new 多少个对象实例,它们都公用一把锁

二、实战使用

1

Volatile可以解决的问题

public class Volatile可以解决的问题 {

    private boolean flag = true;
//    private volatile boolean flag = true;
    private int num = 0;

    public void write() {
        flag = false;
        System.out.println(Thread.currentThread().getName() + "开始执行.....");
    }

    public void read() {
        System.out.println(Thread.currentThread().getName() + "开始执行.....");
        while (flag) {
            num++;
        }
        System.out.println(Thread.currentThread().getName() + "结束.....");
        System.out.println("跳出循环" + num);
    }


    public static void main(String[] args) throws InterruptedException {

        Volatile可以解决的问题 handle = new Volatile可以解决的问题();
        new Thread(handle::read).start();
        Thread.sleep(2000);
        new Thread(handle::write).start();
    }
}

两个线程运行 第一个线程判断 当 flag 为ture ,就一直执行
第二个线程改变flag 的值,时期变为flase;
如果不使用volatile修饰,那么flag 虽然会被改变,但是由于没有强制刷新会主内存,那么其实第一个线程也感知不到
所以仍然会继续num++ ,不会停止;
解决问题的关键在于,volatile可以强制将线程内部的计算结果,刷新会主内存,并且使其他线程的值失效,不得不再次去主内存获取

出现问题的现象
就是如上代码,.不用volatile修饰的时候,就会导致即使改变了flag 的值,但是第一个线程仍然无法感知到,所以就一直运行,不会结束

上述代码中如果在while 循环中加入一个打印语句,例如打印num 数值,或者加入一句打印的话,都会导致非volatile修饰的也能正常退出,这是因为:

线程的flag是从其工作内存取,while语句调用太频繁意味着访问flag太频繁,导致主内存不会及时刷新工作内存的flag值(详细可了解JMM(Java memory Model)),所以一直会访问到flag为false,当加入System.out.println()后,访问flag就会有明显间隔,主内存就会有空去刷新到MyThread线程的工作内存,这样才能正常退出while循环

2

volatile无法解决非原子性操作问题–synchronized

@Data
public class 多线程问题 implements Runnable {

    public static int num = 0;
    private synchronized void add() {
        for (int i = 0; i < 500; i++) {
            num++;
        }
    }
//    public volatile static int num = 0;
//    private void add() {
//        for (int i = 0; i < 500; i++) {
//            num++;
//        }
//    }

    @Override
    public void run() {
        add();
    }

    public static void main(String[] args) {


        for (int i = 0; i < 10; i++) {
            多线程问题 pro = new 多线程问题();

            Thread thread = new Thread(pro);
            thread.setName("第一个线程");

            Thread thread1 = new Thread(pro);
            thread1.setName("第二个线程");

            thread1.start();
            thread.start();
            while (Thread.activeCount() > 1) {
                Thread.yield();
            }
            System.out.println(num);
        }
    }
}

volatile无法解决的问题

出现问题的现象是,计算结果无法保证,计算错误; i++ 非原子操作
所以需要用类似 synchronized 的锁,解决这类问题

总结

类似于 synchronized 的属于悲观锁,相对性的就是乐观锁;
乐观锁多用于“读多写少“的环境,避免频繁加锁影响性能;而悲观锁多用于”写多读少“的环境,避免频繁失败和重试影响性能。

synchronized的不足之处

  • 如果临界区是只读操作,其实可以多线程一起执行,但使用synchronized的话,同一时间只能有一个线程执行。
  • synchronized无法知道线程有没有成功获取到锁
  • 使用synchronized,如果临界区因为IO或者sleep方法等原因阻塞了,而当前线程又没有释放锁,就会导致所有线程等待。

因此就有了juc 的出现~~

本文转自 jimolvxing.blog.csdn.net/article/det…,如有侵权,请联系删除。