二、线程的安全性分析

线程的安全性分析

一、并发编程问题的源头

可见性,由多核cpu缓存导致的,每个cpu访问各自的缓存,不同cpu缓存中的数据不可见。(缓存用于提高I/O存储速度)

原子性,线程切换导致的,Java中一条代码对应底层多条代码(在多条代码的执行过程中,线程切换了。)。

有序性,编译器编译代码时会改变代码的顺序(单线程没有什么影响,多线程影响比较大)

1. 可见性问题

多核cpu访问各自缓存所导致的

不同cpu在执行线程时,cpu访问的数据是各自cpu中的缓存导致

下图中:线程A对于变量X的操作,对于线程B不可见。不具备可见性。

在这里插入图片描述

当stop没有volatile修饰时,主线程中修改stop的值是不会影响子线程中的stop的值。

public class VisableDemo {
    // public static boolean stop=false;
    
    // volatile可以解决可见性、[有序性]
    public volatile static boolean stop=false;

    public static void main(String[] args) throws InterruptedException {
        Thread thread=new Thread(()->{
            int i=0;
            while(!stop){
                i++;
            }
            System.out.println("线程结束: result:"+i);
        });
        thread.start();
        System.out.println("begin start thread");
        Thread.sleep(1000);
        stop=true; //主线程中修改stop的值,使得上面线程可以执行结束
    }
}

2. 原子性问题

由线程切换导致的

count++在Java中可能只是一行指令,但是底层可能是有多条指令的。

在这里插入图片描述

public class AtomicDemo {

    public static int count=0;
    public static void incr(){
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++; //count++并不是一个原子操作:count++ (只会由一个线程来执行)
    }
    
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            // 创建1000个线程
            new Thread(AtomicDemo::incr).start();
        }
        // 为了测试显示的效果做了一个延时,等待子线程执行完毕。
        Thread.sleep(4000);
        System.out.println("result:"+i);
    }
}

输出结果:一定是小于等于1000的值

查看count++源码

IDEA点击到文件对应的字节码.class,邮件open in terminal

javap -v AtomicDemo.class

count++ 对应代码如下:并不是一个原子指令,要么同时成功要么同时失败。

12: getstatic
15: iconst_1
16: iadd
17: putstatic

在这里插入图片描述

3. 编译器带来的有序性问题

例如:final域问题

在构造函数中对非final修饰的变量赋值。

这个赋值操作会被编译器放在构造函数之外。

在这里插入图片描述

二、Java内存模型------Java如何解决可见性有序性问题

Java内存模型(Java Memory Model):Java 解决可见性、原子性、有序性的一套软件机制。

volatile、synchronized、final关键字

Happens-Before原则(告诉你哪些场景不会存在可见性问题)

三、synchronized关键字

原子性:解决方案(Synchronized、AtomicXXX、Lock)

可见性:解决方案(Synchronized、Volatile)

有序性:解决方案(Synchronized、Volatile)

synchronized:同一时刻单线程执行的代码不存在原子性问题。(指令重排序也无所谓)

synchronized修饰的代码同一时刻只能由一个线程执行,虽然不能解决有序性,但是可以解决有序性问题,因为只有一个线程。

synchronized修饰的范围:

  • 修饰实例方法:锁的是同一个对象
  • 修饰静态方法:锁的对象是类
  • 修饰代码块:可以指定锁(对象或者类)

3.1 抢占锁的本质是:互斥。

如何实现互斥?

  • 共享资源
  • 可以是一个标记,0 无锁 1 有锁。
Object lock = new Object;
public void m2() {
    // 代码块
    synchronied (lock) {
        
    }
}

在lock中存放了锁的相关信息

3.2 MarkWord对象头

在这里插入图片描述

3.3 锁主要存在的四种状态:

  • 无锁状态
  • 偏向锁状态:在没有线程竞争的情况下,线程A进入同步代码块,锁偏向线程A,A线程下次再进来的时候就不需要抢占锁。
  • 轻量级锁状态:存在多个线程抢占锁,在偏向锁的基础上,将锁升级。(无锁状态—优化机制)
    • 避免线程阻塞,通过自旋锁阻塞。
  • 重量级锁状态。

在这里插入图片描述

在这里插入图片描述

3.4 CAS机制

以下操作必须是原子的

​ 修改锁的标记

​ 修改线程指针的指向

CompareAndSwap(old、except、update)

old:ThreadA

except:ThreadB

update: ThreadC


四、Volatile关键字

volatile: 可以用来解决可见性、有序性。

可见性的本质:

cpu处理速度快,内存磁盘读取速度慢,导致cpu资源的浪费。引入CPU增加高速缓存。

五、final域:防止指令重排序,解决可见性问题

一旦将引用申明成final,将不能改变这个引用。

参考ppt

对于final域,编译器和处理器要遵守两个重排序规则。

  1. 在构造函数内对一个final域的写入(构造函数中对final修饰的变量赋值)

    与随后把这个被构造对象的引用赋值给一 个引用变量,这两个操作之间不能重排序。

  2. 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操 作之间不能重排序。

这两个规则,可以防止指令重排序,来解决可见性问题。

1 写final域的重排序规则

i的值可能等于0,但j的值一定等于2

在这里插入图片描述

写普通变量i的操作,被编译器重排序到构造函数之外。

在这里插入图片描述

2 读final域的重排序规则

在一个线程中,初次读对象引用与初次读该对象包含的final 域,

JMM禁止处理器重排序这两个操作,编译器会在读final 域操作的前面插入一个LoadLoad屏障。

上面代码中,读普通域i代码,可以在写普通域i之前

在这里插入图片描述

六、Happens-Before规则

参考ppt

  • 程序顺序性规则:不管程序如何重排序,单线程的执行结果一定不会发生变化。
  • 监视器锁规则:一个锁的解锁,在后序这个锁的枷锁之前。
  • Volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读
  • 传递性规则:A happen before B B happens befoe C 那么 A 在 C之前。
  • start规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的 ThreadB.start()操作happens-before于线程B中的任意操作
  • join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意 操作happens-before于线程A从ThreadB.join()操作成功返回

七、原子类Automic

Synchronized能够保证原子性问题,因为加了Synchronized的代码,同一时刻只能由一个线程执行所以不存在原子性问题。

八、ThreadLocal的实现原理

每个线程独立存储数据的空间

public class ThreadLocalDemo {
    private static Integer num=0;
    public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
        protected Integer initialValue(){
            return 0; //初始值
        }
    };
    public static final ThreadLocal<Integer> local1 = new ThreadLocal<Integer>();
    
    public static void main(String[] args) {
        Thread[] threads = new Thread[5];
        //希望每个线程都拿到的是0
        for (int i = 0; i < 5; i++) {
            threads[i]=new Thread(()->{
//                num+=5;
                int num=local.get(); //拿到初始值
                local1.get();
                num += 5;
                local.set(num);
                System.out.println(Thread.currentThread().getName()+"->"+num);
            },"Thread-"+i);
        }
        for(Thread thread:threads){
            thread.start();
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值