多线程

  • 基本概念

    • 程序、进程、线程的基本概念和关系
      • 程序:静态的指令集;不占系统资源;也不被系统调用,不能作为独立运行的单位,以文件形式存储在磁盘上。
      • 进程:程序的执行活动;使用系统资源; 是资源申请、调度、独立运行的单位;一个程序可以对应多个进程。例如著名的QQ多开。
      • 线程:被称为轻量进程;大多数OS将其作为时序调度的基本单位;没有明确的协调情况下,线程相互同时或异步地执行。
        • 与进程关系:线程共享所属进程的内存地址空间,因此同一进程中线程访问相同的变量,并从同一堆中分配对象,从而相对进程间通信机制实现了良好的数据分享。但是需要我们用明确的同步机制来管理共享数据,否则会造成数据的不可预知的更改。
    • 为什么进行多线程编程
      • 多线程优点:
        1. 更好的实现并发
        2. 开发维护性能:恰当使用线程,可以降低开发和维护开销,并提高复杂应用的性能。
        3. 切换:CPU切换线程比切换进程的开销小。
        4. 创建、撤销:线程创建和撤销的开销比进程的小。
    • Java在多线程应用的优势
      • 语言级提供对多线程程序设计的支持
      • 线程模型 1:1 N:1 M:N 自己去了解吧,这里不赘述
    • Java线程的生命周期
      • 创建(新线程):
        1. new创建线程实例后,仅作为对象实例存在
        2. JVM没有为其分配 CPU时间片和其他线程所需运行资源
      • 就绪:
        1. 位于创建状态的线程调用start()方法后只调一次。
        2. 得到除CPU时间之外的其他系统资源,等待JVM的线程调度器按线程优先级调度,从而使线程拥有Cpu时间片
      • 运行:
        1. 就绪态线程获取到cpu 就进入运行态
      • 等待/阻塞
        1. 线程运行过程中被剥夺资源或等待某些事件就进入等待/阻塞状态;sleep()方法被调用(线程主动放弃所占用的cpu资源),线程使用wait()等待条件变量,或处于I/O等待(直到该方法返回之前),调用suspend()方法将线程状态转换为挂起状态。
        2. 线程将释放占有的所有资源,但是并不释放锁,所以容易引发死锁,直到应用程序调用resume()恢复线程运行。等待事件结束或得到足够的资源就进入就绪态。
      • 死亡
        1. 线程结束(run()结束),线程抛出一个未捕获的Exception和Error,调用线程对象的stop()方法后
        2. JVM收回线程占用的资源
  • 线程创建与调度

    • Thread类

      • java.lang.Thread类
      • run方法
      • 实现多线程最直接简单的方法:继承Thread类并重写run方法并构建其对象。
      • Thread的API构造方法
    • Runnable接口

      • 摆脱Java单继承,解决一旦类继承Thread之后不能继承其他父类的问题.

      • 可使用Lambda表达式

      • 实现方式:线程类实现Runnable接口,并重写run方法。然后传入Thread类的参数。

        //实现runnable接口
        public class C implements Runnable{
          public void run(){
            
          }
          public static void main(String[] args){
            C c=new C();
            Thread thread=new Thread(c);
          }
        }
        //lambda表达式通过Runnable实现线程   
        public class C2{
          public static void main(String[] args){
            Runnable runnable=()->{方法体};
            Thread thread=new Thread(runnable);
          }
        }
        
    • 线程启动和停止

      • 线程启动:

        • 主线程:任何Java程序启动,一个线程立刻运行,执行main方法。
        • 主线程特殊之处:
          • 它是产生其他子线程的线程
          • 通常最后结束,因为要执行子线程的关闭工作。
        • 主线程中启动其他线程,不能直接调用线程对象的run()
        • 注意:start()方法调用后并不是立即执行多线程代码,而是将线程为就绪状态,什么时候运行是由OS调度决定。
      • 线程停止:

        • stop():用于停止一个已经启动的线程;已被废弃,不建议使用,因为不安全。

          • 因为它会解锁线程所有已经锁定的监视程序,在这个过程中会出现不确定性,导致这些监视器程序监视的对象出现不一致的情况,或者引发死锁。
        • 如何安全的停止一个正在运行的线程

          • 线程对象run方法执行完后,线程会自然消亡,因此如果需要在运行过程中提前停止线程,可以通过改变共享变量值的方法让run方法结束

            public class ThreadStopEX{
              private boolean flag=true;
              //修改共享变量,促使run方法循环结束,从而完成方法的调用,线程自然消亡。
              //这里我们只要提供一个条件可以让run方法结束就是ok的
              public void stopThread(){
                falg=false;
              }
              public void run(){
                while(){
                  flag;
                }
                public static void main(String[] args) throws Exception{
                  ThreadStopEX tse=new ThreadStopEX();
                  tse.start();
                  tse.stopThread();
                }
              }
            }
            
        • 如何安全的停止一个由于等待某些事件发生而被阻塞的线程呢?

          • 具体场景:线程因为等候资源、数据而调用Thread.sleep方法、Object类的wait方法造成线程阻塞,并使线程处于不可运行状态;此时即时主程序将线程的共享变量设置为false,线程也无法检查循环标志,也就无法中断。
          • 具体解决建议:使用Thread类的interrupt方法:使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态,退出阻塞代码。
        • 判断线程是否通过interrupt方法被终止

          • 静态方法interrupted:判断当前线程是否被中断
          • 非静态方法isInterrupted:判断其他线程是否被中断
          • 注意:
            1. 需要区分捕获到的中断异常是否由我们主动调用interrupt方法引起。因为还有别的因素可能导致该异常,而某些时候即时产生了此异常也应该让线程继续执行,此时我们需要编写代码来进行区分
            2. interrupt()不能阻断I/O阻塞或线程同步引起的线程阻塞。例如一个线程等待键盘输入或等待网络连接等I/O资源。
        • 如何处理由I/O资源引起的线程阻塞时的线程中断问题

          • 关闭底层I/O通道,人为引发异常从而进行共享变量重新赋值而跳出run方法。
        • nio支持非阻塞式的事件驱动读取操作,在这种模式下,不需要关闭底层资源即可通过interrupt方法直接中断其等待操作。

    • 线程不同状态之间的转换

    • 守护线程

      • Java线程划分
        • 守护线程
          • 指在程序运行时在后台提供一种通用服务的线程,比如垃圾回收线程就是一个称职的守护者。
          • 特点:这种线程并不属于程序中不可或缺的部分。所有非守护线程结束时,程序就终止了,同时会杀死进程中的所有守护线程。反之,只要任何非守护线程还在运行,程序就不会终止。
        • 用户线程
          • 与守护线程几乎没有区别,唯一的不同之处就在于虚拟机的退出:如果用户线程已经全部退出运行,只剩下守护线程存在,JVM也就退出了。
      • 线程转换为守护线程:
        • 调用Thread对象的setDaemon(true)方法(默认守护线程属性为false,即默认创建线程对象为非守护的用户线程)
          • 注意
            1. 不能把正在运行的常规线程设置为守护线程即thread.setDaemon(true)必须在thread.start()之前设置,否则会抛出IllegalThreadStateException异常。
            2. Daemon线程中产生的新线程也是Daemon。
            3. 不是所有的应用都可以分配给Daemon线程进行服务,比如读写操作或者计算逻辑。因为可能Daemon Thread没有操作,JVM就已经退出了。
      • 判断线程是否为守护线程:isDaemon()方法
    • 线程类重要方法的作用

      • 这里查看Thread API
      • Thread 的名字
        • 设置名字
          1. 构造方法提供
          2. setName(String name)方法给定
        • 获取名字:getName()
        • 默认名字:如果没有为线程显式提供名字,Java按照thread-0,thread-1—的方式为线程提供默认的名字(main方法启动的主线程名字为main)
      • Thread优先级
        • 优先级:高优先级的线程比低优先级有更高的几率得到执行(即优先级高不一定有优势)
        • java线程的优先级是一个整数,取值范围为1(Thread.MIN_PRIORITY)-5(Thread.NORMPRIORITY)-10(Thread.MAX_PRIORITY)
          • 注意:
            1. 事实上Java线程在没有明确指定的情况下,其优先级并不一定为5,而是和父线程(创建本线程的线程)的优先级保持一致,main默认5
            2. 优先级不能超过1-10,否则抛出IllegalArgumentException,建议使用3个常量来确定。
            3. 线程优先级不能超过其线程组的优先级
        • 设置优先级:setPriority(final修饰,不能被子类重写)
      • 线程组:Java提供的一个线程统一管理工具,构建线程组对象后,可以通过Thread类的构造方法将线程加入线程组。线程组API interrupt可以中断线程组中所有线程。
        • 线程组最大优先级:该线程组的线程优先级不能超过该组优先级。
          • 注意:系统线程组的最大优先级默认为Thread.MAX_PRIORITY
          • 创建线程组的时候最大优先级默认为父线程组的最大优先级(如果未指定父线程组,则其父线程组默认为当前线程所属线程组)
          • setMaxPriority的问题:
            • 更改本线程组及其子线程组(递归)的最大优先级
            • 但不能影响已经创建的直接或间接属于该线程组的线程的优先级。只有试图改变子线程优先级或者创建新的子线程的时候,线程组的最大优先级才起作用。
      • 关于线程优先组需要注意:
        1. Thread.setPriority可能根本不会做任何事情,与os和jvm版本有关
        2. 线程优先级对于不同线程调度器可能有不同含义
        3. 线程优先级通常是全局和局部的优先级设定的组合。Java的setPriority是局部优先级。通常是一种保护方式
        4. 不同系统有不同的线程优先级的取值范围。这样就有可能出现几个线程在同一OS中有不同的优先级,在另一os中却有相同的优先级。
      • 线程主动放弃占据的资源
        • Thread提供2个静态方法
          • sleep():
            1. 当前线程进入阻塞状态:
              • 执行sleep的线程在指定时间内不会执行,同时sleep方法不会释放锁资源(即如果正在运行的线程占有某个资源的同步锁,它不会释放掉这个同步锁,其他线程仍然不能访问该资源)
            2. 即使没有其他等待运行的线程,当前线程也会等待指定时间
            3. 其他等待执行的线程的机会是均等的
              • java并不保证线程在阻塞给定的时间后能够马上执行。
          • yield():
            1. 当前线程转入暂停执行(即就绪可执行)的状态
              • 所以执行yield的线程可能在进入就绪状态后马上又被执行。
            2. 如果没有其他等待运行的线程,当前线程会马上恢复执行
            3. 会将优先级相同或更高的线程运行
              • 所以这里也有了解释
          • yield与sleep的区别:sleep提供阻塞时长,可让低优先级线程得到执行机会,而yield使线程进入就绪状态,没有阻塞时长,只能让相同或更高优先级线程有执行机会,甚至有时JVM认为不符合最优资源调度时会忽略该方法调用(System.gc())
      • Thread.join方法:等待其他线程结束,当前运行的线程可以调用另一线程的join方法,当前运行线程将转到阻塞状态,直到另一个线程执行结束,然后当前运行线程恢复运行。
        • t.join:阻塞调用此方法的线程,直到t线程完成,此线程再继续。
        • 通常用于main主线程,等待其他线程结束后再结束主线程。
        • 解析:join实现通过wait。当main线程调用t.join时,main线程会获得线程对象t的锁,调用该方法的wait方法,直到该对象唤醒main线程。
  • 线程同步

    • 不同步会发生的问题

      • Java内存模型中的数据可见性和JVM重排序

      • Java为保证平台无关性,使应用程序与操作系统内存模型隔离,定义了自己的内存模型。

      • Java内存模型

        1. 内存分主内存和工作内存

        2. 主内存所有线程共享,工作内存每个线程分配一份,各线程工作内存彼此独立,互不可见。

        3. 线程启动,JVM为每个线程分配一块工作内存,不仅包含线程内部定义的局部变量,也包含线程所需使用的共享变量(非线程内构造的对象)的副本,为提高执行效率(副本中寻址并读取数据比直接读取主内存更快)

        4. Java内存模型定义一系列工作内存和主内存之间交互的操作与操作之间顺序的规则。

          ​ 例如共享普通变量,约定在变量在工作内存中发生变化之后,必须要写回主内存;但这个约定只规定结果而并没有规定时机,所以可能在实际写回主内存之前,有很多线程已经在主内存中读取已经无效的数据。

      • JVM指令重排序

        • 重排序:编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。

          • 通过调整指令顺序,在不改变程序语义的前提下,尽可能减少寄存器的读取、存储次数、充分复用寄存器的存储值。

          • 举个例

            指令1:计算一个值赋给变量A并存入寄存器
            指令2:与A无关,但需要占用寄存器(假设占用A的寄存器)
            指令3:要使用A的值,且与第二条指令无关
            按照顺序一致性模型:
            指令1执行后A放入寄存器,
            指令2执行后A不再存在
            指令3A重新被读入寄存器。
            这个过程,A的值没有发生变化。
            JVM指令重排序:
            通常编译器会交换第二条和第三条指令的位置。
            这样第一条指令结束时A存在于寄存器中,然后我们从寄存器直接读取A的值,降低重复读取的开销。
            
        • 顺序一致性模型:即假设指令执行的顺序是被编写在代码中的顺序,与处理器或其他因素无关。这种模型效率很低。

        • 千万不要随意假设指令执行的顺序

        • 数据依赖:如果两个操作访问同一个变量,且这两个操作中有一个为写操作,构成数据依赖。

          • 数据依赖类型

            写后读   a=1;b=a;    写一个变量之后,再读这个位置
            写后写   a=1;a=2;	  写一个变量后,再写这个变量
            读后写   b=a;a=1;    读这个变量后,再写这个变量
            
            这三种情况,只要重排序两个操作的执行顺序,程序执行结果将会被改变。
            因此:编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。
            就是说:单线程环境下,指令执行的最终效果应当与顺序执行下的效果一致,否则优化就失去意义。--as if serial semantics
            
        • 重排序对多线程环境带来的影响隐患

          public class HappensBeforeTest{
            	int a=0;
            	boolean flag=false;
            	public void write(){
                a=1;//1
                flag=true;//2
            	}
            	public void read(){
                if(flag){//3
                  int i=a*a;//4
                }
            	}
          }
          问题:flag变量是个标记,用来标识变量a是否已被写入。假设有两个线程A和B,A首先执行write,随后B线程执行read()方法。线程B在执行操作4时,能否看到线程A在操作1对共享变量a的写入。
          思考:不一定看到。
          分析:操作1和操作2没有数据依赖关系,编译器和处理器可以对这两个操作重排序;同样操作3和操作4没有数据依赖关系,所以也可以进行重排序。
          
          • 单线程中,重排序不会改变执行结果。但在多线程程序中,对存在控制依赖的操作重排序,可能改变程序执行结果。
    • synchronized关键字使用

      • volatile关键词 :用于声明时修饰变量
        • 两条语义
          1. 保证变量在内存模型中的可见
            • volatile变量java要求工作内存中发生变化之后,必须马上写回主内存;线程读取volatile变量,必须马上到主内存中去取最新值而不是读取本地工作内存的副本。
            • 换句话说:线程A对变量X进行修改后,在线程A后面执行的其他线程都能看到变量X的变动。
          2. 禁止指令重排序:对volatile变量的操作不能进行重排序。
        • volatile修饰变量不能保证对其操作的原子性。也就是说volatile缩短了普通变量在不同线程之间执行的时间差,但仍然不能保证原子性。
        • 随着虚拟机的不断优化,如今普通变量的可见性已经好多了所以volatile如今没有啥使用场景
      • Java对32位内的数据的load和save操作都是原子的。
      • 线程同步:需要将多条代码视作一个整体调度单位,希望这个调度单位在多线程环境的调度顺序不影响任何结果。除保证可见性、防止重排序改变语义外,还要对代码进行原子保护。这种就叫线程同步
        • 主要作用:实现线程安全的类。
        • java使用synchronized关键字对操作进行同步处理。
      • JVM的几个规范:
        1. JVM中,每个对象和类在逻辑上都是和一个监视器相关联的
        2. 为了实现监视器的排他性监视能力(即保证资源只能同时被一个线程访问),JVM为每一个对象和类都关联了一个锁,锁住了一个对象,就是获得对象相关联的监视器。
          • 监视器:
            1. 类似令牌,获取令牌的线程才能操作资源,操作完成释放令牌,下一个线程才有机会重新获取令牌。
            2. 进入->获得->持有->释放->退出
            3. 一个线程可以允许多次对同一对象上锁,对于每一个对象来说,JVM维护一个计数器,记录对象被加了多少次锁,没被锁的对象的计数器是0,线程每加锁一次,计数器就加1,每释放一次,计数器减一,当计数器跳到0的时候,锁被完全释放了。
            4. java虚拟机中的一个线程在它到达监视区域开始处的时候请求一个锁,JAVA程序每一个监视区域都与一个对象引用相关联。
      • Java中通过synchronized关键字获得对象锁,实现线程同步。
      • synchronized会降低程序的性能 所以我们会缩短同步区域
      • 实现同步有两种方法:
        • 同步方法
        • 同步代码块
          • 临界区同步块:希望防止多个线程同时访问方法内部的部分代码而不是整个方法
          • synchronized:用来指定某个对象,此对象的锁被用来对花括号内的代码进行同步控制
          • 可以适当降低同步整个方法带来的性能消耗
    • 线程死锁

  • 线程间通讯

  • 线程高级调度

  • JDK1.5+的新工具

  • 任务调度

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值