「课件」原创 => Java's 多线程【转载请标明出处】
第一章·何为多线程?
多线程是一种并发执行的编程方式,它允许程序同时执行多个线程(线程是执行流的最小单位),从而提高程序的性能和效率。每个线程都是独立的执行序列,它们共享相同的进程资源,如内存空间,但拥有自己的寄存器和栈。多线程可以在单个程序中执行多个任务,使得程序能够更充分地利用多核处理器或同时处理多个任务,从而加速程序的运行。
- 进程:是程序执行的一个实例。它拥有自己的地址空间、内存、文件描述符和其他系统资源。每个进程都是独立的,彼此之间不会直接共享内存。
线程的话,其实就是 在同一个进程里面创造开启的,它会共享相同的地址空间和其它资源,但是每个线程又都有自己的 执行任务(这也是多线程的存在意义)
线程切换的开销相对较小,因为线程共享相同的地址空间,切换主要涉及到寄存器和栈的切换。
缺点:线程之间虽然可以更方便地共享数据,但也需要注意 同步和互斥问题。因为共享,所以当然会有冲突。一句话让你理解!
多线程的应用场景:适用于需要共享数据、共享资源、并发执行的任务,尤其在需要快速响应用户输入的应用程序中!
- 然后 给大家再一嘴 有的没的
协程
:也是用于实现并发编程的方式,但它在很多情况下,都认为是比 多线程更好的存在!
- 协程是轻量级的执行单位,相较于线程更加节省资源。
- 由于协程的轻量级特性和更小的切换开销,通常能够在相同硬件上执行更多的并发任务,从而提供更好的性能。
- 最重要的一点是:协程通常使用异步编程模型,通过事件循环等机制来处理并发。由于协程的切换是显式的,可以避免一些多线程中常见的同步问题,如竞态条件(Race Condition)和死锁(Deadlock)。
- 协程的本质是 单线程的 快速切换!所以 管理起来真的简单多了 ~
- 在代码角度上:由于协程通常使用顺序化的代码结构,相较于多线程,代码可能更易于理解和调试。
- 举例子说明吧(毕竟很多人都没有 操作系统的知识前提)
你打开一个应用程序,比如说 QQ、微信、LOL 对吧,它们都会被 加载到 内存中,都有着 系统给它们 分配好的 内存空间。而 实际上,我们的这些应用都是很复杂的,并且要求效率较高的,毕竟要给 用户 友好的体验嘛。如果是 单个线程的话,也就是 我们只有 要给 main 主方法,然后 运行之后,只开启了 一个主线程,去执行 main 方法里面的 逐行代码。这样就显得 效率很低。当你的程序 要去做的事情,我们称为 "任务",变多了之后,我们其实 是会发现 可以并发分工的现象的!
举个例子:比如你输出一个 提示信息,与你现在 去 读取数据库的数据,完全可以并发分工去做的,它们之间 没有太多的纠缠关系。但是如果你只有 一个线程的话,那么就只能 等 一步一步的 代码完成一个一个的任务,这。。。是不是有点儿感觉无语 ~
为了解决这样的问题,操作系统 提供了 多线程的概念,即 一个进程,可以开辟 多个线程 去 并发的执行 各自的任务。提高效率和用户体验。
- 并发:并发是指两个或多个事件在同一时间间隔发生。
- 并行:并行是指两个或者多个事件在同一时刻发生。
多个线程或进程”同时”运行只是感官上的一种表现。事实上进程和线程是并发运行的,OS的线程调度机制将时间划分为很多时间片段(时间片),尽可能均匀分配给正在运行的程序,获取CPU时间片的线程或进程得以被执行,其他则等待。而CPU则在这些进程或线程上来回切换运行。微观上所有进程和线程是走走停停的,宏观上都在运行,这种都运行的现象叫并发,但是不是绝对意义上的同时发生。
- 分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
- 抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
Java中的并行执行可以使用Java的并行库(如ThreadPoolExecutor类)来实现,也可以利用Java arallel Streams)来进行处理。
需要注意的是,并行执行不一定会比并发执行更快,因为并行是多个独立的CPU核心的,并且任务之间存在一定的通信和同步开销。而并发执行可以在单核处理器上进行,任务开销相对较小。另外,并行执行对任务的分解和处理有一定的限制的,某些依赖于前一步骤结果的任务,可能就无法并行执行了。因此,在实际应用中需要根据具体情况来选择并发还是并行。
Java 创建线程的方式:
- 继承 Thread 类
- 实现 Runnable 接口
- 实现 Callable 接口
- 线程池
第二章· Thread 类
当进程创建的时候,会创建一个 主线程(main方法),还有一个就是 JVM 特有的 GC(垃圾回收)线程。
- main函数 叫 “用户线程/主线程” 是自己写的!
- GC 线程是JVM给我们的,也被称为 “守护线程”!
其它的线程,我们可以通过 继承 Thread,重写 里面的 run 方法来编写线程执行体(也就是待执行的任务),再创建线程对象,调用 start() 方法启动线程!
package www.muquanyu.lesson01;
public class ThreadDemo extends Thread {
@Override
public void run()
{
System.out.println("我是子线程!");
}
//主线程
public static void main(String[] args) {
//开启子线程
ThreadDemo threadDemo = new ThreadDemo();
threadDemo.start();
//主线程的执行代码
for(int i = 0;i<20;++i)
{
System.out.println("我是主线程");
}
}
}
第三章·线程六大状态
-
NEW:new 一个 Thread 对象的时候,线程就被创建出来了!然后 没有 调用 start() 之前的状态。
-
RUNNABLE:运行状态,当调用 start 后 会先进
就绪状态,即系统不会立即开启线程!而是通过 CPU 的调度器 让其 进行 随机的 开启(当然 不是真正意义上的随机,CPU 是根据 自身的最优策略选择的!)。观测的时候,就绪也被归在 NEW 状态!
-
BLOCKED:阻塞状态,当访问已被 锁的对象,这时候就会 进入阻塞了!
-
WAITING:等待状态,执行wait,join,park等方法时候,线程会进入等待状态。进行特定的操作才会进入RUNNABLE。
-
TIMED_WAITING:有时间的等待,除了特定的操作解除外,还可以等时间结束解除。
-
TERMINATED:结束状态。正常运行完或者终止都会进入这个状态。
3.1 线程休眠
- sleep(时间)指定当前线程阻塞的毫秒数
- sleep 存在异常 InrerruptedException
- sleep 时间达到后,才会让线程进入就绪状态。
- sleep 可以模拟网络延时,倒计时等。而且比 时钟好用多了。它可以用在各个代码段中间,直接调用 sleep 进行代码段的延迟。这么简便的代码。时钟是 完全不能办到的!!!
public class Demo01 {
public static void main(String[] args) throws InterruptedException {
System.out.println("我是主线程,我即将通过 sleep 进入阻塞状态");
Thread.sleep(1000);
System.out.println("一秒过去了");
Thread.sleep(1000);
System.out.println("第二秒过去了");
}
}
可以提前记住一个概念:每一个对象都有一个锁,而 sleep 是根本不会释放锁的!!!
目前 JDK 其实是比较推荐 让线程自己停止下来的!已经不推荐使用 stop()、destroy() 方法了,这个一定要知道。
- 建议 使用 一个 标志位进行终止变量。 当 flag == false,则终止线程运行。
public class Demo01 {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while(flag){
System.out.println("子线程正在执行!");
}
System.out.println("子线程停止执行!");
});
thread.start();
System.out.println("我是主线程,我即将通过 sleep 进入阻塞状态");
Thread.sleep(1000);
System.out.println("一秒过去了");
Thread.sleep(1000);
System.out.println("第二秒过去了");
for(int i = 0; i < 10000; ++i){
if(i == 3000) Demo01.stop();
}
}
public static void stop(){
flag = false;
}
}
我们知道 线程 一旦被开启,最终都会 自己 走向 死亡状态。 而 这个死亡状态 是在 运行状态 之后 进入的!那么运行状态 在做什么呢?就是在 执行 代码!!!换句话说,事务的代码执行完毕后,线程就会自己 进入 死亡状态! 不需要我们 强制性的关闭。这也是 为什么 Java 不推荐使用 stop()方法 和 destroy() 方法。
思考:如果说 线程之间是并发的,那么 是否可能因为 时间切换频率过快,导致 判断没反应过来,多输出一条信息呢?
答:这是太有可能了,如果你能有这样的 想法,其实就代表你对 线程有了一定的理解!如果不信的话,其实可以自己 去试试。
【作业:尝试 是否会多输出 一条信息?自己想一想 用什么 方式可以 看的清晰】
3.1.1 定时器(更加符合面向对象)
定时器的发明,是为了写代码的时候,更加符合 面向对象的思想。实际上,定时器能干的活儿,Thread.sleep() 都完全 Ok 的 ~
Timer 定时器,内部维护了 一个 TaskQueue 任务队列,然后它也给这个队列 开了个 TimerThread 线程,用来时刻监视 这个队列 里面是否还有待执行的任务了?如果没有待执行的任务,则是会处于休眠状态 TimerThread 线程,因为它害怕还会有新的任务加进队列。除非你用
cancel()
否则不会停止 TimerThread 线程!
import java.util.Timer;
import java.util.TimerTask;
public class 定时器 {
static int num = 0;
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
if(num++ == 10) timer.cancel(); // 将维护队列的线程干掉
System.out.println(Thread.currentThread().getName());
}
},0,1000);
}
}
timer.schedule(TimerTask task, long delay, long period)
task:线程执行的 任务
delay:线程执行前的 sleep() 延迟
period:重复性(循环)执行 task 任务的 间隔时间
3.2 线程中断
其实就是 interrupt()
方法的调用,它会给线程打个中断标记,让线程响应这个标记,然后去进行中断的相应处理。它跟 stop() 是不一样的,stop 是强制性的停止线程,可能导致 资源不完全释放。如果说,让我在这俩个方法中选择的话,肯定选 interrupt(),但实际上,我们不如自己去声明一个 flag 变量 打标记。
interrupt 的本质:其实就是咱们 上面 写的 建议 使用 一个 标志位进行终止变量。 当 flag == false,则终止线程运行。 是的,你没看错,它的本质就是这个。我们刚才已经写出来了 ~ 就是 Thread 里面也有一个 类似于 flag 的标记,然后 我们 通过
Thread.currentThread().isInterrupted()
这样的方法 就可以 查看这个标记 是否为 true,如果为 true 那么就 可以 让 线程自然结束了,这个自然结束的 操作,其实还是 你自己手动写的。
public class Demo01 {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
while(true){
if(Thread.currentThread().isInterrupted())
break;
System.out.println("子线程正在执行!");
}
System.out.println("子线程停止执行!");
});
thread.start();
System.out.println("我是主线程,我即将通过 sleep 进入阻塞状态");
Thread.sleep(1000);
System.out.println("一秒过去了");
Thread.sleep(1000);
System.out.println("第二秒过去了");
for(int i = 0; i < 10000; ++i){
if(i == 3000) thread.interrupt(); // 打标记
}
}
}
3.2.1 中断复位
听着听高大上是吧,实际上就是 将标记 撤销了,说白了就是 类似于 flag = false
也就是 随后再有代码片段 判断 是否有中断标记的话,都会 返回 false,也就是没有。
复位的方法:Thread.interrupted();
3.3 线程暂停
Thread.currentThread().suspend()
:暂停线程
Thread.resume
:恢复线程(从 suspend() 恢复)
3.3.1 wait、notify、notifyall
当谈到 wait()
、notify()
和 notifyAll()
时,老Java程序员,直接就知道了,这仨不就是在Java多线程编程中用于实现线程间协作和同步机制的嘛。下面我会用简单的语言为你解释这三个方法:
- 首先,这三个方法是每个对象都会有的!因为所有的对象的祖宗类是 Object,而 Object 本身就有这三个方法!
- wait() 方法:
当一个线程调用某个对象的
wait()
方法时,它就进入了等待(阻塞)状态,并释放了持有的锁。(与 sleep 不一样是吧 ~ sleep 可不会给你释放锁)这个线程会等待其他线程调用相同对象上的notify()
或notifyAll()
方法来唤醒它。通常,wait()
方法被用于线程等待某个条件的满足。
- notify() 方法:
当一个线程调用某个对象的
notify()
方法时,它会唤醒等待在这个对象上的一个(任意一个)被wait()
方法阻塞的线程。注意,被唤醒的线程并不会立刻执行,而是等待当前线程释放锁才能继续执行。通常,notify()
方法被用于通知等待线程某个条件已经满足。
- notifyAll() 方法:
与
notify()
不同,notifyAll()
方法会唤醒在某个对象上所有被wait()
方法阻塞的线程。这样,所有等待的线程都有机会竞争获取锁并继续执行。通常,notifyAll()
方法在不同线程之间共享状态改变时使用,以确保所有等待的线程都能够得到通知。
此外,Java 中的 wait()
、notify()
和 notifyAll()
必须在同步块内部调用,因为它们依赖于对象的监视器锁。
3.4 线程的优先级(重点)
首先 Java 采用的是 抢占式调度方式!即 Java 提供一个 人为模拟的 线程调度器来监控程序中启动后进入就绪装态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。一般情况下,任务较重的线程会设置较高的优先级。
**PS:虽然 Java 模拟了一个类似于 CPU 调度器的东西,但实际上最后 还得是看 CPU 怎么调度舒服怎么来。只不过事先的 人为干涉,可以起到 大概率的作用! **
- 优先级数字范围:【1 ~ 10】
Thread.currentThread.getPriority()
:获取到 线程的优先级线程对象.setPriority(数字)
:设置线程的优先级- Thread.MAX_PRIORITY 最高优先级
- Thread.MIN_PRIORITY 最低优先级
- Thread.NOM_PRIORITY 常规优先级
- 线程对象.setPriority(Thread.MAX_PRIORITY)
3.4.1 设置优先级的问题
问题:由于 它们是在 主线程里启动子线程的,所以 运行代码的顺次步骤,也可能会对CPU调度的顺序进行 影响。(也就是说即使在没有进行优先级的设置情况下, 谁的 .start()代码 写在前面,谁就可能 先被调度。而且这种问题会影响到 设置优先级的概率!!!)
即,优先级只是人为的 模拟,进行的 大概率调节!
为了避免这种情况,我们一般 要保证 所有线程 的启动代码,都要根据设置的优先级顺次的堆积在一起来写!
package www.muquanyu.lesson03;
//测试线程的优先级
public class PriorityDemo {
public static void main(String[] args) {
//主线程默认优先级
System.out.println(Thread.currentThread().getName() + "--->" + Thread.currentThread().getPriority());
MyPriorityDemo MyPriority = new MyPriorityDemo();
Thread t1 = new Thread(MyPriority);
Thread t2 = new Thread(MyPriority);
Thread t3 = new Thread(MyPriority);
Thread t4 = new Thread(MyPriority);
Thread t5 = new Thread(MyPriority);
Thread t6 = new Thread(MyPriority);
//设置优先级(正确的设置优先级方式)
t2.setPriority(1);
t3.setPriority(4);
t4.setPriority(Thread.MAX_PRIORITY);
t5.setPriority(6);
t6.setPriority(8);
//正确的 线程方式(如果你设置了优先级,那么启动的顺序就按照 优先级的顺序来写!)
t4.start();
t6.start();
t5.start();
t1.start();
t3.start();
t2.start();
}
}
class MyPriorityDemo implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "--->" + Thread.currentThread().getPriority());
}
}
上述这种方式,是在 设置优先级之后,最正确的 写法!!但是也保证不了 CPU 调度的情况一定按照优先级来走。
3.5 线程状态查询
线程对象.getState()
:就可以获取 这个线程当前的状态
3.6 线程礼让与合并线程
3.6.1 线程礼让
-
线程礼让:让当前正在执行的线程暂停,但不是阻塞!
-
线程礼让不会让程序阻塞,而是直接转为 就绪状态。
-
本质上,就是 让 CPU 重新对该线程 进行调度,所以我们说 礼让还不一定成功呢 ~ 这得 看 CPU 的心情!
Thread.yield()
:它的意思就是,让你直接回到 就绪状态。然后再经过 CPU 调度。这个时候,如果 还有线程 比你慢,那就说明 它命不好。可能在你回到就绪状态的时候,正好 CPU 把你排在了 它的 前面。(但这也是 针对于还没有进入运行状态的线程。)
情景解释:小明已经跑出去 十米了,但是小红刚来到起跑线位置,就跟小明说,你回来一趟,咋俩竞赛一次。看看谁跑的快!
3.6.2 合并线程
合并线程(插队线程):有那么一点儿 插队的意思,比如说一个线程正在执行代码呢,然后 另一个线程想要在它执行的过程中,跑起来,那么 直接在 任务代码里面写上 线程对象.join()
这个线程就可以 跑起来了。
只不过合并过来的线程,优先级比较高,而且还会阻塞之前的线程!
应用场景:比较适合 特判任务执行时,遇到的特殊情况,需要执行的特殊任务处理。这个任务优先级必须得高,必须优先处理才行!
package www.muquanyu.lesson03;
public class JoinDemo {
public static JoinThread joinThread = new JoinThread();
public static Thread VIP = new Thread(joinThread,"VIP线程");
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread A = new Thread(myThread,"线程A");
A.start();
}
public static class MyThread implements Runnable{
@Override
public void run(){
System.out.println("开始排队");
try {
VIP.start();
VIP.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我草,才轮到我?");
}
}
public static class JoinThread implements Runnable{
@Override
public void run() {
System.out.println("哈哈,VIP进行插队!");
for(int i = 0;i<3;++i)
{
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+"-->"+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
第四章·Runnable&&初见并发问题
其实 Thread 这个类 就是 Runnable 接口的 实现,所以我们 建议 直接 用 Runnable 接口,而不是 去用 二次封装的 Thread 是有道理的。
需要注意的是,我们 虽然可以实现 Runnable 接口,实现里面的 run() 方法。但仍然需要 new Thread() 创建一个 线程对象,把其实现 Runnable 接口的对象 给包裹住!然后通过 Thread 对象.start() 去启动这个线程!
package www.muquanyu.lesson01;
// 实现 Runnable 接口
public class RunnableDemo implements Runnable {
@Override
public void run() {
for(int i = 0;i<200;++i)
{
System.out.println(Thread.currentThread().getName() + "=>" + i);
}
}
public static void main(String[] args) {
//需要 建立一个 Thread 对象
// 包裹住 这个 实现 Runnable 的类
Thread thread = new Thread(new RunnableDemo());
thread.start();
// 我们可以 让不同的线程 执行同样的 任务
Thread t2 = new Thread(new RunnableDemo());
for(int i = 0;i<200;++i)
{
System.out.println(i);
}
}
- 继承Thread 与 实现 Runnable 接口 => 两者的区别
- 继承 Thrad 类
- 子类继承 Thread 类 具备 多线程能力
- 启动线程:子类对象.start()
- 不建议使用:我们必须避免OOP 单继承的局限性(要是想继承一些其它的强大的类,那就是个问题。。)
- 实现 Runnable 接口
- 实现接口 Runnable 具有多线程能力
- 启动线程:传入目标对象 + Thread 对象.start()
- 推荐使用:避免单继承的局限性,灵活方便,方便同一个对象被多个线程使用
比如说 三个线程 同时 跑 一个 事务!你要是继承 Thread 的那种方式,可是无法实现的。只有 实现 Runnable 接口 才可以。
4.1 使用 lambda 表达式
我们追随 Thread 类的 源代码,会发现 =>
它说我们要 传递 进去一个 实现 Runnable 运行的 这个接口的 对象,这个 对象里面确实 只有一个 方法 run() 是用来 存放我们 想要执行任务的 内容代码的!函数式接口,可以lambda表达式
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = new Thread(()-> {
System.out.println("Hello 我是线程 A");
});
thread.start();
System.out.println("我是主线程");
}
}
你会发现 主线程这条输出在上面,而 线程 A 在下面。其实 这里就已经 有点儿 多线程 那味了 ~ 就是根本 不是 一个线程,一条一条代码执行的。它现在是 并发的。快速的轮换 平均的去执行这两个线程的任务。
可能我们 来两个 循环,会更加明显,是吧 ~
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = new Thread(()-> {
for(int i = 0; i < 100; ++i){
System.out.println("我是子线程 =>" + i);
}
});
thread.start();
System.out.println("我是主线程");
for(int i = 0; i < 100; ++i){
System.out.println("我是主线程 => " + i);
}
}
}
Thread对象.start()
:启动线程
4.2 初见并发问题(竞态条件)
我们都知道 Java 的线程默认是 并发的,而 并发是存在一些问题的!最常见的并发问题 就是 竞态条件:是指在多线程或多进程的程序中,由于执行顺序的不确定性而导致程序的最终状态依赖于线程或进程的执行顺序。具体而言,竞态条件发生在多个线程或进程同时访问共享资源,并且其中一个线程或进程执行的结果会影响其他线程或进程的行为。
竞态条件通常是由于缺乏适当的同步机制,导致并发执行的不确定性而引起的。例如,在没有锁或其他同步手段的情况下,多个线程可能同时访问和修改共享的变量,导致意外的行为。
public class 竞态条件 {
// 共享计数器
private static int counter = 0;
public static void main(String[] args) throws InterruptedException {
// 创建两个线程,分别定义线程的执行逻辑
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
// 读取计数器的值
int value = counter;
// 执行递增操作
value = value + 1;
// 将结果写回计数器
counter = value;
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
// 读取计数器的值
int value = counter;
// 执行递增操作
value = value + 1;
// 将结果写回计数器
counter = value;
}
});
// 启动两个线程
thread1.start();
thread2.start();
// 等待两个线程执行完成
thread1.join();
thread2.join();
// 显示计数器的最终值
System.out.println("最终计数器值: " + counter);
}
}
你看看,这是个啥 ~ 10000 + 10000 = 20000,它这 14332 属实逆天 ~
两个线程在同时操作 共同变量时,进行递增。可能会取到同样的值,然后同样 + 1 再次写回到主存中,那么可能这个 值 就只是 + 1 的结果。而非 + 2 的结果。
总结:当多个 线程对象,操作同一个 资源的情况下,线程是不安全的。会造成竞态条件,导致数据紊乱!
4.2.1 ThreadLocal
我们在解除了 上面的并发问题之后,有的小伙伴就灵机一动。欸?每个线程 难道 非要 使用共享资源嘛?可不可以有自己的 独立的资源,只能我自己用?不能其他人用?万一有这样的需求呢?至此,Java 提供了 类,即 线程本地的意思,是说 我们当前这个线程本地里面有自己的资源,而其它线程里面本地也有自己的资源。但是,ThreadLocal 只允许每个 线程只有 一个自己的本地资源!
public class 线程本地资源 {
public static void main(String[] args) throws InterruptedException {
ThreadLocal<String> local = new ThreadLocal<>();
Thread threadB = new Thread(() -> {
local.set("哇哈哈");
System.out.println(local.get());
});
Thread threadA = new Thread(() -> {
local.set("fcker"); // 将这个字符串交给 local,然后 local 会根据你这个 线程 打个标记,代表你这个线程 的local 资源就是 这个字符串!
System.out.println(local.get());
try {
threadB.start();
threadB.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
threadA.start();
}
}
你会发现,各自 Local 资源 都是各自的!哈哈哈 ~ 有点儿绕。。
4.3 经典问题·龟兔赛跑
package www.muquanyu.lesson01;
//模拟龟兔赛跑
public class Race implements Runnable {
//只能 有一个 胜利者,所以是 静态的
private static String winner;
@Override
public void run() {
for(int i = 0;i <= 100;++i)
{
if(Thread.currentThread().getName().equals("兔子") && i%10==0)
{
try{
Thread.sleep(1);
}catch (InterruptedException e)
{
e.printStackTrace();
}
}
boolean flag = gameOver(i);
if(flag)
{
break;
}
System.out.println(Thread.currentThread().getName() + "-->跑了"+i+"米!");
}
}
private boolean gameOvera(int i) {
return false;
}
//判断是否完成比赛
private boolean gameOver(int steps)
{
if(winner != null)
{
return true;
}
if(steps == 100)
{
winner = Thread.currentThread().getName();
System.out.println("winner is "+ winner);
return true;
}
return false;
}
public static void main(String[] args) {
Race race = new Race();
Thread 兔子 = new Thread(race, "兔子");
Thread 乌龟 = new Thread(race, "乌龟");
兔子.start();
乌龟.start();
}
}
第五章·Callable 接口
这个接口非常的严谨,所以很复杂。多了个 开启服务 和 关闭 服务。并且还得把 线程必须放到 线程池子里面去!
步骤:
- 实现 Callable 接口,需要有 返回值类型
- 重写 里面的 call 方法,还需要 抛出 异常
- 创建 目标对象
- 创建 执行服务:ExecutorService ser = Executors.newFixedThreadPool(线程的数目);//用 Executors 创建一个新的线程池
- 提交执行:Future result1 =ser.submit(t1);
- 获取结果:boolean r1 = result1.get();
- 关闭服务:ser.shutdownNow();
这个,我们暂时也是 只 作为了解呀!其实常用的 最多的,就是 继承 Thread 和 实现 Runnable 接口。
我个人比较喜欢用 Runnable 接口那种的 lambda 表达式
第六章·守护线程和线程同步
6.1 守护(daemon)线程
- 线程分为 用户线程 和 守护线程
- JVM 虚拟机 必须 确保用户线程执行完毕
- JVM 不用等待 守护 线程执行完毕 < 守护线程跟 主线程和用户线程 不是一路子的。 >
- 如:后台记录操作日志,监控内存,垃圾回收等等。都是守护线程!
什么叫做 JVM 虚拟机 不等待 守护线程??
答:就是说 守护线程的 关闭和停止,不是 由 JVM 虚拟机决定的。而是 由 操作系统决定的。当程序真正的被停止后。或者说 当所有的用户线程和主线程 停止后。 守护线程 才会被 系统关闭和停止,而并非 JVM 虚拟机。JVM 虚拟机 只会 关闭和停止 用户线程,当用户线程执行的事务 完毕后(代码执行完了后),JVM 就会 帮这些用户线程 进行 自动的关闭 和停止!而 守护线程 是 先看 一看,整个程序 是否还存在 用户线程 或 主线程 是否还有存在的意义。而决定 自己到底 是否还要 继续守护下去。
我们可以说,守护线程 是 后台线程,它的优先级 较低!整个程序 都不需要等待这个 线程 执行完成。这样 做的 意义就是 当子线程无限循环时,出现退不出程序的情况时候。为了避免 孤儿线程。此时 你把 这个 线程 设定为 守护线程即可,我们就不需要等待这个线程执行完成了!
所以,如果是我们普通用户使用守护线程,一般都是 一些 需要进行 一直 死循环 监视的 线程,尽量的 去设为 守护线程!!!
public class 守护线程 {
public static int num = 10;
public static boolean flag;
public static void main(String[] args) {
Thread monitor = new Thread(() -> {
while(true){
if(num == 0){
System.out.println("检测到 num = 0");
flag = true;
}
}
});
Thread temp = new Thread(() -> {
while(true){
System.out.println("子线程在执行!");
num--;
if(flag){
break;
}
}
});
monitor.setDaemon(true);
temp.start();
monitor.start();
// while (monitor.getState() != Thread.State.TERMINATED){
// System.out.println("monitor -----> " + monitor.getState());
// }
System.out.println("monitor -----> " + monitor.getState());
}
}
注意:在守护线程的任务中,产生的新线程 默认也是守护的!
6.2 线程同步
线程同步是解决 线程并发问题的 最常见方案!
-
现实生活中,我们其实就会遇到 “同一个资源,多个人都想使用” 的问题,比如说 食堂排队在打饭,每个人都想要吃饭,但是必须遵守基本的道德准则和规范规则!! 否则你一闹事 可能会导致 大家 都吃不到饭了。
-
处理多线程问题的时候,多个线程访问同一个对象,并且某些线程还想去修改这个对象。这时候我们就需要线程同步。线程同步其实就是一种等待机制。 多个需要同时访问此对象的线程需要进入 这个 对象 的 “等待池” 形成队列,等待前面的线程使用完毕,下一个线程再去使用!(这样就有规则和规范,并且 效率也不会因此降低多少!!!)
那么 后面排队的人,也就是在 等待池里面的人,怎么会知道 前面的那个线程 正在 处理资源呢?这就涉及到 锁的 概念了!
6.2.1 线程锁的理解
线程锁(Thread Lock)是一种同步机制,用于控制多个线程对共享资源的访问。在并发编程中,多个线程同时访问共享数据可能导致不确定的结果或数据损坏。线程锁通过确保在任何时刻只有一个线程可以访问共享资源,从而防止竞态条件(Race Condition)的发生。
举个场景:你去上厕所 抢坑位,如果里面有人,并且上了锁,那你就没办法了,只能等人家上完 ~ 当然了~ 如果他 没上锁,嘿嘿~ 所以,锁挺重要的。
解决竞态条件的方案:确保在同一时刻只有一个线程能够执行被保护的代码段,从而避免! 那妥了,我直接 咔吧,给 共享的资源 锁上。我先处理,然后你们等会儿,等我处理完了,你们再去处理。呦西 ~
常见的线程锁又有:互斥锁(Mutex)和信号量(Semaphore)操作系统的知识
- 互斥锁用于保护共享资源,确保同一时刻只有一个线程可以访问(也就是操作的原子性)
- 信号量除了可以用于同步外,还可以用于控制对共享资源的访问数量。
第七章·synchronized 与 锁
7.0 Java 中的锁
在 Java 中,每个对象 其实都有其相关联的 监视器锁(也称为内置锁或对象锁,其实本质就是 互斥锁!)
这个锁是由Java虚拟机(JVM)自动提供的,用于实现对象级别的同步。
Java 提供了 synchronized
关键字的两种方式:
- synchronized 方法
- synchronized 代码块
synchronized 这个玩意,实际上就是在 操作对象的监视器锁,不同对象的监视器锁是独立的!因此它们之间互不影响。当一个线程 碰到了一个 使用了 synchronized
关键字修饰的方法 或 代码块时,它就 必须先要 拿到 对象的 监视器锁,其它的线程此时就处于阻塞状态 等待着该线程 把 释放锁。如果你尝试获取到这个锁,那肯定会阻塞!因为这个锁已经被占用了。
线程锁可能会面临的问题:
- 在多个线程竞争下,加锁和释放锁,会导致比较 频繁的切换 和 调度延时,这会 引起 性能问题。
- 如果一个 优先级高的线程 去等待一个 优先级 低的线程 去释放锁,那么这就演化成了 优先级的 倒置问题!是性能的 大问题。
这就说明了 “鱼和熊掌” 二者不能兼得 的道理。你想要性能 那么数据在大概率情况下,是避免不了危险的。你想要安全,那么性能在大概率情况下也是避免不了变慢的。
7.1 synchronized 同步方法
为什么只有方法和方法快呢?
答:因为面向对象推荐,对象的属性最好用 private 封装,然后 用公开的方法 去对其进行操作。所以我们只需要让 synchronized 关键字 作用于 方法和方法快就可以了!
synchronized 修饰的方法,会默认的把 调用该方法的对象载体的同步监视锁拿到。(获取的方法很简单,让这个对象 作为 同步监视器就完事了)
弊端
:
- 锁的粒度较大:如果整个方法都被
synchronized
修饰,即使其中只有一小部分代码需要同步,那么整个方法体的代码也都会被锁住。这可能导致性能瓶颈,因为只有真正需要同步的部分才应该时被锁住的。 - 无法中断:一旦线程进入
synchronized
方法,其他线程必须等待该线程执行完毕才能获取锁。这意味着,如果一个线程在等待锁的过程中被中断,它会一直等待下去,直到再次获取到锁或者被其他方式唤醒。这就是个隐藏的炸弹了~ - 无法设置超时:
synchronized
方法无法设置超时期限,如果一个线程试图获取锁时不能在指定的时间内获取到,就无法进行其他处理。 - 难以适应复杂的同步需求:使用
synchronized
方法,只能用于基本的同步需求。对于更复杂的同步需求,例如支持多个条件,可能需要使用更灵活的同步工具,比如java.util.concurrent
包中的ReentrantLock
。 - 锁的对象 还有很大的局限性,有的时候为了符合逻辑,我们方法的对象可能并不是那个共享的资源。这就导致了 我们没法将其上锁。。
总体来说:synchronized
只能算是最简单的同步方法之一
public class 同步方法 {
public static void main(String[] args) {
Numnum num = new Numnum();
new Thread(num,"A").start();
new Thread(num,"B").start();
}
}
class Numnum implements Runnable {
private int num = 100;
@Override
public synchronized void run() { // 给方法 加 synchronized 那么调用它的 对象就能拿到 锁
while(num > 0)
{
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println(Thread.currentThread().getName() + "-->" + (num--));
}
}
}
你会发现,A 和 B 都是这样子的处理,但是 run 方法 加了 synchronized
关键字,这也就意味着 当 A 拿到锁之后,如果不处理完 run 方法,它时不会释放锁的。而等 A 执行完 run 之后,num 也等于 0 了。则 B 不会 去 num -- 直接就 完事了。
当然了,我们也有 根本 拿不到 正确锁的情况,如下:
public class 竞态条件 {
// 共享计数器
private static int counter = 0;
synchronized public static void main(String[] args) throws InterruptedException {
// 创建两个线程,分别定义线程的执行逻辑
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
// 读取计数器的值
int value = counter;
// 执行递增操作
value = value + 1;
// 将结果写回计数器
counter = value;
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
// 读取计数器的值
int value = counter;
// 执行递增操作
value = value + 1;
// 将结果写回计数器
counter = value;
}
});
// 启动两个线程
thread1.start();
thread2.start();
// 等待两个线程执行完成
thread1.join();
thread2.join();
// 显示计数器的最终值
System.out.println("最终计数器值: " + counter);
}
}
7.2 synchronized 代码块(常用)
synchronized 代码块 就是为了解决 获取正确对象锁的问题而存在的!
同步块:synchronized(Obj){处理事务的代码} /* 待锁的对象 */
Obj 可以是任何对象,但是推荐 使用共享的那块儿资源作为同步监视器
public class 同步代码块 {
// 共享计数器
private static int counter = 0;
public static void main(String[] args) throws InterruptedException {
// 创建两个线程,分别定义线程的执行逻辑
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
synchronized(同步代码块.class){
// 读取计数器的值
int value = counter;
// 执行递增操作
value = value + 1;
// 将结果写回计数器
counter = value;
}
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
synchronized(同步代码块.class){
// 读取计数器的值
int value = counter;
// 执行递增操作
value = value + 1;
// 将结果写回计数器
counter = value;
}
}
});
// 启动两个线程
thread1.start();
thread2.start();
// 等待两个线程执行完成
thread1.join();
thread2.join();
// 显示计数器的最终值
System.out.println("最终计数器值: " + counter);
}
}
【作业】:如何在使用 synchronized 方法的情况下,也把上述的例子 解决冲突?
7.3 悲观锁和乐观锁
synchronized 采用的是 悲观锁策略
悲观锁和乐观锁都属于并发控制的机制,即悲观锁和乐观锁是并发控制的两种不同策略。
- 悲观锁:
- 悲观锁的基本思想是,每次访问共享资源之前,都会先获取锁,确保在同一时刻只有一个线程能够访问共享资源。
- 典型的悲观锁的实现是通过使用互斥锁(例如,Java中的
synchronized
关键字或ReentrantLock
类)来确保同一时刻只有一个线程能够执行关键代码段。
- 乐观锁:
- 乐观锁的基本思想是,认为在大多数情况下,对共享资源的访问是不会发生冲突的。因此,它不会使用互斥锁,而是采用一种更轻量级的方式。
- 典型的乐观锁的实现是通过版本号(Versioning)或时间戳(Timestamp)等机制来进行控制。每个线程在访问共享资源之前,先获取版本号,然后在修改时检查版本号是否被其他线程修改过。
悲观锁 的开销肯定是大的,但绝对安全!并且,尽管乐观锁减少了互斥锁的开销,但需要额外的机制来处理冲突。如果并发冲突的概率较高,那么乐观锁的效果可能不如悲观锁。
7.4 死锁现象
7.4.1 死锁的理解
多个线程各自占有一些共享资源,并且互相等待着其它线程占有资源使用释放锁,才能运行,而导致两个或者多个线程都在等待对方释放锁,最后会出现都阻塞状态的情形(假死)。某一个同步块 同时拥有 “两个以上对象的锁” 时,就可能会发生 "死锁"问题 。
举例:两个线程互相 想操作对方的资源,但是双方都不想给,就形成了死锁
原因:时间切换频率太快,可能两个线程 一下子直接就获取了双方 的锁
然后造成了 双阻塞。这样就会造成 假死!JVM 会 自动终止程序! 这就是死锁现象
7.4.2 导致死锁的原因
-
循环等待(Circular Wait):
- 多个线程形成一个循环等待的环路,每个线程都在等待其他线程持有的资源,导致所有线程都无法继续执行。
-
互斥条件(Mutual Exclusion):
- 线程请求的资源具有互斥性,即一次只能被一个线程占用。如果一个线程持有某个资源,其他线程必须等待,这可能导致死锁。
-
持有并等待(Hold and Wait):
- 一个线程持有某个资源的同时等待另一个线程持有的资源,而其他线程可能在等待第一个线程持有的资源,形成相互等待。这种也是比较常见的
-
不可剥夺条件(Non-preemption):
- 已经被一个线程持有的资源不能被其他线程抢占,只能由持有它的线程主动释放。如果线程持有资源并且在等待其他资源时无法释放已经持有的资源,可能导致死锁。
避免死锁的关键在于破坏上述条件之一或多个。一些常见的预防死锁的方法包括:
-
按序获取锁:确保所有线程按照相同的顺序获取锁,从而避免循环等待。
-
使用超时机制:在获取锁的时候设置一个超时,如果超过一定时间未能获取到锁,就放弃或者尝试其他处理。是解决不可剥夺条件的一个方法
-
一次性获取所有需要的资源(资源分配图方法):一种有效的方法是在开始执行之前,一次性获取所有需要的资源。如果无法获取到所有资源,线程可以释放已经获取的资源,并等待重新获取所有资源。这种方法可以防止"持有并等待"的情况。
public class 死锁 {
static String s1 = "线程1的资源";
static String s2 = "线程2的资源";
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
synchronized (s1){
System.out.println("我是线程1 => 我有" + s1);
synchronized (s2){
System.out.println("我是线程1 => 想要拿到" + s2);
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (s2){
System.out.println("我是线程2 => 我有" + s2);
synchronized (s1){
System.out.println("我是线程2 => 想要拿到" + s1);
}
}
});
thread1.start();
thread2.start();
}
}
不就是因为 同步锁里面又嵌套了一个同步锁吗。
7.4.2 检测死锁
-
打开终端,输入
jps
就会显示当前运行的所有 Java 程序的 进程 ID
-
然后找到 对应程序的 ID 直接输入
jstack 进程ID
就完事了
-
也可以 输入
jconsole
打开检测控制台
其实这种方式是更加方便的。
这种方式,更多的是去远程 检测 部署在 服务器 上的 Java 程序!
第八章·关于一些并行操作
8.1 parallelStream() 并行流
所有的集合类都可以 集合对象.parallelStream()
来获得并行流,它的作用是 启用多线程来 辅助我们进行一些 关于 流的操作。这样可以更加的高效!
public static void main(String[] args) {
List<Integer> list = new ArrayList<>(Arrays.asList(1, 4, 5, 2, 9, 3, 6, 0));
list
.parallelStream() //获得并行流
.forEach(i -> System.out.println(Thread.currentThread().getName()+" -> "+i));
}
forEachOrdered()
:
- 如果流具有已定义的冲突顺序方法,则以该流的冲突顺序方法为该流的每个元素作为使用者(
Consumer
)执行操作。 - 这是终端操作。
- 这种方法保证了在顺序流和并行流中都按顺序执行。
public static void main(String[] args) {
List<Integer> list = new ArrayList<>(Arrays.asList(1, 4, 5, 2, 9, 3, 6, 0));
list
.parallelStream() //获得并行流
.forEachOrdered(System.out::println);
}
8.2 Arrays 数组工具类 中的并行方法
parallelSort
方法:
- 用于并行地对数组进行排序。该方法使用 ForkJoinPool 来充分利用多核处理器进行排序操作,提高排序性能。
// 例子:并行排序
int[] array = {4, 2, 7, 1, 9, 5};
Arrays.parallelSort(array);
parallelPrefix
方法:
- 用于对数组进行并行的前缀累加操作。例如,将数组中的每个元素替换为该元素及其前面所有元素的和。
// 例子:并行前缀累加
int[] array = {1, 2, 3, 4, 5};
Arrays.parallelPrefix(array, (x, y) -> x + y);
parallelSetAll
方法:
- 用于使用并行计算设置数组的元素值。该方法接受一个
IntUnaryOperator
函数,该函数用于计算数组中每个索引位置的元素值。
// 例子:并行设置数组元素值
int[] array = new int[5];
Arrays.parallelSetAll(array, i -> i * i);
parallelForEach
方法:
- 用于对数组中的每个元素执行给定的操作。该方法接受一个
Consumer
函数,该函数对数组中的每个元素执行操作。
// 例子:并行遍历数组并执行操作
int[] array = {1, 2, 3, 4, 5};
Arrays.parallelForEach(array, System.out::println);
8.3 集合类与多线程之间的问题
由于 集合类并没有考虑到 与多线程联用的情况,所以 不建议使用 之前学习的集合类 与 多线程进行 紧密的配合。因为它们线程是不安全的,当然,如果要非得用,那你就得自己 解决 并发问题了。
8.3.1 老的集合类(但线程安全)
- Vector 是在 Java 早期的时候就有的,它们的线程是安全的,可以 随意的与 多线程搭配使用。
- Hashtable<Key的类型, Val 类型> 可以 代替 Map 作为线程安全的 集合类使用。
这俩集合类里面的方法都加了 锁,所以当然 我们可以 频繁的与 多线程一起联用了。
只不过呢,Java 也考虑到了这种情况,所以 它也发明了很多 专门勇于并发编程的集合类,这个我们现在暂时不做讲解。
第九章·生产者与消费者问题
这个问题就是老生常谈的问题了,我相信很多 讲课的,或者学习 多线程的,最后都会 去 练习这个问题!
问题描述:如果说存在n个生产者在工厂里生产产品,和m个消费者 去进行 产品的消耗,那么如何保证 消费者在消耗时,必须 产品数量 > 0,而 生产者在生产时 是否可以限制一定的上限 循循渐进的生产。
提示:可能你需要一个 第三方的容器,用来做中介,可以更好的解决这个问题!
这个容器,可以帮助你 存储产品信息和记录 产品的数量!符合面向对象编程思想
这个容器,最好是拥有 生产和消费方法,这样的话 我们 直接就可以在这两个方法上 用 synchorized 很方便的解决并发问题中的 竞态条件!
import java.util.LinkedList;
// 我们自己新建一个 共享缓冲区的 类,里面封装 实际容器 和 容器大小
// 有点儿类似于 超级数组,是吧 ~
class SharedBuffer {
private LinkedList<Integer> buffer = new LinkedList<>();
private int capacity;
public SharedBuffer(int capacity) {
this.capacity = capacity;
}
synchronized public void produce(int item) throws InterruptedException {
while (buffer.size() == capacity) {
System.out.println("Buffer is full. Producer is waiting...");
wait();
}
buffer.add(item);
System.out.println("Produced: " + item);
// 通知消费者可以消费了
notify();
}
synchronized public int consume() throws InterruptedException {
while (buffer.isEmpty()) {
System.out.println("Buffer is empty. Consumer is waiting...");
wait();
}
int item = buffer.removeFirst();
System.out.println("Consumed: " + item);
// 通知生产者可以生产了
notify();
return item;
}
}
class Producer implements Runnable {
private SharedBuffer buffer;
public Producer(SharedBuffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try {
buffer.produce(i);
Thread.sleep(1000); // 模拟 1秒 生产一个 产品
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生产者 执行任务完毕!");
}
}
class Consumer implements Runnable {
private SharedBuffer buffer;
public Consumer(SharedBuffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try {
int item = buffer.consume();
Thread.sleep(3000); // 模拟 3秒 消耗一个 产品
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费者 执行任务完毕!");
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
SharedBuffer buffer = new SharedBuffer(2);
Thread producerThread = new Thread(new Producer(buffer));
Thread consumerThread = new Thread(new Consumer(buffer));
producerThread.start();
consumerThread.start();
}
}
我们在这里 可以 调节 消费者和生产者 消费和生产的 速率,即 sleep() 延迟时间 就可以模拟处理,然后我们就可以观察到 想要观察的任何现象。
第二种写法:
import java.util.LinkedList;
class Producer implements Runnable {
private LinkedList<Integer> list;
private int maxSize;
public Producer(LinkedList<Integer> list, int maxSize) {
this.list = list;
this.maxSize = maxSize;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try {
produce(i);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生产者 执行任务完毕!");
}
public void produce(int item) {
synchronized (this.list) {
while (this.list.size() == maxSize) {
System.out.println("容器已满,请稍后再生产!");
try {
this.list.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
this.list.add(item);
System.out.println("Produced: " + item);
this.list.notify();
}
}
}
class Consumer implements Runnable {
private LinkedList<Integer> list;
public Consumer(LinkedList<Integer> list) {
this.list = list;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
try {
int item = consume(i);
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费者 执行任务完毕!");
}
public int consume(int item) {
synchronized (this.list) {
while (this.list.size() == 0) {
System.out.println("产品没了,别消费了!");
try {
this.list.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
int consumedItem = this.list.removeFirst();
System.out.println("Consumed: " + consumedItem);
this.list.notify();
return consumedItem;
}
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
LinkedList<Integer> list = new LinkedList<>();
Thread producerThread = new Thread(new Producer(list, 2));
Thread consumerThread = new Thread(new Consumer(list));
producerThread.start();
consumerThread.start();
}
}