Java进阶知识学习:多线程并发

目录,更新ing,学习Java的点滴记录

  目录放在这里太长了,附目录链接大家可以自由选择查看--------Java学习目录

引入

  1. 更新到这里之前,目录中提到的内容都属于顺序编程的内容,即程序中的所有事物在任意时刻都只能执行一个步骤
  2. 我们很熟悉操作系统中的多任务:在同一时刻运行多个程序的能力,比如网页上看文章的时候听着音乐,看视频的时候同时下载着文件等等.操作系统将CPU的时间片分配给每一个进程,让人有一种并行处理的感觉
  3. 编程中很大一部分都可以使用顺序编程来解决,但是仍存在着某些问题,当使用并行执行的方式来运行程序时会显得更加方便且有必要.并行编程可以使程序执行速度得到很大的提高,能够掌握并发可以说是编程能力的一种很大的提升.例如,Web系统是最常见的Java应用系统之一,而基本的Web库类,`Servlet具有天生的多线程行–这很重要,因为Web服务器经常包含多个处理器,而并发是充分利用这些处理器的理想方式.即便是像Servlet这样看起来很简单的情况,只有当你理解并发问题时,才能更好的使用它们.
  4. 与此同时,并行执行的任务难免会产生互相干扰,这时候并发问题就会接踵而至,你可能会想,我通过严格的代码编写和审查,可以编写出正常工作而不出现问题的并发程序是可能的.但是实际上并发问题的不确定性更多.实际情况中,更容易发生的是编写的并发程序在给定条件的时候会工作失败,因为遇到的一些条件可能极少发生,以致于先前对程序进行测试的时候没有发现.
  5. Java是一种多线程语言,提出了并发问题.学习并发编程就像是进入新的领域,但是难度与面向对象编程差不多,花点功夫可以明白其基本机制,真正地掌握实质,需要深入的学习和理解.

什么是线程

  1. 并发编程使我们可以将程序划分为多个分离的,独立运行的任务.通过使用多线程机制,这些独立任务中的每个都将由执行线程来驱动.而单个进程中可以拥有多个并发执行的任务,底层机制是切分CPU时间.
  2. 线程模型为编程带来了便利,它简化了在单一程序中同时交织在一起的多个操作的处理.在使用线程时,CPU将轮流给每个任务分配其占用时间.使用线程机制是一种建立透明的,可扩展的程序的方法.
  3. 程序,进程,线程概念
     1) 程序:Program–指令的集合
     2)进程:Process–进程是程序的一次静态执行过程,占用特定的地址空间,每个进程都是独立的,一个进程可拥有多个并行的线程
     3)线程:是进程中一个单一的顺序控制流,线程又被称为轻量级进程,同一个进程中的线程共享相同的内存单元/内存地址空间,可以访问相同的变量和对象,而且它们从同一堆中执行分配对象|通信|数据交换|同步等操作.由于线程间的通信是在同一地址空间上进行的,所有不需要额外的通信机制,这使得通信更加简便而且信息传递的速度也更快
  4. 进程与线程区别
    在这里插入图片描述
     单个进程一般都包含了N多个线程,如果线程结束,进程并不一定结束,某个进程结束,属于它的线程也都将结束,CPU调度执行的是线程

多线程的实现方式1_继承Thread类

  1. Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例.启动线程的方式是通过Thread类的start()方法.start()方法是一个native方法,它将启动一个新线程并执行run()方法.
  2. 具体步骤
     1) 继承Thread类
     2)重写run()方法
     3)通过start()方法启动线程
  3. 示例
    在这里插入图片描述
    在这里插入图片描述
     结合程序运行结果和代码书写顺序可以发现一个有意思的事情,代码中在main方法内先调用了start()方法开启了另外一个线程,然后执行了main方法中的for循环,按我们之前的经验可能认为要先执行完start()方法的全部内容,然后才执行main方法的for循环,但是程序结果告诉我们并不是这样,结果中很明显可以看出main所在的主线程和start方法开启的新线程是交替执行的,其实我们可以这样理解:调用start方法之后会开辟一个新的线程,就好比一条大河流着流着会出现一些分支,那么程序在不同线程内的执行情况就不一定相同了,深究的话就是两个线程轮流获得CPU的时间片,拿到了就执行拿不到就等待下一次CPU调用,所以这种现象也可以帮助我们初步理解一下线程的小特点.
  4. 缺点
     这里我们就要强调一下Java的一个特点了:单继承,我们如果采用这种方式的话,就必须要继承一个Thread类,此时在你的应用场景中不需要继承其他类来实现其他需求的话,倒也没什么,但是如果你要实现某个功能还需要继承其他类就不行了,这也是这种方法一个很明显的缺点–下面的接口方式就能很好的解决这个烦恼

多线程的实现方式2_实现Runnable接口

  1. 实现Runnable接口的方式可以很好的解决由于Java单继承特点所导致的一些问题,当你实现多线程的类恰哈也需要继承其他类来满足其他功能是,实现Runnable接口是一个不错的选择
  2. 具体步骤
     1) 实现Runnable接口
     2) 实现run()方法
     3) 创建自定义的线程类对象
     4) 以自定义线程类对象做实参传入构造函数创建Thread类
  3. 示例
    在这里插入图片描述

多线程的实现方式3_实现Callable接口

  1. 前面两种实现方式的缺点
     1)实现的run() 没有返回值 2) 不支持泛型 3) 异常必须处理
  2. 第三种方式
     1) `实现Callable接口,重写call方法
     2) Callable功能更加强大
      a. Future接口位于java.util.concurrent包中,可以对具体Runnable、Callable任务的执行结果进行取消(cancel方法,尝试取消执行此任务)、查询是否完成(isDone方法)、获取结果(get方法,等待完成,然后检索其结果)等。
      b. FutureTask是Future接口的唯一实现类
      c. FutureTask同时实现了Runnable,Future接口,它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
     3) 继承结构
    &ems; 在这里插入图片描述
  3. 示例
    在这里插入图片描述

线程状态(生命周期)

  1. 当线程创建被启动以后,它并非一启动就进入了执行状态,也不是一直处于执行状态.在线程的生命周期中,它要经过新建(New),就绪(Runnable),执行(Running),阻塞(Blocked)和死亡(Dead)五种状态.尤其是当线程启动以后,它不可能一直"霸占"这CPU独自运行,因为CPU需要在多条线程之间切换,于是线程状态也会多次切换.
  2. 如果你想要查看一个线程当前的状态,可以使用getState方法
    在这里插入图片描述
  3. 新建状态(New)
     当使用new操作符创建一个新线程时,如new Thread(),该线程还没有开始运行.这意味着它的状态时new.当一个线程处于新创建状态时,程序还没有开始运行线程中的代码.此时由JVM为其分配内存,并初始化其成员变量的值.新建状态通过调用start()方法进入就绪状态
  4. 就绪状态(Runnable)
     处于就绪状态的线程具备了运行条件,但还没有分配到CPU,处于就绪队列中JVM会为其创建方法调用栈和程序计数器,等待CPU调度运行.当系统选定一个等待执行的线程后,它就会从就绪状态进入执行状态,该动作称为"CPU调度".如果系统属于
     扩展: 抢占式调度`的话,会给每一个就绪状态的线程一个时间片来执行任务.当时间片用完,操作系统剥夺该线程的运行权,并给另一个线程运行机会.当选择下一个线程时,操作系统考虑线程的优先级.
  5. 执行状态(Running)
     当处于就绪状态的线程获得了CPU,开始执行run()方法的代码,直到等待某资源而阻塞或者执行结束而死亡.如果在给定的时间片内没有执行结束,就会被强行中断,等待CPU的下一次调度.
  6. 阻塞状态
     阻塞状态是因为线程因为某种原因失去了CPU的使用权,让出了时间片,暂时停止运行.在阻塞状态的线程不能进入就绪队列.只有当引起阻塞的原因消除之后,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续执行.
     当线程处于阻塞状态时,它暂时不活动.它不运行任何代码且消耗最少的资源.直到线程调度器重新激活它.正常情况下,造成阻塞状态的原因有以下几种:
      1) 等待阻塞:正在运行的线程当执行Object.wait()或者Thread.join方法时,JVM会把该线程放入等待队列
      2) 同步阻塞:当一个线程试图获取一个内部的对象锁(非java.util.concurrent库中的锁),而该锁被其他线程持有,则该线程进入阻塞状态.当其他持有该锁的对象释放锁时,并且线程调度器允许本线程持有它是,该线程就会变成非阻塞状态
      3) 计时等待阻塞:线程类中有几个方法含有超时参数,如Thread.sleep和Object.wait.调用它们导致线程进入计时等待阻塞状态.这一状态将一直保持到超时期满或者接受到通知,线程再重新转入运行状态
  7. 死亡状态(Dead)
     死亡状态时线程生命周期中最后一个阶段,线程死亡的原因有三个:
     1) 因run()方法正常退出而自然死亡
     2) 因为一个没有捕获的异常终止了run()方法而意外死亡
     3) 直接调用了该线程的stop()方法来结束该线程而强制死亡—该方法容易造成死锁.该方法会抛出ThreadDeath错误对象,但是stop()方法已经过时,建议不要使用了
  8. 线程状态转换图
    在这里插入图片描述

线程基本方法

  1. 首先给一张图,完整描述了线程常用方法的调用与线程状态切换的关系
    在这里插入图片描述
  2. 线程等待(wait)
     调用该方法的线程进入waiting状态,只有等待另外线程的通知或被中断才会返回,需要注意的是调用wait()方法后,会释放对象的锁,因此,wait方法一般用在同步方法或者同步代码块中
  3. 线程睡眠(sleep)
     sleep导致当前线程休眠,与wait方法不同,sleep不会释放当前占有的锁,sleep(long)会导致线程进入timed-waiting状态
  4. 线程让步(yield)
     yield会使当前线程让出CPU执行时间片,与其他线程一起重新竞争CPU时间片,一般情况下,优先级高的线程有跟大的可能性成功竞争得到CPU时间片,这不是绝对的,有的操作系统对线程优先级不敏感
  5. 线程中断(interrupt)
     中断一个线程,其本意是`给这个线程一个通知信号,会影响这个线程内部的一个中断标识位,这个线程本身并不会因此而改变状态(如阻塞,终止等)
     1) 调用interrupt()方法并不会中断一个正在运行的线程.也就是说处于Running状态的线程并不会因为被中断而终止,仅仅改变了内部维护的中断标识为而已
     2) 若调用sleep()而使得线程处于timed-waiting状态,这时调用interrupt()方法,会抛出InterruptedException异常,从而使线程提前结束timed-waiting状态
     3) 许多声明抛出InterruptedException异常的方法在抛出异常前,都会清除中断标识位,所以抛出异常后如果调用isInterrupted()方法会返回false
     4) 中断状态时线程固有的一个标识为,可以通过此标识位安全的终止线程.比如,你想终止一个线程时,可以调用该线程的interrupt()方法,在线程的run方法内部可以根据isInterrupted()的值来优雅的终止线程
  6. join方法
     join()方法主要作用就是同步,它可以使得线程之间的并行执行变为串行执行。在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行
     使用原因:有些情况下,主线程创建并启动了子线程,但是需要用到子线程返回的结果,也就是主线程需要等待子线程结束后再运行
  7. 线程唤醒
     Object类中的notify()方法,唤醒此对象监视器上单个等待的线程,如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意的. 线程通过调用wait()方法后会在独享的监视器上等待.被唤醒的线程会以正常方式与其他等待被CPU调用的线程竞争,类似的notifyAll(),就是唤醒监视器上等待的所有线程.

获取线程基本信息方式

  1. static Thred currentThread():返回当前正在执行的线程
    在这里插入图片描述
     你可能对控制台打印的信息很好奇,这些信息啥意思,别急,我们看一下源码,鼠标左键移动到toString()然后左键进入
    在这里插入图片描述 从源码中可以看出,第一行是创建了一个线程组,然后判断线程组是否为空,不为空时,返回的信息包括:线程的名称,优先级,线程组的名称,如果线程组为空时,则不打印.
  2. final String getName():返回线程名称
    在这里插入图片描述
  3. final boolean isAlive():判断线程是否处于活动状态
    在这里插入图片描述
     控制台中结果出现的原因是:main主线程先执行,此时新建的线程还没有执行run方法所以是活着的,然后run方法内自然也是活着的.

暂停线程方式

  1. final void join(): 调用该方法的线程强制被执行,其他线程处于阻塞状态,该线程执行完毕之后其他线程再根据CPU的调度而执行
  2. static void sleep(long millis):使当前正在执行的线程休眠millis毫秒,线程处于阻塞状态,不会释放锁,sleep时别的线程也可以访问锁定对象
  3. static void yield();暂停当前正在执行的线程对象暂停(及放弃当前拥有的cup资源), 从运行态直接进入就绪态,并执行其他线程.
  4. final void stop():强制线程停止执行,已过时

终止线程方式

  1. 正常运行结束
     程序运行结束,线程自动结束
  2. 使用退出标志退出线程
     一般run方法执行完,线程就会结束,然后,常常有些线程是伺服线程.他们需要长时间的运行,只有在外部某些条件满足的情况下,才能关闭这些线程.通常情况下使用一个while循环将这些任务放在一起,比如:while(true){…},并在线程中设置一个boolean型的变量,根据条件的满足情况适当改变boolean类型的值进而决定线程的中断还是停止.常见的情况是使用一个Java关键字volatile,使用该关键字声明的变量是同步的,同一时刻只能由一个线程修改
    在这里插入图片描述
  3. Interrupt结束线程,情况分两种
     1)线程处于阻塞状态:比如使用了sleep,同步锁的wait,Socket的receiver,accept等方法时,会使线程处于阻塞状态.当调用线程的interrupt方法时,会抛出InterruptException异常.阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后break跳出循环状态,从而可以结束这个线程的执行.可能以为只要调用interrupt方法线程就会结束,实际上是错的,一定要先捕获异常,之后通过break跳出循环,才能结束run方法
     线程处于未阻塞状态:使用isInterrupted()判断线程的中断标志来退出循环.当使用interrupt()方法时,中断标志会置为true,和使用自定义的标志来控制循环是一样的道理.
    在这里插入图片描述
  4. stop方法退出线程
     程序中可以调用线程对象的stop()方法来强行终止线程,但是stop方法很危险,就像突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果.造成不安全的原因主要是:thread.stop()调用之后,线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁.一般任何加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop后导致了该线程所持有的所有锁的突然释放,那么被保护的数据就有可能出现不一致性.

sleep与wait区别

  1. 对于sleep()方法,该方法属于Thread类中,而wait()方法属于Object类中
  2. sleep()方法导致了程序暂停执行指定时间,让出CPU给其他线程,但是他的监控状态依然保持,当指定的时间到了又会自动恢复到运行状态
  3. 调用sleep()方法的过程中,线程不会释放对象锁
  4. 当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态

start与run区别

  1. start()方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕,可以直接继续执行调用start()方法位置后面的代码
  2. 通过调用Thread类的start()方法来启动一个线程,这时此线程处于新建状态,并没有运行
  3. 方法体run()称为线程体,它包含了要执行这个线程的内容,线程就进入了运行状态,开始运行run函数中的代码.run()方法运行结束,此线程终止,然后CPU再调度其他线程.

线程优先级

  1. 在java程序设计语言中,每个线程都有一个优先级.默认情况下,一个线程继承它的父线程(关于父子线程见示例)的优先级.可以用setPriority方法提高或降低任何一个线程的优先级.可以将优先级设置为MIN_PRIORITY(在Thread类中定义为1)与MAX_PRIORITY(定义为10)之间的任何值.NORM_PRIORITY被定义为5(默认优先级),设置优先级时不能超过范围,否则会抛出异常
  2. 每当线程调度器有机会选择新线程时,它首先选择具有较高优先级的线程.但是线程优先级是高度依赖于系统的.比如:Windows有7个优先级别.一些Java优先级将映射到相同的操作系统优先级.Oracle为Linux提供的Java虚拟机中,线程的优先级被忽略—所有线程具有相同的优先级.
  3. 示例
    在这里插入图片描述
     从结果中我们可以看到main线程优先级是默认值5,而从main线程中创建的线程t2优先级也是5,这里可能有两种想法,一个是t2也是采用默认值优先级5,二是t2线程默认优先级取决于main线程,那好现在我们手动将main线程优先级改变,再次查看t2线程情况
    在这里插入图片描述
     结果发现,t2线程优先级随着main线程在改变,其实这就可以理解为父子线程的关系,虽然线程中并没有严格意义上的父子线程,但是可以这样来理解.
    &esmp;最后我们再反过来测试一下,设置t2线程优先级,查看main线程情况
    在这里插入图片描述

守护线程(服务线程)

  1. 转换方式
     调用线程的setDaemon(true)即可将线程转换为守护线程.该方法必须在线程启动之前调用
  2. 唯一用途:为其他线程提供服务,在没有可服务线程是自动关闭.比如计时线程,定时的发送计时信号给其他线程,当只剩下守护线程是,虚拟机就退出了.
  3. 优先级:守护线程优先级比较低
  4. 生命周期:守护进程(Daemon)是运行在后台的一种特殊进程.它独立于控制终端并且周期性地执行某种任务或等待处理发生的事件.守护线程不依赖于终端,依赖于系统.

同步

  1. 大多数实际应用中,两个或两个以上的线程需要共享对同一数据的存取.如果两个线程存取相同的对象,并且每个线程都调用了一个修改该对象状态的方法,将会发生混乱.
  2. 混乱情况示例–卖票
     共有10张票,由3个窗口去卖票,每个窗口假设有30人排队,期望获得的输出是10张票按顺序依次卖出
    在这里插入图片描述
    在这里插入图片描述
     可以简单解释一下上图中结果的原因:t1线程启动之后调用了run方法,首先拿到了第一张票后运行完了run方法,然后t2线程启动后进入到run方法,此时拿到了第二张票,但是与此同时t3也启动拿到了第3张票,t3比t2提前运行完run()方法,所以就提前打印了,以此类推导致了输出结果的混乱,此外当数据量比较大时,甚至可能出现负值或者相同值,这都是多线程引起的安全性问题
  3. 为了避免多线程引起的对共享数据的操作混乱,必须使用同步存取机制,同步机制可以指定代码块或者方法在某个线程正在调用时,其他线程无法进入,只能等待正在调用中的线程执行完毕才有机会去调用
  4. 同步锁:当多个线程访问同一个数据时,容易出现问题,为了避免情况发生,我们要保证线程同步互斥,就是只并发执行的多个线程,在同一时间内只允许一个线程访问共享数据.`Java中可以使用synchronized关键字来取得一个对象的同步锁.
  5. 同步方式一 同步代码块
    在这里插入图片描述
    在这里插入图片描述
  6. 同步方式二 同步方法
     将需要互斥访问的代码保存在一个同步方法内,然后通过调用该方法实现互斥访问
     同步方法的同步监视器为调用该方法的对象
    在这里插入图片描述
    在这里插入图片描述
  7. 同步方式三_Lock锁
     1) 介绍
      a. JDK1.5之后新增功能,与Synchronized相比,Lock可以提供多种锁方案,更加灵活
      b. Java.util.concurrent.locks中Lock是一个接口,他的实现类是一个java类,而不是作为语言的特性(关键字)来实现
      c. 如果同步代码有异常,要将unLock()方法finally中
     2) 步骤
      a. 创建Lock对象
      b. 调用lock()方法上锁
      c. 调用unlock()方法解锁
     3) Lock与synchronized的区别
      a. Lock是显示锁(手动开启和关闭锁,别忘关闭锁),synchronized是隐式锁
      b. Lock只有代码块锁,synchronized有代码块锁和方法锁
      c. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)
      d. Lock确保一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则他将一直等待(即被阻止),直到该对象被释放。lock()方法会对Lock实例对象进行加锁,因此所有该对象调用lock()方法的线程都会被阻塞,直到该Lock对象的unlock()方法被调用。
     4) 示例代码
public class TestLock implements Runnable {

    private int count = 0;

    //模拟多个线程对count值进行自增操作
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            count++;
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "执行了run方法,count变为:" + count);
        }
    }
}

class TLock {

    public static void main(String[] args) {
        TestLock lock = new TestLock();
        //代理类对象,并起名字
        Thread t1 = new Thread(lock, "A");
        Thread t2 = new Thread(lock, "B");
        Thread t3 = new Thread(lock, "C");
        /**
         * 三个线程每个都会让count自增5次,最终结果是15
         * 我们期望的结果是在自增操作相关代码每次同一时刻只有一个线程在使用
         * 当该线程执行完自增操作后,等待的下一个线程方可进入
         */
        t1.start();
        t2.start();
        t3.start();
    }
}

  但是结果并非如此,由于线程的抢占,输出结果是混乱的.因此我们可以对期望一个线程单独调用的代码加锁
  在这里插入图片描述
  在这里插入图片描述

死锁

  1. 概念:多线程情况下,某个任务在等待另一个任务完成,而后者后再等待别的任务,这样一直下去,直到这个链条上的任务在等待第一个任务释放锁,这得到了一个相互等待的连续循环,没有哪个线程能够继续,称之为死锁
  2. 程序可能运行起来很良好,但是具有潜在的死锁危险.这是,死锁可能发生,而事先没有任何征兆,所以缺陷潜伏在你的程序里面,直到客户发现它出乎意料的发生.因此在编程并发程序是,进行仔细的程序设计以防止死锁是关键部分.
  3. 死锁的条件,同时满足时就会发生死锁
     1) 互斥条件.任务使用的资源中至少有一个是不能共享的.
     2)至少有一个任务它必须持有一个资源且正在等待获取一个当前被别的任务持有的资源.
     3) 资源不能被任务抢占,任务必须将资源释放当成普通时间.
     4) 必须有循环等待,这是一个任务等待其他任务所持有的资源,后者又在等待另一个任务持有的资源,这样一直下去,直到有一个任务在等待第一个任务所持有的资源,使得大家都被锁住.
  4. 如何防止死锁
     因为要发生死锁是,上面4个条件必须同时满足,因此防止死锁的话,只需要破坏其中之一即可.并且,防止死锁最容易的是破坏第4个条件.
  5. Java对死锁并没有提供语言层面上的支持,能否通过仔细的设计程序来避免死锁,这取决于你
  6. 有一个避免死锁的经典算法(银行家算法)
     该算法需要检查申请者对资源的最大需求量,如果系统现存的各类资源可以满足申请者的请求,就满足申请者的请求。这样申请者就可很快完成其计算,然后释放它占用的资源,从而保证了系统中的所有进程都能完成,所以可避免死锁的发生。

Java常见锁

  1. 乐观锁
     乐观锁是一种乐观思想,认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都任务别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(跟上一次的版本号做比较,如果一样则更新),如果失败就要重复读–比较—写的操作
     Java中的乐观岁基本都是通过CAS操作实现的,CAS是一种原子操作,比较当前值跟传入的值是否一样,一样则更新,否则失败
  2. 悲观锁
     1)悲观锁就是悲观思想,认为写多,遇到并发的可能性高,每次去读取数据的时候认为别人会修改,所以每次读写操作的时候都会上锁,这样别人想要读写这个数据就会等待直到拿到所.
     2)Java中的悲观锁就是synchronized,AQS框架下的锁时先尝试CAS乐观锁去获取锁,如果获取不到才转换为悲观锁.
  3. 自旋锁
     1) 原理很简单,如果持有锁的线程能在很短时间内释放锁资源,那么这些等待竞争锁的线程就不需要进入阻塞状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取所,这样就避免用户线程造成更多的消耗
     2)线程自旋是需要消耗CPU的,如果一直获取不到所,那线程也不可能一直消耗CPU,因此要设置一个自旋等待的最大时间,当持有锁的线程执行时间超过最大等待时间时,其他等待线程就会进入阻塞状态.
     3)自旋锁优点
      自旋锁尽可能的减少线程阻塞,这对于锁的竞争不激烈且占用锁时间非常短的代码块来说性能大幅度提升,因为自旋的消耗小于线程阻塞挂起再唤醒的操作消耗,这些操作会导致线程发生两次上下文切换
     4)自旋锁缺点
      如果锁竞争十分激烈,或者持有锁的线程需要长时间占用锁,这时候就不适合使用自旋锁了.因为自旋锁在获取锁前一直占用CPU做无用功,同时又大量线程在竞争一个锁,导致获取锁时间很长,线程自旋消耗大于线程阻塞挂起操作的消耗.
     5) 自旋锁最大等待时间阈值
      自旋锁的目的是为了占着CPU的资源不放,等到获取到锁立即执行.自等待时间的选择很重要.在JDK1.5中时间是写死的,在JDK1.6引入了适应性自旋锁,意味着自旋锁的时间不在固定了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间.
  4. 读写锁
    为了提高性能,Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,灵活控制,如果没有写锁的情况下,读是无阻塞的,在一定程度上提供了程序的执行效率.读写锁分为读锁和写锁,多个读锁不互斥,读锁和写锁互斥,这是由JVM控制的
     1)读锁
      如果你的代码只读数据,可能很多人同时读,但不能同时写,就可以上读锁
     2)写锁
      如果你的代码修改数据,只能一个人在写且不能同时读取,就上写锁.
     3)Java中读写锁有接口java.util.concurrent.locks.ReadWriteLock,也有具体实现ReentrantReadWriteLock
     4)使用读写锁的步骤
      在这里插入图片描述

线程池

  1. 构建一个新的线程是有一定代价的,因为涉及与操作系统的交互.如果程序中创建了大量的声明周期很短的线程,应该使用线程池.一个线程池中包含了许多准备运行的空闲线程.将Runnable对象交给线程池管理,就会有一个线程调用run方法.当run方法退出时,线程不会死亡,而是在线程池中准备为下一个请求提供服务.
  2. 另外一个使用线程池的理由是减少并发线程的数据.创建大量的线程会大大降低性能甚至使虚拟机崩溃.如果有一个会创建很多线程的算法那,应该使用一个线程池来限制并发线程的总数
  3. 基本原理:控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果任务数量超过了最大数量,则排队等候,等其他线程执行完毕,再从队列中取出任务来执行.主要特点:线程复用,控制最大并发数,管理线程
  4. 线程复用
     每一个Thread类都有一个start方法,当调用start启动线程时,Java虚拟机会调用该类的run方法.那么该类的run()方法就是调用了Runnable对象的run()方法.`可以继承Thread类,在其start方法中添加不断循环调用传递过来的Runnable对象.这就是线程池的实现原理.
  5. 构建线程池方法
    在这里插入图片描述
    Java里面线程池的顶级接口是Executor,但是严格意义上Executor并不是一个线程池,而是一个执行线程的工具.真正的线程池接口是ExecutorService
     1) newCachedThreadPool—缓存线程池
      创建一个可根据需要自动创建新线程的线程池.对于之前构造的线程将会重用他们.对于执行很多短期异步任务的程序而言,这些线程池会提高性能.调用execute将重用以前的构造的线程(如果线程可用),如果现有线程没有可用的,则创建一个新线程并添加到池中,保存在线程池中的线程如果在60s内没有被使用则会被移除.因此长时间保持空闲的线程池不会使用任何资源
      在这里插入图片描述
     2) newFixedThreadPool----定长线程池
      创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程.当所有线程都处于工作状态时,如果有新的任务要执行,将会在队列中等待.如果某线程因为意外导致线程终止,那么会产生一个新线程来代替它继续工作.除非某个线程被显式关闭,不然它会一直存在线程池中
      在这里插入图片描述
      在这里插入图片描述
      从结果可以看出,始终只存在3个线程来执行任务,如果是newCachedThreadPool的话,就会自动创建10个线程
     3) newScheduledThreadPool----定时线程池
      创建一个线程池,它在指定延迟后运行命令或者定期执行
      示例一:延迟执行
      在这里插入图片描述
      示例二:周期执行
      在这里插入图片描述
     4) newSingleThreadExecutor—单线程线程池
      返回一个线程池(该线程池只有一个线程),这个线程可以在线程死后重新启动一个线程来代替原来的线程继续执行
      在这里插入图片描述
      从结果可以看出始终只有一个线程在运行
  6. 线程池的组成
     1) 线程池管理器:用于创建并管理线程池
     2) 工作线程:线程池中的线程
     3) 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
     4) 任务队列:用于存放待处理的任务,提供一种缓冲机制
    在这里插入图片描述
  7. 拒绝策略
     线程池中的线程用完了,无法继续为新任务提供服务,同时任务等待队列也满了,再也塞不下任何任务,这时候就需要使用拒绝策略机制来解决问题
     JDK内置拒绝策略:
     1) AbortPolicy:直接抛出异常,阻止系统正常运行
     2) CallerRunPolicy:只要线程池没有关闭,就使用调用者所在的线程来运行该任务
     3) DiscardOldestPolicy:丢弃最老的请求,也就是即将被执行的任务,并尝试再次提交当前任务
     4) DiscardPolicy:默默丢弃无法处理的任务,不做任何处理,如果待执行的任务运行被丢弃,这是最好的办法
  8. Java线程池工作过程
     1)线程池刚刚创建的时候,里面没有一个线程,任务队列是作为参数传进来的,不过就算队列里面有任务,线程池也不会马上执行它们
     2)当调用execute()方法添加一个任务时,线程池会做如下判断:
      a. 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务
      b. 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
      c. 如果这时候队列满了,而且正在运行的线程数量小于maximunPoolSize,那么还是要创建非核心线程来运行这个任务
      d. 如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会抛出异常RejectExecutionException(这个情况不一定,要依据拒绝策略的设置)
     3)当一个线程完成任务时,他会从队列中取出下一个任务来执行
     4)当一个线程无事可做,超过一定时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于corePoolSize,那么这个线程会被停掉,所以线程池中所有任务一旦都完成任务且没有后续任务需要执行时,就会收缩到corePoolSize大小

总结

  1. 使用多线程的主要原因:
     1) 要处理很多任务,应用并发能够更有效的使用计算机
     2) 能够更好的组织代码
     3) 更便于用户使用
  2. 多线程缺陷:
     1) 等待共享资源的时候性能降低
     2) 需要处理线程的额外CPU花费
     3) 如果程序设计比较糟糕,则会导致不必要的复杂度
     4) 可能产生一些不安全行为,竞争,死锁等
     5) 不同平台导致不一致性

猜你喜欢

转载自blog.csdn.net/qq_41649001/article/details/106728581