多线程-初阶(1)

news/2024/10/4 23:34:39/
本节⽬标
认识多线程
掌握多线程程序的编写
掌握多线程的状态
掌握什么是线程不安全及解决思路
掌握 synchronized、volatile 关键字

1. 认识线程(Thread)

1.1 概念

1) 线程是什么

⼀个线程就是⼀个 "执⾏流". 每个线程之间都可以按照顺序执⾏⾃⼰的代码. 多个线程之间 "同时" 执⾏着多份代码.
2) 为啥要有线程
⾸先, "并发编程" 成为 "刚需".
线程之间的共享变量存在 主内存 (Main Memory).
每⼀个线程都有⾃⼰的 "⼯作内存" (Working Memory) .
当线程要读取⼀个共享变量的时候, 会先把变量从主内存拷⻉到⼯作内存, 再从⼯作内存读取数据.
3) 进程和线程的区别
  1. 进程是包含线程的. 每个进程⾄少有⼀个线程存在,即主线程。
  2.  进程和进程之间不共享内存空间. 同⼀个进程的线程之间共享同⼀个内存空间。
  3. 进程是系统分配资源的最⼩单位,线程是系统调度的最⼩单位。
  4. ⼀个进程挂了⼀般不会影响到其他进程. 但是⼀个线程挂了, 可能把同进程内的其他线程⼀起带⾛(整个进程崩溃).

4) Java 的线程 和 操作系统线程 的关系 

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

 1.2 第⼀个多线程程序

感受多线程程序和普通程序的区别:
每个线程都是⼀个独⽴的执⾏流
多个线程之间是 "并发" 执⾏的.

可以使用jconsole观察线程的状态,具体怎么使用后面学习.

 1.3 创建线程

⽅法1 继承 Thread 类
继承 Thread 来创建⼀个线程类,
java">class MyThread extends Thread {@Overridepublic void run() {System.out.println("这⾥是线程运⾏的代码");}
}
创建 MyThread 类的实例
java">MyThread t = new MyThread();
调⽤ start ⽅法启动线程
java">t.start(); // 线程开始运⾏
⽅法2 实现 Runnable 接⼝
1. 实现 Runnable 接⼝
java">class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("这⾥是线程运⾏的代码");}
}
2. 创建 Thread 类实例, 调⽤ Thread 的构造⽅法时将 Runnable 对象作为 target 参数.
Thread t = new Thread ( new MyRunnable ());

 方法3:匿名内部类,创建Thread子类

方法4:匿名内部类,创建Runnable子类

 方法5:lambda表达式

2. Thread 类及常⻅⽅法

 2.1 Thread 的常⻅构造⽅法

演示:

java">Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

 2.1 Thread 的⼏个常⻅属性(重点)

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否为后台线程

isDaemon()

是否存活isAlive()
是否被中断isInterrupted()

解释:

ID 是线程的唯⼀标识,不同线程不会重复
名称是各种调试⼯具⽤到
状态表⽰线程当前所处的⼀个情况,下⾯我们会进⼀步说明
优先级⾼的线程理论上来说更容易被调度到(抢占资源)
关于后台线程,需要记住⼀点: JVM会在⼀个进程的所有⾮后台线程结束后,才会结束运⾏。
是否存活,即简单的理解,为 run ⽅法是否运⾏结束了
线程的中断问题,等等我们会解释到。

是否为后台线程:

 

一次都没有打印就结束,因为main线程结束的很快。mian是前台线程,thread是后台线程。也可以判断线程是否为后台线程。

是否存活:

2.2线程的中断(重点)

 ⽬前常⻅的有以下两种⽅式:

1. 通过共享的标记来进⾏沟通
2. 调⽤ interrupt() ⽅法来通知

标志位 中断(终止)

 调用intterrupt()

 解释

 但是这里有一个问题:

会一直陷入死循环,为什么,

解释:

解决方法:

加上break

2.3等待⼀个线程 - join()

比如说:现在有两线程a,b,在a线程中调用b.join()

就是a线程等待b线程先结束,a线程在执行。

join的作用:就是能让先结束的线程先结束

 2.4 获取当前线程引⽤

这个方法我们已经很熟悉了,用来获取当前线程的引用。

 2.5休眠当前线程(sleep)

3. 线程的状态

打印线程状态:线程变量名.getState();

 

这个我就不多演示,后面我会给大家带来jcomsole工具,java自带工具的使用。 

 4. 多线程带来的的⻛险-线程安全 (重点)

4.1 观察线程不安全 

比如说:现在让两个

代码:

java">public class Demo9 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {// 对 count 变量增5w次for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(() -> {// 对 count 变量增5w次for (int i = 0; i < 50000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();// 预期结果应该是 10wSystem.out.println("count: " + count);}
}

结果:

原因:

 

4.2 线程安全的概念 

想给出⼀个线程安全的确切定义是复杂的,但我们可以这样认为:
如果多线程环境下代码运⾏的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

4.3 线程不安全的原因

线程调度是随机的
随机调度使⼀个程序在多线程环境下, 执⾏顺序存在很多的变数.
程序猿必须保证 在任意执⾏顺序下 , 代码都能正常⼯作
原⼦性

什么是原⼦性

我们把⼀段代码想象成⼀个房间,每个线程就是要进⼊这个房间的⼈。如果没有任何机制保证,A进⼊房间之后,还没有出来;B 是不是也可以进⼊房间,打断 A 在房间⾥的隐私。这个就是不具备原⼦性的。
是不是只要给房间加⼀把锁,A 进去就把⻔锁上,其他⼈是不是就进不来了。这样就保证了这段代码的原⼦性了。 有时也把这个现象叫做同步互斥,表⽰操作是互相排斥的。
⼀条 java 语句不⼀定是原⼦的,也不⼀定只是⼀条指令
⽐如刚才我们看到的 count++,其实是由三步操作组成的:
1. 从内存把数据读到 CPU
2. 进⾏数据更新
3. 把数据写回到 CPU

可⻅性

可⻅性指, ⼀个线程对共享变量值的修改,能够及时地被其他线程看到.
Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型.
⽬的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到⼀致的 并发效果.

 

  • 线程之间的共享变量存在 主内存 (Main Memory).
  • 每⼀个线程都有⾃⼰的 "⼯作内存" (Working Memory) .
  • 当线程要读取⼀个共享变量的时候, 会先把变量从主内存拷⻉到⼯作内存, 再从⼯作内存读取数据.
  • 当线程要修改⼀个共享变量的时候, 也会先修改⼯作内存中的副本, 再同步回主内存.
由于每个线程有⾃⼰的⼯作内存, 这些⼯作内存中的内容相当于同⼀个共享变量的 "副本". 此时修改线程1 的⼯作内存中的值, 线程2 的⼯作内存不⼀定会及时变化.

演示过程:

 1) 初始情况下, 两个线程的⼯作内存内容⼀致.

2) ⼀旦线程1 修改了 a 的值, 此时主内存不⼀定能及时同步. 对应的线程2 的⼯作内存的 a 的值也不⼀定 能及时同步.
这个时候代码中就容易出现问题.
此时引⼊了两个问题:
1) 为啥整这么多内存?
实际并没有这么多 "内存". 这只是 Java 规范中的⼀个术语, 是属于 "抽象" 的叫法. 所谓的 "主内存" 才是真正硬件⻆度的 "内存". ⽽所谓的 "⼯作内存", 则是指 CPU 的寄存器和⾼速缓存.

2) 为啥要这么⿇烦的拷来拷去? 

因为 CPU 访问⾃⾝寄存器的速度以及⾼速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级,也就是⼏千倍, 上万倍)。
⽐如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的. 但是如果只是 第⼀次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9 次读数据就不必直接访问内存了. 效率就⼤⼤提⾼了.
那么接下来问题⼜来了, 既然访问寄存器速度这么快, 还要内存⼲啥??
答案就是⼀个字: 贵

4.4 解决之前的线程不安全问题

代码:

java">public class Demo9 {private static int count = 0;public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(() -> {// 对 count 变量进⾏⾃增 5w 次for (int i = 0; i < 50000; i++) {synchronized (locker) {count++;}}});Thread t2 = new Thread(() -> {// 对 count 变量进⾏⾃增 5w 次for (int i = 0; i < 50000; i++) {synchronized (locker) {count++;}}});t1.start();t2.start();t1.join();t2.join();// 预期结果应该是 10wSystem.out.println("count: " + count);}
}

结果:

5. synchronized 关键字 - 监视器锁 monitor lock

5.1 synchronized 的特性

1) 互斥

synchronized 会起到互斥效果, 某个线程执⾏到某个对象的 synchronized 中时, 其他线程如果也执⾏ 到同⼀个对象 synchronized 就会阻塞等待.
进⼊ synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁

synchronized⽤的锁是存在Java对象头⾥的。  

理解 "阻塞等待".
针对每⼀把锁, 操作系统内部都维护了⼀个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试 进⾏加锁, 就加不上了, 就会阻塞等待, ⼀直等到之前的线程解锁之后, 由操作系统唤醒⼀个新的线程, 再来获取到这个锁。
注意:
上⼀个线程解锁之后, 下⼀个线程并不是⽴即就能获取到锁. ⽽是要靠操作系统来 "唤醒". 这也就 是操作系统线程调度的⼀部分⼯作.
假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B ⽐ C 先来的, 但是 B 不⼀定就能获取到锁, ⽽是和 C 重新竞争, 并不遵守先来后到的规则.
synchronized的底层是使⽤操作系统的mutex lock实现的。

 2) 可重⼊

synchronized 同步块对同⼀条线程来说是可重⼊的,不会出现⾃⼰把⾃⼰锁死的问题;

理解 "把⾃⼰锁死" 

⼀个线程没有释放锁, 然后⼜尝试再次加锁.
// 第⼀次加锁, 加锁成功
lock();
// 第⼆次加锁, 锁已经被占⽤, 阻塞等待.
lock();
按照之前对于锁的设定, 第⼆次加锁的时候, 就会阻塞等待. 直到第⼀次的锁被释放, 才能获取到第⼆个锁. 但是释放第⼀个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想⼲了, 也就⽆法进 ⾏解锁操作. 这时候就会 死锁。
这样的锁称为 不可重⼊锁.

 Java 中的 synchronized 是 可重⼊锁,像c++和python是不可重入锁。

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 锁的是什么. 两个线程竞争同⼀把锁, 才会产⽣阻塞等待.
两个线程分别尝试获取两把不同的锁, 不会产⽣竞争.

 

5.3 Java 标准库中的线程安全类 

Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, ⼜没有任何加锁措施,比如说:
ArrayList
LinkedList
HashMap
TreeMap
HashSet
TreeSet
StringBuilder

但是还有⼀些是线程安全的. 使⽤了⼀些锁机制来控制。

Vector (不推荐使⽤)
HashTable (不推荐使⽤)
ConcurrentHashMap
StringBuffer
StringBuffer 的核⼼⽅法都带有 synchronized。

还有的虽然没有加锁, 但是不涉及 "修改", 仍然是线程安全的

String

6. volatile 关键字

volatile 能保证内存可⻅性。
volatile 修饰的变量, 能够保证 "内存可⻅性".
按照翻译:

来我们写一个代码,只要输入0就会停止线程。

代码:

java">public class Demo10 {static volatile int n = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {while(true) {//啥都不写}});Thread t2 = new Thread(() -> {Scanner sc = new Scanner(System.in);System.out.println("请输入一个整数:");n = sc.nextInt();});}
}

结果:

结果捏

这就是可见性问题。

原因:

 

 volatile 不保证原⼦性

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原⼦性, volatile 保证的是内存可⻅性.
再比如说:上面代码我们给count变量加上volatile保证可见性
保证内存可见性的目的是为了避免变量修改的时候被系统优化了。
代码:
java">public class Demo9 {private volatile static  int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(() -> {// 对 count 变量进⾏⾃增 5w 次for (int i = 0; i < 50000; i++) {count++;}});t1.start();t2.start();t1.join();t2.join();// 预期结果应该是 10wSystem.out.println("count: " + count);}
}

结果:

好了今天就讲到这里。


http://www.ppmy.cn/news/1534651.html

相关文章

深入浅出:现代JavaScript开发者必知必会的Web性能优化技巧

亲爱的读者们&#xff0c;欢迎来到本期博客。今天&#xff0c;我们将深入探讨JavaScript开发者在日常工作中如何提升Web性能。在快节奏的Web开发世界中&#xff0c;性能优化至关重要。本文将分享一些实用技巧&#xff0c;帮助你构建快速、高效的Web应用。 1. 使用CDN加速资源加…

量子计算:颠覆未来计算的革命性技术

量子计算&#xff1a;颠覆未来计算的革命性技术 量子计算作为下一代颠覆性技术&#xff0c;正在引领计算领域的重大变革。与传统计算机基于比特的二进制运算不同&#xff0c;量子计算通过量子比特&#xff08;qubits&#xff09;在叠加态和纠缠态下实现并行计算&#xff0c;能…

C# 入坑JAVA 潜规则 注解 列表 listMch,该列表存储了一个映射(Map)的集合 等 入门系列3

java注解 好像和C# 特性 差不多 Data Builder NoArgsConstructor AllArgsConstructor 在Java中&#xff0c;Data、Builder、NoArgsConstructor和AllArgsConstructor是Lombok库提供的注解&#xff0c;它们用于简化Java对象的创建和处理。Lombok是一个流行的Java库&#xff0c;…

滚雪球学MySQL[2.2讲]:基本数据操作详解:插入、查询、更新与删除

全文目录&#xff1a; 前言2.2 基本数据操作1. 插入数据&#xff08;INSERT&#xff09;基本语法示例1&#xff1a;向所有列插入数据示例2&#xff1a;插入部分列的数据 2. 查询数据&#xff08;SELECT&#xff09;基本语法示例1&#xff1a;查询所有数据示例2&#xff1a;查询…

力扣10.1

983. 最低票价 在一个火车旅行很受欢迎的国度&#xff0c;你提前一年计划了一些火车旅行。在接下来的一年里&#xff0c;你要旅行的日子将以一个名为 days 的数组给出。每一项是一个从 1 到 365 的整数。 火车票有 三种不同的销售方式 &#xff1a; 一张 为期一天 的通行证售…

ECCV 2024 | 融合跨模态先验与扩散模型,快手处理大模型让视频画面更清晰!

计算机视觉领域顶级会议 European Conference on Computer Vision&#xff08;ECCV 2024&#xff09;将于9月29日至10月4日在意大利米兰召开&#xff0c;快手音视频技术部联合清华大学所发表的题为《XPSR: Cross-modal Priors for Diffusion-based Image Super-Resolution》——…

Windows 开发工具使用技巧

Windows 开发工具使用技巧 1. Visual Studio 快捷键&#xff1a;掌握常用快捷键&#xff0c;比如 F5&#xff08;启动调试&#xff09;&#xff0c;Ctrl K, Ctrl C&#xff08;注释选择代码&#xff09;&#xff0c;Ctrl K, Ctrl U&#xff08;取消注释&#xff09;。 调…

Vue2如何在网页实现文字的逐个显现

目录 Blue留言&#xff1a; 效果图&#xff1a; 实现思路&#xff1a; 代码&#xff1a; 1、空字符串与需渲染的字符串的定义 2、vue的插值表达式 3、函数 4、mounted()函数调用 结语&#xff1a; Blue留言&#xff1a; 在国庆前夕&#xff0c;突发奇想&#xff0c;我想…