一、并发基础补充
1、java中有几种新启线程的方式
严格来说只有两种,第一种通过继承Thread类;第二种通过实现Runnable接口,把Runnable对象作为参数传给Thread;
还有一种实现Callable接口的方法其实是Runnable方法的一个特例,我们不能把实现的Callable对象直接作为参数传给Thread,而是要将callable对象封装成FutureTask对象再作为构造参数传给thread,而FutureTask实现自RunnableFuture,RunnableFuture又继承自Runnable,所以说本质还是穿了一个Runnable;
2、线程的状态
严格来说只有synchronized关键字这一种方法能使线程进入阻塞状态,其他比如sleep、wait、join、lock.tryLock等均会进入等待或等待超时状态;
3、死锁
多个线程在执行过程中由于竞争资源或者彼此通信而造成的一种阻塞现象;
危害:线程不能继续正常工作了,但仍然是活着的;没有办法自动恢复,只能重启程序;
解决:内部通过顺序比较确定拿锁的顺序;采用尝试拿锁的机制;
4、其他线程安全概念
活锁:多个线程在尝试拿锁的过程中互相谦让,造成同一线程总是拿到同一把锁,在尝试拿另一把锁的时候因为拿不到而将已经持有的锁释放的过程;解决办法:让每个线程随机休眠一小段时间,错开拿锁的时间;
线程饥饿:低优先级的线程总是得不到执行;
5、ThreadLocal辨析
ThreadLocal可以让每个线程拥有一个属于自己的变量的副本,不会和其他线程的变量副本冲突,实现了线程的数据隔离;
我们可以通过成员之间的引用关系来理解threadlocal是怎么存储的,thread对象中存在threadlocalmap对象,而此map对象维护了一个entry数组,每个entry数组中存在多个entry,每个entry使用键值对的形式存储数据,其中key为threadlocal的弱引用,value为我们为线程隔离保存的变量值;
二、线程的并发工具类简介
forkjoin
forkjoin在处理分治算法的这类问题中非常有用,比如经典的快排、归并排序、二分查找等。forkjoin的原理就是在必要的情况下,将一个大任务拆分(fork)成若干小任务,再将一个个小任务运算的结果进行join汇总;
forkjoin使用的标准范式:
-
通过new方法创建ForkJoinPool对象;
-
创建用于执行任务的ForkJoinTask对象:
ForkJoinTask是抽象类,我们一般通过继承它的两个抽象子类RecursiveAction和RecursiveTask来实现我们自己的task任务,action用于没有返回值的任务,task用于有返回值的任务;我们需要在自己的任务类中实现compute抽象方法,在此方法中定义我们自己的处理逻辑,可以在此方法中继续细化任务,然后调用invoke方法执行细化的任务;通过任务的join方法返回细化任务的结果;
-
通过调用ForkJoinPool的invoke、execute或者submit执行第二部创建的任务对象: invoke用于同步执行,并且返回结果值; execute用于异步执行,无返回值; submit用于异步执行,有返回值;
-
调用我们实现的task对象的join方法等待结果,注意join方法是一个阻塞方法,调用后会一直等待直到任务执行完成;
CountDownLatch
称之为闭锁,这个类能够使一个线程等待其他线程完成各自的工作之后再执行;
实现原理:
通过一个计数器来实现,当我们新建一个CountDownLatch对象时传入一个数值作为等待任务数;首先在等待线程上调用CountDownLatch对象的await方法进入等待状态,每当子线程完成一个任务之后通过调用CountDownLatch的countDown方法将计数器减一,当计数器为0时表示所有任务都已完成,这时等待线程就可以和所有的子线程一起执行了。
注意,任务数与线程数没有必然的联系,可能某个子线程处理了好几个任务,即调用了多次countDown方法,也可能某个子线程一次countDown方法也没有调用;还有子线程调用完countDown方法之后可能还在继续运行当中,所以最后计数器变为0之后等待线程和继续运行的子线程同时运行;
三、CAS基本原理compare and swap
1、什么是原子操作?如何实现原子操作?
原子操作指的是一个操作要么全部执行完成,要不一点也不执行;synchronized加锁机制和CAS都能实现原子操作;但是由于synchronized加锁机制是一个非常重量级的操作,性能方面不如CAS;
2、CAS原理:利用现代计算机都支持的CAS指令,循环这个指令直到成功为止;
多个线程同时进行操作,只有一个线程进入cas原子操作,比较(compare)内存中变量的值和旧值是否相等,如果相等,将旧值swap为新值;如果不相等,说明有别的线程修改了此值,则使用内存中的新值重新进行一遍操作,在进行比较,如此循环进行下去;
3、CAS中存在的问题:
ABA问题:指的是另一个线程修改了值,但又很快修改了回去,另一个线程比较的时候发现值没变,以为没有线程操作过,其实是被修改过然后又被修改回来了;
开销问题:如果有很多线程对一个变量进行修改,那么会造成cpu执行很多的重复操作;
只能保证一个变量的共享操作;
4、JDK中提供的相关原子类:
基本类型原子类:AtomicInterger、AtomicBoolean、AtomicLong等
数组原子类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray等
引用类型原子类:
AtomicReference:此类可以用于解决CAS只能保证一个变量的共享操作问题,我们可以把想改变的变量封装成一个新的对象;
AtomicMarkableReference和AtomicStampedReference可以解决ABA问题,markable可以表示是否更改过,stamped可以表示如何更改过;