java并发编程之美第二章读书笔记

并发编程的其他基础知识

什么是多线程的并发编程

并发:

同一时间段内多个任务同时都在执行,且执行都没有执行结束,强调的是在一个时间段内同时执行,而一个时间段由多个时间积累而成的,所以并发的多个任务在单位时间内并不一定同时执行

并行:

单位时间内多个任务同时在执行

为什么要进行多线程并发编程

多核CPU时代打破了单核CPU对多线程的性能限制,多个CPU"意味着每个线程可以使用自己的CPU运行,这减少了线程上下文切换的开销,但是随着对应用系统性能和吞吐量要求的提高,出现了海量数据和请求的要求,迫切需要高并发编程

java的线程安全问题

共享资源:

该资源被多个线程所持有或者说多个线程都可以访问该资源

线程安全问题:

当多个线程同时读写一个共享资源并且没有任何同步措施的时候,导致出现脏数据或者不可预见结果的其他问题

java中共享变量的内存可见性问题

将所有的变量都存放在主内存,当线程使用变量的时候,会把主内存的变量复制到自己的工作空间或者工作内存,线程读写变量操作的是自己工作内存中的变量

java中的synchronized关键字

关键字介绍

java提供的一个原子性内置锁,java的每一个对象都可以把他当作同步锁来使用,这些java内置的使用者看不到的锁被称为内部锁,与叫做监视器锁.

扫描二维码关注公众号,回复: 16210259 查看本文章

也是一种排他锁,也就是一个线程获取了这个锁后,其他线程必须等待该线程释放锁后才能获取该锁

java中的线程和操作系统的原生线程一一对应,当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的,而synchronized的使用就会导致上下文切换

内存语义

把在synchronized块内使用到的变量从线程的工作内存中清楚,这样子在synchronized块内使用的变量就不会从线程的工作内存中获取,而是从主内存中获取,退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存

而synchronized的使用就会导致上下文切换带来线程调度开销

java中的volatile关键字

确保对一个变量的更新对其他线程马上可见,当一个变量使用volatile关键字时,线程在写入变量的时候就不会把值缓存在寄存器或者其他地方,而是会把值刷新到主内存,当其他线程读取该共享变量的时候,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值

内存语义

和synchronized相似,当线程写入了volatile变量值时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存)读取volatile变量值时就相当于进入了同步块(先清空本地内存变量值,再从主内存获取最新值).

public class ThreadNotSafeIntegerTest {
    private int value;
    public int get(){
        return value;
    }
    
    public void set(int value){
        this.value=value;
    }
}

synchronized 方法

public class ThreadNotSafeIntegerTest {
    private int value;
    public synchronized int get(){
        return value;
    }

    public synchronized void set(int value){
        this.value=value;
    }
}

volatile方法

public class ThreadNotSafeIntegerTest {
    private volatile int value;
    public  int get(){
        return value;
    }

    public void set(int value){
        this.value=value;
    }
}

volatile并不能保证操作的原子性

什么时候使用volatile关键字

写入变量值不依赖变量的当前值

因为如果依赖当前值,将获取-计算-写入三步操作,这三步不是原子性操作,而volatile不能保证原子性

读写变量值没有加锁

因为加锁本身已经保证了内存可见性,这时候不需要啊变量声明为volatile

java中的原子操作

一系列操作时,这些操作要么全部执行,要么全部不执行,不存在执行性其中一部分的情况.

如下代码就是线程不安全的问题

public class ThreadNotSafeCount {
    private Long value;
    
    public Long get(){
        return value;
    }
    public void inc(){
        ++value;
    }
}

使用synchronized修饰就可以保证原子性

public class ThreadNotSafeCount {
    private Long value;

    public synchronized Long get(){
        return value;
    }
    public synchronized void inc(){
        ++value;
    }
}

java中的CAS操作

锁在并发处理中占据一席之地,但是当一个线程没有获取到锁时就会被阻塞挂起,导致线程上下文的切换喝调度开销,java提供了非阻塞的volatile关键字来解决共享变量的可见性,一定程度上解决了锁带来的开销问题,但是只能保证共享变量的可见性,不能解决读-改-写等原子性操作
CAS即 Compare and Swap 是JDK提供的非阻塞原子性操作,通过硬件保证了比较–更新操作的原子性

JDK里面Unsafe提供了一系列的compareAndSwap方法

boolean compareAndSwapLong(Object obj,long valueOffset,long expect,long update)方法

比较并交换

四个参数分别是

      • 对象内存位置
      • 对象中变量的偏移量
      • 变量预期值
      • 新的值
    • 如果对熊obj中内存偏移量为valueOffset的变量的值为expect,则使用新的值update置换旧的值expect

ABA问题

关于CAS 操作有个经典的ABA 问题,具体如下:假如线程I使用 CAS 修改初始值为A的变量X,那么线程I会首先去获取当前变量X 的值(为A),然后使用 CAS 操作尝试修改X的值为 B,如果使用CAS 操作成功了,那么程序运行一定是正确的吗?其实未必,这是因为有可能在线程I获取变量X的值A后,在执行 CAS 前,线程I使用CAS修改了变量X的值为 B,然后又使用 CAS 修改了变量X 的值为 A。所以虽然线程I执行 CAS时X的值是A,但是这个A已经不是线程I获取时的A了。这就是 ABA 问题。

ABA 问题的产生是因为变量的状态值产生了环形转换,就是变量的值可以从A到B,然后再从B到A。如果变量的值只能朝着一个方向转换,比如A到B,B到C,不构成环形,就不会存在问题。JDK 中的AtomicStampedReference 类给每个变量的状态值都配备了一个时间戳,从而避免了 ABA 问题的产生。

Unsafe

Unsafe类的重要方法

如何使用Unsafe类

public class UnsafeTest {

    //获取实例
    static final Unsafe unsafe=Unsafe.getUnsafe();
    //记录state变量在类中的偏移值

    static final long stateOffset;

    private volatile  long state=0;

    static {
        try {
            stateOffset=unsafe.objectFieldOffset(UnsafeTest.class.getDeclaredField("statee"));

        }catch (Exception e){
            System.out.println(e.getLocalizedMessage());
            throw new Error(e);
        }
    }

    public static void main(String[] args) {
        UnsafeTest unsafeTest=new UnsafeTest();
        Boolean success=unsafe.compareAndSwapInt(unsafeTest,stateOffset,0,1);
        System.out.println(success);
    }

}

运行结果

@CallerSensitive
public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}



public static boolean isSystemDomainLoader(ClassLoader var0) {
        return var0 == null;
    }

正规渠道不能使用Unsafe方法,要想使用可以用反射方法来获取Unsafe的实例方法

public class TestUnsafe {
    static final Unsafe unsafe;

    static final long stateOffset;

    private volatile long state=0;
    static {
        try {
            Field file=Unsafe.class.getDeclaredField("theUnsafe");
            file.setAccessible(true);
            unsafe=(Unsafe) file.get(null);
            stateOffset=unsafe.objectFieldOffset(TestUnsafe.class.getDeclaredField("state"));
        }catch (Exception e){
            System.out.println(e.getLocalizedMessage());
            throw new Error(e);
        }
    }

    public static void main(String[] args) {
        TestUnsafe test=new TestUnsafe();
        Boolean success=unsafe.compareAndSwapInt(test,stateOffset,0,1);
        System.out.println(success);
    }
}

java指定重排序

java内存模型允许编译器喝处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序,在单线程下重排序可以保证最终的执行结果与程序顺序执行结果一致,但是在多线程下存在安全问题

public  class MoreThreadTest {


    /**
     * 这段代码没有声明volatile变量,也没有使用任何同步措施
     * 多线程下存在共享内存可见性问题
     * 可以通过对共享变量声明成volatile就可以避免指令重排序问题
     */
    public static class ReadThread extends Thread{
        @Override
        public void run(){
            while (!Thread.currentThread().isInterrupted()){
                if(ready){
                    System.out.println(num+num);
                }
                System.out.println("read thread");
            }
        }

    }
    public static class WriteThread extends Thread{
        @Override
        public void run(){
            num=2;
            ready=true;
            System.out.println("writeThread set over");
        }
    }
    private static int num =0;
    private static boolean ready=false;

    public static void main(String[] args) throws InterruptedException{
        ReadThread readThread=new ReadThread();
        readThread.start();
        WriteThread writeThread=new  WriteThread();
        writeThread.start();
        Thread.sleep(1000);
        readThread.interrupt();
        System.out.println("main exit");

    }

}

当volatile变量时,可以确保volatile写之前的操作不会被编译器重排序到volatile写之后,读volatile变量时,可以确保volatile读之后的操作不会被编译器重排序到volatile读之前

伪共享

伪共享是什么

为了解决计算机系统中内存与CPU之间运行速度差问题,会在CPU与主内存之间添加一级或者多级高速缓存存储器(Cache)

一般被集成在CPU内部(CPU Cache)

在Cache内部是按行存储的,其中一行称为一个Cache行,Cache行是Cache与主内存进行数据交换的单位,每行的大小一般为2的幂次方字节

当CPU访问某个变量的时候,首先回去看CPU Cache内是否存在变量,如果有则直接从其中获取否则就去主内存中获取变量,然后把该变量所在的内存区域的一个Cache行大小的内存复制到Cache,由于存放到Cache行的内存块而不是单个变量,所以可能把多个变量存放到同一个Cache中,当多个线程同时修改一个缓存行的里面的多个变量的时候,由于同时只能有一个线程操作缓存行,所以相比将每个变量放到一个缓存行,性能会有所下降,这就是伪共享

多个线程不能同时去修改自己所使用的CPU中相同缓存行里面的变量,更坏的情况是,如果CPU只有一级缓存,则会导致频繁的访问主内存

为什么会出现伪共享

是因为多个变量被放入了一个缓存行,并且多个线程同时去写入缓存行中不同的变量

原因:

因为缓存与内存交换的数据的单位就是缓存行,所以多个变量会被放入同一个缓存行

public class ForContentTest {
    static final int LINE_NUM=1024;
    static final int COLUM_NUM=1024;

    public static void main(String[] args) {
        long[][] array=new long[LINE_NUM][COLUM_NUM];
        long startTime =System.currentTimeMillis();

        for (int i=0;i<LINE_NUM;++i){
            for (int j=0;j<COLUM_NUM;++j){
                array[i][j]=i*2+j;

            }
            long endTime=System.currentTimeMillis();
            long cacheTime=endTime-startTime;
            System.out.println("cache time :"+cacheTime);
        }
    }

}
public class ForContentTest2 {
    static final int LINE_NUM=1024;
    static final int COLUM_NUM=1024;

    public static void main(String[] args) {
        long[][] array=new long[LINE_NUM][COLUM_NUM];
        long startTime =System.currentTimeMillis();

        for (int i=0;i<LINE_NUM;++i){
            for (int j=0;j<COLUM_NUM;++j){
                array[j][i]=i*2+j;

            }
            long endTime=System.currentTimeMillis();
            
            System.out.println("no cache time :"+(endTime-startTime));
        }
    }
}

代码一比代码二快了不少原因

数组中数组元素的内存地址是连续的,当访问数组的第一个元素的时,会把第一个元素后的若干个元素一块存入缓存行,这样子顺序访问数组里面的元素时直接命中,就不会去主内存读取了.

第二个则是跳跃式的访问数组元素,不是顺序的,这样子破坏了程序访问的局部性原则,并且缓存是容量控制的,当缓存满了会根据一定淘汰算法替换缓存行,这会导致从内存置换过来的缓存行的元素还没等读取到就被置换了

所以在单个线程下顺序修改一个缓存行中的多个变量,会充分利用程序员运行的局部性原则,从而加速程序的运行,而在多线程下修改一个缓存行的多个变量时候就会竞争缓存行,从而降低程序运行性能

如何避免伪共享

通过字节填充的方式来避免该问题,也就是创建一个变量时使用填充字段填充改变量所在的缓存行,这样就避免了将多个变量存放在同一个缓存行中

public final static class FilledLong{
    public volatile long value;
    public long p1,p2,p3,p4,p5,p6;
}

假设缓存行为64个字节’

这六个变量加上volatile变量以及一个FilledLong类对象一共占用64个字节正好是可以存入同一个缓存行

小结

本节讲述了伪共享如何产生,如何避免,并证明在多线程下访问同一个缓存行的多个变量时才会出现伪共享,在单线程下访问一共缓存行里面的多个变量反而会对程序运行起到加速作用

锁的概述

乐观锁与悲观锁

悲观锁:

对数据被外界修改保持保守态度,认为数据很容易被其他线程修改,所以在数据被处理之前先对数据进行加锁,并在整个数据处理过程中,使数据处于锁定状态

实现:

往往依赖数据库提供的锁机制,即数据库中,在对数据记录操作前给记录加排他锁,如果获取锁失败,则说明数据正在被其他线程修改,当前线程则等待或者抛出异常,如果获取锁成功,则对记录进行操作,然后提交事务后释放排他锁

乐观锁:

数据在一般情况下不会造成冲突,所以在访问记录前不会加排他锁,而是在数据提交更新时候,才会正式对数据冲突与否进行检测.

公平锁和非公平锁

根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁

公平锁:

表示线程获取锁的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程最早获取到锁

非公平锁:

不一定先到先得

ReentrantLock提供了公平锁和非公平锁的实现

  1. 公平锁:ReentrantLock pairLock=new ReentrantLock(true)
  2. 非公平锁:ReentrantLock pairLock=new ReentrantLock(false)

如果不传递参数默认是非公平锁

公平锁会带来性能的开销

独占锁和共享锁

根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁

独占锁:

能保证任何时候都只能有一个线程得到锁ReentrantLock就是以独占锁方式实现的

是一种悲观锁,由于每次访问资源都先加上互斥锁,限制了并发性,因为操作并不会影响数据的一致性,而独占锁只允许同一时间由一个线程读取数据,其他线程必须等待当前线程释放锁才能进行读取

共享锁:

ReadWriteLock读写锁,允许一个资源被多个线程同时进行读操作是一种乐观锁,放宽了加锁的条件,允许多个线程同时进行读操作

什么是重入锁

当一个线程要获取一个被其他线程持有的独占锁时候,该线程会阻塞,

那么当一个线程再次获取他自己已经获取的锁时候是否会被阻塞呢?

如果不被阻塞那么该锁是可重入的,也就是说只要该线程获取了该锁,就可以无限次地进入该锁锁住的代码

自旋锁

当前线程在获取锁的时候,如果发现锁已经被其他线程占用,不会马上阻塞自己,在不放弃CPU的使用权的情况下,多次尝试获取(默认次数10)很有可能后面几次尝试中其他线程已经释放了锁,如果尝试指定次数之后仍然没有获取到锁则当前线程则会被阻塞挂起,自旋锁是适应了CPU时间旱区线程阻塞与调度的开销,但是很有可能这些CPU时间白白浪费啦

总结

本章主要讲述并发编程的基础知识,为后面在高级篇讲解并发包源码打下了基础并结合图示形象的讲述了为什么要使用多线程编程,多线程编程存在的线程安全问题,以及什么是内存可见性问题,然后讲述了synchronized和volatile关键字,并且强调前者既保证内存的可见性又保证了原子性,后者则主要保存了内存可见性,但是二者的内存语义非常相似,最后讲解了什么是CAS和线程间同步以及各组锁概念

猜你喜欢

转载自blog.csdn.net/weixin_60257072/article/details/130181594
今日推荐