Loading...

多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理” 。
在计算机编程中,一个基本的概念就是同时对多个任务加以控制。许多程序设计问题都要求程序能够停下手头的工作,改为处理其他一些问题,再返回主进程。可以通过多种途径达到这个目的。最开始的时候,那些掌握机器低级语言的程序员编写一些“中断服务例程”,主进程的暂停是通过硬件级的中断实现的。尽管这是一种有用的方法,但编出的程序很难移植,由此造成了另一类的代价高昂问题。中断对那些实时性很强的任务来说是很有必要的。但对于其他许多问题,只要求将问题划分进入独立运行的程序片断中,使整个程序能更迅速地响应用户的请求 。

1、相关概念

  • 并发:指两个或多个事件在同一个时间段内发生(交替执行)。
  • 并行:指两个或多个事件在同一时刻发生(同时执行)。
  • 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多 个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创 建、运行到消亡的过程。
  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程 中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
  • 线程调度:
    • 分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
    • 抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为 抢占式调度。

【注意】多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。

  • 主线程:Java中执行main方法的线程。
  • 单线程程序:Java程序中只有一个线程,从上到下依次执行。

进程是资源分配的基本单位,线程是处理器调度的基本单位。

2、多线程程序

Java使用 java.lang.Thread 类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是 完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。
多线程执行时,在栈内存中,每一个执行线程都有一片自己所属的栈内存空间。进行方法的压栈和弹栈。
当执行线程的任务结束了,线程自动在栈内存中释放了。但是当所有的执行线程都结束了,那么进程就结束了。

2.1、继承Thread类

java.lang.Thread

2.1.1、构造方法

public Teread() :分配一个新的线程对象。
public Thread(String name) :分配一个指定名字的新的线程对象。
public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
public Thread(Runnable target, String name) :分配一个带有指定目标新的线程对象并指定名字。

2.2.2、常用方法

public String getName() :获取当前线程名称。
public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
public void run() :此线程要执行的任务在此处定义代码。
public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
public static Thread currentThread() :返回对当前正在执行的线程对象的引用。
public void setName(String name) :设置线程名称。

使用步骤:

  • 创建一个Thread类的子类
  • 在Thread类的子类中重写Thread中的run方法,设置线程任务(开启线程要做什么?)
  • 创建Thread类的子类对象
  • 调用Thread类中的方法start方法,开启新的线程,执行run方法。

2.2.3、示例

public class MyThread extends Thread { public MyThread(String name) { super(name); } @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(getName() + " 正在执行:" + i); } } }
public class ThreadTest { public static void main(String[] args) { // 创建自定义的线程对象 MyThread myThread = new MyThread("我的线程"); // 开启新线程 myThread.start(); // 主方法中的for循环 for (int i = 0; i < 10; i++) { System.out.println("main线程:" + i); } } }

2.3、实现Runnable接口

通过实现Runnable接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个执行目标。所有的多线程 代码都在run方法里面。Thread类实际上也是实现了Runnable接口的类。
Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。 而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。

2.3.1、示例

public class MyRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + " 正在执行:" + i); } } }
public class RunnableTest { public static void main(String[] args) { // 创建线程任务对象 MyRunnable myRunnable = new MyRunnable(); // 创建线程对象 Thread thread = new Thread(myRunnable, "我的线程"); // 启动线程 thread.start(); // main方法中的for循环 for (int i = 0; i < 10; i++) { System.out.println("main线程:" + i); } } }

2.3.2、Thread和Runnable的区别

Runnable的优势:

  1. 多个相同程序代码的线程共享资源;
  2. 避免单继承的局限性;
  3. 提高代码健壮性,解耦,代码和线程解耦;
  4. 线程池中只能放入实现Runnable或Callable类的线程,不能直接放入继承Thread类的线程。

【注意】在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用 java命令执行一个类的时候,实际上都会启动一个JVM,每一个JVM其实在就是在操作系统中启动了一个进程。

2.3.3、匿名内部类的方式实现线程的创建

public class TestRunnable { public static void main(String[] args) { Runnable runnable = new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + " 正在执行:" + i); } } }; Thread thread = new Thread(runnable, "我的线程"); thread.start(); for (int i = 0; i < 10; i++) { System.out.println("main线程:" + i); } } }

3、线程安全

3.1、线程安全

如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样 的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写 操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步, 否则的话就可能影响线程安全。

3.2、线程同步

Java中提供了同步机制 (synchronized)来解决线程安全问题。
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制:

  1. 同步代码块;
  2. 同步方法;
  3. 锁机制。

3.3、同步代码块

synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

  • 格式
synchronized (同步锁对象) { // 可能出现线程安全问题的代码(访问了共享数据的代码) }
  • 注意
    1. 锁对象可以是任意对象;
    2. 必须保证多个线程使用的锁对象是同一个;
    3. 锁对象的作用:把同步代码块锁住,只让同步代码块在一个线程中执行。
  • 同步技术原理

使用了一个锁对象,叫做同步锁/对象锁/对象监听器。同步中的线程没有执行完毕不会释放锁,同步外的线程没有锁不能进入同步。最终线程安全,但效率低。

public class TicketRunnable implements Runnable { // 总票数 private int ticketNum = 100; // 锁对象 private final Object lock = new Object(); @Override public void run() { // 获取线程名称 String name = Thread.currentThread().getName(); // 模拟一直卖票 while (ticketNum > 0) { synchronized (lock) { if (ticketNum > 0) { System.out.println(name + " 正在卖出 " + ticketNum--); } } } } }
public class SaleTicket { public static void main(String[] args) { // 创建线程执行对象 TicketRunnable runnable = new TicketRunnable(); // 创建3个线程对象(3个售票窗口) Thread thread1 = new Thread(runnable, "窗口1"); Thread thread2 = new Thread(runnable, "窗口2"); Thread thread3 = new Thread(runnable, "窗口3"); // 启动线程 thread1.start(); thread2.start(); thread3.start(); } }

3.4、同步方法

使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

  • 格式
public synchronized void method() { // 可能产生线程安全问题的代码 }
  • 同步方法中,同步锁
    • 对于非static方法:同步锁就是this;
    • 对于static方法:同步锁是当前方法所在类的字节码对象(类名.class)。
public class TicketRunnable implements Runnable { // 总票数 private int ticketNum = 100; // 锁对象 private final Object lock = new Object(); @Override public void run() { // 获取线程名称 String name = Thread.currentThread().getName(); // 模拟一直卖票 while (ticketNum > 0) { sale(name); } } public synchronized void sale(String name) { if (ticketNum > 0) { System.out.println(name + " 正在卖出 " + ticketNum--); } } }

3.5、Lock锁

java.util.concurrent.locks.Lock:接口。提供了比synchronized代码块和synchronized方法更广泛的锁定操作, 同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。
Lock锁也称同步锁,加锁与释放锁方法化了。
public void lock() :加同步锁。
public void unlock() :释放同步锁。

public class TicketRunnable implements Runnable { // 总票数 private int ticketNum = 100; // 锁对象 private final Lock lock = new ReentrantLock(); @Override public void run() { // 获取线程名称 String name = Thread.currentThread().getName(); // 模拟一直卖票 while (ticketNum > 0) { lock.lock(); if (ticketNum > 0) { System.out.println(name + " 正在卖出 " + ticketNum--); lock.unlock(); } } } }

4、线程状态

4.1、概述

java.lang.Thread.State:枚举类。当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。

线程状态 导致状态方法条件
NEW(新建) 线程刚被创建,但是并未启动。还没调用start方法。
Runnable(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操 作系统处理器。
Blocked(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状 态;当该线程持有锁时,该线程将变成Runnable状态。
Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。Object.wait()方法使一个线程进入无限等待状态。进入这个 状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
Timed Waiting(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态 将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、 Object.wait。
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

【注意】只有锁对象(对象监视器)才能调用wait()和notify()方法。

4.2、Timed Waiting(计时等待)

一个正在限时等待另一个线程执行一个(唤醒)动作的线程处于这一状态。

  • 示例

实现一个计数器,计数到100,在每个数字之间暂停1秒,每隔10个数字输出一个字符串

public class Test3 extends Thread { @Override public void run() { for (int i = 0; i <= 100; i++) { System.out.println("============ " + i); if (i % 10 == 0) { System.out.println(i); } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { new Test3().start(); } }
  • 注意
    1. 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协 作关系。
    2. 为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程 中会睡眠。
    3. . sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。
    4. sleep()中指定的时间是线程不会运行的最短时间。因此,sleep()方法不能保证该线程睡眠到期后就 开始立刻执行。

计时等待.png

计时等待.png

4.3、Bolcked(锁阻塞)

一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。
Runnable、Waiting以及Time Waiting状态都会在某种情况下进入阻塞状态。

锁阻塞.png

锁阻塞.png

4.4、Waiting(无限等待)

一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。
一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的 Object.notify()方法 或 Object.notifyAll()方法。
waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系, 多个线程会争取锁,同时相互之间又存在协作关系。
当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法那么A线程就进入 了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了 notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入 Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。

无限等待.png

无限等待.png

5、等待唤醒机制

5.1、线程间通信

  • 概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。
  • 为什么要处理线程间通信:

多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们 希望他们 有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

  • 如何保证线程间通信有效利用资源:

多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效 的利用资源。而这种手段即—— 等待唤醒机制。

5.2、等待唤醒机制

等待唤醒机制是多个线程间的一种协作机制。是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将 其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。

  • 等待唤醒中的方法

wait() :线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时 的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象 上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中。
notify() :选取所通知对象的 wait set 中的一个线程释放。
notifyAll() :释放所通知对象的 wait set 上的全部线程。

  • 【注意】哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而 此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调 用 wait 方法之后的地方恢复执行。
    • 如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
    • 否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态。
  • 调用wait和notify方法需要注意的细节
    1. . wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对 象调用的wait方法后的线程。
    2. wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继 承了Object类的。
    3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。

5.3、生产者与消费者问题

等待唤醒机制其实就是经典的“生产者与消费者”的问题。

  • 需求

包子铺线程生产包子,吃货线程消费包子。当包子没有时(包子状态为false),吃货线程等待,包子铺线程生产包子 (即包子状态为true),并通知吃货线程(解除吃货的等待状态),因为已经有包子了,那么包子铺线程进入等待状态。 接下来,吃货线程能否进一步执行则取决于锁的获取情况。如果吃货获取到锁,那么就执行吃包子动作,包子吃完(包 子状态为false),并通知包子铺线程(解除包子铺的等待状态),吃货线程进入等待。包子铺线程能否进一步执行则取 决于锁的获取情况。

  • 代码
public class BaoZi { private String pi; private String xian; private boolean flag = false; // 包子资源状态 public String getPi() { return pi; } public void setPi(String pi) { this.pi = pi; } public String getXian() { return xian; } public void setXian(String xian) { this.xian = xian; } public boolean isFlag() { return flag; } public void setFlag(boolean flag) { this.flag = flag; } @Override public String toString() { return "BaoZi{" + "pi='" + pi + '\'' + ", xian='" + xian + '\'' + ", flag=" + flag + '}'; } }
public class ChiHuo implements Runnable { private BaoZi bz; public ChiHuo(BaoZi bz) { this.bz = bz; } @Override public void run() { while (true) { synchronized (bz) { if (!bz.isFlag()) { try { System.out.println("【" + Thread.currentThread().getName() + "】没有包子!!!"); bz.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("【" + Thread.currentThread().getName() + "】正在吃 " + "【" + bz.getPi() + bz.getXian() + "】包子..." ); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } bz.setFlag(false); bz.notifyAll(); } } } }
public class BaoZiPu implements Runnable { private BaoZi bz; public BaoZiPu(BaoZi bz) { this.bz = bz; } @Override public void run() { int count = 0; while (true) { synchronized (bz) { if (bz.isFlag()) { try { bz.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.print("【" + Thread.currentThread().getName() + "】制作第 " + (++count) + " 个包子!"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } if (count % 2 == 0) { bz.setPi("薄皮"); bz.setXian("猪肉馅"); }else { bz.setPi("厚皮"); bz.setXian("韭菜馅"); } bz.setFlag(true); System.out.println("【" + bz.getPi() + bz.getXian() + " 】"); bz.notifyAll(); } } } }
public class TestBaoZi { public static void main(String[] args) { BaoZi baoZi = new BaoZi(); Thread thread1 = new Thread(new ChiHuo(baoZi), "吃货1"); Thread thread2 = new Thread(new ChiHuo(baoZi), "吃货2"); Thread thread3 = new Thread(new ChiHuo(baoZi), "吃货3"); Thread thread4 = new Thread(new BaoZiPu(baoZi), "包子铺1"); Thread thread5 = new Thread(new BaoZiPu(baoZi), "包子铺5"); Thread thread6 = new Thread(new BaoZiPu(baoZi), "包子铺6"); thread1.start(); thread2.start(); thread3.start(); thread4.start(); thread5.start(); thread6.start(); } }

6、线程池

游泳池.jpg

游泳池.jpg

6.1、线程池的思想

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间。
在Java中可以通过线程池来使线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务。

线程池原理.bmp

线程池原理.bmp

6.2、线程池的概念

其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作, 无需反复创建线程而消耗过多资源。

  • 合理利用线程池能够带来三个好处:
    1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
    2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
    3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内 存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

6.3、线程池的使用

java.util.concurrent.Executor:线程池的顶级接口。并不是一个线程 池,而只是一个执行线程的工具。
java.util.concurrent.ExecutorService:真正的线程池接口。
java.util.concurrent.Executors:线程池工厂类。提供了一些静态工厂,生成一些常用的线程池。官 方建议使用Executors工程类来创建线程池对象。

  • Executors线程池工厂类的静态方法

public static ExecutorService newFixedThreadPool(int nThreads) :创建线程池的方法。返回线程池对象。(创建的是有界线 程池,也就是池中的线程个数可以指定最大数量)。

  • ExecutorService线程池对象的方法

public Future submit(Runnable task) :获取线程池中的某一个线程对象,并提交一个Runnable任务用于执行。
【注意】Future接口:用来记录线程任务执行完毕后产生的结果。
public void shutdown() :关闭/销毁线程池的方法。一般不用。

  • 线程池使用步骤
    1. 使用Executors工厂类的 public static ExecutorService newFixedThreadPool(int nThreads) 方法创建一个线程池。
    2. 创建一个实现Runnable接口的线程执行对象类。
    3. 调用ExecutorService的 submit(Runnable task) 方法,传递线程任务,并开启线程执行run()。
    4. 调用ExecutorService的 shoutdown() 方法,销毁线程池(不建议执行)。
public class MeRunnable implements Runnable { @Override public void run() { System.out.println("来人呐!" + Thread.currentThread().getName()); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("叩见老佛爷!!!" + Thread.currentThread().getName()); } }
public class ThreadPool { public static void main(String[] args) { // 创建线程池对象 ExecutorService executorService = Executors.newFixedThreadPool(2); // 创建线程任务对象 MeRunnable meRunnable = new MeRunnable(); // 调用一个线程,执行线程任务 executorService.submit(meRunnable); executorService.submit(meRunnable); executorService.submit(meRunnable); executorService.submit(meRunnable); executorService.submit(meRunnable); } }

感谢小伙伴们的关注!
你的点赞、评论、关注、收藏是对博主的最大鼓励!
持续更新JavaSE学习笔记!欢迎订阅专栏!

最后修改:2022 年 03 月 03 日
如果觉得我的文章对你有用,请随意赞赏