JUC(尚GG)

一、Lock接口
1、管程(锁)

        管程在操作系统中叫做监视器(Monitor),在Java中叫做锁;锁是一种同步机制,保证在同一时间,只有一个线程可以访问被保护的资源(数据或是代码)

        JVM同步是基于进入和退出操作的,是使用管程对象进行实现的。

2、用户线程和守护线程

用户线程:主线程结束了,用户线程还在运行,JVM存活

守护线程:没有用户线程了,主线程也结束了,都是守护线程,JVM结束

3、synchronized关键字

修饰方法;修饰代码块;自动上锁和解锁,不会出现死锁的问题;

4、多线程编程步骤

        ①创建资源类,在资源类创建属性和操作方法

        ②创建多个线程,调用资源类的操作方法

5、创建线程的多种方式

        ① 继承Thread接口

        ② 实现Runnable接口

        ③ 实现Callable接口

        ④ 使用线程池

6、Lock接口

        为了避免出现死锁的情况需要将lock.unlock放在try...catch...finally中的finally里面

二、线程间通信
1、资源类操作方法

        ① 判断

        ② 干活

        ③ 通知

2、虚假唤醒

        虚假唤醒是一种现象,它只会出现在多线程环境中,指的是在多线程环境下,多个线程等待在同一个条件上,等到条件满足时,所有等待的线程都被唤醒,但由于多个线程执行的顺序不同,后面竞争到锁的线程在获得时间片时条件已经不再满足,线程应该继续睡眠但是却继续往下运行的一种现象。

        情景:AB提供苹果,CD吃苹果,苹果就是成员变量,如果苹果数为1,则AB不再提供苹果,等待CD吃苹果,当苹果数为0时,DC不再吃苹果,等待AB提供苹果;一般情况下苹果数值可能为1或0,如果出现虚假唤醒的情况,则苹果数可能大于1或者小于0;

        在资源操作类中,如果是用的if做判断,第一次A抢到CPU资源,会判断苹果数,如果等于1,则等待,如果不等于1则提供苹果,也就是苹果数加1;第一次A给苹果数加1,之后wait,后面A又抢到CPU执行资源,由于wait方法的特性是在哪里睡就会在哪里醒,导致这次醒来之后会进行加1操作,苹果数会大于1,小于1的情况和大于1的情况类似;

        解决办法是将判断写在while中,无论wait方法在哪里睡,在哪里醒,每次醒来都需要再次进行判断,而if不会;

三、线程间定制化通信
1、定制化通信

        线程的执行顺序是不确定的,是谁抢到资源谁就进行运算,定制化通信就是要让线程按照约定的顺序进行输出

四、ArrayList集合的线程安全

        ArrayList等集合的方法里面没有加上synchronized关键字,一边去一边放会产生并发修改异常

解决方案

1、Vector(JDK1.0)

        List<String> list = new Vector<>();

        所有方法都被synchronized修饰,安全,当多个线程一起竞争,效率很低

2、Collections

        List<String> list = Collections.synchronizedList(new ArrayList<>());

        所有方法都被synchronized修饰,安全,当多个线程一起竞争,效率很低

3、CopyOnWriteArrayList

        List<String> list = new CopyOnWriteArrayList<>();

        写时复制技术        

        1)CopyOnWriteArrayList实现了List接口,因此它是一个队列。
        2)CopyOnWriteArrayList包含了成员lock。源码里面可以看到CopyOnWriteArrayList的add方法里面加入了ReentrantLock,每一个CopyOnWriteArrayList都和一个监视器锁lock绑定,通过lock,实现了对CopyOnWriteArrayList的互斥访问。
        3)CopyOnWriteArrayList包含了成员array数组,这说明CopyOnWriteArrayList本质上通过数组实现的。
        4)CopyOnWriteArrayList的“动态数组”机制 -- 它内部有个“volatile数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile数组”。这就是它叫做CopyOnWriteArrayList的原因!CopyOnWriteArrayList就是通过这种方式实现的动态数组;不过正由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList效率很 低;但是单单只是进行遍历查找的话,效率比较高。
        5)CopyOnWriteArrayList的“线程安全”机制 -- 是通过volatile和监视器锁Synchrnoized来实现的。
        6)CopyOnWriteArrayList是通过“volatile数组”来保存数据的。一个线程读取volatile数组时,总能看到其它线程对该volatile变量最后的写入;就这样,通过volatile提供了“读取到的数据总是最新的”这个机制的 保证。
         7)CopyOnWriteArrayList通过监视器锁Synchrnoized来保护数据。在“添加/修改/删除”数据时,会先“获取监视器锁”,再修改完毕之后,先将数据更新到“volatile数组”中,然后再“释放互斥锁”;这样,就达到了保护数据的目的。

五、HashSet和HashMap线程不安全
1、HashSet不安全的解决方案:CopyOnWriteArraySet

         (1) CopyOnWriteArraySet继承于AbstractSet,这就意味着它是一个集合。

         (2) CopyOnWriteArraySet包含CopyOnWriteArrayList对象,它是通过CopyOnWriteArrayList实现的。而CopyOnWriteArrayList本质是个动态数组队列,

        所以CopyOnWriteArraySet相当于通过通过动态数组实现的“集合”!                 

        (3) CopyOnWriteArrayList中允许有重复的元素;但是,CopyOnWriteArraySet是一个集合,所以它不能有重复集合。因此,CopyOnWriteArrayList额外提供了addIfAbsent()和addAllAbsent()这两个添加元素的API,通过这些API来添加元素时,只有当元素不存在时才执行添加操作!

        (4) CopyOnWriteArraySet的“线程安全”机制,和CopyOnWriteArrayList一样,是通过volatile和互斥锁来实现的。

2、HashMap不安全的解决方案:ConcurrentHashMap 

        在 JDK1.7 版本中,ConcurrentHashMap 由数组 + Segment + 分段锁实现,其内部分为一个个段(Segment)数组,Segment 通过继承 ReentrantLock 来进行加锁,通过每次锁住一个 segment 来降低锁的粒度而且保证了每个 segment 内的操作的线程安全性,从而实现全局线程安全。
        为了进一步优化性能,在 jdk1.8 版本中,对 ConcurrentHashMap 做了优化,取消了分段锁的设计,取而代之的是通过 cas 操作和 synchronized 关键字来实现优化,而扩容的时候也利用了一种分而治之的思想来提升扩容效率

3、CAS(Compare and Swap) 自旋锁

        CAS机制中使用了3个基本操作数:内存地址V旧的预期值A,要修改的新值B

        更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

CAS的缺点:

1) CPU开销过大

        在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。

2) 不能保证代码块的原子性

        CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。

3) ABA问题(使用带版本号的原子引用解决)

        假设t1线程工作时间为10秒,t2线程工作时间为2秒,那么可能在A的工作期间,主内存中的共享变量 A已经被t2线程修改了多次,只是恰好最后一次修改的值是该变量的初始值,虽然用CAS判定出来的结果是期望值,但是却不是原来那个相当于是只关心共享变量的起始值和结束值,而不关心过程中共享变量是否被其他线程动过。有些业务可能不需要关心中间过程,只要前后值一样就行,但是有些业务需求要求变量在中间过程不能被修改。

ConcurrentHashMap中为什么 key 和 value 不允许为 null

        并发编程中,null 值容易引来歧义, 假如先调用 get(key) 返回的结果是 null,那么我们无法确认是因为当时这个 key 对应的 value 本身放的就是 null,还是说这个 key 值根本不存在,这会引起歧义,如果在非并发编程中,可以进一步通过调用 containsKey 方法来进行判断,但是并发编程中无法保证两个方法之间没有其他线程来修改 key 值,所以就直接禁止了 null 值的存在。

六、Synchronized锁的八种情况

public static synchronized void methodBoth()

public synchronized void methodSynchronized ()

public  static void methodStatic ()

public  void methodStatic ()

1、static+synchronized 锁的是当前类的class对象(锁大楼)

     (1)一个对象 + (static+synchronized方法) +  普通方法

        先执行普通方法,再执行static+synchronized方法

     (2)一个对象 + (static+synchronized方法) + synchronized方法

        先执行synchronized方法,再执行static+synchronized方法

     (3)两个对象 + (static+synchronized方法) + synchronized方法

        先执行synchronized方法,再执行static+synchronized方法

     (4)两个对象 + (static+synchronized方法) +   普通方法

        先执行  普通方法,再执行static + synchronized方法

2、synchronized锁的是调用方法的对象,即当前对象,即this(锁楼里面的某个房间)

        (1)如果是一个对象,里面有两个synchronized修饰的方法,当两个线程,调用同一对象的两个方法,则谁竞争成功谁先执行,没有竞争成功的需要等待前面的同步方法执行结束;

        (2)如果是两个对象,里面有都两个synchronized修饰的方法;当两个线程,调用同一对象的两个方法,则相互不干扰;

3、普通方法和锁无关,先执行
七、多线程的锁情况
1、公平锁

会判断当前队列是否为空,为空才会竞争锁资源;效率低;不会有线程竞争不到资源;

private final ReentrantLock = new ReentrantLock(true);

2、非公平锁

不会判断当前队列情况,直接竞争锁资源;效率高;会有线程竞争不到资源;

private final ReentrantLock = new ReentrantLock();

3、ReentrantLock

ReentrantLock是可重入锁

什么是可重入锁(递归锁)

        解释一:可重入就是说某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。

        解释二:可重入锁又称递归锁,是指同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提是锁对象得是同一个对象),不会因为之前已经获取过锁还没有释放而阻塞。

        main方法 + main方法中调用其他的方法

        main 方法获取锁

        m1方法获取锁

        m1方法释放锁

        main方法释放锁

4、死锁

        两个或者两个以上的进程在执行过程中,因为争夺资源而造成一种互相等待的现象,如果没有外力干涉,他们无法再执行下去。

产生死锁的四个必要条件

● 互斥:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

● 请求保持:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

● 不可剥夺:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。

● 循环等待:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

八、Callable接口
Callable接口Runnable接口
有返回值无返回值
无返回值会抛出异常不会抛出异常
实现方法为call()方法实现方法为run()方法
九、辅助类
1、CountDownLatch 线程计数器

CountDownLatch countDownLatch = new CountDownLatch(6);

        CountDownLatch 是通过一个计数器来实现的,当我们在 new 一个 CountDownLatch 对象的时候,需要带入该计数器值,该值就表示了线程的数量。

        每当一个线程完成自己的任务后,计数器的值就会减 1 。当计数器的值变为0时,就表示所有的线程均已经完成了任务,然后就可以恢复等待的线程继续执行了。

2、CyclicBarrier 循环栅栏

 3、Semaphore 信号灯

acquire()

release()

 十、对写锁

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。

乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量

表锁:锁住的是整个表的数据

行锁:锁住的是一行数据

读锁:共享锁,都有可能发生死锁

写锁:独占锁,都有可能发生死锁

读写锁:一个资源可以被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程,读写互斥,读读共享

 1、锁降级(写锁降级为读锁)        
 2、阻塞队列

  3、阻塞队列的分类

 ArrayBlockingQueue(常用):由数组结构组成的有界阻塞队列

LinkedBlockingQueue(常用):由链表组成的有界阻塞队列

DelayQueue:使用优先级队列实现的延迟无界阻塞队列 

ProrityBlockingQueue:支持优先级排序的无界阻塞队列

SynchronousQueue:不存储元素的阻塞队列,即单个元素的队列

LinkedTransferQueue:由链表组成的无界阻塞队列

LinkedBlockingDeque:由链表组成的双向阻塞队列

十一、ThreadPool线程池
1、概念

线程池(ThreadPool)是一种基于池化思想管理和使用线程的机制:它是将多个线程预先存储在一个“池子”内,当有任务出现时可以避免重新创建和销毁线程所带来性能开销,只需要从“池子”内取出相应的线程执行对应的任务即可。

2、使用线程池的好处

(1)降低资源消耗(复用线程,减少线程频繁新建、销毁等带来的开销

(2)提高响应速度

(3)提高线程的可管理性

3、线程池的特征

(1)线程池中的线程处于一定的量,可以很好的控制线程的并发量(银行的柜台)

(2)线程可以重复被使用,在显示关闭之前,都将一直存在

(3)超出一定量的线程被提交时需在队列中等待

4、线程池的创建

(1)通过 ThreadPoolExecutor 创建的线程池

(2)通过 Executors 创建的线程池(Executors 是工具类)

5、创建的线程池的分类

线程池的创建方式总共包含以下 7 种(其中 6 种是通过 Executors 创建的,1 种是通过 ThreadPoolExecutor 创建的):

(1)Executors.newFixedThreadPool():创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
(2)Executors.newCachedThreadPool():创建一个可缓存(可扩容)的线程池,若线程数超过处理所需,缓存一段时间后会回收多余的线程,若线程数不够,则新建线程;
(3)Executors.newSingleThreadExecutor():创建单个线程数的线程池,它可以保证先进先出的执行顺序;
(4)Executors.newScheduledThreadPool():创建一个可以执行延迟任务的线程池;
(5)Executors.newSingleThreadScheduledExecutor():创建一个单线程的可以执行延迟任务的线程池;
(6)Executors.newWorkStealingPool():创建一个抢占式执行的线程池(任务执行顺序不确定)JDK 1.8 添加
(7)ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置

代码底层都是去new ThreadPoolExecutor,其实就一种创建线程的方式,只是有了封装而已

6、多线程代码块

Executors.newFixedThreadPool()

public static void main(String args[]) {
    // 一池5线程 5个窗口
    try{
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        for(int i = 0; i<10; i++) {
            threadPool.execute(()->{
                System.out.println(Thread.currentThread().getName() + "办理业务");
            });
        }
    } catch(Exception e) {
        e.printStcakTrace();
    } finally {
        threadPool.shutdown();
    }
}
7、线程池七大参数

(1)corePoolSize核心线程数(常驻线程数),线程池中始终存活的线程数。

(2)maximumPoolSize最大线程数,线程池中允许的最大线程数,当线程池的任务队列满了之后可以创建的最大线程数。

(3)keepAliveTime最大线程数存活时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程。

(4)unit单位是和keepAliveTime存活时间配合使用的,合在一起用于设定线程的存活时间。参数 keepAliveTime 的时间单位有以下 7 种可选:

TimeUnit.DAYS:天
TimeUnit.HOURS:小时
TimeUnit.MINUTES:分
TimeUnit.SECONDS:秒
TimeUnit.MILLISECONDS:毫秒
TimeUnit.MICROSECONDS:微妙
TimeUnit.NANOSECONDS:纳秒


(5)workQueue阻塞队列,用来存储线程池等待执行的任务,均为线程安全。它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种,包含以下 7 种类型:

        1)ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列。
        2)LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列。
        3)SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们。
        4)PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。
        5)DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素
        6)LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。
        7)LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
        较常用的是 LinkedBlockingQueue 和 Synchronous,线程池的排队策略与 BlockingQueue 有关

(6)threadFactory:线程工厂,主要用来创建线程。

(7)handler:拒绝策略,拒绝处理任务时的策略,系统提供了 4 种可选:

AbortPolicy:拒绝并抛出异常。(默认策略)
CallerRunsPolicy:使用当前调用的线程来执行此任务。
DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。
DiscardPolicy:忽略并抛弃当前任务。

8、线程池的执行流程

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值