多线程

java多线程

进程

  • 独立性

    • 拥有独立资源
    • 独立的地址
    • 无授权其他进程无法访问
  • 动态性

    • 与程序的区别是:进程是动态的指令集合,而程序是静态的指令集合
    • 加入时间概念
    • 有自己的生命周期和不同的状态
  • 并发性

    • 多个进程可以在单核处理器并发执行
    • 多个进程互不影响
    • 不并行的区别是:并行是同一时刻多个进程在多个处理器同时执行,而并发是指在同一个时刻只能执行一条执行,但相互切换迅速,宏观上看是执行多个指令

线程

  • 轻量级进程(Light Weight Process)

    • 程序中的一个顺序控制流程,同时也是CPU的基本调度单位。
  • 多线程

    • 进程由多个线程组成,彼此间完成不同的工作,交替执行, 称为多线程。
  • 执行特点:

    • 线程抢占式执行

      • 效率高
      • 可防止单一线程长时间独占CPU
    • 在单核CPU中,宏观上同时执行,微观上顺序执行。

  • 组成

    • CPU时间片:操作系统(OS)会为每个线程分配执行时间。

    • 运行数据:

      • 堆空间:存储线程需使用的对象,多个线程可以共享堆中的对象。
      • 栈空间:存储线程需使用的局部变量,每个线程都拥有独立的栈。
    • 线程的逻辑代码。

    • 线程

      • 用户态(应用程序)
      • 内核态(系统调用cpu)
      • 1:1
  • 创建线程

    • 创建线程第一种方式:继承Thread, 重写run()方法

      • 1.继承Thread类
      • 2.覆盖run()方法
      • 3.创建子类对象
      • 4.调用start()方法
    • 创建线程第二种方式:实现Runnable接口

      • 1.实现Runnable接口
      • 2.覆盖run()方法
      • 3.创建实现类对象
      • 4.创建线程对象
      • 5.调用start()方法
  • 线程ID和名称

    • 获取线程线程ID和线程名称

      • 在Thread的子类中调用this.getId()或this.getName()
      • 使用Thread.currentThread().getId()和Thread.currentThread().getName()
    • 修改线程名称

      • 调用线程对象的setName()方法
      • 使用线程子类的构造方法赋值
  • 线程方法

    • 休眠:

      • public static void sleep(long millis)

        • 当前线程主动休眠 millis 毫秒。

          • sleep是线程进入休眠状态
          • sleep释放cpu, 没有释放锁对象
    • 放弃:

      • public static void yield()

        • 当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片。
    • 加入:

      • public final void join()

        • 允许其他线程加入到当前线程中。当前会阻塞,直到加入线程执行完毕。
    • 优先级

      • 线程对象.setPriority(int)

        • 线程优先级为1-10,默认为5,优先级越高,表示获取CPU机会越多。
    • 线程打断

      • 线程对象.interrupt()

        • 打断线程,被打断线程抛出InterruptedException异常。
    • 守护线程

      • setDaemon(true)设置为守护线程。
      • 线程有两类:用户线程(前台线程)、守护线程(后台线程)。
      • 如果程序中所有前台线程都执行完毕了,后台线程会自动结束。
      • 垃圾回收器线程属于守护线程。
    • 等待:

      • 必须在对obj加锁的同步代码块中。在一个线程中,调用obj.wait() 时,此线程会释放 其拥有的所有锁标记。同时此线程阻塞在o的等待队列中。释放锁,进入等待队列。

      • public final void wait()

        • wait是线程进入等待队列等待
        • wait释放了cpu, 锁也被释放了
      • public final void wait(long timeout)

    • 通知(唤醒):

      • 必须在对obj加锁的同步代码块中。从obj的Waiting中释放一个或全部线程。对自身 没有任何影响。
      • public final void notify()
      • public final void notifyAll()
      • 注意: 唤醒的线程随机
  • 线程同步

    • 线程不安全:

      • 当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致。
      • 临界资源:共享资源(同一对象),一次仅允许一个线程使用,才可保证其正确性。
      • 原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可打乱或缺省。
    • 同步代码块:

      • synchronized(临界资源对象){ //对临界资源对象加锁 //代码(原子操作) }
      • monitor
      • 每个对象都有一个互斥锁标记,用来分配给线程的。
      • 只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块。
      • 线程退出同步代码块时,会释放相应的互斥锁标记。
  • 线程的状态

    • 新建状态(New)

      • 当一个线程的实例bai被创建即使用new关键字和duThread类或其子类创建一个线程对象后,此时该线程处于新zhi生(new)状态,处于新生状态的线程有自己的内存空间,但该线程并没有运行,此时线程还不是活着的(notalive)。
    • 就绪状态(Runnable)

      • 一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。 处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态。对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。此时线程是活着的(alive)。

        • JDK5之后就绪、运行统称Runnable

          • 初始状态 Ready 就绪状态
          • Running 运行状态
    • 阻塞状态(Blocked)

      • 阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。处于Blocking状态的线程仍然是活着的(alive)。

        • 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
    • 无限期等待(Waiting )

      • 通过调用join() 使线程进入无限期等待状态,处于Blocking状态的线程仍然是活着的(alive)。

        • 运行(running)的线程执行join()时,JVM会把该线程置为Waiting状态。当sleep()状态超时线程重新转入可运行(runnable)状态。
        • 运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。被唤醒时抢占cpu使用权,抢到以后继续执行后续代码,
    • 限期等待(Timed Waiting)

      • 通过调用Thread.sleep(long ms)使线程进入无限期等待状态,处于Blocking状态的线程仍然是活着的(alive)。

        • 运行(running)的线程执行Thread.sleep(long ms)时,JVM会把该线程置为阻塞状态。当sleep()状态超时,线程重新转入可运行(runnable)状态。
    • 终止状态(Terminated)

      • 当一个线程的run()方法运行完毕或被中断或被异常退出,该线程到达终止状态。此时可能仍然存在一个该Thread的实例对象,当该Thread已经不可能在被作为一个可被独立执行的线程对待了,线程的独立的callstack已经被dissolved。一旦某一线程进入终止状态,他就再也不能进入一个独立线程的生命周期了。对于一个处于终止状态的线程调用start()方法,会出现一个运行期(runtimeexception)的异常;处于Dead状态的线程不是活着的(notalive)。 为了确定线程在当前是否存活着,需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false.

        • 有两个原因会导致线程死亡:

            1. run方法正常退出而自然死亡,
            1. 一个未捕获的异常终止了run方法而使线程猝死。
  • 生命周期

    • 新建

      • new一个Thread
    • 就绪

      • 执行start方法
    • 运行

      • 开始执行run方法
    • 阻塞

      • 进入阻塞

        • 调用sleep方法主动放弃处理器资源
        • 想获得一个同步检测器,但该同步检测器被其他资源所占有
        • 调用一个阻塞时的IO方法,在该方法返回前,线程阻塞
        • 等待某个notify通知
        • 调用suspend挂起
      • 解除阻塞

        • 调用阻塞式IO方法已返回
        • 成功后的了试图要得到的同步检测器
        • 等待某个通知时,其他线程发出来通知
        • 处于挂起的线程调用了resume方法
    • 死亡

      • run方法结束
      • 程序抛出了一个未捕获的异常
      • 调用stop方法
  • 线程通信

    • 等待:

      • public final void wait()
      • public final void wait(long timeout)
      • 必须在对obj加锁的同步代码块中。在一个线程中,调用obj.wait() 时,此线程会释放 其拥有的所有锁标记。同时此线程阻塞在o的等待队列中。释放锁,进入等待队列。
    • 通知:

      • public final void notify()
      • public final void notifyAll()
      • 必须在对obj加锁的同步代码块中。从obj的Waiting中释放一个或全部线程。对自身 没有任何影响。

高级多线程

  • 线程池

    • 现有问题:

      • 线程是宝贵的内存资源、单个线程约占1MB空间,过多分配易造成内存溢出。
    • 概念

      • 线程容器,可设定线程分配的数量上限。
      • 将预先创建的线程对象存入池中,并重用线程池中的线程对象。
      • 避免频繁的创建和销毁。
    • 原理

      • 将任务提交给线程池,由线程池分配线程、运行任务,并在当前任务结束后复用线程。
  • 步骤:

    • 获取线程池

      • 通过Executors工厂类获取线程池
    • 创建线程

      • Runnable runnable = new Runnable()
    • 将线程(任务)提交给线程池

      • executorService.submit(runnable);
    • 关闭线程池

      • executorService.shutdown();

        • //shutdown() 关闭 等待所有的任务执行完毕后才关闭
        •     //shutdownNow() 尝试关闭当前正在执行的线程  关闭所有没有完成的任务
          
  • 获取线程池

    • 所在包java.util.concurrent

    • Executor:线程池的顶级接口。

    • ExecutorService:线程池接口,可通过submit(Runnable task) 提交任务代码。

    • Executors工厂类:通过此类可以获得一个线程池。

      • 1 newFixedThreadPool() 固定大小线程池

        • ExecutorService executorService = Executors.newFixedThreadPool(4);
      • 2 newCacheThreadPool() 动态线程池

        • ExecutorService executorService1 = Executors.newCachedThreadPool();
      • 3 newSingleThreadPool() 单线程池

        • ExecutorService executorService2 = Executors.newSingleThreadScheduledExecutor();
      • 4 newScheduledThreadPool() 调度线程池

      • ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1) ;核心线程数

        • 延迟执行,只执行一次

          • public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);1.线程对象2.延迟时间3.时间单位(枚举值)
        • 周期执行,一秒执行一次,不能关闭线程池。

          • 固定频率

            • public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period, TimeUnit unit);1.线程对象2.线程开始延迟时间3.线程间延迟时间4.时间单位
          • 固定延迟

            • public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit);1.线程对象2.线程开始延迟时间3.线程间延迟时间4.时间单位
    • 使用ThreadPoolExecutor类 创建线程池

      • 创建方法

        • public ThreadPoolExecutor(int corePoolSize,

          • 核心线程数
        • int maximumPoolSize,

          • 最大线程数
        • TimeUnit unit,

          • 时间单位 枚举值 TimeUnit
        • long keepAliveTime,

          • 线程保持存活时间
        • BlockingQueue workQueue,

          • 请求队列
        • ThreadFactory threadFactory,

          • 线程创建工厂
        • RejectedExecutionHandler handler);

          • 拒绝策略(四种)

            • AbortPolicy 抛弃当前任务 抛出异常

              • AbortPolicy 抛弃任务,并抛出异常 默认的拒绝策略
                • 这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。
                • 如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。
            • DiscardPolicy 放弃当前任务 不抛出异常

              • DiscardPolicy 放弃任务,没有异常
                • 使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。
            • DiscardOldestPolicy 放弃老的任务 添加拒绝的任务 放弃等待队列里面的

              • DiscardOldestPolicy 放弃老的任务,添加拒绝的任务
                • 此拒绝策略,是一种喜新厌旧的拒绝策略。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。
            • CallerRunsPolicy 线程池的创建线程程序执行,调用任务的线程执行

              • CallerRunsPolicy 线程池的创建线程执行,主线程执行
                • 如果任务被拒绝了,则由调用线程(提交任务的线程)直接执行此任务。
      • 创建线程池演示代码

        • ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 3, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>(1), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
        • threadPoolExecutor.submit(new Runnable()
        • 创建三个任务 有两个在运行 一个在等待队列 在创建一个 到达最大线程数执行任务 在创建一个 会看拒绝策略
  • 线程安全

    • Collections工具类中提供了多个可以获得线程安全集合的方法。

      • public static Collection synchronizedCollection(Collection c)
      • public static List synchronizedList(List list)
      • public static Set synchronizedSet(Set s)
      • public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
      • public static SortedSet synchronizedSortedSet(SortedSet s)
      • public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m)
    • JDK1.2提供,接口统一、维护性高,但性能没有提升,均以synchonized实现。

  • CAS算法

    • CAS:Compare And Swap(比较交换算法)

    • 其实现方式是基于硬件平台的汇编指令,是靠硬件来实现的,效率高。(底层cpu指令)

    • 并且比较和交换过程是同步的。

    • CAS是一种乐观锁。

    • CAS比较交换算法,修改的方法包含三个核心参数(V,E,N)

      • V:要更新的变量、E:预期值、N:新值。
      • 只有当V==E时,V=N;否则表示已被更新过,则取消当前操作。
    • 乐观锁:

      • 总是认为是线程安全,不怕别的线程修改变量,如果修改了再重新尝试,直到成功。
    • 悲观锁:

      • 总是认为线程不安全,不管什么情况都进行加锁,要是获取锁失败,就阻塞。
      • synchronized是悲观锁。
  • Callable接口

    • public interface Callable{ public V call() throws Exception; }

    • JDK5加入,与Runnable接口类似,实现之后代表一个线程任务。

    • Callable具有泛型返回值、可以声明异常。

    • 步骤:

      • //1创建Callable对象

        • Callable callable=new Callable()
      • //2把可调用对象转成任务

        • FutureTask task=new FutureTask<>(callable)
      • //3创建线程对象,把任务交给线程

        • Thread thread=new Thread(task);
      • //4启动线程

        • thread.start();
      • //5获取线程执行的结果,get()等待任务执行完毕才会继续执行。

        • Integer sum = task.get();
  • Future接口

    • 概念:

      • 异步接收ExecutorService.submit()所返回的状态结果,当中包含了 call()的返回值。
    • 方法:

      • V get()以阻塞形式等待Future中的异步处理结果(call()的返回值)。
  • Lock接口

    • JDK5加入,与synchronized比较,显示定义,结构更灵活。

    • 提供更多实用性方法,功能更强大、性能更优越。

    • 常用方法:

      • //创建锁

        • Lock lock=new ReentrantLock();
      • void lock() //获取锁,如锁被占用,则等待。

      • boolean tryLock() //尝试获取锁(成功返回true。失败返回false,不阻塞)。

      • void unlock() //释放锁。

    • Condition

      • Condition接口也提供了类似Object的监视器方法,与Lock配合可以实现 等待/通知模式。
      • Condition可以通俗的理解为条件队列。当一个线程在调用了await方法以 后,直到线程等待的某个条件为真的时候才会被唤醒。
    • JDK1.6之后synchronize的优化:

      • JDK1.6之前只有两种状态

        • 无锁
        • 重量级锁
      • JDK1.6之后四种状态
        (只能升级不能降级)

        • 用户态(效率高)

          • 无锁
          • 偏向锁
          • 轻量级锁
        • 内核态

          • 重量级锁
      • 优化策略:

        • 锁消除

          • 局部变量

          • JVM配合

          • 开启逃逸分析-XX:+DoEscapeAnalysis

          • 开启锁消除标记-XX:+EliminateLocks

            • JDK1.8自动开启
          • 开启标量替换-XX:+EliminateAllocations

        • 锁粗化

          • for 循环里面上锁 经过锁粗化 在for循环外上锁
        • 自旋锁(JDK1.4包含)

          • 自旋锁是在轻量级锁升级重量级时使用的优化策略,在没有获得锁对象的情况下,先自旋等待,单位时间内获得锁对象,就不升级,否则升级成重量级锁
        • 自适应自旋锁

    • synchronized和Lock区别

  • ReentrantLock(重入锁)

    • Lock接口的实现类,与synchronized一样具有互斥锁功能。

    • 步骤

      • 创建重入锁对象

        • Lock lock = new ReentrantLock();
      • 显示开启锁

        • lock.lock();
      • 显示释放锁

        • lock.unlock();
    • 考虑可能出现异常,释放锁必须放入 finally代码块中,避免无法释放。

  • ReentrantReadWriteLock(读写锁)

    • 一种支持一写多读的同步锁,读写分离,可分别分配读锁、写锁。

    • 支持多次分配读锁,使多个读操作可以并发执行。

    • 互斥规则:

      • 写-写:互斥,阻塞。
      • 读-写:互斥,读阻塞写、写阻塞读。
      • 读-读:不互斥、不阻塞。
      • 在读操作远远高于写操作的环境中,可在保障线程安全的情况下,提高运行效率。
    • 代码

  • CopyOnWriteArrayList

    • 线程安全的ArrayList,加强版读写分离。
    • 写有锁,读无锁,读写之间不阻塞,优于读写锁。
    • 写入时,先copy一个容器副本、再添加新元素,最后替换引用。
    • 使用方式与ArrayList无异。
    • List list=new CopyOnWriteArrayList<>();
  • CopyOnWriteArraySet

    • 线程安全的Set,底层使用CopyOnWriteArrayList实现。
    • 唯一不同在于,使用addIfAbsent()添加元素,会遍历数组,
    • 如存在元素,则不添加(扔掉副本)。
    • Set set = new CopyOnWriteArraySet<>();
  • ConcurrentHashMap

    • 初始容量默认为16段(Segment),使用分段锁设计。
    • 不对整个Map加锁,而是为每个Segment加锁。
    • 当多个对象存入同一个Segment时,才需要互斥。
    • 最理想状态为16个对象分别存入16个Segment,并行数量16。
    • 使用方式与HashMap无异。
    • JDK1.8改为CAS无锁算法。
    • HashMap<String,String> map=new ConcurrentHashMap<>();

Queue接口(队列)

  • Collection的子接口,表示队列FIFO(First In First Out)

  • 常用方法:

    • 抛出异常:

      • boolean add(E e) //顺序添加一个元素(到达上限后,再添加则会抛出异常)
      • E remove() //获得第一个元素并移除(如果队列没有元素时,则抛异常)
      • E element() //获得第一个元素但不移除(如果队列没有元素时,则抛异常)
    • 返回特殊值:推荐使用

      • boolean offer(E e) //顺序添加一个元素 (到达上限后,再添加则会返回false)
      • E poll() //获得第一个元素并移除 (如果队列没有元素时,则返回null)
      • E keep() //获得第一个元素但不移除 (如果队列没有元素时,则返回null)
  • ConcurrentLinkedQueue

    • 创建:

      • Queue queue = new ConcurrentLinkedQueue<>();
      • queue.offer(“hello”);
      • queue.poll();
      • queue.peek();
    • 线程安全、可高效读写的队列,高并发下性能最好的队列。

    • 无锁、CAS比较交换算法,修改的方法包含三个核心参数(V,E,N)

    • V:要更新的变量、E:预期值、N:新值。

    • 只有当V==E时,V=N;否则表示已被更新过,则取消当前操作。

  • BlockingQueue接口(阻塞队列)

    • Queue的子接口,阻塞的队列,增加了两个线程状态为无限期等待的方法。

    • 方法:

      • void put(E e) //将指定元素插入此队列中,如果没有可用空间,则等待。
      • E take() //获取并移除此队列头部元素,如果没有可用元素,则等待。
    • 可用于解决生产生、消费者问题。

    • 子类

      • ArrayBlockingQueue:

        • BlockingDeque queue = new ArrayBlockingQueue<>(6);
      • LinkedBlockingQueue:

        • BlockingDeque queue = new LinkedBlockingDeque<>(6);

多线程的三个特性

  • 原子性(互斥性)

    • 一个或多个操作不能被分割,要么全部执行,要么就都不执行

      • Atomic

        • AtomicInteger integer2=new AtomicInteger(0);
        • AtomicStampedReference integer=new AtomicStampedReference<>(0, 0);带版本号 解决ABA问题(第一个线程修改值,第二个线程又修改回来,第三个线程获取的值和期望值一样)
  • 可见性

    • 多个线程访问同一个变量,一个线程修改了这个变量,别的线程能立即看到修改的值
    • volatile关键字保证内存可见性
  • 有序性

    • 程序执行的顺序按照代码的先后顺序执行,但是处理器为了提高程序运行效率,可能会对输入代码进行优化,他不保证程序中各个语句的执行顺序和编写顺序一致,但最终结果是一致的
    • 处理为了优化会进行重排,单线程没有问题,多线程可能有问题,所以禁止重排序
  • synchronized 可保证原子性和可见性,但不能保证有序性.

  • volatile可保证可见性和禁止指令重排,但不能保证原子性.

  • Lock接口间接借助了volatile关键字实现了可见性和有序性

在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值