当前位置: 代码迷 >> 综合 >> CoreJava Day14
  详细解决方案

CoreJava Day14

热度:23   发布时间:2024-02-24 03:40:35.0
  • 同一时间点网站访问巨大,被称为高并发

多线程

  • 进程 —> 在操作系统 ( OS ) 中正在执行的应用程序

  • 多进程 —> 支持多个应用程序同时执行

  • 多线程并发执行原理

    • 微观上串行 —> cpu将时间片分配给哪一个进程,哪一个进程执行对应的任务 —> 进程一个一个被执行
    • 宏观上并行 —> 多个线程一起执行
  • 线程 —> 又称轻量级进程

    • 在进程中执行的每个任务,一个进程中同时可以执行多个任务,即多线程
    • 线程是进程的任务执行单元( 单位 )
    • main函数为主线程,以main函数的开始为开始,以main函数的结束为结束。
  • 线程组成

    • CPU:获取cpu时间片 —> 多线程并发执行原理也是微观上串行,宏观上并行
    • 数据 —> 堆空间共享,栈空间独立 —> 一个线程一个栈空间,相互独立,但是堆空间只有一个,是共享的;只要能获取线程中某一个对象的首地址,就可以对此线程进行操作;但是每一个线程都有自己独一份的栈空间,其中数据不共享
    • 代码:实现多线程

代码实现多线程

  • 借助java.lang.Thread类

    • 定义一个类继承Thread类
    • 重写public void run(){ }方法,将线程任务定义在run方法中
    • 创建线程对象 —> 用定义的类创建即可
    • 开启线程 —> jvm默认执行调用线程中run方法 —> 一个线程对象不能被多次开启 —> 直接对象.方法名只是普通的方法调用,与线程无关
  • 借助java.lang.Runnable接口

    • 定义一个类实现Runnable接口,同时实现run方法

    • 创建目标(任务)对象 —> 用定义的类创建

    • 创建线程对象,借助JDK中提供的线程类java.lang.Thread,并将目标对象作为参数进行传递

      Thread t2 = new Thread(m);//m为实现了Runnable类型的对象
      
    • 开启线程:t2.start(); —> jvm虚拟机默认执行run方法

  • 线程池

    • 一个线程对象的存在只为特定的一个任务而存在,任务执行完毕则线程被销毁,导致频繁的创建线程和频繁的销毁线程,降低JVM性能,从而使用线程池
    • 线程池 —> 线程容器,将预先创建好的线程对象存放池中,而且线程池中线程对象可以被反复使用,避免频繁创建线程和销毁线程,从而提高jvm的性能
    • 常用接口 —> java.util.concurrent包中 —> 简称juc,并发包
      • Executor —> 线程池的顶级接口
      • ExecutorService —> 子接口,线程池核心接口
        • submit(Runnable task) —> 将Runnable类型任务提交给线程池,线程池会分配空闲线程执行对应的线程任务,如果线程池暂时没有空闲线程对象,则该任务需要等待,等待有空闲线程对象
        • submit ( Callable c ) :将Callable类型的任务提交给线程池
        • void shutdown() —> 关闭线程池,同时将线程池中的线程对象销毁(将提交的任务全部执行完之后进行销毁线程)
    • 常用类 —> java.util.concurrent包中 —> 简称juc,并发包
      • Executors —> 获取线程池对象的工具类,定义了大量的静态方法,可通过类名直接调用即可
        • public static ExecutorService newFixedThreadPool(int n):获取固定数量线程对象的线程池,参数 n 代表指定线程池中预先创建对象个数
        • public static ExecutorService newCachedThreadPool():获取不固定个数线程对象的线程池;当提交任务时,如果线程池中没有空闲线程,则创建新的线程对象;若长时间没有接收任务的线程对象,会被自动清理销毁
  • 借助Callable接口 + 线程池技术 —> java.util.concurrent 包中,JDK5.0版本提出

    • 目前线程任务定义在run方法中,但run方法有两个缺点
      • 方法没有返回值
      • run方法内部出现异常时只能积极处理异常,无法消极声明异常
    • Callable应用等价于Runnable,用于定义线程任务,只是有返回值,可以消极处理异常
      • Callable是一个泛型接口,接口泛型类型决定方法的返回值类型
      • V call( ) throws Exception:带有返回值并可以消极处理
    • Future —> 位于java.util.concurrent包中
      • Future用于接收submit提交任务的返回值
      • Future是一个泛型接口,泛型类型取决于submit提交的Callable泛型的类型
      • V get ( ):从Future获取任务执行之后的结果,返回值类型取决于Future的泛型类型

线程同步

  • 临界资源 —> 多线程并发时,同时被多个线程共享的同一个对象 —> 一个对象同时被多个线程共享

  • 原子操作 —> 不可分割的多步操作,被视作一个整体,其步骤和顺序不能被破坏

  • 线程同步 —> 保证临界资源的正确的一种技术手段 —> 多线程并发时,为了保证临界资源的正确性的,而不能破坏临界资源中的原子操作

  • 线程同步方式

    • 同步代码块 —> 对临界资源进行加锁 —> 定义在方法内部 —> synchronized ( 临界资源 ){//原子操作}

      • 当线程获取锁标记时,如果锁标记没有被其他线程占用,则成功获取锁标记,获取锁标记之后才能执行同步代码块{ }中内容;当{ }中所有内容全部执行完,会自动释放当前线程占用的锁标记;如果锁标记被其他线程占用,则当前线程进入阻塞状态(Blocked状态:等待锁标记),直到被占用的锁标记释放才结束阻塞状态,从而进入就绪状态,同时获取cpu时间片才可以执行同步代码块中内容
    • 同步方法 —> 被synchronized修饰的方法称为同步方法

      ? —> 修饰符 synchronized 方法返回值类型 方法名 ( 形参 ){ //原子操作 }

      • 同步方法等价于同步代码块 —> synchronized(this){ //原子操作 }

      • 每一个对象都具有单一的锁标记(监视器)

      • 线程同步时,必须保证多个线程共用同一个对象的锁标记,才能保证共享资源的正确性,达到互斥作用,从而保证线程同步(保证临界资源的正确性)

        ArrayList Vector
        线程不安全,内部为非同步方法,但是无法保证数据的安全性 线程安全,内部方法为同步方法
        并发效率高 并发效率相对低
    • Lock(锁) —> 灵活的控制加锁和释放锁

      • jdk5.0提供的线程同步一种加锁策略,位于java.util.concurrent.locks包中的接口
      • void lock() —> 获取锁 —> 如果锁被其他线程占用,当前线程进入堵塞状态
      • void unlock() —> 释放锁
      • 实现类 —> ReentrantLock
sleep():形参为毫秒 wait()
位于Thread类中静态方法,使当前线程进入休眠(有限期的等待状态),释放时间片,但是不释放锁标记 位于Object类中成员方法,使当前线程进入等待状态,释放cpu时间片,同时释放锁标记

线程的状态 —> 线程的生命周期

  • 一个时间点上,一个线程只可能处于其中的一种状态

  • 初始状态(New) —> 线程对象被创建,即为初始状态。只在堆中开辟内存,与常规对象无异

  • JDK5.0之后就绪状态、运行状态统称为Runnable

    • 就绪状态(Ready) —> 调用start()之后,进入就绪状态。等待OS选中,并分配时间片
    • 运行状态(Running) —> 获得时间片之后,进入运行状态,如果时间片到期,则回到就绪状态
  • 计时等待(Timed Waiting) —> 当前线程Running状态时可以进入,但醒了之后回到Ready状态重新等待时间片

    • static void sleep(long ms) —> 让当前运行中的线程进入休眠状态(有限期等待状态),时间单位ms,处于休眠状态下的线程释放cpu时间片资源,不释放拥有的锁标记 —> 可以直接通过Thread.sleep();使用即可
  • 无计时等待(Waiting) —> 在A线程中,调用join的线程不结束,则A线程一直等待。A线程会等待在A中所有调用join方法的线程,若有两个线程调用join,则这两个线程继续抢时间片

    • final void join() —> 在一个线程中利用某一个线程对象调用其join方法,则这个线程让步与调用join方法的线程
  • 阻塞状态(Blocked) —> synchronized锁定对象,在运行状态时进入堵塞状态,结束时返回到就绪状态

    • 当线程获取锁标记时,锁标记被其他线程占用
  • 终止状态(Terminated) —> 主线程main()或独立线程run()结束,代表线程的任务结束;进入终止状态,并释放持有的时间片

死锁

  • 两个线程或是多个线程占用着对方想用的锁资源,都不进行释放,导致相互等待对方释放锁资源,从而产生一种无限期的一种等待现象
  • 一旦出现死锁现象,不会抛出异常,也没有任何提示,所有线程进入阻塞状态;如果没有外力干预,程序将无法继续进行下去
  • 解决方案
    • 尽可能调整加锁方案
    • 尽可能的避免出现同步嵌套使用
    • 试图使用线程间的通信 等待 - 通知解决
  • 线程间的通信 —> 等待 - 通知
    • 等待 —> public final void wait() throws InterruptedException —> 位于java.lang.Object类中方法
      • 在一个线程中调用wait方法,则当前线程会进入等待状态
      • 可以使用任意对象调用wait() 方法,但是必须处理调用它的对象所有的同步代码块中
      • 选择使用临界资源调用wait() 方法
      • wait() 方法会让当前线程释放cpu,同时释放锁标记
    • 通知(唤醒) —> notify() / notifyAll() —> 位于java.lang.Object类中
      • notify() —> 通知一个处于等待状态的线程结束等待
      • notifyAll() —> 通知所有的线程从等待状态结束
      • 可以使用任意对象调用notify() 方法,但是必须使用在调用它的对象所在的同步代码块中
      • 选择使用临界资源调用notify() 方法
      • notify() 方法只是起到通知作用,不会使当前线程释放锁标记和cpu

集合的扩充

  • 目前电脑为多核cpu,从微观意义上已经实现并行

  • 写操作 —> 会对集合中数据进行改变 —> 加锁

  • 读操作 —> 不会对集合中数据进行改变 —> 不加锁

  • CopyOnWriteArrayList —> 位于java.util.concurrent包中一个集合类 —> List实现类

    • 此类中所有读操作没有加锁(没有synchronized和Lock的使用),写操作加锁(Lock)

    • 多线程并发时,读与读之间不阻塞,写于写之间阻塞

    • 在进行写操作时,会在原有文本的基础上拷贝一个新的数组,在新数组上进行写操作,写操作结束之后,会用新文本替换旧文本

    • 牺牲写的操作效率提高读的操作效率,所以通常在读操作次数远远大于写操作次数时使用,线程安全

      ArrayList CopyOnWriteArrayList Vector
      线程不安全,读操作和写操作为非同步方法 线程安全,写操作加锁,读操作不加锁,牺牲写操作的效率提高读操作的效率 线程安全,读操作和写操作都为同步方法
      并发效率高 在读操作次数远远大于写操作时,并发效率仅次于ArrayList 并发效率较低
  • ConcurrentHashMap —> 位于java.util.concurrent包中

    • 线程安全,并发效率相对高效

    • 采用***分段锁***,缩小锁定对象范围,减少锁冲突的可能性,提高并发效率

    • 在全局操作不频繁的情况下,并发效率相对较高

    • jdk8.0及以上采用CAS ( 比较交换算法,三个参数控制比较) 和 synchronized实现并发高效

      HashMap ConcurrentHashMap Hashtable
      线程不安全 线程安全 线程安全
      并发效率高 并发效率相对较高 并发效率较低
  • Queue接口 —> 位于java.util包中 —> 队列 —> Collection子接口,模拟队列存储结构 —> 先进先出,后进后出 —> First In First Out —> FIFO

  • LinkedList —> Queue实现类

    • boolean add(E e):将指定的元素插入此队列
    • ***boolean offer(E e)***:往队列中添加一个元素,成功true,反之
    • E peek():获取但不移除此队列的头,队列为空返回null
    • ***E poll()***:获取并移除此队列的头,队列为空返回null
    • remove():获取并移除此队列的头
  • BlockingQueue —> Queue子接口,阻塞队列,队列中添加了两个线程状态为无限期等待的方法

    • 常用方法
      • void put(Object o) —> 往队列中存储元素,没有可用空间则等待
      • E take() —> 从队列中获取队头元素,没有可取元素则等待
    • 实现类
      • ArrayBlockingQueue —> 数组实现,需要手动设置容量大小
      • LinkedBlockingQueue —> 链表实现,可以手动设置容量大小,不指定则默认容量为Integer类型最大值
  • ConcurrentLinkedQueue —> 位于java.util.concurrent包中,是Queue的实现类

    • 链表实现
    • 没有任何锁但是线程安全,采用无锁比较交换算法 ( CAS ) —> 三个参数控制比较
  相关解决方案