Java 多线程知识点

Java多线程并发


Java 多线程知识点_第1张图片
知识结构

一、 java多线程创建方式

  1. 继承Tread类

    将自己的类继承Tread类,并重写run()方法。

    Tread类的start()方法是一个native方法

  2. 实现Runable接口

    将自己的类实现Runable接口,重写run方法。

    实例出一个task。再实例化一个Thread,并传入task。

  3. 实现Callable接口(有返回值)

    执行callable task 可以返回一个Future 对象,通过get方法能获得任务的返回值。

二、 4种线程池


  1. newCachedTreadPool

    适用于短期异步任务。

    • 当任务申请线程,若有空闲线程,则重用这个空闲线程。
    • 若无空闲线程,则创建新线程。
    • 会终止并从缓存移出60秒未被使用的线程。
  2. newFixedTreadPool

    固定数量的线程池,用队列排队。
    适用于子啊任意点大多数线程都处于活动状态的程序。
    有任务申请线程

    • 队列满,排队等待,不满,得到线程。
    • 若某个线程异常终止 , 创建新线程继续任务。
    • 在线程被显式关闭前,线程将一直存在于线程池。
  3. newScheduledThreadPool

    可设置内部线程延迟执行或定期执行。

  4. newSingelThreadExcutor

    线程池只有单个线程。

三、 线程生命周期


Java 多线程知识点_第2张图片
线程生命周期
它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5 种状态。
  1. 新建状态。JVM分配内存,并初始化成员变量。

  2. 就绪状态。调用start()方法后,处于就绪状态。JVM创建方法栈和PC,等待调度运行。

  3. 运行状态。得到CPU控制权,开始执行run方法。

  4. 阻塞状态

    让出CPU使用权

    • 等待阻塞 object.wait。JVM将线程放入该对象的等待锁池。
    • 同步阻塞 lock 。同步锁被别的线程占用,JVM将线程放入锁池(look pool) ;
    • 其他阻塞 sleep/join 或IO ,不释放锁,当超时,或IO结束,线程重新变成Runable。

四、 终止线程的4种方式


  1. 正常结束

  2. 使用退出标志
    对于一些守护线程,当外部条件满足时才能退出。这个时候可以用类似于While(!biaozhi) 循环 的方式控制线程是否终止。

  3. Interrupt()方法
    该方法实质是设置中断标志位为true ,程序员通过检测中断标志位来结束线程。

    • 线程处于阻塞状态: 如调用了sleep、wait等方法后线程阻塞。此时调用线程的interrupt方法会抛出InterruptException异常。通过捕获这个异常然后break跳出循环,终止线程。

    • 未阻塞 :通过调用isInterrupted() 判断线程中断标志位来退出循环。

      while (!isInterrupted()) {to do}

    注意 interrupted()方法返回状态后,会重置状态。

    public class ThreadSafe extends Thread {
    public void run() {
    while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
        try{
            Thread.sleep(5*1000);//阻塞过程捕获中断异常 来退出
        }catch(InterruptedException e){
            e.printStackTrace();
            break;//捕获到异常之后,执行break 跳出循环
            }
            }
        }
    }
    
  4. 使用stop方法.不安全

    线程会突然释放所有的锁,可能导致数据的破坏。

五、 sleep 和 wait 的区别


  1. sleep属于 Thread 类 . wait 属于Object类

  2. sleep后会自动恢复运行,而wait需要notify

  3. sleep不会释放锁,wait释放锁

六、 start和run的区别


  1. start() 是线程启动方法。run是线程业务代码方法

  2. start() 之后创建线程处于就绪状态,如果直接调用run()。会在主线程直接运行业务代码。

七、 守护线程


为用户线程提供服务的线程

设置方法setDaemon(true)

特性

  1. 守护线程可以不依赖于控制终端(应用)而依赖于系统。

  2. 在所有用户线程结束之后再自动结束。

八、java锁


  1. 乐观锁
    读数据的时候不上锁, 写数据的时候上锁。

    写入的时候先读出版本号V1,然后进行CAS原子操作。比较V1和加锁后的V2是否相同,若相同,更新;若不同,更新失败。

  2. 悲观锁
    读写数据的时候都加锁。
    比如: Synchronized 同步 和 RetreenLock 锁

  3. 自旋锁

    让未获得锁的线程不挂起,仍然保持内核态。等待锁的释放。此时会消耗cpu。

    适用于持有锁的线程会在短时间释放锁的情况。

    优点:

    • 减少线程阻塞,提高性能。自旋消耗小于线程阻塞挂起和幻想的消耗。

    • 若持有锁的线程长时间占有锁。则自旋所需的cpu性能白白消耗,同时是的其他需要cpu的线程没有获得cpu造成浪费。

    自旋锁时间阈值
    自旋周期jad1.5的时候固定的,jdk1.6的时候是自适应的。

  4. Synchronized同步锁

    悲观锁。独占式、可重入。

    • 作用于方法, 锁实例。当调用实例中被锁方法,阻塞
    • 作用于静态方法, 锁Class实例。Class相关数据只存在于方法区(元空间) ,即锁住所有调用该方法的线程。
    • 作用于对象时,当调用被锁实例中任何方法,阻塞。

    核心组件

    Java 多线程知识点_第3张图片
    Synchronized的组件

    1) Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
    
    2) Wait Set:哪些调用wait 方法被阻塞的线程被 放置在这里;
    
     3) Entry List:Contention List 中那些有资格成为候选资源的线程被移动到Entry List 中;
    
    4) OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为OnDeck;
    
    5) Owner:当前已经获取到所资源的线程被称为Owner;
    
    6) !Owner:当前释放锁的线程。
    

    Synchronized 实现

    1. 当 Owner 线程 unlock时, JVM将EntryList中的一个线程移入OnDeck 。同时,将ContentionList中的一些线程移入EnryList。
    2. onDeck中的线程有竞争锁的权利,而不是直接得到锁。它与处于自旋状态的线程竞争锁。
    3. 持有锁的线程调用wait方法, 该线程释放锁,并进入Wait Set--等待池。当调用notify时,将Wait Set 中线程加入EnryList--有资格竞争锁的等待队列。
    4. Synchronized是非公平锁,线程在进入ContentList之前,会先尝试获得自旋锁,并可能直接与OnDeck中的线程产生竞争,抢占锁资源。
    5. 加锁实际上是在竞争monitor对象
  1. ReentrantLock 可重入锁

    ReentrantLock 和 synchronized都是可重入锁.
    可重入锁与不可重入锁的区别

    不可重入锁:只判断这个锁有没有被锁上,只要被锁上申请锁的线程都会被要求等待。实现简单
    
     可重入锁:不仅判断锁有没有被锁上,还会判断锁是谁锁上的,当就是自己锁上的时候,那么他依旧可以再次访问临界资源,并把加锁次数加一。
    
     设计了加锁次数,以在解锁的时候,可以确保所有加锁的过程都解锁了,其他线程才能访问。
    

    关键源码 (上锁): while(isLocked && lockedBy != thread){ wait(); } 判断了是否上锁,并且请求锁的线程是否是当前线程。

    ReentrantLock 与 synchronized相比:

    1. 优势:可中断、公平锁、多个锁。
    2. 使用lock 和 unlock 加锁解锁。解锁必须在finally控制块中完成。

    Condition 类和Object类锁的区别

    1. Condition.await 和Object.wait等效
    2. signal 和 notify 等效
    3. ReentrantLock可以唤醒指定条件线程。

    Lock 和 tryLock

    1. tryLock 没获得锁返回false。
    2. lock 没获得锁阻塞 ,等待获得锁.

    Lock 和 lockIterrupibly

    1. lock被中断不会抛出异常, 而 lockIterruptibly会抛出异常.

    熟悉的方法

    1. void lock(): 执行此方法时, 如果锁处于空闲状态, 当前线程将获取到锁. 相反, 如果锁已经
    被其他线程持有, 将禁用当前线程, 直到当前线程获取到锁.
    2. boolean tryLock():如果锁可用, 则获取锁, 并立即返回true, 否则返回false. 该方法和
    lock()的区别在于, tryLock()只是"试图"获取锁, 如果锁不可用, 不会导致当前线程被禁用,
    当前线程仍然继续往下执行代码. 而lock()方法则是一定要获取到锁, 如果锁不可用, 就一
    直等待, 在未获得锁之前,当前线程并不继续向下执行.
    3. void unlock():执行此方法时, 当前线程将释放持有的锁. 锁只能由持有者释放, 如果线程
    并不持有锁, 却执行该方法, 可能导致异常的发生.
    4. Condition newCondition():条件对象,获取等待通知组件。该组件和当前的锁绑定,
    当前线程只有获取了锁,才能调用该组件的await()方法,而调用后,当前线程将缩放锁。
    

    不熟悉的方法

    1. getHoldCount() :查询当前线程保持此锁的次数,也就是执行此线程执行lock 方法的次
    数。
    2. getQueueLength():返回正等待获取此锁的线程估计数,比如启动10 个线程,1 个
    线程获得锁,此时返回的是9
    3. getWaitQueueLength:(Condition condition)返回等待与此锁相关的给定条件的线
    程估计数。比如10 个线程,用同一个condition 对象,并且此时这10 个线程都执行了
    condition 对象的await 方法,那么此时执行此方法返回10
    4. hasWaiters(Condition condition) : 查询是否有线程等待与此锁有关的给定条件
    (condition),对于指定contidion 对象,有多少线程执行了condition.await 方法
    5. hasQueuedThread(Thread thread):查询给定线程是否等待获取此锁
    6. hasQueuedThreads():是否有线程等待此锁
    7. isFair():该锁是否公平锁
    8. isHeldByCurrentThread(): 当前线程是否保持锁锁定,线程的执行lock 方法的前后分
    别是false 和true
    9. isLock():此锁是否有任意线程占用
    10. lockInterruptibly():如果当前线程未被中断,获取锁
    11. tryLock():尝试获得锁,仅在调用时锁未被线程占用,获得锁
    12. tryLock(long timeout TimeUnit unit):如果锁在给定等待时间内没有被另一个线程保持,
    则获取该锁。
    

    非公平锁 : JVM随机或就近分配锁的机制。ReentrantLock(false) 构造函数设置公平/非公平

    公平锁 : 先申请锁的先得到锁。ReentrantLock(true)

  1. Semaphore 信号量

    阈值设置,运行多少个线程同时操作资源。
    阈值设置为 1 即互斥

    信号量 减1 : Semaphore.acquire()

    信号量 加1 : Semaphore.release()

  1. AtomicInteger 原子操作Integer

    还可以通过AtomicReference将对象的所有操作转化为原子操作。

    保证操作的原子性 ,效率比lock高

  2. ReadWriteLock 读写锁

    读锁: 读取的时候保证多个人读,不能同时写

    写锁 : 写入的时候保证一个人写,且不能读

  1. 锁优化

    • 减少锁持有时间
    • 减少锁粒度 , 降低锁的竞争
    • 锁分离 , 例如读写锁
    • 锁粗化 如果对一个锁不停请求,同步释放也会作大量
    • 锁消除 编译器干的事

十、 线程的基本方法


  1. wait

    线程阻塞 , 释放锁,进入wait set 线程等待池

  2. sleep

    线程休眠,不释放锁 , 进入time-wating状态

  3. yeld

    线程让步 , 让出cpu使用权,与其他线程竞争时间片

  4. interrupt()

    线程中断 , 但不直接中断线程。

    • 给出通知信号 , 使得time-waiting状态的线程抛出interruptException异常。中断位会清零。
    • 设置中断位为 1
  5. join等待其他线程终止

    其他线程调用join方法,当前线程阻塞 。 不放弃锁 , 等到相关线程结束,转为就绪状态。

  6. Object.notify

    线程唤醒 , 唤醒对象监视器上等待的单个线程,竞争cpu使用权。

    为什么notify和wait都在object类上?

    答:因为notify和 wait需要作用在同一个锁上。而锁可以是任意对象,可以被任意对象调用的方法是定义在object类中。

  7. 其他方法:

    1. sleep():强迫一个线程睡眠N毫秒。 
    2. isAlive(): 判断一个线程是否存活。  
    3. join(): 等待线程终止。  
    4. activeCount(): 程序中活跃的线程数。
    5. enumerate(): 枚举程序中的线程。  
    6.  currentThread(): 得到当前线程。  
    7.  isDaemon(): 一个线程是否为守护线程。  
    8.   setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线 程依赖于主线程结束而结束)  
    9.    setName(): 为线程设置一个名称。  
    10.    wait(): 强迫一个线程等待。  
    11. setPriority(): 设置一个线程的优先级。
    12. getPriority()::获得一个线程的优先级。
    

十一线程上下文切换


  1. 线程上下文切换,同一颗cpu上 ,任务状态的保存和再加载

  2. 上下文

    某一时间点,cpu寄存器和计数器内容

  3. PCB 进程控制块-切换帧

    表示进程状态的信息块。

  4. 上下文切换活动

    1. 挂起进程(线程) , 将进程信息存储在内存
    2. 内存中检索下一个进程上下文,并在CPU寄存器中恢复
    3. 跳转到PC ,运行程序
  5. 引起上下文切换原因

    • 时间片用完
    • IO阻塞
    • 多任务抢占锁,未获得锁
    • 硬件中断

十二线程池原理


  1. 主要特点 : 线程复用 , 控制最大并发数 , 线程管理

  2. 线程复用: ?

    在Thread strat() 方法中不断循环调用传递过来的Runnable对象 , 并使用Queue组织这些Runnable对象。

  3. ThreadPoolExecutor

    构造方法如下:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize, 
                              long keepAliveTime, 
                              TimeUnit unit, 
                              BlockingQueue workQueue) { 
    
      this(corePoolSize, maximumPoolSize, keepAliveTime, unit , workQueue, 
      Executors.defaultThreadFactory(), defaultHandler); 
    }
    
    1. corePoolSize:指定了线程池中的线程数量。 
    2. maximumPoolSize:指定了线程池中的大线程数量。 
    3. keepAliveTime:当前线程池数量超过corePoolSize时,多余的空闲线程的存活时间,即多少时间内会被销毁。 
    4. unit:keepAliveTime的单位。 
    5. workQueue:任务队列,被提交但尚未被执行的任务。 
    6. threadFactory:线程工厂,用于创建线程,一般用默认的即可。 
    7. handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。
    
  4. 线程池工作过程

    1. 线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面 有任务,线程池也不会马上执行它们。 
    
    2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断: 
       a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务; 
       b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
       c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要 创建非核心线程立刻运行这个任务; 
       d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池 会抛出异常RejectExecutionException。 
    
    3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
    
    4. 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运 行的线程数大于 corePoolSize,那么这个线程就被停掉。
       所以线程池的所有任务完成后,它 终会收缩到 corePoolSize 的大小。 
    

你可能感兴趣的:(Java 多线程知识点)