关于多线程

关于多线程,第1张

目录

1. 线程

1.1 为什么引入线程?

1.1.1 进程 VS 线程

1.1.2 单核CPU VS 多核CPU

1.2 基本概念

1.2.1 程序 VS 进程 VS 线程

1.3 引入线程的好处

1.4 Java的线程与 OS 线程的关系

2. JVM中的线程

2.0 使用场景

2.1 Java 线程在代码中的体现

2.2 创建线程

2.2.1 通过继承 Thread 类,并且重写 run 方法

 2.2.2 通过实现 Runnable 接口,并且重写 run方法

2.3 线程和方法调用栈

2.4 线程中常见的属性(通过 Thread 对象进行 *** 作)

2.5 Thread 的常见静态方法

2.5.1 等待一个线程-join()

2.5.2 中断一个线程

2.5.3 获取当前线程引用

2.5.4 休眠当前线程

2.5.5 Thread.yield()    

3. JVM 内存

4. 线程安全

4.1 概念

4.2 线程不安全现象的原因

4.2.1 开发者角度

4.2.2 系统角度

4.3 违反原子性的场景

5. 线程同步(解决线程安全问题)

5.1 synchronized——同步锁

5.1.1 概念   

5.1.2 使用示例

5.2 java.util.concurrent.locks.lock

5.2.1 synchronized VS Lock

5.3 volatile 机制

6. 线程通信

6.1 wait()

6.2 notify()/ notifyAll()

    6.3 说明:

    6.4 sleep()和wait()的异同?

7. 多线程案例——单例模式

8. 线程池

8.1 好处

8.2 标准库中的线程池

8.2.1 线程池实现的接口

8.2.2 线程池构造方法的理解

 8.2.3 拒绝策略

8.2.4 池大小

8.3 Executors 创建线程池的几种方式


1. 线程 1.1 为什么引入线程? 1.1.1 进程 VS 线程

        (1)调度

              • 没有引入线程时,进程是资源分配的基本单位和独立调度、分派的基本单位

              • 引入线程后,进程是资源分配的基本单位,线程是调度和分派的基本单位

         (2)系统开销

              进程的系统开销大于线程的系统开销。

              • 创建或撤销进程时,系统都要为之分配或回收资源;进程切换时涉及到整个当前进程 

                CPU环境的保存以及新被调度运行的进程CPU环境

              • 同一进程中的多个线程具有相同的地址空间,相互之间的同步和通信比较容易

         (3)并发性

              • 进程间可并发

              • 一个进程中的多个线程可并发

         (4)内存空间

              • 进程和进程之间不共享内存空间

              • 同一个进程的线程之间共享同一个内存空间

1.1.2 单核CPU VS 多核CPU

         (1)单核CPU:一种假的多线程,在一个时间单元内,只能执行一个线程的任务

         (2)多核CPU:在一个时间单元内,可以执行多个线程的任务

        一个Java应用程序 java.exe 至少有三个线程:main()主线程、gc()垃圾回收线程、异常处理

       线程

        单核CPU不能满足需求,因此需要多核CPU,并发编程可以更充分利用多核CPU资源

        引入线程的目的:为了减少程序并发执行时所付出的时间和空间开销,使 OS 具有更好的并发性

1.2 基本概念 1.2.1 程序 VS 进程 VS 线程

     (1)程序

           是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象

     (2)进程

          进程是程序的一次执行过程,或是正在运行的一个程序。

          • 程序是静态的,进程是动态的:有它自身的产生、存在和消亡的过程——生命周期

          • 进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域

     (3)线程

          进程可以进一步细化为线程,是一个程序内部的一条执行路径。

          • 若一个进程同一时间并行执行多个线程,就是支持多线程的

          • 线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(PC),线程切换

            的开销小

          • 一个进程中的多个线程共享相同的内存单元/内存地址空间——>它们从一堆中分配对象,

            可以访问相同的变量和对象,使得线程间通信便捷,但多个线程 *** 作共享的系统资源可能

            会带来安全隐患

   

1.3 引入线程的好处

        (1)创建、销毁、调度线程比进程快

        (2)两个线程的切换花费时间少

        (3)同一进程内的线程共享内存和文件,线程之间相互通信无需调用内核,故不需要额外的

                 通信机制

        (4)线程能独立执行

1.4 Java的线程与 OS 线程的关系

        Java中的Thread 类可视为是对 OS 提供的API 进行了进一步封装和抽象

2. JVM中的线程 2.0 使用场景

        用于提升效率或阻塞的场景

2.1 Java 线程在代码中的体现

        java.lang.Thread 类(包括其子类)的一个对象

2.2 创建线程 2.2.1 通过继承 Thread 类,并且重写 run 方法

       (1)继承 Thread 创建一个线程类

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("这里是线程运行的代码");
   }
}

        (2)创建 MyThread 类的实例

MyThread t = new MyThread();

        (3)调用 start方法启动线程

t.start(); // 线程开始运行
 2.2.2 通过实现 Runnable 接口,并且重写 run方法

         (1)实现 Runnable 接口             

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("这里是线程运行的代码");
   }
}

        (2)创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.

Thread t = new Thread(new MyRunnable());

        (3)调用 start方法启动线程

 t.start(); // 线程开始运行

     注:

     (1)一个已经调用过 start()方法则不能再次调用 start(),会出现异常

     (2)不能直接调用 run()的方式启动线程,若调用 run() 则与多线程没有关系,完全是在同一线

              程(主线程下)运行代码

     (3)继承 Thread 类, 直接使用 this 就表示当前线程对象的引用.

     (4)实现 Runnable 接口, this 表示 MyRunnable 的引用. 需要使用 Thread.currentThread()

         例:

               

                t.start()把线程的状态从新建变成了就绪状态,不分配CPU 从子线程进入就绪队列时, 

            子线程与主线程地位平等所以哪个线程被选中分配CPU是不一定的。

                但大概率是主线程先执行,t.start()是主线程的语句,即这条语句执行说明主线程在

            CPU 上(主线程是运行状态)所以大概率是t.start()下一条语句先执行

2.3 线程和方法调用栈

    每个线程都有自己独立的调用栈(每个线程都是一个独立的执行流)

    多个线程之间是“并发”执行的

2.4 线程中常见的属性(通过 Thread 对象进行 *** 作)

    2.4.1 id:getId()

              JVM 进程内部分配的唯一 id,只能 get 不能 set

    2.4.2 name:getName()

                名称是各种调试工具用到的

                默认情况,线程名字遵守 Thread_0  能 get 也能 set

                可以通过 setName() 或 Thread(..)构造方法设置

    2.4.3 状态:getState()

              Java代码中的线程状态(只能获取不能设置,状态变更由JVM 控制)

             (1)理论中的状态

                 

             (2)实际代码中的状态

                    

                      blocked:专指 synchronized锁失败

                      waiting:不带超时的阻塞

                      time_waiting:带超时的阻塞

    2.4.4 优先级:getPriority()

                  优先级高的线程理论上更容易被

                  线程可以get / set自己的优先级(不能强制让某个线程先被调度)

    2.4.5 是否后台线程:isDaemon()

             前台线程 vs 后台线程/精灵线程

             前台线程:一般做交互工作(手动写的线程默认都是前台线程,除非修改)

             后台线程:一般做支持工作

            JVM 会在一个进程的所有前台线程结束后,才会结束运行(与后台线程无关,即使后台线程工作也正常退出)

    2.4.6 是否存活:isAlive()

             run 方法是否运行结束

2.5 Thread 的常见静态方法 2.5.1 等待一个线程-join()

2.5.2 中断一个线程

       (1)通过共享的标记来进行沟通

       (2)调用 interrupt() 方法来通知

              

               A主动让B停止: b.interrupt();

                     情况1:B此时正常执行代码

                                  只是给B发了一个消息(给B设置一个停止标志)实际上不会影响B的运行

                     情况2:B可能正处于休眠状态

                                  JVM处理:以异常形式通知B

               B感知A让其停止:                        

                      情况1:B正在正常执行代码

                                  静态方法 Thread.interrupted()        检测当前线程是否被终止

                     情况2:B可能正处于休眠状态,则B无法立即执行Thread.interrupted()

                                  捕获了 InterruptedException

2.5.3 获取当前线程引用

          public static Thread currentThread();

2.5.4 休眠当前线程

         调用sleep(…) 就是让当前线程从 运行状态 ->阻塞状态;等待时间过去后线程从阻塞-> 就

          绪,被再次调度时开始执行之前的指令

2.5.5 Thread.yield()    

           让线程让出 CPU,线程从 运行->就绪,随时可以继续被调度回CPU

           主要用于执行一些耗时较久的计算任务时,为防止计算机处于“卡顿”的现象,不时地让出

           CPU 给OS 内的其他进程

           yield 不改变线程的状态, 但是会重新去排队

3. JVM 内存

每个线程拥有自己独立的:虚拟机栈、程序计数器

多个线程共享同一个进程中的结构:方法区、堆

代码中

    局部变量:保存在栈帧中(也就是栈中)线程私有

    类对象、静态属性:保存在方法区,线程之间共享(前提是有访问权限)

    对象(对象内部的属性):保存在堆中,线程之间共享(前提是线程有该对象的引用)

4. 线程安全 4.1 概念

        如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

4.2 线程不安全现象的原因 4.2.1 开发者角度

          修改共享数据

         (1)多个线程之间 *** 作同一块数据(共享数据)

         (2)至少有一个线程在修改共享数据

                 

          多线程代码中,哪些情况不需要考虑线程安全问题?

         (1)多个线程之间互相没有任何数据共享时

         (2)多个线程之间即使有共享数据,但都是读 *** 作而不是写 *** 作

4.2.2 系统角度

          前提:(1)Java代码中的一条语句,很可能对应多条指令

                (2)线程调度可以发生在任意时刻,但不会切割指令

                (3)CPU 中为了提升数据获取速度,一般在CPU 中设置缓存(L0-L4)

                (4)现代CPU 一般 L1、L2 是每个CPU 各自独有,L3 是所有 CPU共有一个

                (5)Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型.

                                          主存储/主内存:真是内存

                                          工作存储/工作内存:CPU中缓存的模拟

                (6)可见性,一个线程对共享数据的修改能够及时的被其他线程看到

                     • 线程之间的共享变量存在主内存

                     • 每一个线程都有自己的 "工作内存" 

                     • 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从

                       工作内存读取数据.

                     • 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主

                       内存.

                     • 一个线程内对数据的 *** 作,在工作内存处理的数据在没有同步回主内存时,其他

                       线程不可见

                (7)重排序:执行的指令和书写指令不一致

                             JVM 规定了一些重排序的基本原则:happened-before

     原因:(1)原子性被破坏(前提(1)(2))

                (2)可见性被破坏(前提(3)(4)(5)(6))

                (3)代码重排序导致

4.3 违反原子性的场景

   (1)read-write

        例: i++;     array[size] = e; size++;

       (2)check-update

            例: if(a == 10){a = …;}

5. 线程同步(解决线程安全问题) 5.1 synchronized——同步锁 5.1.1 概念   

              synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线

          程如果也执行到同一个对象 synchronized 就会阻塞等待

              进入 synchronized 修饰的代码块, 相当于 加锁

              退出 synchronized 修饰的代码块, 相当于 解锁

5.1.2 使用示例

         (1)修饰普通方法

public class SynchronizedDemo {
    public synchronized void methond() {
   }
}

        (2)修饰静态方法

public class SynchronizedDemo {
    public synchronized static void method() {
   }
}

                  如果 *** 作共享数据的代码完整的声明在一个方法中,可以将此方法声明同步的;

                  同步方法仍然涉及到 synchronized同步监视器,只是不需要显式的声明;

                  非静态的同步方法,同步监视器是:this;

                  静态的同步方法,同步监视器是:当前类本身;

         

         (3)修饰代码块

// 锁当前对象
public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            
       }
   }
}

// 锁类对象
public class SynchronizedDemo {
    public void method() {
        synchronized (SynchronizedDemo.class) {
       }
   }
}

                 

              说明:• *** 作共享数据的代码,即为需要被同步的代码

                         • 同步监视器,俗称锁。任何一个类的对象都可以充当锁

                           要求:多个线程必须共用同一把锁

                         • 在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用

                           当前类充当同步监视器

                         • 在实现Runnable接口创建多线程的方式中,可考虑使用this充当同步监视器

            *** 作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程效率低

5.2 java.util.concurrent.locks.lock

          

class A{ 
   private final ReentrantLock lock = new ReenTrantLock(); 
      public void m(){ 
         lock.lock(); 
         try{ 
                 //保证线程安全的代码; 
         } finally{ 
              lock.unlock(); 
         } 
      }
   } 

}
// 注意:如果同步代码有异常,要将unlock()写入finally语句块
5.2.1 synchronized VS Lock

             (1)Lock是显式锁(手动开启和关闭),synchronized是隐式锁,出了作用域自动释放

              (2)Lock只有代码块锁,synchronized 有代码块锁和方法锁

              (3)使用Lock 锁, JVM 将花费较少的时间来调度线程,性能更好

               • 优先使用顺序:

               Lock -> 同步代码块(已经进入了方法体,分配了相应资源)-> 同步方法(在方法体外)

5.3 volatile 机制

前提:

      JVM 的 *** 作长度是32位

      基本类型(除long、double)的字面量赋值、引用类型(JVM强行规定)在JVM角度都是原子的;     

      long、double 长度为64,高32位+低32位分别写入则不是原子的,用volatile 修饰则可保证原子性

作用:

      volatile 修饰的变量能保证内存可见性

      volatile不保证原子性,但能保证long、double直接赋值的原子性

      引用实例化对象的禁止重排序

6. 线程通信 6.1 wait()

作用:一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。

           令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当               前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器               的所有权后才能继续执行。

    • 在当前线程中调用方法: 对象名.wait() 

    • 使当前线程进入等待(某对象)状态 ,直到另一线程对该对象发出 notify (或notifyAll) 为止

    • 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁) 

    • 调用此方法后,当前线程将释放对象监控权 ,然后进入等待 

    • 在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行。

6.2 notify()/ notifyAll()

        notify():一旦执行此方法,就会唤醒被 wait 的一个线程。如果有多个线程被 wait,就唤醒优                        先级高的

        notifyAll ():一旦执行此方法,就会唤醒被 wait的所有线程

       • 在当前线程中调用方法: 对象名.notify() 

       • 功能:唤醒等待该对象监控权的一个/所有线程。 

       • 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)

    6.3 说明:

          (1)wait()、notify()、notifyAll()三个方法必须使用在同步代码块或同步方法中

          (2)wait()、notify()、notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步

                  监视器。否则,会出现IllegalMonitorStateException异常

          (3)因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,                       因此这三个方法只能在Object类中声明。

    6.4 sleep()和wait()的异同?

        相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态

        不同点:(1)两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()

                      (2)调用的要求不同:sleep()可以在任何需要的场景下调用。wait()必须使用在同                                    步代码块中

                      (3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,

                               sleep()不会释放锁,wait()会释放锁

7. 多线程案例——单例模式

单例模式(singleton):设计模式之一,单例模式能保证某个类在程序中只存在唯一一份实例,而不会创建出多个实例

具体实现方式:

           (1)饿汉模式:在类加载期间就进行对象的实例化

           (2)懒汉模式:第一次用到的时候进行对象的实例化

                  二次判断解释:第一次判断只需要考虑当一个对象还未初始化时对其进行实例化 *** 作,

                                而后只需要返回实例化的结果而不用再次实例化。第二次判断,保证if

                                和初始化的原子性。

                  加volatile解释:代码可能会重排序为先赋值后初始化,导致在多线程环境下使得另一

                                  个线程得到错误的对象,所以需要将instance用volatile修饰

8. 线程池 8.1 好处

     减少每次启动、销毁线程的损耗

8.2 标准库中的线程池 8.2.1 线程池实现的接口

            

8.2.2 线程池构造方法的理解

             

 8.2.3 拒绝策略

             

8.2.4 池大小

           A 将根据 corePoolSize() 和 maxPoolSize()设置的边界自动调整池大小。当在方法   

        execute(Runnable)中提交新任务,并且运行的线程数少于 corePoolSize 线程时,将创建一个

       新线程来处理请求,即使其他工作线程处于空闲状态也是如此。如果正在运行的线程数大于核

       心PoolSize,但小于最大值PoolSize线程,则仅当队列已满时,才会创建新线程。通过设置相

      同的核心池大小和最大池大小,可以创建固定大小的线程池。通过将 maxPoolSize 设置为基本

      无界的值(如 ),可以允许池容纳任意数量的并发任务。最典型的是,核心和最大池大小仅在

      构造时设置,但它们也可以使用 setCorePoolSize(int)和 setMaximumPoolSize(int) 动态更改。

8.3 Executors 创建线程池的几种方式

     • newFixedThreadPool: 创建固定线程数的线程池

     • newCachedThreadPool: 创建线程数目动态增长的线程池.

     • newSingleThreadExecutor: 创建只包含单个线程的线程池.

     • newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.      

     Executors 本质上是 ThreadPoolExecutor 类的封装.

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/924393.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-16
下一篇 2022-05-16

发表评论

登录后才能评论

评论列表(0条)

保存