并发知识补充

130 阅读10分钟
  • java中启动新线程的方式

    • 概述:

      • java天生就是多线程的,只有两种新线程的方式面试亮点

        • 派生自Thread
        • 实现Runnable接口
        • 这个Callable,Thread是不能接受这个实例的,没有这种实例
        • 线程池里面也是不行的
  • java中的线程的状态(生命周期)

    • 终止一个线程:

      • 线程执行完run方法后抛出未被捕获的异常
      • 调用interrupt
    • 线程的状态:六种

      1. 初始:new出了一个新的线程对象

        • 但是new 出了一个新的线程对象并不意味着这个线程启动了,需要调用start方法,这个时候线程进入运行态(就绪态,运行中)
      2. 运行态

        1. 运行中:线程被分配了CPU时间片
        2. 就绪态:时间片用完了,或者被OS剥夺,或者自己放弃;那么就等OS分配时间片
      3. 等待状态:调用wait()

        • 调用:notify,notifyAll
      4. 等待超时:

        • wait是自己是不带时间戳(长度),没有人唤醒他,那么它就会一直等待
        • 此时可以在调用wait方法的时候传入一个时间长度,超过了这个时间还没有被唤醒,那么就自己从等待态恢复到就绪或者运行(要拿到CPU分配的时间片)
      5. 阻塞态:

        • 被同步代码块包裹(synchronized)的时候没有拿到锁,就阻塞;重新获取锁就进入运行态
      6. 终止状态:线程执行结束

      示意图:

图片.png - 常见问题:

    1.  调用sleep(1000)后,线程等待1S,这个时候线程进入什么状态?

        -   等待状态(主动调用某个方法后进来的)

            -   不会进入等待超时,sleep方法一般是带有时间戳的

    1.  如果使用了显示锁:比如说调用了lock但是没有拿到锁,那么线程进入什么状态?

        -   不会进入阻塞(被动进入,拿不到锁,不得不进来)态:只有在调用synchronized关键字之后,拿不到锁,才会进入阻塞态

        -   lock这种显示锁,在底层中使用的是LookSupport;

            -   没有拿到锁,这个时候会进入等待态或超时等待
            -   底层就很麻烦,在Linux底层中有API

    1.  在操作系统中会将运行态中的两个分开,java是合起来
  • 死锁:在计算机诞生的那一天就存在

    • 概述:

      • 两个(或以上)的进行在执行过程中,由于竞争资源或者彼此通信而造成的一种阻塞现象
      • 如果没有外力作用,那么他们无法继续推进下去,此时称系统处于死锁状态
    • 死锁的必要条件:

      1. 多个操作者(M>=2),争夺多个资源(N>=2),N<=M

        • 当单操作者的情况下,不会造成死锁
        • 当资源数只有一个,也不会造成死锁
      2. 操作者争夺资源的顺序不对

        • 规定了资源竞争循序,也不会造成死锁
      3. 操作者拿到资源不放手

        • 放手就不会有死锁
    • 学术化定义:满足死锁

      1. 互斥条件,资源别操作者独享
      2. 请求与保持,拿到了一个资源请求其他线程的资源
      1. 资源不剥夺,线程拿到的资源不能被剥夺
      2. 环路等待,拿了A想拿B,另外一个拿了B想拿A
    • 避免死锁的算法

      1. 有序资源分配法
      2. 银行家算法
    • 代码实现

      1. 定义两个锁

         private static Object No1 = new Object();
         private static Object No2 = new Object();
        
      2. 构建两个拿锁的方法

         //第一个拿锁的方法
         private static void thread1Catch() throws InterruptedException{
             String threadName = Thread.currentThread().getName();
             synchronized (No1){
                 System.out.println("thread1Catch catach the No1");
                 Thread.sleep(1000);
                 synchronized (No2){
                     System.out.println("thread1Catch catch the No2");
                 }
             }
         }
         //第二个拿锁的方法
         private static void thread2Catch() throws InterruptedException{
             String threadName = Thread.currentThread().getName();
             synchronized (No2){
                 System.out.println("thread2Catch catach the No1");
                 Thread.sleep(1000);
                 synchronized (No1){
                     System.out.println("thread2Catch catch the No2");
                 }
             }
         }
        
      3. 添加两个线程

         //子线程:Thread1
         private static class Thread1 extends Thread{
             private String name;
         ​
             public Thread1(String name){this.name = name;}
             
             @Override
             public void run() {
                 Thread.currentThread().setName("name");
                 try {
                     thread1Catch();
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
             }
         }
             public static void main(String[] args) {
                 //主线程,代表Thread2
                 Thread.currentThread().setName("Thread2");
                 Thread1 thread1 = new Thread1("Thread1");
                 thread1.start();
                 thread2Catch();
             }
        
    • 死锁的表现:

      1. 程序活的好好的,但是线程不干事情;
      2. 程序无法自己解决这个问题,只能重开
      3. 没有任何的报错信息,有相关的日志
    • 死锁的解决办法:

      1. 强制规定资源的竞争顺序,每次竞争同一个资源

      2. 解决拿到资源不放手的问题?

        • Lock接口:这个是在synchronized之外的,并且这个是花样拿锁

          • 花样拿锁

            1. 拿锁的时候可以中断
            2. 也可以尝试去拿锁
    • 使用Lock解决死锁问题

      • 概述:采用尝试拿锁的机制,tryLock():返回true,代表拿到了锁;然后再尝试去拿第二把锁;如果说拿到了,那么就进行业务代码;但是在第二次拿锁的时候,如果拿不到第二把锁,就将第一把锁也放弃;将这个代码块放在循环中;整体来说,同一时刻,只能有一个线程,拿到两把锁 即使两个线程拿锁的顺序不同也无所谓了

         //代码示例:
         private static Lock No13 = new ReentrantLock();//第一个锁
             private static Lock No14 = new ReentrantLock();//第二个锁
         ​
             //先尝试拿No13 锁,再尝试拿No14锁,No14锁没拿到,连同No13 锁一起释放掉
             private static void fisrtToSecond() throws InterruptedException {
                 String threadName = Thread.currentThread().getName();
                 Random r = new Random();
                 while(true){
                     if(No13.tryLock()){
                         System.out.println(threadName
                                 +" get 13");
                         try{
                             if(No14.tryLock()){
                                 try{
                                     System.out.println(threadName
                                             +" get 14");
                                     System.out.println("fisrtToSecond do work------------");
                                     break;
                                 }finally{
                                     No14.unlock();
                                 }
                             }
                         }finally {
                             No13.unlock();
                         }
         ​
                     }
                     //Thread.sleep(r.nextInt(3));
                 }
             }
        
      • 实现细节:为什么在while循环的末尾要加上短暂的休眠时间

        • 如果说去掉这个,将导致拿锁的时间被大大延长,加一个sleep(让出CPU,不会释放锁,写代码的时候一般将其放在synchronized之外),让他们拿锁的时间错开一点(很少的时间),此时引入活锁的概念

        • 活锁:一直在拿锁,释放锁

          • 概述:两个线程看起来十分忙碌,但是没有干什么实际的事情

          • 死锁:就是一直阻塞掉了,

             //线程A拿到了锁1<1>,此时尝试去拿锁2,拿不到,将锁1释放
             A<1>(2)---()()---<1><2>-----()()---<1><2>--……
             B<2>(1)---()()---<2>(1)---()()---<2>(1)……
            
          • 资源:锁,打印机的外设,句柄等能够被多线程持有的

    • 重要概念

      • 线程饥饿:线程的优先级太低了,一直拿不到CPU分配的时间片
      • 锁的初始化:
      • 锁的优化:
  • ThreadLocal

    • 概述:线程本地变量,也可以叫线程的本地存储;可以让线程拥有一份属于自己的本地变量副本,不会与其他的线程的变量副本冲突,实现了线程的数据隔离

    • 近似理解:跟Map做类比

      • 相当于搞了一个Map(<thread1,value>):根据线程拿到value,处理好了,然后再将value扔进去就行了

         public class MyThreadLocal<T> {
             /*存放变量副本的map容器,以Thread为键,变量副本为value,而且为了线程安全都加上锁*/
             private Map<Thread,T> threadTMap = new HashMap<>();
         ​
             public synchronized T get(){
                 return  threadTMap.get(Thread.currentThread());
             }
         ​
             public synchronized void set(T t){
                 threadTMap.put(Thread.currentThread(),t);
             }
         }
        
      • 测试系统的ThreadLoacal

         package cn.enjoyedu.concurrent.base.threadlocal;
         ​
         /**
          *@author Mark老师   享学课堂 https://enjoy.ke.qq.com 
          *
          *类说明:演示ThreadLocal的使用
          */
         public class UseThreadLocal {
         ​
             static ThreadLocal<String> threadLocal = new ThreadLocal<>();
             static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
         // static MyThreadLocal<String> threadLocal = new MyThreadLocal<>();
         ​
             /**
              * 运行3个线程,每个线程都给他搞一个id,第0号进程的id就是0
              */
             public void StartThreadArray(){
                 Thread[] runs = new Thread[3];
                 for(int i=0;i<runs.length;i++){
                     runs[i]=new Thread(new TestThread(i));
                 }
                 for(int i=0;i<runs.length;i++){
                     runs[i].start();
                 }
             }
             
             /**
              *类说明:测试线程,线程的工作是将ThreadLocal变量的值变化,并写回,看看线程之间是否会互相影响;
              * 将线程的自定义id(0号进程的id就是0)通过set传进去,通过get打印出来
              */
             public static class TestThread implements Runnable{
                 int id;
                 public TestThread(int id){
                     this.id = id;
                 }
                 public void run() {
                     String threadName = Thread.currentThread().getName();
                     threadLocal.set("线程"+id);
                     if(id==1) {
                         threadLocal2.set(id);//线程1才会执行
                     }
                     System.out.println(threadName+":"+threadLocal.get());
                 }
             }
         ​
             public static void main(String[] args){
                UseThreadLocal test = new UseThreadLocal();
                 test.StartThreadArray();
             }
         }
        
      • 运行截图(系统的ThreadLocal):

图片.png

  • 测试自定义的ThreadLocal

        ```
         package cn.enjoyedu.concurrent.base.threadlocal;
         ​
         
          *类说明:演示ThreadLocal的使用
          */
         public class UseThreadLocal {
         ​
         //    static ThreadLocal<String> threadLocal = new ThreadLocal<>();
         //    static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
             static MyThreadLocal<String> threadLocal = new MyThreadLocal<>();
         ​
             /**
              * 运行3个线程,每个线程都给他搞一个id,第0号进程的id就是0
              */
             public void StartThreadArray(){
                 Thread[] runs = new Thread[3];
                 for(int i=0;i<runs.length;i++){
                     runs[i]=new Thread(new TestThread(i));
                 }
                 for(int i=0;i<runs.length;i++){
                     runs[i].start();
                 }
             }
             
             /**
              *类说明:测试线程,线程的工作是将ThreadLocal变量的值变化,并写回,看看线程之间是否会互相影响;
              * 将线程的自定义id(0号进程的id就是0)通过set传进去,通过get打印出来
              */
             public static class TestThread implements Runnable{
                 int id;
                 public TestThread(int id){
                     this.id = id;
                 }
                 public void run() {
                     String threadName = Thread.currentThread().getName();
                     threadLocal.set("线程"+id);
         //            if(id==1) {
         //                threadLocal.set(id);//线程1才会执行
         //            }
                     System.out.println(threadName+":"+threadLocal.get());
                 }
             }
         ​
             public static void main(String[] args){
                 UseThreadLocal test = new UseThreadLocal();
                 test.StartThreadArray();
             }
         }
        ```
    
    • 运行截图(测试自定义的ThreadLocal):

      image-20220223100032429

    • 自定义ThreadLocal的问题?

      1. 只能放一个数据(将key换成Map就解决了)
      2. 线程并发:加了锁的
      3. 内部会产生激烈的竞争:使用ThreadLocal的本意就是降低竞争,让线程都有属于自己的一套副本,用了变量,然后放回去;但是用了HashMap,看起来是解决了这个问题了,但是实际上不是的;这个就相当于将每个线程的变量副本都放到一个柜子里面了;这个时候,线程竞争的就不是变量了,竞争的是这个柜子了;竞争到柜子后,再取出变量,处理好,再放回去;自定义的ThreadLocal会对Map产生激烈的竞争,JDK中不是这样去实现的
    • ThreadLocal的JDK内部实现原理:static ThreadLocal threadLocal = new ThreadLocal<>();

      • set方法

         public void set(T value) {
         //拿到当前线程,并将其作为参数传给getMap()函数
             Thread t = Thread.currentThread();
             ThreadLocalMap map = getMap(t);
             if (map != null) {
                 map.set(this, value);
             } else {
                 createMap(t, value);
             }
         }
        
      • getMap(t)

         ThreadLocalMap getMap(Thread t) {
             return t.threadLocals;
         }
        
      • threadLocals

         //ThreadLocalMap是当前线程的成员变量
         ThreadLocal.ThreadLocalMap threadLocals = null;
        
      • ThreadLocalMap(ThreadLocal.java):定义了Entry数组(以Thread作为键,Object作为value)

         static class ThreadLocalMap {
         ​
                 Entry(ThreadLocal<?> k, Object v) {
                     super(k);
                     value = v;
                 }
             }
         ​
                 private static final int INITIAL_CAPACITY = 16;
                 //Entry型的数组        
                 private Entry[] table;
        
      • 持有关系:类的定义位置没有任何问题,ThreadLocalMap是静态内部类,不依赖外部类

         class Thread{
             ThreadLocalMap threadLocals;        
         }
         class ThreadLocalMap{
                 Entry[]
         }
         class Entry{
                 ThreadLocal<?>k,Object v;
         }
        
      • 为什么要使用数组?每一个线程中有不同的容器(Entry型的数组,同时具体的threadLocal实例作为独立确定一条Entry)用来存放变量;同时对于每一个线程来说,是可以存放多个threadLcoal的实例的,所以在每个线程独有的ThreadLocalMap里面搞多个Entry数据条(也就是存在Entry[]):所以说,实现了线程的数据隔离;

      • 在所有的并发工具类中ThreadLocal的性能最好,这个就没有竞争了

      • 当调用set的时候,向Entry中加入一条数据,key就是ThreadLocal,value就是set的参数

  • CAS:现代CPU中提供的指令

    • 概述:比较交换,基于这个实现了很多的原子类(Atomic开头的都是原子类型的变量)

    • 重要概念:

      • 原子:不可再分的

      • 原子操作:要么内部操作都完成,要么内部操作一个都不干

        • synchronized包裹的代码块就是,没有插一脚的说法
      • 如何实现原子操作:

        • synchronized:是比较重,主要是去拿锁,当同步代码块的操作简单,那么就引入了轻量级原子操作(CPU中的CAS,比较交换(CPU决定的原子操作,安全))
    • CAS的工作表现:以i++,四个线程为例

      1. 四个线程拿到资源的初值

      2. 四个线程在线程内部进行计算(得到i++的结果)

      3. 此时引入比较交换(CAS)

        1. 比较:将线程中保存的资源的初值与当前资源的值进行比较(检测在线程计算过程中,这个资源有没有被修改(ABA问题))

        2. 交换:如果比较相等,那么执行交换操作,线程将自己计算出来的值,拿给这个资源

          • 不对的话,再把这个值取出来,再重复之前的操作,相当于一个循环,直到所有的线程都结束
    • 乐观锁与悲观锁

      • 悲观锁:认为总有人要改我的东西(synchronized)

      • 乐观锁:认为没有人改我的东西(直接拿值出来,先处理了),如果发生了改变,那么就执行CAS操作

      • 自旋:死循环,不断在这个上面访问

      • 性能:

        • 一般来说CAS(原子变量)都是比锁快的,因为线程拿不到锁,进入阻塞(存在上下文切换),一次情况下一次切换会消耗五千到两万个时钟周期;JDK的并发编程发展会向着无锁化发展,包括在synchronized底层也是大量使用了这种自旋的操作
        • synchronized在特定的场景下(竞争异常激烈,人为设置的)性能优于原子操作,开发基本遇不到
    • CAS的三大问题:不会淘汰锁

      • ABA:在线程1写入内存的时候发生,看程序猿是否介意变量是被改过(加一个版本戳)

        • 背景:变量需要从A成为B
        • 场景:线程1,执行A--->B,但是在线程1执行期间(这个时候,线程1将A作为参数进行计算B),此时闯入线程2(A--->C--->A),当线程1在写入内存的时候(进行比较交换)发现此时资源为A,跟线程1之前保存的资源初值相同,此时线程1就认为该资源没有被修改,线程1将B放入资源(但是是发生了改变的)
        • 就像有杯水,我喝了,然后接满了,杯子主人就认为没有人喝过;
      • 开销问题:自旋长期不成功(就考虑加锁)

        • 一直在尝试,每次都跑的慢,每次都要比较
      • 只能保证一个共享变量的原子操作:CAS一般是针对于计算机内存的具体地址

        • 多个线程共享变量,那就用锁了
  • CAS中的基本类型使用

    • 版本戳(解决ABA问题):在安卓开发中基本很少用到原子变量

      • AtomicMarkableReference:变量有没有被动过
      • AtomicStampedReference:变量被动过几次
    • 更新引用类型:(AtomicMarkableReference、AtomicStampedReference、AtomicReference)

      • AtomicReference:解决CAS只能保证一个共享变量的同步问题

        • 加入需要同时修改两个变量,想办法将这两个变量放到一个对象中去,将修改这两个变量替换为修改这个对象,改了之后直接用对象去替换原来的对象即可
    • 整形变量的处理

      • getAndIncrement与incrementAndGet:就是i++与++i的区别(最终会调用到native,看不到具体的实现,在java中的i++会创建新的对象)

         public class UseAtomicInt {
             static AtomicInteger ai = new AtomicInteger(10);
         ​
             public static void main(String[] args) {
                 //i++
                 ai.getAndIncrement();
                 System.out.println(ai);
                 //++i
                 ai.incrementAndGet();
                 System.out.println(ai);
             }
         }
        
      • 运行截图:

        image-20220223112222524

      • compareAndSet:同样执行变量加法,返回值不同(加了的新值,旧值)

         public class UseAtomicInt {
             static AtomicInteger ai = new AtomicInteger(10);
         ​
             public static void main(String[] args) {
                 //新值
                 ai.addAndGet(24);
                 System.out.println(ai);
                 
                 ai.getAndAdd(24);
                 System.out.println(ai);
             }
         }
        
      • 运行截图:

        image-20220223112534362