JAVA并发编程

并发编程:
synchronized (悲观锁,非公平锁 ) 与 lock(reetrantLock,readwriteLock)(非公平锁、乐观锁)

synchronized:1.6之前为重型锁:线程获取不到锁会直接进入(锁池)队列等待直到上一个线程释放锁之后才可以争取锁

                        1.6之后引入了锁升级概念:JAVA对象头部(MarkWord)包含锁的信息,锁升级状态由无锁到第一次加锁升级为偏向锁,第二次加锁发生锁竞争则升级为轻量级锁,轻量级锁通过CAS自旋尝试获取锁,如仍然获取不到锁则升级为重量级锁进入锁池等待即1.6之前的方式

reetrantLock:底层采用CAS+AQS方式实现的锁机制,当一个线程想要获取锁先通过CAS自旋方式尝试获取锁;如获取不到则将当前线程封装为Node对象并存入AQS双向队列中,

                          1:AQS双向队列中已有线程在等待则将当前线程挂接在已有线程后面并将指针互相连接上,且挂接成功后会将上一个Node节点的状态(waitStatus)更新为-1表示上一个Node节点后面挂接的Node节点可被唤醒,处于AQS队列的头部Node节点会通过CAS方式尝试获取锁资源

                          2:AQS双向队列中没有线程在等待,则会创建一个虚拟的头部节点(head)并将当前Node节点挂接在虚拟节点之后,且将虚拟头部节点(head)的状态(waitStatus)设置为-1,以便更高效率的唤醒后面的线程节点

AQS常见问题:

1:为什么要有一个虚拟的head节点:

      (1) 因为AQS提供了ReentrantLock的基本实现,而ReentrantLocka释放锁资源时,需要去考虑是否需要执行unparkSuccessor方法,去唤醒后继节点。

      (2) 因为Node节点中存在waitStatus的状态,默认情况下状态为0,如果当前节点的后继节点线程挂起了,那么就将当期节点的状态设置为-1。这个状态的出现是为了避免重复唤醒或者释放资源的问题。

      (3)因为AQS中排队的Node中的线程如果挂起了,是无法自动唤醒的。需要释放锁或者释放资源后,再被释放的线程去唤醒挂起的线程。因为唤醒节点需要从整个AQS双向链表中找打离head最近有效节点去唤醒。而这个找离head最近的Node可能需要遍历整个双向链表。如果AQS中,没有挂起的线程,代表不需要去遍历AQS双向链表去找离head最近的有效节点。

      (4)为了避免出现不必要的循环链表操作,提供了一个-1的状态。如果是第一个Node进来,必须先初始化一个虚拟的head节点作为头节点,来监控后继接节点中是否有挂起的线程。

2:AQS为什么选择使用双向链表,而不是单向链表

      (1)AQS中一般是存放没有获取到资源的Node,而在竞争锁资源时,ReentranLock提供了一个方法,lockInterruptibly方法,也就是线程在竞争锁资源的排队途中,允许中断。中断后会执行cancelAcquire方法,从而将当前节点状态置为1,并且从AQS队列中移除。如采用单向链表,当前节点只能找到后继或者前继节点。这样是无法将前继节点指向后继节点,需要遍历整个AQS从头或者从尾去找。单向链表在移除AQS中排队的Node时,成本很高。

     (2)当前在唤醒后继节点时,如果是单向链表也会出问题,因为节点插入方式的问题,导致只能单向的去找有效节点去唤醒,从而造成很多无效的遍历操作,如果是双向链表可以解决这个问题

readwriteLock:读读不互斥,读写互斥,写写互斥;与reetranLock的区别在于更在细化的对锁的颗粒度做了区别,以适用读、写的不同场景下的效率提升

concurrentHashMap:多线程并发的线程安全集合类;采用数组+单向链接+红黑树的数据结构 ,当数组长度大于64(实则为了优化效率,数组的查询时间复杂度为O(1))且单向链表长度大于8(优化效率,单向链接的查询时间复杂度为O(n))时单向链接将转化成红黑树结构(时间复杂度Ologn)

CAS:比较并交换;程序运行时会先从内存中获取数据到CPU进行计算,再将计算完的结果写入到内存           缺陷:CAS只能保证对一个变量的操作是原子性的,无法实现多行代码操作的原子性

                             1:如果多个线程同一时间从内存中获取同一份的数据进行计算则会引发数据不一致性的问题(其他线程不能及时拿到某一线程计算完写入的内存中的数据)

                              2:因此CAS的方式就是数据在CPU计算完之后写入到内存时,会比较此时内存中的值与当前CPU中获取的值是否一致,如一致则认为没有其它的线程修改过值正常写入,反之就认为值被其它线程篡改过则写入失败,再次通过CAS自旋方式直到写入成功为止

                              3:ABA问题,当有两个线程(线程1,线程2)并发执行,将内存中A 更新为B线程1首先获取到CPU时间分片开始执行,线程2可能由于网络或某种原因  处于阻塞状态。继而线程1执行完毕,此时内存中值被更新为B,如此时进来线程3且获得CPU的时间片开始执行操作将B更新为A ,执行完毕。继而线程2获得CPU时间片开始执行,发现内存的A与预期值A相等,就认为内存中的值未被其它线程篡改,从而直接修改值,引发数据问题

                               4:ABA问题解决,引入版本号机制,不只是比较预期值与原值的一致性且需比较本号的一致性,JAVA中的提供的类 AtomicStampeReference

                               5:自旋时间过长问题:(1)可以指定CAS的总共循环次数,如果超过这个次数,可将线程挂起或直接失败;(2)CAS执行失败一次后将操作暂存起来,在后面获取结果的时候再将暂存的操作全部执行返回最后的结果

volatile: 1:可见性(前提):CPU本身设置了三级缓存(L1、L2、L3),每次计算会从主内存中拉取数据保存到三级缓存中(提升CPU计算效率,如每次从主内存中获取数据效率较慢),但因此带来的问题是,CPU是多核的,每个线程的工作内存(CPU三级缓存)都是独立的,每个线程中做修改时,只修改自己的工作内存,没有及时的同步到主内存,导致数据不一致问题。

                 2:volatile关键字:用来修饰成员变量。如果属性被volatile修饰,相当于会通知CPU,对当前属性的操作,不允许使用CPU的缓存,必须去主内存操作

                      内存语义:volatile属性被写:当写一个volatile变量。JMM会将当前线程对应CPU缓存及时的刷新到内存中 

                                         volatile属性被读:当读一个volatile变量。JMM会将对应的CPU缓存中的内存设置为无效,必须去主内存中重新读取共享变量

                 3:synchronized、Lock、final 都可以解决可见性的问题

                      synchronized:涉及到了synchronized的同步代码块或者是方法,获取资源后,将内部涉及到的变量从CPU缓存中移除,必须去主内存中重新拿数据,且在释放锁之后,会立即将CPU缓存中的数据同步到主内存。                          

                       Lock:基于volatile实现的,Lock锁内部进行加锁和释放锁时,会对一个由volatie修饰的state属性进行加减操作,CPU会将修改的数据,从CPU缓存立即同步到主内存,同时也会将其它的属性也立即同步到主内存中。

                       final:final修饰的属性,在运行期间是不允许修改的,间接保证了可见性,所有多线程读取final属性值可定是一样的。       

                  4:有序性:Java程序被编译后,在执行前需要再次转为CPU可以识别的指令,CPU在执行这些指令时,为了提升执行效率,在不影响最终结果的前提下,会对指令进行重排。(为了尽可能的发挥CPU的性能)在多线程下指令乱排序可能会引发一系列问题, volatile同样可以做到禁止指令重排序的效果(内存屏障概念,将内存屏障看成一条指令,会再两个操作之间,添加上一到指令,这个指令就可以避免上下执行的其他指令进行重排序)。DCL(单例模式下的双重校验锁)     

 Executors创建线程池的几种方式:

  newFixedThreadPool:固定线程数的线程池,当添加的线程数超过定义的线程数时则进入阻塞队列(LinkedBlockingQueue),遵循FIFO(先进先出)原则等待线程池中有空闲的线程数时再 进行工作

  newScheduledThreadPool:固定线程数的定时任务线程池,底层采用延迟队列(DelayedWorkQueue),可按固定的周期或者延迟多久的时间执行任务

newCachedThreadPool:创建存活时间为60秒的线程池,当前第一次有任务进来会直接创建新线程,如60秒内再次有任务进入会复用已创建的存活线程执行任务,如60秒内无任务进入则会结束当前线程池。如60秒外有任务进入则会创建新的线程执行;因为任务只要提交线程池中,就必然会有线程处理

newSingleThreadExecutor:单例线程池,线程池中只有一个线程在工作,后续进来的任务会进入到阻塞对列中等待,遵循FIFO(先进先出)原则,因此单例线程池适合按顺序执行的一系列任务

 newWorkStealingPool:并行计算底层使用ForkJoinPool;核心思想分而治之,线程窃取;使用场景,将一个大的任务按照一定的规则拆分(需手动编写拆分逻辑) 并放到当前线程的阻塞队列当中,其它的空闲线程可以去处理有任务的线程的阻塞列队中的任务 ,优点:线程池中空闲线程可以被充分利用                     

线程数计算:

核心线程数=cpu核数 * cpu利用率 * (1 + w/c)=cpu核数 * (1-阻塞系数)

sleep、wait、join、yield

sleep:让线程休眠,期间会让出cup,在同步代码块中,不会释放锁

wait(必须先获取对应的锁才能调用):让线程进入等待状态,释放当前线程持有的锁资源只有在notify或者notifyAll方法调用后才会唤醒,然后争夺锁

join:线程之间协同方式,线程A必须等待线程B运行完毕后才可以执行,语法:ThreadB.join()

yield:让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。但实际中无法保证yield()达到让步的目的,因为让步的线程可能被线程调度程序再次选中

守护线程

JAVA中有两类线程,User Thread(用户线程)、Daemon Thread(守护线程)

任何一个守护线程都是整各JVM中所有非守护线程的保姆:

只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。Daemon的作用是为其他线程的运行提供便利服务,典型的应用就是GC

ThreadLocal

线程的本地变量不是存放在ThreadLocal实例中的,而是放在调用线程的ThreadLocals变量里面

ThreadLocal类型的本地变量是存放在具体的线程空间上的,其本身相当于一个装载本地变量的载体,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法时从它的threadLocals中取出变量。如调用线程一直不终止,那么这个本地变量会一直存放在他的threadLocals中,所以不使用本地变量的时候需要调用remove方法,防止内存泄漏

1:ThreadLocal是为了方便每个线程处理自己的状态而引入的一个机制。

2:每个Thread内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量,该成员变量用来存储实际的ThreadLocal变量副本

3:ThreadLocal并不是线程保存对象的副本,仅仅起到一个索引的作用。主要目的是为每一个线程隔离一个类的实例,实例的作用范围仅限于线程内部

预防死锁

互斥条件:同一时间只能有一个线程获取资源

不可剥夺条件:一个线程已经占有的资源,在释放之前不会被其它线程抢占

请求和保持条件:线程等待过程中不会释放已占有的资源

循环等待条件:多选线程互相等待对方释放资源

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值