【JAVA】 04-Java中的多线程

链接:

目录:

<>


  • <>

内容待整理:

多线程引入

概述

  • 多线程:

    • 进程:正在执行中的程序,其实是应用程序在内存中运行的那片空间。
    • 线程:进程中的一个执行单元,负责进程中的程序的运行,一个进程中至少要有一个线程。
    • 多线程程序:一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
    • 程序启动了多线程,有什么应用呢?可以实现多部分程序同时执行,专业术语:“并发”

多线程运行原理

  • 多线程的使用需要合理使用CPU资源,线程过多会导致降低性能。
  • CPU处理程序时是通过快速切换完成的,在我们看来好像随机一样(其实有自己的规律)

多线程的创建

主线程的运行方式

  • 主线程:

    • 在之前的代码中,JVM启动后,必然有一个执行路径(线程)从main方法开始,一直执行到main方法结束。
    • 这个线程在java中称为主线程。
  • 问题分析:

    • 当主线程在这个程序中执行时,如果遇到了循环而导致在指定位置停留时间过长,无法执行下面的程序。
    • 可不可以实现一个主线程负责执行其中一个循环,由另一个线程负责其他代码的执行,实现多部分的代码同时执行。
    • 这就是多线程技术可以解决的问题。

多线程的创建:

  • 通过API中的英文Thread的搜索(实在搜不到先去百度搜中文),查到了Thread类
  • 通过阅读Thread类中的描述

多线程的创建一:继承Thread类

  • 步骤:

    • 1.1 定义一个类继承Thread。
    • 1.2 重写run方法。
    • 1.3 创建子类对象,就是创建线程对象。
    • 1.4 调用start方法,开启线程并让线程执行,同时还会告诉jvm去调用run方法。
  • 面试:线程对象调用run方法和调用start方法的区别?

    • 调用run方法不开启线程,并让JVM调用run方法,在开启的线程中执行
    • 调用start方法开启线程,并让JVM调用run方法在开启的线程中执行。
  • 原理:继承Thread类的原理

    • 继承Thread类:

      • 因为Thread类描述线程事物,具备线程应有的功能。
      • 那为什么不直接创建Thread类的对象呢?
        Thread t1 = new Thread();
        t1.start();
      • 这么做没有错,但该start调用的是Thread类中的run方法
      • 而这个run方法没有做什么事情,更重要的是,这个run方法中没有定义我们需要让线程执行的代码。
    • 创建线程的目的是什么?

      • 为了建立单独的执行路径,让多部分代码实现同时执行。
      • 也就是说,线程创建并执行需要给定的代码(专业术语:线程的任务)。
      • 对于之前所讲的主线程,它的任务定义在了main函数中。
      • 自定义的线程需要执行的任务都定义在run方法中。
      • Thread类中的run方法内部的任务并不是我们所需要的,只要重写run方法,
      • 既然Thread类已经定义了线程任务的位置,只要在位置中定义任务代码即可
      • 所以进行了重写run方法的动作。

内存与线程名称获取

  • 内存:

    • 多线程执行时,在栈内存中,每一个执行线程都有一片自己所属的栈内存空间,进行方法的压栈和弹栈。
    • 当执行线程的任务结束了,线程自动就在栈内存中释放了。
    • 当所有的执行线程都结束时,进程就结束了。
  • 获取线程名称:

    • 获取当前线程对象:Thread: currentThread()
    • 获取名称:getName();
    • 故:Thread.currentThread().getName();
    • 主线程的名称:main
    • 自定义的线程:Thread-1 线程多个时,数字顺延。Thread-n,n从0开始

多线程的创建二:实现Runnable接口

  • 步骤与理解:

    • 1、定义类,实现Runnable接口:
      • 避免了继承Thread类的单继承局限性
    • 2、覆盖接口中的run方法,将线程任务代码定义到run方法中
    • 3、创建Thread类的对象:
      • 只有创建Thread类的对象才可以创建线程。
    • 4、将Runnable接口的子类对象作为参数传递给Thread类的构造函数
      • 因为线程已被封装到Runnable接口的run方法中,而这个方法所属于Runnable接口的子类对象,
      • 所以将这个子类对象作为参数传递给Thread的构造函数,这样,线程对象创建时,就可以明确要运行的线程的任务。
    • 5、调用Thread类的start方法开启线程
  • 第二种方式实现Runnable接口:

    • 避免了单继承的局限性,所以较为常用。
    • 实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分叫线程对象,一部分是线程任务
    • 继承Thread类:线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,又有线程任务。
    • 实现Runnable接口:
      - 将线程任务单独分离出来,封装成对象,类型就是Runnable接口类型。
      • Runnable接口对线程对象和线程任务进行解耦。
      • 用Runnable来标明线程任务,用Thread来明确线程对象。

线程的状态图

多线程的安全问题

发生:

  • 用sleep模拟临时阻塞

问题产生的原因:

  • 1、线程任务中在操作共享的数据
  • 2、线程任务操作共享数据的代码有多条(运算有多个)

解决思路:

  • 只要让一个线程在执行线程任务时将多条操作共享数据的代码执行完,
  • 在执行过程中,不要让其他线程参与运算,就可以了。

代码体现:同步代码块synchronized

  • 格式:
    synchronized(对象){ //需要被同步的代码。}
    //对象:相当于锁。火车上的卫生间-->同步锁

同步代码块的好处:

  • 解决了多线程的安全问题。

同步的弊端:

  • 降低了程序的性能。
    但是为了安全,可以损失点性能。

同步的前提:

  • 必须保证多个线程在同步中使用的是同一个锁
    • synchronized(new Object()) 时有几个线程有几把锁
    • 同步区分:用锁 -- 不同的对象,不同的锁,就是不同的同步
  • 解决了什么问题呢:
    • 当多线程安全发生时,加入了同步问题依旧时,就要通过这个同步的前提来判断同步是否写正确。

同步函数:同步的另一种体现形式

  • 简化同步代码块为同步函数(例如:public synchronized void sale())
  • 同步函数的锁:this(因为函数必须被对象调用)
  • 验证:
    • 写一个同步代码块,写一个同步函数
    • 如果同步代码块中的锁对象和同步函数中的锁对象是同一个,就同步了,不会有错误数据
    • 如果不是同一个锁对象,就不同步,就会出现错误数据。
    • 设置两个线程:一个线程在同步代码块中执行,另一个在同步函数中执行。
    • 总结:同步函数使用的锁是this

同步函数和同步代码块的区别:

  • 同步函数使用的锁是固定的this。当线程任务只需要一个同步时完全可以使用同步函数。
  • 同步代码块使用的锁可以是任意对象。当线程任务中需要多个同步时,必须通过锁来区分,这时必须使用不同代码块。
  • 同步代码块较为常用。

static同步函数使用的锁

  • 不是this,而是字节码文件对象 类名.class

单例懒汉式的并发访问(高频考点)

  • 并发访问有安全隐患,所以加入同步机制解决安全问题
  • 但是,同步的出现却降低了效率。
  • 提高效率:减少判断锁的次数,可以通过双重判断的方式。

同步的另一个弊端:死锁

  • 情况之一:
    • 当线程任务出现了多个同步时(多个锁)时,如果同步中嵌套了其他同步,这时容易引发一种现象:死锁
    • 这种情况能避免就避免
    • 注意:要会写一个死锁程序
      //可能死锁,也可能大和谐了
      //Thread-0 synchronized(obj1){ synchronized(obj2){} }
      //Thread-1 synchronized(obj2){ synchronized(obj1){} }

多线程间通信

生产者消费者问题

  • 面试会考,划重点
  • 生产和消费同时执行,需要多线程。
  • 但是执行的任务不同,处理的资源却相同 -- 线程间的通信。
  • 代码框架:
    • 1、描述资源
    • 2、描述生产者,因为具备着自己的任务
    • 3、描述消费者,因为具备着自己的任务
  • 问题1:数据错误,已经被生产很早期的商品,才被消费到,出现了线程安全问题。加入同步解决。
    • 使用同步函数。问题已解决,不会再消费到之前很早期的商品
  • 问题2:发现了连续生产却没有消费,同时对同一个商品进行多次消费。
    希望的结果应该是:生产一个商品,就被消费掉,再生产下一个商品。
  • 搞清楚几个问题:
    • 生产者什么时候生产?消费者什么时候消费?
    • 当盘子中没有面包时,就生产;如果有了面包,就不要生产。
    • 当盘子中已有面包时,就消费;如果没有面包,就不要消费。
  • 思考:
    • 生产者生产了商品后,应该告诉消费者来消费。这时的生产者应该处于等待状态。
    • 消费者消费了商品后,应该告诉生产者来生产。这时的消费者应该处于等待状态。
    • 等待:wait(); ---- 需要InterruptedException捕获异常
    • 告诉:notify(); -- 唤醒
  • 问题解决:实现了生产一个消费一个。

等待/唤醒机制

  • 注:对象监视器:锁
  • wait(); -- 会让线程处于等待状态,其实就是将线程临时存储到了线程池中。
  • notify(); -- 会唤醒线程池中任意一个等待的线程。立刻有执行资格,但不一定立刻有执行权
  • notifyAll(); -- 会唤醒线程池中所有的等待线程。
  • 记住:
    • 这些方法必须使用在同步中,因为必须要标识wait,notify等方法所属的锁(对象监视器)
    • 同一个锁上的notify,只能唤醒该锁上的被wait的线程。
  • 为什么这些方法定义在Object类中呢?
    • 因为这些方法必须标识所属的锁,而锁可以是任意对象。
    • 任意对象可以调用的方法必然是Object类。
  • 举例:小朋友抓人游戏

多生产多消费:

  • 问题1:生成了商品没有被消费,同一个商品被消费多次。
    • 被唤醒的线程没有判断标记,造成问题1产生
    • 解决:只要让被唤醒的线程必须判断标记就可以了。
      将if判断标记的方式改为while判断标记
    • 记住:只要是多生产多消费,必须while判断
  • 问题2:while判断后,死锁了。
    • 原因:生产方唤醒了线程池中的生产方的线程。本方唤醒了本方。
    • 解决:希望本方要唤醒对方。没有对应方法,所以,唤醒所有。
  • 仍然有一些小遗憾:效率低了。唤醒所有会有点多余。

JDK1.5Lock接口

  • 提供了多生产多消费的解决方案
  • 在java.util.concurrent.locks软件包中提供相应的解决方案
  • Lock接口:比同步更厉害,有更多的操作。lock(),-- 获取锁 unlock();-- 释放锁
    - 提供了一个更加面向对象的锁,在该锁中,提供了更多的显式的锁操作。
    - 可以替代同步。
  • 先把同步改为lock:unlock要放在try-finally的finally中确保一定会执行到

JDK1.5Condition方法

  • 图示:

  • 已经将旧锁替换成新锁,那么锁上的监视器方法(wait,notify,notifyAll)也应该替换成新锁的监视器方法
  • 而jdk1.5中将这些原有的监视器方法封装到了一个Condition对象中。
  • 想要获取监视器方法,需要先获取Condition对象。
  • Condition对象的出现其实就是替换了Object中的监视器方法。
    • await();
    • signal();
    • signalAll();
  • 将所有的监视器方法替换成了Condition。但是效率低的问题仍然存在。

解决多生成多消费的效率问题

  • 图示

  • 希望本方可以唤醒对方中的一个。
  • 老程序中可以通过两个锁嵌套完成,但是容易引发死锁。
  • 新程序中可以解决这个问题:只用一个锁。
  • 可以在一个锁上加上多个监视器对象。

Condition示例

  • API文档的“java.util.concurrent.locks 接口 Condition”下的示例,多生产多消费问题

eclipse

END

原文地址:https://www.cnblogs.com/anliux/p/11422827.html