多线程安全2

重要概念:

  1. 共享变量:
    共享变量是指可以同时被多个线程访问的变量,共享变量是被存放在堆里面,所有的方法内临时变量都不是共享变量
  2. 重排序:
    重排序是指为了提高指令运行的性能,在编译时或者运行时对指令执行顺序进行调整的机制。重排序分为编译重排序和运行时重排序。编译重排序是指编译器在编译源代码的时候就对代码执行顺序进行分析,在遵循as-if-serial的原则前提下对源码的执行顺序进行调整。as-if-serial原则是指在单线程环境下,无论怎么重排序,代码的执行结果都是确定的。运行时重排序是指为了提高执行的运行速度,系统对机器的执行指令的执行顺序进行调整。
  3. 可见性:
    内存的可见性是指线程之间的可见性,一个线程的修改状态对另外一个线程是可见的,用通俗的话说,就是假如一个线程A修改一个共享变量flag之后,则线程B去读取,一定能读取到最新修改的flag。

内存不可见的两个原因:

  1. cache机制导致内存不可见

    我们都知道,CPU的运行速度是远远高于内存的读写速度的,为了不让cpu为了等待读写内存数据,现代cpu和内存之间都存在一个高速缓存cache(实际上是一个多级寄存器),如下图:
    在这里插入图片描述
    线程在运行的过程中会把主内存的数据拷贝一份到线程内部cache中,也就是working
    memory。这个时候多个线程访问同一个变量,其实就是访问自己的内部cache。 在这里插入图片描述
    上面例子出现问题的原因在于:线程A把变量flag加载到自己的内部缓存cache中,线程B修改变量flag后,即使重新写入主内存,但是线程A不会重新从主内存加载变量flag,看到的还是自己cache中的变量flag。所以线程A是读取不到线程B更新后的值。

  2. 重排序导致内存不可见
    除了cache的原因,重排序后的指令在多线程执行时也有可能导致内存不可见,由于指令顺序的调整,线程A读取某个变量的时候线程B可能还没有进行写入操作呢,虽然代码顺序上写操作是在前面的。

volatile关键字

volatile修饰的变量不允许线程内部cache缓存和重排序,线程读取数据的时候直接读写内存,同时volatile不会对变量加锁,因此性能会比synchronized好。另外还有一个说法是使用volatile的变量依然会被读到cache中,只不过当B线程修改了flag之后,会将flag写回主内存,同时会通过信号机制通知到A线程去同步内存中flag的值
1)修改volatile变量时会强制将修改后的值刷新的主内存中。
2)修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。

代码演示

class ThreadVolatileDemo extends Thread {
    public boolean flag = true;
    @Override
    public void run() {
        System.out.println("开始执行子线程....");
        while (flag) {
        }
        System.out.println("线程停止");
    }
    public void setRuning(boolean flag) {
        this.flag = flag;
    }

}
public class ThreadVolatile {
    public static void main(String[] args) throws InterruptedException {
        ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();
        threadVolatileDemo.start();
        Thread.sleep(3000);
        threadVolatileDemo.setRuning(false);
        System.out.println("flag 已经设置成false");
        Thread.sleep(1000);
        System.out.println(threadVolatileDemo.flag);
    }
}

执行结果如下:

在这里插入图片描述
主线程修改了flag值之后无法刷新到子线程的内存中(flag对于子线程不可见)

加上volatile

在这里插入图片描述
在这里插入图片描述

volatile 非原子性
public class VolatileNoAtomic extends Thread {
    private static volatile int count;

    // private static AtomicInteger count = new AtomicInteger(0);
    private static void addCount() {
        for (int i = 0; i < 1000; i++) {
            count++;
            // count.incrementAndGet();
        }
        System.out.println(count);
    }
    @Override
    public void run() {
        addCount();
    }

    public static void main(String[] args) {
        VolatileNoAtomic[] arr = new VolatileNoAtomic[100];
        for (int i = 0; i < 10; i++) {
            arr[i] = new VolatileNoAtomic();
        }
        for (int i = 0; i < 10; i++) {
            arr[i].start();
        }
    }

}
运行结果

在这里插入图片描述
未出现10000得值,数据原子性别破坏

使用AtomicInteger原子类

public class VolatileNoAtomic extends Thread {
    //private static volatile int count;

    private static AtomicInteger count = new AtomicInteger(0);
    private static void addCount() {
        for (int i = 0; i < 1000; i++) {
            //count++;
             count.incrementAndGet();
        }
        System.out.println(count);
    }
    @Override
    public void run() {
        addCount();
    }

    public static void main(String[] args) {
        VolatileNoAtomic[] arr = new VolatileNoAtomic[100];
        for (int i = 0; i < 10; i++) {
            arr[i] = new VolatileNoAtomic();
        }
        for (int i = 0; i < 10; i++) {
            arr[i].start();
        }
    }
}
运行结果

在这里插入图片描述

volatile与synchronized

1)volatile本质是告诉JVM当前变量在寄存器中的值是不确定的,需要从主存中读取。synchronized则是锁定当前变量,只有当前线程可以访问该变量,其它线程被阻塞。

2)volatile仅能使用在变量级别,synchronized则可以使用在变量、方法。

3)volatile仅能实现变量修改的可见性,而synchronized则可以保证变量修改的可见性和原子性。《Java编程思想》上说,定义long或double时,如果使用volatile关键字(简单的赋值与返回操作),就会获得原子性。(常规状态下,这两个变量由于其长度,其操作不是原子的)

4)volatile不会造成线程阻塞,synchronized会造成线程阻塞。

5)使用volatile而不是synchronized的唯一安全情况是类中只有一个可变的域

ThreadLocal

ThreadLocal的实例代表了一个线程局部的变量,每条线程都只能看到自己的值,并不会意识到其它的线程中也存在该变量。
它采用采用空间来换取时间的方式,解决多线程中相同变量的访问冲突问题
每个Thread的对象都有一个ThreadLocalMap,当创建一个ThreadLocal的时候,就会将该ThreadLocal对象添加到该Map中,其中键就是ThreadLocal,值可以是任意类型。
在该类中,我觉得最重要的方法就是两个:set()和get()方法。当调用ThreadLocal的get()方法的时候,会先找到当前线程的ThreadLocalMap,然后再找到对应的值。set()方法也是一样。

hreadLocal类接口很简单,只有4个方法:

  • void set(Object value) 设置当前线程的线程局部变量的值。
  • public Object get() 该方法返回当前线程所对应的线程局部变量。
  • public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK
    5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
  • protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

代码示例

class Res {
    // 生成序列号共享变量
    public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        };
    };
    public Integer getNum() {
        int count = threadLocal.get() + 1;
        threadLocal.set(count);
        return count;
    }
}

public class ThreadLocaDemo2 extends Thread {
    private Res res;

    public ThreadLocaDemo2(Res res) {
        this.res = res;
    }
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + "---" + "i---" + i + "--num:" + res.getNum());
        }
    }
    public static void main(String[] args) {
        Res res = new Res();
        ThreadLocaDemo2 threadLocaDemo1 = new ThreadLocaDemo2(res);
        ThreadLocaDemo2 threadLocaDemo2 = new ThreadLocaDemo2(res);
        ThreadLocaDemo2 threadLocaDemo3 = new ThreadLocaDemo2(res);
        threadLocaDemo1.start();
        threadLocaDemo2.start();
        threadLocaDemo3.start();
    }
}

运行结果

在这里插入图片描述

练习:设计 4 个 ( 可以是 n 个 ) 线程,其中两个线程每次对 j 增加 1 ,另外两个线程对 j 每次减少 1

public class ThreadTest {
    private int j;
    //+1
    private synchronized void add(){
        j++;
        System.out.println(Thread.currentThread().getName()+"add:"+j);
    }
    //-1
    private synchronized void sub(){
        j--;
        System.out.println(Thread.currentThread().getName()+"sub:"+j);
    }
    //加线程
    class Add implements Runnable{
        @Override
        public void run(){
            for(int i=0;i<10;i++){
                add();
            }
        }
    }
    //减线程
    class Sub implements Runnable{
        @Override
        public void run(){
            for(int i=0;i<10;i++){
                sub();
            }
        }
    }
    public static void main(String[] args) {
        ThreadTest threadTest=new ThreadTest();
//创建2个线程类
        Add add=threadTest.new Add();
        Sub sub=threadTest.new Sub();
        for(int i=0;i<2;i++){
            Thread thread=new Thread(add);
            thread.start();
            thread=new Thread(sub);
            thread.start();
        }
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值