java并发编程(上篇)

基础概念

串行:一个时间段内,多个任务一个执行完后,才能执行下一个
并行:一个时间段内,多个任务同时执行,但需要依赖于多核cpu
并发:一个时间段内,多个线程在单个核心运行,系统不断切换线程,但同一时刻只有一个线程运行
同步:协同步调,按预定的先后次序进行运行,就是得等一个线程执行完,才能执行下一个线程
异步:是指进程不需要一直等待下去,而是继续执行下面的操作
进程:资源分配的最小单位
线程:程序执行的最小单位
管程:是一种高级的同步原语。任意时刻管程中只能有一个活跃进程(比如这个方法创建了,创建管程对象,方法结束,销毁管程对象)
守护线程:一般和用户线程做对比,用户线程在执行过程如果阻塞但没有得到释放,就会一直阻塞,但守护线程是不管有没有执行结束,只要用户线程结束了,守护线程也会结束,比如垃圾回收线程,因为用户线程都结束了,已经没有必要垃圾回收,所以把回收线程作为守护线程,一定程度上节省了开销

上下文切换

使用多线程的目的是为了程序能够并发执行,提高效率,但在一定程度上会带来更多的开销,因为线程之间需要来回切换,也就是上下文切换,而且可能会导致死锁
简单的死锁代举例
synchronized 拿到锁之后,如果没有一些强制终止线程的操作,得等程序完全执行完毕后,才会释放锁,所以两个线程,一个拿到d1的对象锁,一个是d2的对象锁,然后互相等待,造成死锁

public class DeadLock {
    public static void main(String[] args) {
        Resource r = new Resource();
        new Thread(() -> {
            r.method1();
        }, "t1").start();

        new Thread(() -> {
            r.method2();
        }, "t2").start();
    }
}

class Resource {
    DeadLock d1 = new DeadLock();
    DeadLock d2 = new DeadLock();

    public void method1() {
        synchronized (d1) {
            System.out.println("执行方法一");
            try {
                Thread.sleep(50);
                synchronized (d2) {
                    System.out.println("执行方法1");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public void method2() {
        synchronized (d2) {
            System.out.println("执行方法2");
            try {
                Thread.sleep(50);
                synchronized (d1) {
                    System.out.println("执行方法2");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
并发机制的底层原理实现

了解volatile

volatile是一个轻量级的锁,比锁更加方便,保证了共享变量之间的可见性,有序性,但不能保证原子性

volatile如何保证可见性
它使得所有对volatile变量的读写都会直接刷到主存,但仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性,对于复合操作i++,实际上也是由多个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,依然会出现问题,volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。
从内存屏障分析
写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
没有使用volatile的情况
因为t1线程频繁地从主存中读取run的值(使用while(run)),JIT即时编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run的访问以提高效率
在这里插入图片描述
1 秒之后,main线程修改了run的值, 并同步至主存。而 t线程是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
在这里插入图片描述
JMM线程操作的两条基本规定:

线程与主内存:线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写 线程间的工作内存:不同线程之间无法直接访问其他线程工作内存中的变量,线程间变量值的传递需要经过主内存来完成
在这里插入图片描述
volatile如何保证有序性
有序性也就是说禁止指令重排,
ps:为什么要进行重排序:重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,有时候会改变程序语句的先后顺序(不存在数据依赖关系,可以重排序;存在数据依赖关系,禁止重排序)
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
说了这么多,什么是内存屏障呢?
内存屏障是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作,避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性,但volatile无法保证原子性
volatile为什么无法实现原子性
使用volatile会有线程安全问题:多个线程同时读取这个变量,也能同时刷新到主存,所以不能保证线程安全,也就是说volatile字段的操作不是原子性的,volatile变量只能保证可见性(一个线程修改后其它线程能够理解看到此变化后的结果),要想保证原子性,目前为止只能加锁!
举个例子:比如有两个线程A ,B,A去读主存中的共享变量,此时还没修改,线程B也去读主存中的共享变量,此时A修改完写回主存,但B读取到的依旧是旧值,这就产生了数据不一致。而为了避免出现这种竞争现象,需要利用到另外一个关键字synchronized
应用场景,单例的双锁模式

public class LhanSingleton2 {

    private LhanSingleton2() {}
    //volatile关键字,避免没有实例化就直接给变量赋值为null
    private static volatile LhanSingleton2 instance;

    public static  LhanSingleton2 getInstance() {
        //调用的时候再实例化该instance
        if(instance==null) {//节省内存空间,懒汉式
            synchronized (LhanSingleton2.class) {
//为了安全,避免重复实例化,如果这里没有进行判断,则下个方法进来调用,也会再实例化一次
                if(instance==null) {
                	//这里可能会发生指令重排序
                	//先赋值,再实例化
                    instance = new LhanSingleton2();
                }
            }
        }
        return instance;
    }
 }

对于volatile关键字,当且仅当满足以下所有条件时可使用

你能确保只有单个线程更新变量的值。
该变量没有包含在具有其他变量的不变式中。

synchronized实现原理及应用

synchronized很多人会称呼为重量级锁,但随着java1.6进行了各种优化后,有些情况并不那么重量,他有个锁升级过程(无锁----偏向锁—轻量级锁—重量级锁),**synchronized是基于Monitor来实现同步的。**它能够保证有序性,可见性和原子性
synchronized实现同步基础:
对于普通同步方法,锁是当前实例对象
对于静态同步方法,锁是当前类的Class对象
对于同步代码块,锁的是synchronized()括号里配置的对象
synchronized用的锁是存在java对象头里的
以 32 位虚拟机为例,普通对象的对象头结构如下,其中的Klass Word为指针,指向对应的Class对象;
在这里插入图片描述
其中 Mark Word 结构为
在这里插入图片描述

Monitor 原理

Monitor被翻译为监视器或者说管程,每个java对象都可以关联一个Monitor,如果使用synchronized给对象上锁(重量级),该对象头的Mark Word中就被设置为指向Monitor对象的指针
在这里插入图片描述
刚开始时Monitor中的Owner为null
当Thread-2 执行synchronized(obj){}代码时就会将Monitor的所有者Owner 设置为 Thread-2,上锁成功,Monitor中同一时刻只能有一个Owner
当Thread-2 占据锁时,线程Thread-3,Thread-4竞争不到,就会进入EntryList中变成BLOCKED状态
Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁
WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程

锁的升级过程

无锁:没有任何线程执行该对象,我们可以看到是否为偏向锁为0,锁标志位01,即无锁状态。
偏向锁:在轻量级锁(下面介绍)中,我们可以发现,如果同一个线程对同一个对象进行重入锁时,也需要执行CAS操作,这是有点耗时滴,那么java6开始引入了偏向锁的东东,只有第一次使用CAS时将对象的Mark Word头设置为入锁线程ID,之后这个入锁线程再进行重入锁时,发现线程ID是自己的,那么就不用再进行CAS了(只对一个线程可以,多个线程会撤销)
在这里插入图片描述
轻量级锁: 多线程交替同步(不是在同一时刻争夺资源)执行代码块的情况下,线程间没有竞争,使用轻量级锁可以避免重量级锁引入的性能消耗,标志位是00,在刚才偏向锁的基础上,如果有另外一个线程也想错峰使用该资源,通过对比线程id是否相同,Java内存会立刻撤销偏向锁(需要等待全局安全点),进行锁升级的操作。
白话:同学A在使用自习教室外面写了自己的名字,所以同学B来也想要使用自习教室,他需要提醒同学A,不能使用偏向锁,同学A将自习教室门口的名字擦掉,换成了一个书包,里面是自己的书籍。这样在同学A不使用自习教室的时候,同学B也能使用自习教室,只需要将自己的书包也挂在外面即可。这样下次来使用的同学就能知道已经有人占用了该教室。
底层的执行流程

1、每次指向到synchronized代码块时,都会创建锁记录(Lock Record)对象,每个线程都会包括一个锁记录的结构,锁记录内部可以储存对象的Mark Word和对象引用reference
2、让锁记录中的Object reference指向对象,并且尝试用cas(compare and sweep)替换Object对象的Mark Word ,将Mark Word 的值存入锁记录中 如果cas替换成功,那么对象的对象头储存的就是锁记录的地址和状态01
如果cas失败,有两种情况
如果是其它线程已经持有了该Object的轻量级锁,那么表示有竞争,将进入锁膨胀阶段
如果是自己的线程已经执行了synchronized进行加锁,那么再添加一条 Lock Record 作为重入的计数(重入锁)
4、当线程退出synchronized代码块的时候,如果获取的是取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一 当线程退出synchronized代码块的时候,如果获取的锁记录取值不为 null,那么使用cas将Mark Word的值恢复给对象 成功则解锁成功
失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

重量级锁:如果在尝试加轻量级锁的过程中,cas操作无法成功,这是有一种情况就是其它线程已经为这个对象加上了轻量级锁,这是就要进行锁膨胀,将轻量级锁变成重量级锁。(就是同同一时刻有多个线程竞争),Java内存会申请一个Monitor对象来实现。锁标志位10,升级为重量级锁,也叫锁膨胀

上面一直提到CAS,下面就简单讲下CAS把
CAS是自旋锁,但不是真正的锁,它是并发原语,判断某个位置的值是否为预期值,底层是unsafe,原语的执行是连续的, 不允许被中断,是一条cpu的原子指令,不会造成数据不一致问题,既保证了一致性,又保证了并发性,而且不用加锁

//设置主内存的值
AtomicInteger atomicInteger = new AtomicInteger(5);
//主内存的值与当前拿到的值相同,说明没有被其他线程修改过,可以进行更新  true当前的值10
System.out.println(atomicInteger.compareAndSet(5, 10) + "当前的值" + atomicInteger.get());
//主内存的值与当前拿到的值不同,说明有被其他线程修改过,不可以进行更新  false当前的值10
System.out.println(atomicInteger.compareAndSet(5, 20) + "当前的值" + atomicInteger.get());
for (int i = 0; i < 4; i++) {
//自增操作保证原子性,底层使用源码的getAndAddInt()方法
System.out.println(atomicInteger.getAndIncrement());
}

底层使用源码的getAndAddInt()方法,里面的do–while方法,一直比较当前的值与主存的值是否相同,不同则重新读取,重新比较,最终才确定更新
缺点:开销比较大,会一直循环(自旋锁的一种体现),只允许操作一个共享变量,会造成ABA问题
ABA问题
ABA问题就是两个线程去读取数据,但一个线程处理时间长,一个处理短,处理时间短的线程可能将 A改为B 然后又将B改为A ,处理时间长的线程现在并不知道中间经历了什么操作,由于比较的也是A,所以也能修改成功
解决方案

添加版本号,每次修改都添加一个版本号,如果比较值相等的情况,还会比较版本号,只要当前要修改的版本号低于主内存的版本号,证明是修改过的,所以修改失败

参考链接
参考链接
参考链接
参考链接

多线程上篇暂时写到这,有错误欢迎指正,觉得不错记得点个赞噢!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值