并发编程要点

一.Synchronized 与Lock 的区别1、Synchronized 内置的Java关键字,Lock是一个Java类2、Synchronized 无法判断获取锁的状态,Lock可以判断3、Synchronized 会自动释放锁,lock必须要手动加锁和手动释放锁!可能会遇到死锁4、Synchronized 线程1(获得锁->阻塞)、线程2(等待);lock就不一定会一直等待下去,lock会有一个trylock去尝试获取锁,不会造成长久的等待。5、Synchronized 是
摘要由CSDN通过智能技术生成

一.Synchronized 与Lock 的区别

1、Synchronized 内置的Java关键字,Lock是一个Java类

2、Synchronized 无法判断获取锁的状态,Lock可以判断

3、Synchronized 会自动释放锁,lock必须要手动加锁和手动释放锁!可能会遇到死锁

4、Synchronized 线程1(获得锁->阻塞)、线程2(等待);lock就不一定会一直等待下去,lock会有一个trylock去尝试获取锁,不会造成长久的等待。

5、Synchronized 是可重入锁,不可以中断的,非公平的;Lock,可重入的,可以判断锁,可以自己设置公平锁和非公平锁;

6、Synchronized 适合锁少量的代码同步问题,Lock适合锁大量的同步代码;

2.虚假唤醒:

在使用wait()和nitify()方法时,判断的条件不能使用if,须得使用while

虚假唤醒问题,就是用if判断的话,唤醒后线程会从wait之后的代码开始运行,但是不会重新判断if条件,直接继续运行if代码块之后的代码,而如果使用while的话,也会从wait之后的代码运行,但是唤醒后会重新判断循环条件,如果不成立再执行while代码块之后的代码块,成立的话继续wait。

原因:符合条件的线程结束后调用notifyall()可能会唤醒符合wait条件的线程,从而不wait直接就运行。

     private int num = 0;
 ​
     // +1
     public synchronized void increment() throws InterruptedException {
         // 判断等待  ,不能用if,而须用while;
         while (num != 0) {
             this.wait();
         }
         num++;
         System.out.println(Thread.currentThread().getName() + "=>" + num);
         // 通知其他线程 +1 执行完毕
         this.notifyAll();
     }

二:集合不安全

如List,在并发编程中List不是线程安全的

解决方案:

1.vector

2.Collections.synchronizedList(new ArrayList<>());

3.CopyOnWriteArrayList<>();

Set:CopyOnWriteArraySet<>();Collections.synchronizedSet(new HashSet<>());

Map:ConcurrentHashMap;

四个BlockingQueue Api: 实现类ArrayBlockingQueue

 

三:线程池的使用

线程的创建耗费资源,如果线程使用完还能重复使用,则可以节省资源:

线程池的好处:

1.降低系统资源

2.提高响应速度

3.方便线程管理

线程复用、可以控制最大并发数、管理线程;

 

由于使用Executors工具类创建的线程池,初始化最大的线程数是Integer.MAX_Value,可能会造成OOM内存溢出,同时为了清楚了解线程池的运行规则,线程池的创建须通过ThreadExecutorsPool的方式去创建,

四:ThreadExecutorsPool的三大方式和七大参数

三大方式:

ExecutorService threadPool = Executors.newSingleThreadExecutor();//单个线程

ExecutorService threadPool2 = Executors.newFixedThreadPool(5); //创建一个固定的线程池的大小

ExecutorService threadPool3 = Executors.newCachedThreadPool(); //可伸缩的

七大参数:

     public static void main(String[] args) {
         // 获取cpu 的核数
         int max = Runtime.getRuntime().availableProcessors();
         System.out.println("max = " + max);
         ExecutorService service =new ThreadPoolExecutor(
                 2,     //核心线程数,一直保持的
                 max,               //最大线程数
                 3,    //大于核心线程数的空闲线程的超时时间
                 TimeUnit.SECONDS,  //时间单位
                 new LinkedBlockingDeque<>(3),  //线程阻塞队列
                 Executors.defaultThreadFactory(),      //线程工厂
                 new ThreadPoolExecutor.AbortPolicy()   //拒绝策略
         );
         try {
             for (int i = 1; i <= 10; i++) {
                 service.execute(() -> System.out.println(Thread.currentThread().getName() + "ok"));
             }
         }catch (Exception e) {
             e.printStackTrace();
         }
         finally {
             service.shutdown();
         }
     }
    

四种拒绝策略:

 

五:ForkJoinTask

ForkJoin 在JDK1.7,并行执行任务!提高效率~。在大数据量速率会更快!

Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。Fork/Join框架要完成两件事情:

任务分割:首先Fork/Join框架需要把大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割

执行任务并合并结果:分割的子任务分别放到双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里,启动一个线程从队列里取数据,然后合并这些数据

ForkJoinTask使用Fork/Join框架,首先需要创建一个ForkJoin任务。该类提供了在任务中执行fork和join的机制。通常情况下我们不需要直接集成ForkJoinTask类,只需要继承它的子类,Fork/Join框架提供了两个子类:

RecursiveAction

用于没有返回结果的任务

RecursiveTask

用于有返回结果的任务

ForkJoinPool

ForkJoinTask需要通过ForkJoinPool来执行。

任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务(工作窃取算法);

Fork/Join框架的实现原理

ForkJoinPool由ForkJoinTask数组和ForkJoinWorkerThread数组组成,ForkJoinTask数组负责将存放程序提交给ForkJoinPool,而ForkJoinWorkerThread负责执行这些任务;

工作窃取:

实现原理是:双端队列!从上面和下面都可以去拿到任务进行执行!

执行完任务的线程可在获取未执行的任务;

指令重排

内存屏障的问题:首先是内存架构问题导致了指令的重排,根源是cpu运行速度远超主存的io速度导致计算机系统采用cpu、缓存、主存的架构,复杂系统缓存分三级,越靠近cpu的缓存速度越快数据越少,每一级是上一级的百分之八十左右数据量。具体的指令重排原理是:x=1、y=2、z=x+y,x地址在主存,y地址在缓存中,cpu会先获取缓存中的数据,再获取主存数据,所以是y=2、x=1、z=x+y的执行顺序,这是指令重排,缓存屏障:分写缓存屏障和读缓存屏障,写缓存屏障会把缓存的最新数据刷新到主存,主存对所有线程可见,所以来一个线程就不会发生指令重排,因为都访问主存,速度慢了;读内存屏障是将主存最新数据刷新到缓存,读取数据的时候都从缓存中读,快。

六:JMM和Volatile

1.对Volatile的了解:

Volatile是Java虚拟机提供的轻量化同步机制

1.保证可见性

2.不保证原子性

3.禁止指令重排

Volatile的可见性:

为了提高处理速度,处理器与内存不会直接通信,处理器会把速度从内存读到内部缓存中来,当处理器把更新的数据写回到内存中时,其他的处理器缓存中的数据可能会过时;

当用Volatile关键字修饰变量后,JVM会向处理器发送一条lock前缀指令,每个处理器会检查自己的缓存的数据是否过期

2.JMM

JMM:JAVA内存模型,不存在的东西,是一个概念,也是一个约定!

关于JMM的一些同步的约定:

1、线程解锁前,必须把共享变量立刻刷回主存;

2、线程加锁前,必须读取主存中的最新值到工作内存中;

3、加锁和解锁是同一把锁;

线程中分为 工作内存、主内存

8种操作:

Read(读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用;

load(载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中;

Use(使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令;

assign(赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中;

store(存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用;

write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中;

lock(锁定):作用于主内存的变量,把一个变量标识为线程独占状态;

unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;

image-20200812215247240

3.JMM对这8种操作给了相应的规定:

  1. 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write

  2. 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存

  3. 不允许一个线程将没有assign的数据从工作内存同步回主内存

  4. 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作

  5. 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁

  6. 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值

  7. 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量

  8. 对一个变量进行unlock操作之前,必须把此变量同步回主内存

4.Volatile保证可见性:

可见性:一个线程对共享变量的修改,更够及时的被其他线程看到

Volatile实现内存可见性是通过store和load指令完成的;也就是对volatile变量执行写操作时,会在写操作后加入一条store指令,即强迫线程将最新的值刷新到主内存中;而在读操作时,会加入一条load指令,即强迫从主内存中读入变量的值。

     private volatile static Integer number = 0;
 ​
     public static void main(String[] args) {
         //main线程
         //子线程1
         new Thread(()->{
             while (number==0){
             }
         }).start();
         
         try {
             TimeUnit.SECONDS.sleep(2);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         
         //子线程2
         new Thread(()->{
             while (number==0){
             }
 ​
         }).start();
 ​
         try {
             TimeUnit.SECONDS.sleep(2);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         number=1;    //当后面内存中的变量值改变时,线程中的值也被改变,就跳出循环
         System.out.println(number);
     } 

5.Volatile不保证原子性:

原子性:即不可再分了,不能分为多步操作。比如赋值或者return。比如"a = 1;"和 "return a;"这样的操作都具有原子性。类似"a += b"这样的操作不具有原子性,在某些JVM中"a += b"可能要经过这样三个步骤: ① 取出a和b ② 计算a+b ③ 将计算结果写入内存

     private static volatile int num = 0;
 ​
     private static void add(){
         num++;
     }
 ​
     public static void main(String[] args) {
         for (int i = 0; i < 20; i++) {
             new Thread(()->{
                 for (int j = 0; j < 1000; j++) {
                     add();
                 }
             }).start();
         }
         while (Thread.activeCount()>2){
             Thread.yield();
         }
 ​
         System.out.println("num = " + num);
     }

Num不是原子操作,因为其可以分为:读取Num的值,将Num的值+1,写入最新的Num的值。 对于Num++;操作,线程1和线程2都执行一次,最后输出Num的值可能是:1或者2

【解释】输出结果1的解释:当线程1执行Num++;语句时,先是读入Num的值为0,倘若此时让出CPU执行权,线程获得执行,线程2会重新从主内存中,读入Num的值还是0,然后线程2执行+1操作,最后把Num=1刷新到主内存中; 线程2执行完后,线程1由开始执行,但之前已经读取的Num的值0,所以它还是在0的基础上执行+1操作,也就是还是等于1,并刷新到主内存中。所以最终的结果是1

一般在多线程中使用volatile变量,为了安全,对变量的写入操作不能依赖当前变量的值:如Num++或者Num=Num*5这些操作。

使用atomic类来保证操作的原子性:

     private static volatile AtomicInteger num = new AtomicInteger();
 ​
     private static void add(){
         num.incrementAndGet();
     }
 ​
     public static void main(String[] args) {
         for (int i = 0; i < 20; i++) {
             new Thread(() -> {
                 for (int j = 0; j < 1000; j++) {
                     add();
                 }
             }).start();
         }
         while (Thread.activeCount() > 2) {
             Thread.yield();
         }
 ​
         System.out.println("num = " + num);
 ​
     }

6.禁止指令重排:

我们写的程序,计算机并不是按照我们自己写的那样去执行的

源代码–>编译器优化重排–>指令并行也可能会重排–>内存系统也会重排–>执行

处理器在进行指令重排的时候,会考虑数据之间的依赖性!

volatile可以避免指令重排:

volatile中会加一道内存的屏障,这个内存屏障可以保证在这个屏障中的指令顺序。

内存屏障:CPU指令。作用:

1、保证特定的操作的执行顺序;

2、可以保证某些变量的内存可见性(利用这些特性,就可以保证volatile实现的可见性)

7.Synchronized和Volatile的比较: 1)Synchronized保证内存可见性和操作的原子性 2)Volatile只能保证内存可见性 3)Volatile不需要加锁,比Synchronized更轻量级,并不会阻塞线程(volatile不会造成线程的阻塞 synchronized可能会造成线程的阻塞。) 4)volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化(如编译器重排序的优化). 5)volatile是变量修饰符,仅能用于变量,而synchronized是一个方法或块的修饰符。 volatile本质是在告诉JVM当前变量在寄存器中的值是不确定的,使用前,需要先从主存中读取,因此可以实现可见性。而对n=n+1,n++等操作时,volatile关键字将失效,不能起到像synchronized一样的线程同步(原子性)的效果。

七:CAS和ABA

悲观锁:具有强烈的独占和排他特性,总是假定最坏的情况,每次取数据时都认为数据被其他线程所更改;

乐观锁:相对于悲观锁而言,乐观锁总是假定是最好的情况,每次去数据都认为数据没有被其他线程所更改,但会在更新数据前判断数据有没有被更改,一般使用版本好机制判断。

什么是CAS:CAS就是Compare And Swap的简写,比较和交换,CAS包含三个操作数,V内存位置,E期望值,U新值,如果内存位置的值与期望值相匹配,则修改为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS也称自旋锁,它会在死循环里不断进行CAS操作,直到成功位置。实际上CAS也是一种乐观锁。

CAS目的:前面也讲到CAS是为解决非原子操作引起的并发安全问题(解决原子操作问题),同时优化性能(提高性能)而产生,CAS的原子操作是由CPU在指令级别上进行保证。

CAS操作的源码:

     public final int getAndAddInt(Object var1, long var2, int var4) {
         int var5;
         do {
             var5 = this.getIntVolatile(var1, var2);
         } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
 ​
         return var5;
     }

这是一次自加的操作,对象地址和值偏移量得到值v5,再用native方法再使用v1,v2参数在内存中获取最新值与v5进行比较,匹配后成功后,加1(v4),会在循环中直到成功,才跳出循环,返回v5,这是一个自旋操作。

有CAS操作就会有ABA问题:

ABA:其他线程把共享变量从A改成了B,很快又改回了A,CAS检查时看起来没有变化,实际上产生了变化。解决这个问题使用版本号,在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。同时,Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

使用带版本好原子引用类来解决ABA问题:

测试代码:

     static AtomicStampedReference<Integer> atomicStampedReference = new                      AtomicStampedReference<Integer>(1,1);
 ​
     public static void main(String[] args) {
         // CAS compareAndSet : 比较并交换!
             new Thread(() -> {
                 int stamp = atomicStampedReference.getStamp(); // 获得版本号
                 System.out.println("a1版本1=>" + stamp);
 ​
                 try {
                     TimeUnit.SECONDS.sleep(1);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 // 修改操作时,版本号更新 + 1
                 atomicStampedReference.compareAndSet(1, 2,
                         atomicStampedReference.getStamp(),
                         atomicStampedReference.getStamp() + 1);
 ​
                 System.out.println("a1版本2=>" + atomicStampedReference.getStamp());
                 // 重新把值改回去, 版本号更新 + 1
                 System.out.println(atomicStampedReference.compareAndSet(2, 1,
                         atomicStampedReference.getStamp(),
                         atomicStampedReference.getStamp() + 1));
                 System.out.println("a1版本3=>" + atomicStampedReference.getStamp());
             }, "a").start();
 ​
             // 乐观锁的原理相同!
             new Thread(() -> {
                 int stamp = atomicStampedReference.getStamp(); // 获得版本号
                 System.out.println("b1版本1=>" + stamp);
                 try {
                     TimeUnit.SECONDS.sleep(2);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
                 System.out.println(atomicStampedReference.compareAndSet(1, 3,
                         stamp, stamp + 1));
                 System.out.println("b1版本2=>" + atomicStampedReference.getStamp());
             }, "b").start();
         }

CAS实现原子操作的三大问题

  • ABA问题:其他线程把共享变量从A改成了B,很快又改回了A,CAS检查时看起来没有变化,实际上产生了变化。解决这个问题使用版本号,在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。同时,Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

  • 循环时间长开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。可以看到看到getAndAddInt方法执行时,如果CAS失败,会一直进行尝试。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

  • 只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。如:AtomicReference<User> ar = new AtomicReference<>();

八:各种锁的理解

公平锁:非抢占式的,FIFO,先来先进的;

非公平锁:非常不公平,允许插队,可以改变顺序;

可重入锁:即该线程已获得该锁的情况下还可以获取到这把锁;

自旋锁:在死循环中操作获得成功才能解除的锁;

 public final int getAndAddInt(Object var1, long var2, int var4) {
     int var5;
     do {
         var5 = this.getIntVolatile(var1, var2);
     } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
     return var5;
 }

死锁:两个线程互相等待对方持有的资源,但对方都不放手;

悲观锁:具有强烈的独占和排他特性,总是假定最坏的情况,每次取数据时都认为数据被其他线程所更改;

乐观锁:相对于悲观锁而言,乐观锁总是假定是最好的情况,每次去数据都认为数据没有被其他线程所更改,但会在更新数据前判断数据有没有被更改,一般使用版本好机制判断。

排除死锁:

使用jps -l指令获得java运行程序进程

使用jstack 进程号打印进程堆栈信息

img

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值