本篇目标:

  • 认识多线程
  • 掌握多线程程序的编写
  • 掌握多线程的状态
  • 掌握什么是线程不安全及解决思路
  • 掌握 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 来创建⼀个线程类.

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

2.创建 MyThread 类的实例

MyThread t = new MyThread();

3.调用 start 方法启动线程

t.start();		// 线程开始运⾏

方法二 实现 Runnable 接口

1.实现 Runnable 接口

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

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

Thread t = new Thread(new MyRunnable());

3.调用 start 方法

t.start();		// 线程开始运⾏

对比上面两种方法:
• 继承 Thread 类, 直接使用 this 就表示当前线程对象的引用.
• 实现 Runnable 接口, this 表示的是 MyRunnable 的引用. 需要使用Thread.currentThread()

其他变形:

  • 匿名内部类创建 Thread 子类对象
// 使⽤匿名类创建 Thread ⼦类对象
Thread t1 = new Thread() {
 	@Override
 	public void run() {
 	System.out.println("使⽤匿名类创建 Thread ⼦类对象");
 	}
};
  • 匿名内部类创建 Runnable 子类对象
// 使⽤匿名类创建 Runnable ⼦类对象
Thread t2 = new Thread(new Runnable() {
 	@Override
 	public void run() {
 	System.out.println("使⽤匿名类创建 Runnable ⼦类对象");
 	}
});
  • lambda 表达式创建 Runnable 子类对象
// 使⽤ 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 对象创建线程对象,并命名
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

2.2 Thread 的几个常见属性

属性获取方法
IDgetld0)
名称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) 修饰代码块: 明确指定锁哪个对象。
锁任意对象

public class SynchronizedDemo {
 	private Object locker = new Object();
 
 	public void method() {
 		synchronized (locker) {
 		}
 	}
}

锁当前对象

public class SynchronizedDemo {
 	public void method() {
 		synchronized (this) {
 		}
 	}
}

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

public class SynchronizedDemo {
 	public synchronized void methond() {
 	}
}

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

public class SynchronizedDemo {
 	public synchronized static void method() {
 	}
}

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

总结

多线程几乎是面试必问题,柚柚们一定要好好理解喔!!!
在这里插入图片描述

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部