程序员

并发编程基础概念

2020-10-11  本文已影响0人  笔记本一号

CPU高速缓存、CPU多级缓存、CPU寄存器和CPU乱序执行优化

由于cpu的运算速度远远大于主内存的工作速度,为了能让主内存不要太落后cpu,引入了高速缓存,随着计算机的复杂程度的提升,高速缓存与主存的速度差异越来越大,随后加入了多级缓存:三级缓存。由此进一步缓解cpu与主存的速度差异。 image.png CPU寄存器也是属于内存,CPU内部也是需要内存的,CUP在这部分内存操作的速度远远大于在主内存和高速缓存中的操作速度,其实CPU与主内存互不认识,它们之间的交互都交给高速缓存和CPU寄存器,高速缓存充当了寄存器与主内存的交互地带,每当数据需要被CPU计算时,先从主内存中的数据读到高速缓存中,再从高速缓存读取到寄存器中让CPU处理,CPU处理完数据后返回主内存也需要让寄存器把数据拷贝到高速缓存中然后主内存才能读取到CPU处理完事的数据 image.png

指令重排

为了充分提升cpu性能,cup可以对代码的乱序执行,在单核单线程情况下这种这种机制能保证运算结果与代码顺序执行后的结果一致,但是多线程情况下就会出现结果不一致的情况。这个也称作cpu的指令重排

JVM堆栈和计算机结构的关系

存在堆上的对象可以被栈上的持有这个对象引用的方法所访问,如果栈上两个方法都拥有了这个对象的引用,可能会发生并发问题,因为栈上的线程拥有了这个对象的私有拷贝 image.png image.png

JMM与并发:

主存:计算机主内存,堆栈主要就是存在主内存中,堆栈内存属于主内存的一部分

工作内存:工作内存就是开辟给线程处理数据的内存空间类似CPU的寄存器我们也认为它就是寄存器和高速缓存,工作内存其实是一个不存在的逻辑区域,这是把线程与主内存间的交互区域给抽象出来,在JVM内存模型的角度来看我们还可以认为它就是线程的栈内存,里面存放的是栈内每个线程的对堆对象的私有拷贝和线程所要执行的方法的局部变量 image.png

由于线程是将工作内存的共享变量拷贝到工作内存进行处理,每个线程的工作内存都是线程私有的,所以线程之间的操作是互相不可见的,因此会产生线程并发问题

JMM同步的八大操作:

jmm并发八大操作
八大操作的规则:

1、read与load不允许单独出现、write与store也不允许单独出现,并且它们必须是按顺序的操作,不允许只read不load或者只store不write,只有read完后才能load,只有store完之后才能write,但是没有要求他们连续执行中间是可以穿插其他的指令操作的,两套操作具有原子性。
2、变量在工作内存中发生变化,必须更新到主内存中,新的变量只能在主内存中产生,不允许在工作内存中使用未被初始化的变量,就是在store和use前必须做assign和use,通过load或者assign才能拿到变量初始化的值。
3、一个变量同一时刻只允许一个线程执行lock操作这个是同步性,同一个lock允许被同一条线程执行多次,但是必须执行相同次数的unlock变量才被解锁,这就是可重入性。
4、变量被执行lock时,将清空工作内存中此变量的值,执行引擎使用此变量之前,需重新执行load操作,这就是可见性
5、当对变量进行unlock操作时之前,必须将此变量从工作内存中同步回主内存。

线程安全性:

当多个线程访问某个类时,不管运行环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的协同或者同步,这个类都能表现出正确的行为,那么这个类是线程安全的

实现线程安全主要是通过锁来实现,锁的主要作用就是实现同步,同步的可以分为两种:阻塞同步和非阻塞同步
阻塞同步:又称悲观锁,主要是使同一时刻只有获取锁的线程才能进行操作,其他线程只能阻塞,如synchronized和ReentrantLock都是阻塞同步
非阻塞同步:又称乐观锁,全程不上锁,主要是在修改数据前通过检测是否有其他线程竞争,有则放弃修改,采取其他措施补偿,如重试、自旋等,CAS就是非阻塞同步的

线程安全性的三点特性:

synchronized以及ReentrantLock都是拥有这些特性的

原子性:

提供互斥访问,同一时刻只允许一个线程进行对它进行操作操作
代码演示:
原子类AtomicIntegerFieldUpdater实现一个CAS

public class BingFa2 {
    private static ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()*2+1);
    public volatile int count=0;
    private static AtomicIntegerFieldUpdater<BingFa2> updater=
            AtomicIntegerFieldUpdater.newUpdater(BingFa2.class,"count");
    CountDownLatch countDownLatch = new CountDownLatch(1000);

    static BingFa2 bingFa2=new BingFa2();
    public void test1() throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            executorService.submit(() -> {
                int qw;
                do {
                  //从底层拿值,期望值
                     qw = updater.get(bingFa2);
                  //加一
                }while (!updater.compareAndSet(bingFa2,qw,count+1));
              countDownLatch.countDown();
            });
        }
        executorService.shutdown();
        countDownLatch.await();
        System.out.println(updater.get(bingFa2));
    }

    public static void main(String[] args) throws InterruptedException {
        bingFa2.test1();
    }
}
线程安全

原子类AtomicReference

public class AtomicReferenceTest {
    private static AtomicReference<Integer> compare =
            new AtomicReference<>(0);
    public void test1() throws InterruptedException {
        //比较值后在赋值,赋值成功则返回true,否则false
        System.out.println(compare.compareAndSet(0, 1));//true
        System.out.println(compare.compareAndSet(2, 1));//false
        System.out.println(compare.compareAndSet(1, 3));//true
        System.out.println(compare.get());
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicReferenceTest atomicReferenceTest = new AtomicReferenceTest();
        atomicReferenceTest.test1();
    }
}
底层是cas

LongAdder的演示,LongAdder在并发高的时候可以分散热点数据性能较好,但是并发很高时数据可能不一致,所以要求数据精准的场景不要用它,但是在并发高很高的场景要求性能高的使用它比较好,并发很多低的还是用其他的Atomic类比较好,如AtomicLong,AtomicInteger

public class LongAddTest {
    private static ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()*2+1);
    private static LongAdder longAdder=new LongAdder();
    CountDownLatch countDownLatch = new CountDownLatch(1000);
    public void test1() throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            executorService.submit(() -> {
             //加一
                longAdder.increment();
                countDownLatch.countDown();
            });
        }
        executorService.shutdown();
        countDownLatch.await();
        System.out.println(longAdder.longValue());
    }

    public static void main(String[] args) throws InterruptedException {
        LongAddTest longAddTest=new LongAddTest();
        longAddTest.test1();
    }
}
线程安全
CAS的ABA问题:

ABA问题就是在CAS操作时,其他线程将变量由A改为了B,然后又改回了A,这时CAS从主内存中得到的变量是A值和工作内存中的值一样,这个结果虽然一样,但是毕竟被其他线程修改过,和原子性思想不符,解决这个问题只需要在变量的版本号加1,每次修改变量都会修改版本号,这样就很好的解决了ABA问题

可见性:

一个线程对主内存的修改可以及时被其他线程观察到

造成共享变量不可见的原因有三点:

1、线程的交叉执行 2、指令重排序加线程交叉执行 3、工作内存中的共享变量发生变化没有及时更新到主内存中。或者是主内存的共享变量发生变化没有更新值工作内存中

可见性的实现手段:
synchronized:
这个其实我在上面的jmm八大操作的lock和unlock中就提到了:

volatile:
禁止cup的指令重排优化,在对volatile变量写操作时,加入一个store屏障,禁止指令的重排,强制将变量的更新刷新到主内存中,在对volatile变量读操作时,加入一个load屏障,禁止指令的重排,强制的将主内存的变量刷新到工作内存中,因此volatile保证了线程的可见性

线程在读volatile变量时会将本地内存的变量值置为无效,重新读取主内存的值,线程在写volatile变量后会将本地内存的成员变量的值刷新到主内存中,如此实现了线程之间的可见性

volatile能实现线程对一个变量修改后的可见性,但是无法保证两个线程同时操作一个变量的发生的不安全现象,volatile依旧是线程不安全的
happens-before:
要保证线程的可见性,则线程之间要存在happens-before原则:

happens-before

有序性:

程序执行的顺序按照代码的先后顺序执行

多线程下指令重排序会造成线程不安全:

public class Singleton {
    private static Singleton unsafeObject=null;
    //线程不安全,发生了指令重排,原因是unsafeObject是不可见的
    public static Singleton unsafeObject(){
        if (unsafeObject==null){
            synchronized (Singleton.class){
                if (unsafeObject==null) {
                    return new Singleton();
                }
            }
        }
        return unsafeObject;
    }
}
    //Singleton成员变量加上volatile禁止指令重排序,就解决了上面代码的线程不安全问题
    private static volatile Singleton unsafeObject=null;

synchronized的底层原理:

https://juejin.cn/post/6973571891915128846

synchronized通过对象头和monitor实现的

对象头:

synchronized的锁对象是存储在对象头里,下面是对象头的组成,主要由Mark Word 和Class Metadata Address组成,由于synchronized是重量级锁性能差,jvm对进行了优化,利用对象头实现了其而对象头实现了轻量级锁和偏向锁 对象头 Mark Word

锁优化:

由于synchronized锁过于重量级,在性能上一直都很差,比ReentrantLock差,HotSpot一直对锁进行优化如:自旋锁、自适应自旋锁、锁粗化、锁消除、轻量级锁、偏向锁等,现在synchronized性能不比ReentrantLock差,并且还在不断的优化中,由于都是系统级的优化,所以synchronized前途是比ReentrantLock光明的

重量级锁:

最基础的实现方式,JVM会阻塞未获取到锁的线程,在锁被释放的时候唤醒这些线程。阻塞和唤醒操作是依赖操作系统来完成的,所以需要从用户态切换到内核态,开销很大。加上monitor调用的是操作系统底层的互斥量(mutex),而使用操作系统本身也有用户态和内核态的切换,又增加了开销,所以JVM引入了自旋的概念,减少上面说的线程切换的成本。

自旋锁:

如果锁被其他线程占用的时间很短,那么其他获取锁的线程只要稍微等一下就好了,主要是使线程不放弃CPU时间然后执行忙循环,没必要进行用户态和内核态之间的切换,等的状态就叫自旋。在JDK6中是默认开启的通过-XX:-/+UseSpinning设置关闭/开启,通过-XX:PreBlockSpin设置自旋次数,默认是10。在JDK6中引入了自适应自旋锁,使自旋的时间不在固定,并且可以省略自旋,由同一个锁对象的上次自旋时间决定是否需要省略本次的自旋操作,如果一个锁对象自旋很少成功,虚拟机会直接放弃自旋,相反如果同一锁对象自旋经常成功虚拟机会允许它自旋得久一些。这种机制让虚拟机一定程度上节省了自旋浪费的时间。

锁消除:

虚拟机会检测我们的代码中不可能出现出现数据共享竞争的地方,将多余的锁进行消除

锁粗化:

如果一系列操作都对同一个对象反复的加锁和解锁,这种现象甚至出现在循环体中,对系统的造成不必要的消耗,那么虚拟机将会把这个锁的范围扩大,把多次加锁解锁的地方变成只需要一次加锁和解锁

轻量级锁:

JDK1.6之后加入,它的目的并不是为了替换前面的重量级锁,而是在实际没有锁竞争的情况下,将申请互斥量这步也省掉。锁实现的核心在于对象头(Mark Word)的结构,对象自身会有信息表示是否被锁住和锁是什么类型的,如果代码进入同步块时,检测到对象未锁定,即标志位为01。那么当前线程就会在自身栈帧中建立一个名为锁记录的区域拷贝一份对象的Mark Word信息叫做Displace Mark Word,再使用CAS的方式将对象Mark Work更改为指向这个区域的指针,如果更改成功了就算加上锁了成功了,那么当前线程就算拥有了这个对象的锁了,上锁成功后这个对象的Mark Word的锁标志位将会变为00。(这样就不需要获取系统mutex变量,只是改了个值,但是如果有竞争的话,就要升级成重量级锁,这样反倒变慢了)

偏向锁:

偏向锁将同步操作全部省略,进一步的对锁进行了优化,-XX:+UseBiasedLocking可以开启,它主要实现是一个对象锁第一次被线程获取的时候对象头的标志位会被设置为“01”表示偏向模式,同时使用CAS把获取这个对象锁的线程ID记录在对象的Mark Word中,如果CAS成功代表获取偏向锁成功,持有这个偏向锁的线程每次进入这个对象锁相关的代码块时,虚拟机将不会对这个线程做任何操作,但是如果有其他的线程来尝试获取这个对象锁的时候,偏向模式就会被撤销,标志位将会变为无锁的"01"或者轻量级锁的"00",由此可以看出锁是一个逐步升级的过程,不会一开始上来就重量级锁。锁一般只会升级不会降级,避免降级之后冲突导致效率不行并且又得升级。但是降级其实是允许的

偏向锁可以提高带有同步但无竞争的程序的性能,但是锁总是会被多个线程竞争的,所以偏向模式将会是多余的,还是禁止掉偏向锁优化比较好。-XX:-UseBiasedLocking禁止

隐式锁 monitor:

每个对象里面隐式的存在一个叫monitor(对象监视器)的对象,这个对象源码是采用C++实现的,monitor内部有一个count变量,调用monitorenter就是尝试获取这个对象,成功获取到了就将值+1,离开就将值-1。如果是线程重入,在将值+1,说明monitor对象是支持可重入的。如果synchronize在方法上,那就没有上面两个指令,取而代之的是有一个ACC_SYNCHRONIZED修饰,表示方法加锁了。它会在常量池中增加这个一个标识符,获取它的monitor,所以本质上是对象锁是一样的。

每一个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现)。每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态。下图是ObjectMonitor的C++源码

ObjectMonitor

ObjectMonitor中有两个队列_WaitSet和_EntryList,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入_EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。
monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因(关于这点稍后还会进行分析)

一个monitor对象包括这么几个关键字段:_cxq(下图中的ContentionList),_EntryList ,_WaitSet,_owner。其中_cxq ,_EntryList ,_WaitSet都是由ObjectWaiter组成的链表结构,_owner指向持有锁的线程。当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个ObjectWaiter对象插入到_cxq的队列尾部,然后暂停当前线程。当持有锁的线程释放锁前,会将cxq中的所有元素移动到_EntryList中去,并唤醒_EntryList的队首线程。如果一个线程在同步块中调用了wait方法,会将该线程从_EntryList移除并加入到_WaitSet中,然后释放锁。当wait的线程被notify之后,会将对应的线程从_WaitSet移动到_EntryList中

image.png

利用javap -verbose 反编译这段代码

public class SynchronizedTest {
    public synchronized void methodtest(){
        System.out.println("方法锁");
    }
    public void objectlock(){
        synchronized (this){
            System.out.println("对象锁");
        }
    }
    public void classlock(){
        synchronized (SiSuo.class) {
            System.out.println("类锁");
        }
    }
    public synchronized static void staticlock(){
     System.out.println("静态方法锁");
    }
    public static void main(String[] args) {
        SynchronizedTest test=new SynchronizedTest();
        test.methodtest();
        test.objectlock();
        test.classlock();
        SynchronizedTest.staticlock();
    }
}

image.png image.png image.png image.png

线程的状态:

由上面分析的monitor知道线程在获取锁时会被封装成monitor进入ContentionList,EntryList ,WaitSet,owner几个队列代表着线程的不同状态 image.png

线程中共有六种状态:

线程的这些状态可以通过sleep、wait、notify、notifyAll、yield、interrupt进行转换

sleep不必多说,平时我们用过,它可以使线程进入期限等待中,sleep状态下的线程只会让出CPU执行时间,不会让出锁

interrupt:提醒系统线程应该被终止,如果线程处于阻塞状态,线程会退出阻塞如Thread.sleep();,抛出异常。如果线程是正常活动的,将会把线程的中断标志设置为true,然后线程正常运行不受影响.interrupt中断仅仅是设置中断标记位。对于NEW|TERMINATED 线程,终端完全不起作用;对于RUNNABLE或者BLOCKED线程,只会将中断标志位设为true;WAITING线程对中断较为敏感,会抛出异常,同时会将中断标志位清除变为false。

public void interrupt(): 设置当前线程的中断标志位,非WAITING线程不会受任何影响的,仅此而已。
public boolean isInterrupted(): 判断当前线程中断标志位是否被标记为中断,不会清除标志位
public static boolean interrupted():这是一个静态方法,返回当前线程是否标记中断,同时清除标志位

wait、notify、notifyAll:
wait:线程执行wait,让线程进入无限期等待状态,只有其他线程执行notify或者notifyall才被唤醒,wait只能在synchronized的代码块或在synchronized类中使用,它会让线程不但需要让出CPU执行时间,还必须让出锁给其他线程

wait被执行后,线程失去CPU时间和锁,线程被扔进等待池,此线程在等待池中不会去竞争锁,直至有其他线程对锁执行notify或者notifyall,线程才会被扔进锁池中去竞争锁,notify和notifyall的区别在于,notify只会随机唤醒一个wait()的线程,而notifyall会唤醒所有wait()的线程

yield:线程暗示CPU,愿意让出CPU时间给其他线程,但是CPU不一定会理会这个暗示,最终的决定线程是否让出CPU时间由CPU决定

此外还有一个join方法,代表着“插队”,哪个线程调用join代表哪个线程插队先执行——但是插谁的队是有讲究了,不是说你可以插到队头去做第一个吃螃蟹的人,而是插到在当前运行线程的前面,比如系统目前运行线程A,在线程A里面调用了线程B.join方法,则接下来线程B会抢先在线程A面前执行,等到线程B全部执行完后才继续执行线程A。join内部是调用了wait方法对目前正在运行的线程进行阻塞,注意这里线程执行后是调用notifyAll进行唤醒的,也就是B调用notifyAll唤醒A

安全发布对象:

单例模式安全发布演示
//线程安全
//懒汉
public class Singleton {
 private Singleton(){}
//使用volatile禁止指令重排
    private static volatile Singleton unsafeObject=null;
    public static Singleton unsafeObject(){
        if (unsafeObject==null){
            synchronized (Singleton.class){
                if (unsafeObject==null) {
                    return new Singleton();
                }
            }
        }
        return unsafeObject;
    }
}
//线程安全
//饿汉
public class Singleton {
 private Singleton(){}
//使用volatile禁止指令重排
    private static Singleton unsafeObject=new Singleton();
    public static Singleton unsafeObject(){
        return unsafeObject;
    }
}
//枚举方式线程安全
public class SingletonEunm {
    private SingletonEunm(){}
    //枚举类相比饿汉模式更加节省资源,原因是枚举调用的时候才创建,并且只创建一次,不像静态代码在项目启动时提前创建好,资源浪费
    private static SingletonEunm getSingletonEunm(){
        return InstanceSingleton.INSTANCE.getSingleton();
    }
    private enum InstanceSingleton{
        INSTANCE;
        private SingletonEunm singletonEunm;
        //对于枚举类JVM保证只初始化一次,并且是调用的时候才初始化
        InstanceSingleton(){
            singletonEunm=new SingletonEunm();
        }
        public SingletonEunm getSingleton(){
            return singletonEunm;
        }
    }
}
使用final安全发布演示
public class Final {
    private static final int SAFEINT=2333;//这个基本类型不可修改,安全
//SAFESTR这个引用不可在指向其他对象,“2333”字面量也是对象,所以SAFESTR这个引用无法指向其他对象也就是无法修改它的字面量了,安全
    private static final String SAFESTR="2333";
//ALLOW这个引用无法指向其他的对象了,但是可以修改指向对象的内容
   private static final Map<String,String> ALLOWMAP= new HashMap();
 private static Map<String,String> UNMAP= new HashMap();
//利用Gava使GUNMAP内容不可变,final 使GUNMAP引用不可指向其他对象
 private static final Map GUNMAP= ImmutableMap.of("k1","v1","k2","v2");
    private static final List GUNLIST= ImmutableList.of("111","222");
    private static final Set GUNSET= ImmutableSet.of("111","222");
 static {
        UNMAP.put("111","222");
 //使用Collections.unmodifiableMap使Map里的内容不可变
        UNMAP= Collections.unmodifiableMap(UNMAP);
    }
上一篇下一篇

猜你喜欢

热点阅读