多线程-初阶(1)

news2024/11/22 22:20:28
本节⽬标
认识多线程
掌握多线程程序的编写
掌握多线程的状态
掌握什么是线程不安全及解决思路
掌握 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 来创建⼀个线程类,
class MyThread extends Thread {
     @Override
     public void run() {
         System.out.println("这⾥是线程运⾏的代码");
     }
}
创建 MyThread 类的实例
MyThread t = new MyThread();
调⽤ start ⽅法启动线程
t.start(); // 线程开始运⾏
⽅法2 实现 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:匿名内部类,创建Thread子类

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

 方法5:lambda表达式

2. Thread 类及常⻅⽅法

 2.1 Thread 的常⻅构造⽅法

演示:

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 观察线程不安全 

比如说:现在让两个

代码:

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();
        // 预期结果应该是 10w
        System.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 解决之前的线程不安全问题

代码:

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();
        // 预期结果应该是 10w
        System.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) 修饰代码块: 明确指定锁哪个对象.

锁任意对象 ,顾名思义只要是一个对象就行,无论是什么对象。
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 锁的是什么. 两个线程竞争同⼀把锁, 才会产⽣阻塞等待.
两个线程分别尝试获取两把不同的锁, 不会产⽣竞争.

 

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

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

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

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

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

String

6. volatile 关键字

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

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

代码:

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保证可见性
保证内存可见性的目的是为了避免变量修改的时候被系统优化了。
代码:
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();
        // 预期结果应该是 10w
        System.out.println("count: " + count);
    }
}

结果:

好了今天就讲到这里。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2188679.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据在内存中的存储【上】

一.整型在内存中的存储 在讲解操作符的时候&#xff0c;我们就讲过了下面的内容&#xff1a; 整数的2进制表示方法有三种&#xff0c;即 原码、反码和补码 有符号的整数&#xff0c;三种表示方法均有符号位和数值位两部分&#xff0c;符号位都是用0表示"正"&#xff…

Java之队列

1. 概念 队列&#xff1a;只允许在一端进行插入数据操作&#xff0c;在另一端进行删除数据操作的特殊线性 特点&#xff1a; 队列具有先进先出FIFO(First In First Out) 入队列&#xff1a;进行插入操作的一端称为队尾&#xff08;Tail/Rear&#xff09; 出队列&#xff1a;进…

Pikachu-Sql-Inject - 基于时间的盲注

基于时间的盲注&#xff1a; 就是前端的基于time 的盲注&#xff0c;什么错误信息都看不到&#xff0c;但是还可以通过特定的输入&#xff0c;判断后台的执行时间&#xff0c;从而确定注入。 mysql 里函数sleep() 是延时的意思&#xff0c;sleep(10)就是数据库延时10 秒返回内…

【C++】异常处理

目录 一、C语言中传统的异常处理方式&#xff1a; 二、C中的异常处理方式&#xff1a; 三、异常的使用 1、关于抛出与捕获&#xff1a; 2、关于异常的抛出和匹配&#xff1a; 3、异常的重新抛出&#xff1a; 4、异常安全&#xff1a; 5、异常规范&#xff1a; 四、异常…

idea 同一个项目不同模块如何设置不同的jdk版本

在IntelliJ IDEA中&#xff0c;可以为同一个项目中的不同模块设置不同的JDK版本。这样做可以让你在同一个项目中同时使用多个Java版本&#xff0c;这对于需要兼容多个Java版本的开发非常有用。以下是设置步骤&#xff1a; 打开项目设置&#xff1a; 在IDEA中&#xff0c;打开你…

Git 下载及安装超详教程(2024)

操作环境&#xff1a;Win 10、全程联网 一、什么是Git&#xff1f; Git 是一个开源的分布式版本控制系统&#xff0c;由 Linus Torvalds 创立&#xff0c;用于有效、高速地处理从小到大的项目版本管理。Git 是目前世界上最流行的版本控制系统&#xff0c;被广泛用于软件开发中…

论文翻译 | Generated Knowledge Prompting for Commonsense Reasoning

摘要 整合外部知识是否有利于常识推理&#xff0c;同时保持预训练序列模型的灵活性&#xff0c;这仍然是一个悬而未决的问题。为了研究这个问题&#xff0c;我们开发了生成知识提示&#xff0c;它包括从语言模型生成知识&#xff0c;然后在回答问题时提供知识作为附加输入。我们…

TCP ---滑动窗口以及拥塞窗口

序言 在上一篇文章中我们介绍了 TCP 中的协议段格式&#xff0c;以及保证其可靠传输的重传机制&#xff0c;着重介绍了三次握手建立连接&#xff0c;四次挥手断开连接的过程(&#x1f449;点击查看)。  这只是 TCP 保证通信可信策略的一部分&#xff0c;现在让我们继续深入吧&…

盲拍合约:让竞拍更公平与神秘的创新解决方案

目录 前言 一、盲拍合约是什么&#xff1f; 二、盲拍合约工作原理 1、合约创建与初始化 2、用户出价&#xff08;Bid&#xff09; 3、出价结束 4、披露出价&#xff08;Reveal&#xff09; 5、处理最高出价 6、结束拍卖 7、退款与提款 三、解析盲拍合约代码…

02:(寄存器开发)流水灯/按键控制LED

寄存器开发 1、LED流水灯2、按键控制LED 1、LED流水灯 通过第一章的学习&#xff0c;我们已然知晓了LED的点亮和熄灭的方式&#xff0c;下面学习流水灯的制作流程。 流水灯呈现的样子&#xff1a;就是第一个LED灯点亮&#xff0c;延迟一段时间&#xff0c;第一个LED灯熄灭第二…

2020大厂web前端面试常见问题总结

本篇收录了一些面试中经常会遇到的经典面试题以及自己面试过程中遇到的一些问题。通过对本篇知识的整理以及经验的总结&#xff0c;希望能帮到更多的前端面试者。 1.web前端项目的结构是怎样的&#xff1f;文件有哪些命名规范&#xff1f; 项目结构规范 页面文件&#xff1a;以…

树莓派5:换源(针对Debian12)+安装包管理器Archiconda(图文教程+详细+对初学者超级友好)

目录 一、安装官方发行版系统&#xff08;Debian&#xff09;二、换源&#xff08;记得参考上述教程ssh连接到树莓派Terminal&#xff0c;or外接一块Hdmi显示屏&#xff09;2.1 查看自己安装的树莓派镜像架构2.2 查询自己的系统版本2.3 打开清华大学开源软件镜像站网站2.3.1 传…

浅析Golang的Context

文章目录 1. 简介2. 常见用法2.1 控制goroutine的生命周期&#xff08;cancel&#xff09;2.2 传递超时&#xff08;Timeout&#xff09;信息2.3 传递截止时间&#xff08;Deadline&#xff09;2.4 传递请求范围内的全局数据 &#xff08;value&#xff09; 3 特点3.1 上下文的…

FWA(固定无线接入),CPE(客户终端设备)简介

文章目录 FWA&#xff08;Fixed Wireless Access&#xff09;&#xff0c;固定无线接入CPE&#xff08;Customer Premise Equipment&#xff09;&#xff0c;用户驻地设备 FWA&#xff08;Fixed Wireless Access&#xff09;&#xff0c;固定无线接入 固定无线接入&#xff08…

Geogebra009—构建正六边形

继续巩固一下基础&#xff0c;本篇我们来做一个正六边形 目录 一、成品展示二、涉及内容三、做图步骤1. 绘制一个以A点为圆心过B点的圆circle1&#xff1b;2. 以B点为圆心过A点绘制另外一个圆circle2&#xff1b;3. 绘制两个圆的交点&#xff0c;得到顶点C和D&#xff1b;4. 以…

Leetcode: 0001-0010题速览

Leetcode: 0001-0010题速览 本文材料来自于LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer&#xff08;第 2 版&#xff09;》、《程序员面试金典&#xff08;第 6 版&#xff09;》题解 遵从开源协议为知识共享 版权归属-相同方式…

奔驰GLS450升级原厂电吸门效果怎么样

奔驰GLS450升级原厂电吸门后&#xff0c;能带来以下效果&#xff1a; • 关门更优雅&#xff1a;只需轻轻推车门到基本关闭的位置&#xff0c;当车门距离车门锁大约6毫米时&#xff0c;传感器便会启动电动马达将车门安静地拉入&#xff0c;然后固定住&#xff0c;告别传统关门…

HTML+CSS基础用法介绍五

目录&#xff1a; 结构伪类选择器盒子模型-边框线盒子模型-内边距盒子模型-解决盒子被撑大盒子模型-外边距与版心居中小知识&#xff1a;清除浏览器中所有标签的默认样式内容溢出控制显示方式盒子模型-圆角 &#x1f40e;正片开始 结构伪类选择器 什么是结构伪类选择器&…

18.安卓逆向-frida基础-调试实战2

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a;图灵Python学院 本人写的内容纯属胡编乱造&#xff0c;全都是合成造假&#xff0c;仅仅只是为了娱乐&#xff0c;请不要盲目相信。 工…