JUC

java util ConCurrent

线程的上下文切换:线程切换的时候,需要将程序的状态进行保存

线程和进程:
进程是某一个功能的应用程序运行的状态
线程是进程里的,一个进程可以有多个线程,线程比进程要小,执行在进程里执行

并发和并行
并发:同一时间段里,不同的线程访问同一个资源,会有线程资源抢占的问题
并行:同一时刻,不同的进程访问同一个的资源,不会有资源抢占的问题

serial和current
当程序执行时间较短时,那么单线程的执行效率较高
当程序执行时间较长时,那么并发的执行效率较高

Synchronized
1.标记在普通方法上,加锁的对象是this
2.标记在静态方法上,加锁的对象是当前类的Class对象
3.标记在同步代码块上,加锁的对象是括号里面的内容
命令: javap -v class字节码文件名 查看当前类字节码的结构信息
可以查看到Synchronized的状态
Synchronized底层实现原理:
新的线程进来之后,会争抢一个monitor监视器,如果争抢到了就会拿到一个对象锁,然后进行操作资源,然后释放锁;如果没有争抢到monitor监视器,那么就会进入到同步队列中,进入阻塞状态,在抢到的线程释放锁之后,会发送一个通知告诉同步队列的线程可以去争抢监视器了,线程去争抢监视器,争抢到的就去操作资源,没争抢到的就进入同步队列阻塞;拿到锁的线程操作资源的时候,如果调用了wait()方法,那么该线程将会进入到等待队列并释放锁,等其他线程调用notify()或者notifyAll()方法时,该线程将会进入到同步队列,准备抢夺监视器;
JUC

线程的状态:
new Thread开启一个新的线程,线程进入初始化,调用start()方法,线程会进入到运行前的就绪状态,如果拿到cpu权限,那么就会开始运行,运行中会受到一些影响,例如sleep、wait、join等进入超时等待状态,或者是等待状态,超时等待状态有一个等待时间,到达这个时间就会恢复到运行状态,也可以使用notify或者notifyAll等方法直接恢复到运行状态;而等待状态是没有时间期限恢复的,只能是手动调用notify或者notifyAll方法唤醒;如果资源上了锁,没抢到的线程将会进入阻塞状态,等待释放锁之后重新争抢锁;线程在执行完资源后就会终止;
JUC
sleep不会释放锁;wait会释放锁
开启Synchronized锁时,如果线程没争抢到锁,那么它会进行BLOCKED阻塞状态,实际没有进入到同步代码块里面;
开启Lock锁时,如果线程没争抢到锁,那么它会进入WAITING等待状态,实际进入到了同步代码块里面;

jps查看当前java程序的线程及进程号
jstack 进程号 可以查看该java进程的堆和栈的信息 可以看到线程的状态及其锁的情况

线程的死锁:
两个线程,互相拿着对方需要的锁不释放,两个线程都进入阻塞状态;
守护线程:
当一个线程守护另一个线程时,主线程如果挂掉了,那么守护线程也会马上挂掉,不会再执行剩下任何代码了,包括finally方法
使用setDaemon()方法设置是否开启守护线程,true代表开启
线程之间的通信(生产者和消费者问题): 1.判断 2.干活 3.通知
使用wait()和notify()进行线程间的通信,如果出现了多个线程,使用while进行判断(防止出现虚假唤醒)
notifyAll()不会释放锁,而是通知在等待队列的线程进入到同步队列中,准备争夺监视器;
Lock锁是先获得一个Condition对象,再调用await()方法和signal()方法进行线程间的通信

Look接口:
实现类:ReentratLock 可重入锁…
Lock锁更加面向对象 ,使用对象操作锁
Condition c = look.newCondition() 接口
await()方法类似obj的wait()
signal()方法类似obj的notify()
signalAll()方法类似obj的notifyAll()
使用Condition的方法比Object的好处在于效率较高,可以精准唤醒,标准位决定哪个线程被通知,不需要系统自己去随机

线程安全的集合:
1.谈谈你对集合的理解
集合是一个Collection接口和Map接口,其中Collection接口有2个子接口分别是List和Set;List的常用实现类用ArrayList和LinkedList,ArrayList是一个有序的、可重复的集合,它是线程不安全的,它的底层是边长数组实现的,默认初始长度是10,在第一次调用add方法的时候初始化,在jdk1.7之前是创建的时候初始化长度为10;ArrayList底层数组元素达到了最大长度时进行1.5倍扩容,数组的临界最大长度是int的最大值;LinkedList底层是一个双向链表数据结构,它是线程不安全的,它插入数据是根据上一个节点和下一个节点进行插入,这样的好处的插入的速度比较快,但是遍历的效率较低;一般遍历集合操作较多的时候我们用到ArrayList,而增删改集合操作较多的时候用LinkedList;如果需要使用线程安全的List可以使用Conllections工具类对不安全的集合转化成安全的,也可以使用Vector集合,它和ArrayList很相似,是线程安全的,最大的不同是它的扩容机制的2倍扩容;

另一个集合接口Set常用有3个实现类,HashSet、LinkedHashSet和TreeSet,HashSet是线程不安全的,特点是无序、不可重复性,它底层实际是一个HashMap,将key值用hashCode和equlas进行去重,value是new Object的一个常量固定值,取值的时候只取key值;LinkedHashSet是一个链表的集合,它是线程不安全的,适合做插入修改操作,不适合遍历;TreeSet是一个不安全的集合,它可以是有序的、不重复的集合;

还有一个集合接口是Map,常用的实现类有HashMap,HashTable,ConCurrentHashMap,HashMap是线程不安全的,d底层数据是node数组+node链表+红黑树;创建HashMap实例时,初始化数组长度为0,当第一次使用put方法添加kv键值对时,数组长度为16,底层会根据HashCode算法将key值得出一个数组的索引下标,然后将value值添加到这个下标所在位置,如果出现重复的下标就会有一个链表结构,通过equals判断元素是否重复,如果不重复则将该value添加到链表上;当数组元素达到数组长度加载因子0.75时,就会进行2倍扩容,如果数组元素达到了64以上或者node链表达到8个并且超过了数组长度0.75,那么底层数据结构就变为数组+红黑树;HashTable是线程安全的,它是将HashMap整个加了一把synchronized锁,效率比较低;ConCurrentHashMap是线程安全的,它是在底层每一个哈希槽上加一把锁,这样的话一个哈希槽一个线程访问,但其他的哈希槽可以被其他线程访问,效率比HashTable高很多;

2.线程安全和不安全的List
不安全:ArrayList、LinkedList
安全:Vector
Collections工具类可以将集合变为线程安全的
CopyOnWriteArrayList(多线程下推荐使用) 线程安全的list
写时复制,读写分离
当一个线程对ArrayList进行写操作时,会使用Arrays工具类的copy方法复制一份并将数组长度扩容+1,然后其他线程想来读arrayList时只读到原来那份数据,新copy的数组给当前线程进行写操作并且加lock锁,当写完成后将更新的数组覆盖掉原来的数组,然后释放锁;这样可以实现读和写的容器不一样,不会出现多个线程既要读又要写,出现并发修改异常;每次读操作都是原来版本的数组,写操作是copy数组和数组长度+1的数组
ConcurrentModficationException 并发修改异常

3.线程安全和不安全的Set
不安全: HashSet、LinkedHashSet、TreeSet
安全:CopyOnWriteArraySet
CopyOnWriteArraySet底层原理是将数组复制一份,在线程进来写的时候,其他线程不能进行写操作,如果进行读的操作,那么将会读到另一份没有在写操作的数组,在写操作完成后,那么另一份数组就会马上被销毁,然后又将新的数组重新复制一份出来,继续循环;这样可以避免出现并发修改异常;
4.线程安全和不安全的Map
不安全:HashMap、TreeSet
安全:HashTeble、ConCurrentHashMap(效率更高)
HashTeble是将整个HashMap加了锁,而ConCurrentHashMap是将每一个哈希槽加上锁,一共有16把不同的锁,这样一个线程在操作其中一个槽的时候,其他线程可以操作其他的槽,而HashTable中一个线程操作一个槽时,其他线程操作不了任何槽;

Callable接口:
和Runnable接口的异同?
1.有无返回值
Callable中方法的返回值和泛型类型一致
2.会不会抛异常
3.方法名 call — run
Runnable的子接口是RunnableFuture
RunnableFuture接口的实现是FutureTask
FutureTask的构造方法要么传入Callable接口要么传入Runnable接口

当我们实现Callable接口操作线程时,就使用FutureTask实例,将Callable接口传入,再将FutureTask丢到Thread中去,这样就能开启线程了;
FutureTask中的get方法是获取Callable接口call方法的返回值的,一般写在最后,call方法可以单独使用一个线程同时执行,在执行完后返回结果就行,最后get方法获取接口,这样可以防止线程阻塞,提高运行效率;

Lock锁(基于AQS实现):
ReentrantLock(可重入锁)
一个线程在外层拿到锁进入方法时,如果中层和内层也是同一把锁,那么这个线程不用释放锁可以直接进入到中层和内层,防止发生死锁;
ReentrantReadWriteLock(读写锁)
通过readLock()方法获得读锁对象
通过writeLock()方法获取写锁对象
读读共享、读写互斥、写写互斥

CountDownLatch 
        倒计时锁,在创建对象时候给定一个数字,会生成一个这个数字的计数器,其他线程调用countDown方法时,计数器会减1,当计数器变为0时,await方法被阻塞的线程会被唤醒,继续执行;

CyclicBarrier
        集齐七个龙珠召唤神龙;
        达到指定的数值,那么就会开始执行所有线程;
        构造方法第一个参数是数值,第二个参数是runnable接口开启的线程,满足所有条件后才会执行这个线程;其他线程每次执行完都调用await方法,数值都会+1,初始值为0,数值加到构造方法设置的数值时,就会执行第二个参数线程;和CountDownLatch效果相反;

Semaphore
        信号灯 抢车位;
        多个资源的互斥使用,并发线程数的控制
        创建对象的时候参数给定资源的数值,只允许该数值的线程同时访问;
        调用acquire方法表示当前线程抢占到了,资源数减1,finally中调用release方法,资源数加1,唤醒等待的线程;没抢到资源的线程进入等待;

阻塞队列BlockingQueue
不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,BlockingQueue帮我们做了,通过线程间的通信实现的;
Queue是Collection的子接口
BlockingQueue是Queue的子接口
BlockingQueue有7个实现类,常用的有3个

ArrayBlockingQueue
底层是一个数组组成的有界队列,必须给定数组长度大小;
put方法和task方法是同一把锁,所以put方法和task方法不能同时进行
LinkedBlockingQueue
底层是一个链表组成的有界队列,数组长度默认为Integer的最大值;
put方法和task方法不是同一把锁,所以put方法和task方法可以同时进行,适合做高并发访问;
SynchronousQueue
不储存元素的阻塞的队列,put方法建立在task方法上,必须不同线程一个添加元素,另一个获取元素;

必须阻塞: put()添加元素 take()获取元素 当队列是满的,添加元素就会被阻塞;当对列是空的,获取元素就会被阻塞
抛出异常:add()添加元素 remove()获取元素 element()查看首元素 当队列是满的,添加元素就会抛出异常;当队列是空的,获取元素就会抛出异常
特殊值:offer() 添加元素 poll()获取元素 当队列是满的,添加元素就会返回false;当队列是空的,获取元素就会返回null;如果正常情况添加获取会返回true
超时:设置超时时间;offer(value,time,unit) poll(value,time,unit) 当队列满的添加元素或空队列获取元素就会进入阻塞,达到给定的时间还是阻塞就退出队列;

JUC

CAS:
CompareAndSet
比较并交换,Unsafe类加自旋锁、乐观锁实现
用于保证操作内存中变量的原子性和可见性;
AtomicInteger、AtomicBoolean…原子类
三个值:内存地址、预期值、更新值

有两个线程,都将内存中的变量拷贝到自己的工作内存中,当一个线程先操作更改了这个变量并且写入到了主内存中,此时另一个线程就会使用cas判断拷贝来预期的值和此时内存中的值是否一样,如果一样才会进行写入主内存;如果不一样就会重新去获取一次主内存中变量的值,再拷贝到工作内存中,此时又会使用cas判断预期值和主内存中的值是否一样,一样则写入主内存,否则再重新获取;

CAS算法只能判断一个变量的预期值和内存的值是否一样,synchronized可以判断一个代码块中所有的变量;
CAS在线程数少的时候效率比synchronized要高
使用CAS会出现ABA问题,解决ABA问题需要加一个版本号,写入的时候需要再判断版本号是否相同来决定更新;
使用CAS对cpu的开销较大

使用原子整型类操作i++底层原理:
调用getAndIncrement()方法,实际上调用的是Unsafe类的getAndAddInt(obj,valueOffset,update)方法,会将存放这个变量的引用地址和该对象传入,在方法中有一个do-while循环,在do中调用getIntVolatile方法,这是一个native本地方法,将对象和变量的引用地址传入,去主内存中获取到这个变量的值,赋值给var5;
在while循环中,调用compareAndSwapInt方法,这是一个native本地方法,将去主内存对这个变量的值和刚刚拿到的工作内存中的值进行比较,如果不相同的话则修改失败,返回false,此时whlie判断条件前加了取反号,变为true,就会再次进行循环,再次去主内存获取到这个变量的值,再进入比较并交换的方法,直到比较期望值和内存值相同,就进行修改;返回true返回取反变为false,跳出循环,返回最新值;

ABA问题:狸猫换太子
线程从主内存中拷贝变量到自己工作内存中,如果线程在操作native方法时被挂起,有别的线程将主内存中的值改成别的,又将主内存中的值改回来;线程唤醒后去操作时根据CAS比较并交换发现值和原来一样,然后操作成功; 虽然主内存中的那个值最终和原来一样,但不代表这个程序是没问题的;

如何解决ABA问题?
修改版本号
使用AtomicStampedReference解决ABA问题
泛型是需要操作的对象类型

线程池:
为什么要使用线程池?
当我们要使用多线程的时候,每次都要创建一个Thread对象,然后调用start方法开启线程,如果有上千个线程那么就要在堆里new上千次Thread对象,这样太损耗性能和资源,所以如果我们有一个线程池,每次使用多线程的时候去线程池中拿一个线程对象,用完之后再放回去,这样可以节省很大的资源,提高性能;
线程池的优势:资源复用、管理线程、控制最大并发数
如何使用线程池
Executor 线程池接口
ExecutorService 子接口
Executors(大厂一般禁用) 线程池的工具类
1.newFixedThreadPool()方法创建固定线程数量的线程池,参数表示线程对象数量,适合长期任务;
2.newSingleThreadExecutor()方法创建单个线程的线程池,并且保证了执行任务的有序性;
3.newCacheThreadPool()方法创建可扩展的线程池,多个线程执行,适合短期异步任务;
一般都禁用Executors工具类中的线程池,因为前两者都是默认创建了LinkedBlockingQueue队列,请求队列大小为Integer的最大值,会消耗太多内存资源;
可扩展线程池的核心线程数为0,最大线程数是Integer的最大值,浪费内存资源

ThreadPoolExecutor
是ExecutorService的实现类;
构造方法参数:
第一个参数 corePoolSize :核心线程数
第二个参数 maximumPoolSize :最大线程数
第三个参数 keepAliveTime :空闲线程数经过该时间后如果还是空闲状态就被清理掉
第四个参数 unit :第三个参数的时间单位
第五个参数 workQueue :需要执行的线程大于核心线程数时,未被执行的线程就放到这个阻塞队列中,队列必须是BlockingQueue的实现类
第六个参数 threadFactory :线程工厂,用于生成线程池中的线程对象
第七个参数 handler :当线程数大于最大线程数加队列长度时,线程的拒绝策略

线程池的工作原理:
1.创建线程池后,当前线程数为0
2.在提交任务的时候,线程池根据任务数进行判断后创建线程;当任务数小于核心线程数时,线程池会马上创建核心线程;当任务数大于核心线程数时,未被执行的任务就会进入到阻塞队列中等待被执行,此时只有核心线程会执行任务;当任务数占满阻塞队列并且小于最大核心数+阻塞队列时,线程池会创建非核心线程,执行在队列外面的任务;当阻塞队列占满并且任务数大于最大核心数+阻塞队列时,线程池会启用拒绝策略;
3.当执行完任务后调用shutdown方法放回线程对象;
4.当空闲线程一段时间后依然处于空闲状态,那么它就会被清理掉;

JUC

JUC

拒绝策略:
1.AbortPolicy
直接抛异常 拒绝处理任务
2.CallerRunsPolicy
将超出的任务交给请求任务的线程处理,不让线程池进行处理;
3.DiscardOldestPolicy
抛弃队列等待最久的任务,处理新请求的任务
4.DiscardPolicy
抛弃新请求的任务;

线程池配置合理的最大线程数:
先调用Runtime.getRuntime.availableProcessors()获取当前服务器的cpu线程数;
如果是CPU密集型,设置cpu核心数+1的线程池数
如果是IO密集型,大量空闲时间,cpu核心*2

LockSupport:
线程的等待唤醒机制的加强改良版;
park() unpark() 阻塞线程 解除阻塞线程

回顾线程的等待唤醒机制:
1.Object类中的wait和notify
a.如果将synchronized同步代码块注释掉,那么等待唤醒机制就会报错
b.如果先进行notify再进行wait,那么就会一直处于等待状态,没有线程唤醒它
总结:1.使用wait和notify进行等待唤醒机制,那么必须在同步方法或者同步代码块里;
2.必须先wait再notify

2.Condition中的await和signal
a.如果将lock和unlock注释掉,那么等待唤醒机制就会报错
b.先signal再await,会一致处于等待状态,没有线程唤醒它
总结:和上面一样;

3.使用LockSupport的park和unpark
使用了一个凭证,为0的时候阻塞,为1的时候唤醒,默认为0,最大值只能为1;
park和unpark方法底层实际调用的是UNSAFE.park,UNSAFE的park方法调用实际上是native本地方法;
unpark可以在park之前执行
LockSuppot是一个阻塞工具类,park方法每次调用都需要消耗一个凭证然后正常退出,否则就会阻塞;而unpart方法则生成一个凭证,凭证的累加值最大为1,所以unpart方法先执行一次生成一个凭证,再调用part方法会马上消耗掉这个凭证,相等于没有阻塞;但是如果调用多个unpart方法也不会生成多个凭证,多个part方法调用会出现阻塞情况;

AQS:
AbstractQueuedSynchronizer 抽象的队列同步器
AQS中主要属性:head(头节点)、tail(尾节点)、state(持有锁的状态)他们全是用volatile修饰的
AQS中有一个静态内部类Node,用于封装线程的节点,其中主要属性有:waitStatus(线程的状态)、prev(前一个节点)、next(后一个节点)、thread(线程)、nextWaiter(下一个节点服务)他们全是用volatile修饰的
CLH队列(变种双向链表)+state实现同步机制

AQS继承关系
JUC
JUC
AQS底层原理:
以ReentrantLock非公平锁的lock()方法为例,来看看加锁和释放锁的流程

1.调用lock方法,实际调用的是内部类Sync的lock方法,判断当前是公平锁还是非公平锁,如果是非公平锁则调用的是NonfairSync类的lock方法
JUC
2.使用cas尝试设置锁的状态值,由0设置为1,如果成功说明当前线程抢占锁成功,将当前线程设置成资源持有的线程

3.如果设置失败说明第一次抢占锁失败了,将进入到acquire方法,也是核心的方法
JUC

4.先进入tryAcquire方法,此方法是aqs提供的模板方法,子类必须实现否则抛异常
JUC
5.非公平锁实际调用的是nonfairTryAcquire方法,此方法主要是尝试再次抢占锁
JUC
获取到锁的状态值,如果状态值为0则进行cas设置状态值为1,如果设置成功将当前线程设置成资源的持有线程;
如果获取失败判断当前线程是否是资源的持有线程,主要体现在可重入锁;
如果以上都不是则返回false
继续进入到addWaiter方法

JUC
6.将当前线程封装成一个Node节点,获取到CLH队列的尾节点,如果尾节点为空说明是第一次添加队列,调用enq初始化队列,并将当前线程节点传入

JUC
7.进入自旋,如果尾节点为空,则cas设置一个新建的空节点(哨兵节点)为头节点,如果设置成功将尾节点引用指向头节点;
第二次执行尾节点是哨兵节点,走else代码块,将当前线程节点的上一个节点指向傀儡节点,并将队列的尾节点cas设置成当前线程节点,并将傀儡节点的下一个节点指向当前节点,此时当前节点入队列成功
8.此时将进入到acquireQueued方法
JUC

判断当前线程是否中断获取锁,默认认为是不中断的;
进入自旋,将当前线程节点的上一个节点获取到,也就是傀儡节点,判断傀儡节点是否为头节点,返回true后指向tryAcquire方法再次尝试抢占锁,抢占成功则将当前线程节点设置成头节点,并将前一个节点指向null,将傀儡节点的后一个节点指向null,当前线程节点变为新的傀儡节点
如果再次抢占失败,则进入到shouldParkAfterFailedAcquire方法
JUC 9.获取到傀儡节点的线程状态,如果线程不为-1也不大于0,则cas修改为-1;
返回false再次走进这个方法,此时线程状态为-1,则返回true,将进入parkAndCheckInterrupt方法

JUC
10.将当前线程使用LockSupport.park方法进行阻塞
11.来到unlock方法的源码,实际调用的是Sync类的release方法

JUC
进入到ReentrantLock的tryRelease方法
JUC
将获取到当前的锁状态减去1得到变量c
如果当前线程不是资源占有的线程,那么抛监视器状态不合法异常
如果变量c等于0,则将资源占有线程设置成null
将最新的锁状态设置成c也就是0
返回true
12.如果头节点不为空且头节点的状态值不为0则进入unparkSuccessor方法
JUC
获取到头节点的线程状态,当前是-1,则将线程状态设置成0,如果头节点的下一个节点不为空,则调用unpark方法唤醒这个线程
13.回到parkAndCheckInterrupt方法,此时线程会被唤醒,再次进入到自旋中的判断

JUC

14.再次尝试抢占锁,如果抢占成功则将当前线程设置成资源占有的线程,并且将队列头节点设置成抢占成功的线程节点,并将上一个节点指向null,将傀儡节点的下一个节点指向null,这样旧的傀儡节点会被GC回收,新的傀儡节点就是这个线程节点,然后跳出循环,去执行相关业务代码了。

Synchronized原理:

jdk1.6之前是重量级锁,每次都是上锁获取monitor监视器对象,那么状态值+1,释放掉monitor监视器对象状态值-1,没抢到monitor的队列将会被放在EntryList队列里等待其他线程释放monitor,而线程被执行wait、sleep等方法时会进入WaitSet队列等待。

jdk1.6之后对syn锁进行了优化;给锁进行了状态优化,分为无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态;
偏向锁:比较线程id
轻量级锁:cas争抢锁,自旋,锁膨胀
重量级锁:只能使用monitor来控制同步了
锁只能升级无法降级;

上一篇:JUC之AbstractQueuedSynchronizer-ConditionObject


下一篇:Git学习:idea连接远程仓库