【JavaEE初阶】多线程上部

文章目录

  • 本篇目标:
  • 一、认识线程(Thread)
  • [二、Thread 类及常见方法](#二、Thread 类及常见方法)
    • [2.1 Thread 的常见构造方法](#2.1 Thread 的常见构造方法)
    • [2.2 Thread 的几个常见属性](#2.2 Thread 的几个常见属性)
    • [2.3 启动⼀个线程 - start()](#2.3 启动⼀个线程 - start())
    • [2.4 中断⼀个线程](#2.4 中断⼀个线程)
    • [2.5 等待⼀个线程 - join()](#2.5 等待⼀个线程 - join())
    • [2.6 获取当前线程引用](#2.6 获取当前线程引用)
    • [2.7 休眠当前线程 - sleep()](#2.7 休眠当前线程 - sleep())
  • 三、线程的状态
    • [3.1 观察线程的所有状态](#3.1 观察线程的所有状态)
  • [四、 多线程带来的的风险-线程安全 (重点)](#四、 多线程带来的的风险-线程安全 (重点))
    • [4.1 线程安全的概念](#4.1 线程安全的概念)
    • [4.2 线程不安全的原因](#4.2 线程不安全的原因)
  • [五、synchronized 关键字 - 监视器锁 monitor lock](#五、synchronized 关键字 - 监视器锁 monitor lock)
    • [5.1 synchronized 的特性](#5.1 synchronized 的特性)
    • [5.2 synchronized 使用示例](#5.2 synchronized 使用示例)
  • 总结

本篇目标:

  • 认识多线程
  • 掌握多线程程序的编写
  • 掌握多线程的状态
  • 掌握什么是线程不安全及解决思路
  • 掌握 synchronized关键字

提示:以下是本篇文章正文内容

一、认识线程(Thread)

1.概念:

1.1 线程是什么?

线程: ⼀个线程就是⼀个 "执行流". 每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 "同时" 执行着多份代码.

1.2 为什么要有线程?

首先, "并发编程" 成为 "刚需".

  • 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU资源.
  • 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程

其次, 虽然多进程也能实现 并发编程, 但是线程比进程更轻量.

  • 创建线程比创建进程更快.
  • 销毁线程比销毁进程更快.
  • 调度线程比调度进程更快

最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 "线程池"(ThreadPool) 和 "协程"(Coroutine)

1.3 进程和线程的区别

  • 进程是包含线程的. 每个进程至少有⼀个线程存在,即主线程。
  • 进程和进程之间不共享内存空间. 同⼀个进程的线程之间共享同⼀个内存空间
  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
  • ⼀个进程挂了⼀般不会影响到其他进程. 但是⼀个线程挂了, 可能把同进程内的其他线程⼀起带走(整个进程崩溃).

1.4 Java的线程和操作系统线程的关系

  • 线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了⼀些 API 供用户使用(例如 Linux 的 pthread 库).
  • Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进⼀步的抽象和封装

2.创建线程

方法一:继承 Thread 类

1.继承 Thread 来创建⼀个线程类.

java 复制代码
class MyThread extends Thread {
	@Override
	public void run() {
 	System.out.println("这⾥是线程运⾏的代码");
 	}
}

2.创建 MyThread 类的实例

java 复制代码
MyThread t = new MyThread();

3.调用 start 方法启动线程

java 复制代码
t.start();		// 线程开始运⾏

方法二 实现 Runnable 接口

1.实现 Runnable 接口

java 复制代码
class MyRunnable implements Runnable {
 	@Override
 	public void run() {
 	System.out.println("这⾥是线程运⾏的代码");
 	}
}

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

java 复制代码
Thread t = new Thread(new MyRunnable());

3.调用 start 方法

java 复制代码
t.start();		// 线程开始运⾏

对比上面两种方法:

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

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

其他变形:

  • 匿名内部类创建 Thread 子类对象
java 复制代码
// 使⽤匿名类创建 Thread ⼦类对象
Thread t1 = new Thread() {
 	@Override
 	public void run() {
 	System.out.println("使⽤匿名类创建 Thread ⼦类对象");
 	}
};
  • 匿名内部类创建 Runnable 子类对象
java 复制代码
// 使⽤匿名类创建 Runnable ⼦类对象
Thread t2 = new Thread(new Runnable() {
 	@Override
 	public void run() {
 	System.out.println("使⽤匿名类创建 Runnable ⼦类对象");
 	}
});
  • lambda 表达式创建 Runnable 子类对象
java 复制代码
// 使⽤ lambda 表达式创建 Runnable ⼦类对象
Thread t3 = new Thread(() -> System.out.println("使⽤匿名类创建 Thread ⼦类对象"));
Thread t4 = new Thread(() -> {
 	System.out.println("使⽤匿名类创建 Thread 子类对象");
});

二、Thread 类及常见方法

Thread 类是 JVM 用来管理线程的⼀个类,换句话说,每个线程都有⼀个唯⼀的 Thread 对象与之关联。

2.1 Thread 的常见构造方法

方法 说明
Thread() 创建线程对象
Thread(Runnable target) 使用Runnable 对象创建线程对象
Thread(String name) 创建线程对象,并命名
Thread(Runnable target, String name) 使用Runnable 对象创建线程对象,并命名
java 复制代码
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

2.2 Thread 的几个常见属性

属性 获取方法
ID getld0)
名称 getName()
状态 getState()
优先级 getPriority()
是否后台线程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupted( )
  • ID 是线程的唯⼀标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的⼀个情况,下面我们会进⼀步说明
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住⼀点:JVM会在⼀个进程的所有非后台线程结束后,才会结束运行。
  • 是否存活,即简单的理解,为 run 方法是否运行结束了
  • 线程的中断问题,下面我们进⼀步说明

2.3 启动⼀个线程 - start()

调用 start 方法, 才真的在操作系统的底层创建出⼀个线程。

2.4 中断⼀个线程

目前常见的有以下两种方式:

  1. 通过共享的标记来进行沟通
  2. 调用 interrupt() 方法来通知

2.5 等待⼀个线程 - join()

有时,我们需要等待⼀个线程完成它的工作后,才能进行自己的下⼀步工作。

方法 说明
public void join() 等待线程结束
public void join(long millis) 等待线程结束,最多等millis毫秒
public void join(long millis, int nanos) 同理,但可以更高精度

2.6 获取当前线程引用

方法 说明
public static Thread currentTkread() 返回当前线程对象的引用

2.7 休眠当前线程 - sleep()

方法
public static void sleep(long millis) throws InterruptedException
public static void sleep(long millis, int nanos)throws InterruptedException

三、线程的状态

3.1 观察线程的所有状态

线程的状态是⼀个枚举类型 Thread.State

  • NEW: 安排了工作, 还未开始行动
  • RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
  • BLOCKED: 这几个都表示排队等着其他事情
  • WAITING: 这几个都表示排队等着其他事情
  • TIMED_WAITING: 这几个都表示排队等着其他事情
  • TERMINATED: 工作完成了

四、 多线程带来的的风险-线程安全 (重点)

4.1 线程安全的概念

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

4.2 线程不安全的原因

  • **线程调度是随机的 **,这是线程安全问题的 罪魁祸首, 随机调度使⼀个程序在多线程环境下, 执行顺序存在很多的变数,程序猿必须保证在任意执行顺序下 , 代码都能正常工作。
  • 修改共享数据,多个线程修改同⼀个变量。
  • 原子性
  • 可见性
  • 指令重排序

【拓展】:Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型

目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到⼀致的并发效果。

  • 线程之间的共享变量存在 主内存 (Main Memory).
  • 每⼀个线程都有自己的 "工作内存" (Working Memory) .
  • 当线程要读取⼀个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
  • 当线程要修改⼀个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.

由于每个线程有自己的工作内存, 这些工作内存中的内容相当于同⼀个共享变量的 "副本". 此时修改线程1 的工作内存中的值, 线程2 的工作内存不⼀定会及时变化。

此时引柚柚们可能会有这些问题:

  • 为啥要整这么多内存?
  • 为啥要这么麻烦的拷来拷去?
  1. 为啥整这么多内存?

    实际并没有这么多 "内存". 这只是 Java 规范中的⼀个术语, 是属于 "抽象" 的叫法.

    所谓的 "主内存" 才是真正硬件角度的 "内存". 而所谓的 "工作内存", 则是指 CPU 的寄存器和高速缓存。

  2. 为啥要这么麻烦的拷来拷去?

    因为 CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级,也就是几千倍, 上万倍).

那么接下来问题又来了, 既然访问寄存器速度这么快, 还要内存干啥??

答案就是⼀个字: 贵

五、synchronized 关键字 - 监视器锁 monitor lock

5.1 synchronized 的特性

1) 互斥

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同⼀个对象 synchronized 就会阻塞等待.

  • 进入 synchronized 修饰的代码块, 相当于 加锁
  • 退出 synchronized 修饰的代码块, 相当于 解锁
    注意:
  • 上⼀个线程解锁之后, 下⼀个线程并不是立即就能获取到锁. 而是要靠操作系统来 "唤醒". 这也就是操作系统线程调度的⼀部分工作.
  • 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不⼀定就能获取到锁,而是和 C 重新竞争, 并不遵守先来后到的规则.
    2) 可重入
    synchronized 同步块对同⼀条线程来说是可重入的,不会出现自己把自己锁死的问题。

5.2 synchronized 使用示例

synchronized 本质上要修改指定对象的 "对象头". 从使用角度来看, synchronized 也势必要搭配⼀个具体的对象来使用。

1) 修饰代码块: 明确指定锁哪个对象。

锁任意对象

java 复制代码
public class SynchronizedDemo {
 	private Object locker = new Object();
 
 	public void method() {
 		synchronized (locker) {
 		}
 	}
}

锁当前对象

java 复制代码
public class SynchronizedDemo {
 	public void method() {
 		synchronized (this) {
 		}
 	}
}

2) 直接修饰普通方法: 锁的 SynchronizedDemo 对象

java 复制代码
public class SynchronizedDemo {
 	public synchronized void methond() {
 	}
}

3) 修饰静态方法: 锁的 SynchronizedDemo 类的对象

java 复制代码
public class SynchronizedDemo {
 	public synchronized static void method() {
 	}
}

我们重点要理解,synchronized 锁的是什么. 两个线程竞争同一把锁, 才会产生阻塞等待

总结

多线程几乎是面试必问题,柚柚们一定要好好理解喔!!!

相关推荐
考虑考虑32 分钟前
JDK9中的dropWhile
java·后端·java ee
想躺平的咸鱼干41 分钟前
Volatile解决指令重排和单例模式
java·开发语言·单例模式·线程·并发编程
hqxstudying1 小时前
java依赖注入方法
java·spring·log4j·ioc·依赖
·云扬·1 小时前
【Java源码阅读系列37】深度解读Java BufferedReader 源码
java·开发语言
Bug退退退1232 小时前
RabbitMQ 高级特性之重试机制
java·分布式·spring·rabbitmq
小皮侠2 小时前
nginx的使用
java·运维·服务器·前端·git·nginx·github
Zz_waiting.3 小时前
Javaweb - 10.4 ServletConfig 和 ServletContext
java·开发语言·前端·servlet·servletconfig·servletcontext·域对象
全栈凯哥3 小时前
02.SpringBoot常用Utils工具类详解
java·spring boot·后端
兮动人3 小时前
获取终端外网IP地址
java·网络·网络协议·tcp/ip·获取终端外网ip地址
呆呆的小鳄鱼3 小时前
cin,cin.get()等异同点[面试题系列]
java·算法·面试