一.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(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
3.JMM对这8种操作给了相应的规定:
-
不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
-
不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
-
不允许一个线程将没有assign的数据从工作内存同步回主内存
-
一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是对变量实施use、store操作之前,必须经过assign和load操作
-
一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
-
如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
-
如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
-
对一个变量进行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 进程号打印进程堆栈信息