JavaWeb~多线程带来的风险(线程安全问题)--synchronized和volatile关键字的使用

  • 结果:

88477

  • 多次运行上述代码可以发现其结果并不都是10 0000 而是在50000~10 0000之间,这与我们认为的逻辑上发生冲突,这就是一个线程的安全问题

2.了解线程安全的概念

==========================================================================

  • 如果多线程环境下代码运行的结果符合我们逻辑上的预期的,即是在单线程环境下应该得到的结果,则我们就说这个线程是安全的

  • 如果多线程并发执行某个代码,有逻辑上的错误,那就是线程不安全

3.线程不安全的几大原因

===========================================================================

3.1线程是抢占式执行(不安全的万恶之源)


  • 线程之间的调度完全有内核负责,用户代码中是感知不到的,也无法控制的(如线程之间谁先执行,谁后执行谁执行到哪里从CPU上下来,这样的过程都是欧冠胡无法控制也无法感知的)

  • 所以如上述的increase让count++操作可以分为三步

  1. load 将内存中的数据读取到CPU中

  2. incr 将CPU中的数据++

  3. save 将结果保存到内存中

  • 当CPU执行到上面三个步骤任何一步的时候,都可能会被调度器调走,让给其他线程来执行,或者有多个CPU让修改同一个数据的俩个线程同时执行了, 这俩个都会造成线程不安全

  • 比如上述代码的 t 线程 读取数据,让数据++之后还未将数据保存在内存中, t1 线程就又一次读取了数据进行++操作,这样看似执行了俩次++操作,但最后往内存中保存数据的时候就只是执行了一次++的那个数据, 就造成了最后的结果不是10 0000原因

3.2不是原子性的


  • 如果有办法将上述三个load incr sava 操作变成一个整体,就是说只要执行了一次操作就必须把三个步骤都执行完, 才能让其他线程去执行这三个操作中的任何一个,这就是后面要说到的上锁操作.

  • 不保证原子性会给多线程带来什么问题?

如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。比如一开始那个例子

3.3多个线程尝试修改同一个变量


这是最经典也是肯定会出现线程不安全的 所以必须上锁

  • 有以下几个是情况即使不上锁是线程安全的
  1. 如果是一个线程修改一个变量 线程安全

  2. 如果是多个线程尝试去读取同一个变量 线程安全

  3. 如果是多个线程去修改不同的变量 也是线程安全

  • 但是 如果是多个线程一个读数据 一个修改数据 此时也可能导致线程不安全

3.4内存可见性导致的线程安全问题


  • 为了提高效率,JVM在执行过程中,会尽可能的将数据在工作内存中(也就是CPU)执行,但这样虽然提高了效率会造成一个问题,共享变量在多线程之间不能及时看到改变,这个就是可见性问题。

  • 最典型的就是一个线程读数据 一个线程写(修改)数据此时读数据的线程可能会将数据读到cpu中 然后会造成改数据的那个线程修改了线程但是没有让读数据的那个线程识别到 造成线程不安全 这就是内存可见性问题

  • 如下面代码

下面代码就是在线程 t 中一直是在获取count中的数据 所以编译器就对其进行优化 将count值一开始就读入CPU中 所以在此后线程 t1 对count值修改后 线程 t 并不能知道

public class ThreadTest {

static class Count {

public int count = 0;

public void increase() {

count++;

}

}

public static void main(String[] args) {

Count count = new Count();

Thread t = new Thread() {

@Override

public void run() {

while (count.count == 0) {

}

System.out.println(“线程t执行完毕”);

}

};

Thread t1 = new Thread() {

@Override

public void run() {

try {

Thread.sleep(1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(“进行count++”);

count.increase();

}

};

t.start();

t1.start();

}

}

3.4.1volatile的理解

  • volatile 是用来解决一个线程读 一个线程写的场景

  • 如果是俩个线程写数据 就需要用加锁来解决

  • 加了volatile之后,对这个内存的读取操作肯定是从内存中读取,不加的时候,读取操作就可能不是在内存中读取而是自己读取CPU上旧的数据

  • 原理

1)将当前处理器缓存行的数据写回到系统内存。

2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

3.4.2volatile的使用

  • 程序员想让哪个数据每次读取都得在内存中读取就在哪个数据的初始化前面加上volatile

在这里插入图片描述

3.5指令重排序导致多线程不安全


  • Java的编译器在编译代码时,会针对指令进行优化,调整指令的先后顺序,保存原有逻辑不变的情况下,提高程序的运行速率

  • 现代编译器优化能力很强,优化后的代码执行速率很快,单线程的情况下不会有问题, 但是在多线程的优化是不容易实现的,可能会导致出现问题

  • 比如下面的操作:

  1. 去校门口卖吃的

  2. 写作业

  3. 去校门口拿快递

  • 如果是单线程就会优化为1 -> 3 -> 2 方式执行 可以优化过程 这就是指令重排序

  • 但是如果实在多线程下就会出错 因为可能是在你写作业的时候, 快递才被送来 或者在你写作业的时候快递会被人修改一些东西的时候,如果此时进行了指令重排序 代码就会出错

3.6 总结volatile


  1. 保证修饰的数据线程可见,也就是写数据时将处理器中的缓存直接写入到内存中,并且使其他cpu中保存了改地址的数据无效

  2. 不保证原则性

  3. 可以依托操作系统的内存屏障防止这个数据的指令重排序(因为操作系统指令级别也会有指令优化,所以直接使用操作系统的内存屏障实现)。在有数据依赖关系上的指令是禁止重排序的,而synchronize保证同步代码块,所以即使出现指令重排序但是不会有异常出现。

  • 如下面代码:

public class ThreadTest2 {

private static class Counter {

private int n1 = 0;

private int n2 = 0;

private int n3 = 0;

private int n4 = 0;

private int n5 = 0;

private int n6 = 0;

private int n7 = 0;

private int n8 = 0;

private int n9 = 0;

private int n10 = 0;

public void write() {

n1 = 1;

n2 = 2;

n3 = 3;

n4 = 4;

n5 = 5;

n6 = 6;

n7 = 7;

n8 = 8;

n9 = 9;

n10 = 10;

}

public void read() {

System.out.println("n1 = " + n1);

System.out.println("n2 = " + n2);

System.out.println("n3 = " + n3);

System.out.println("n4 = " + n4);

System.out.println("n5 = " + n5);

System.out.println("n6 = " + n6);

System.out.println("n7 = " + n7);

System.out.println("n8 = " + n8);

System.out.println("n9 = " + n9);

System.out.println("n10 = " + n10);

}

}

public static void main(String[] args) {

Counter nums = new Counter();

Thread t = new Thread() {

@Override

public void run() {

nums.read();

}

};

Thread t1 = new Thread() {

@Override

public void run() {

nums.write();

}

};

t.start();

t1.start();

}

}

  • 测试结果:

n1 = 0

n2 = 0

n3 = 0

n4 = 4

n5 = 5

n6 = 6

n7 = 7

n8 = 8

n9 = 9

n10 = 10

4.如何解决线程不安全问题

============================================================================

  1. 改抢占式执行(这个有操作系统内核实现的检查调度的方式,无法解决)

  2. 将操作变为原子性的(这个可以实现 上锁就行 而且适用范围广)

  3. 不让多个线程修改同一个变量(这个能不能有办法不一定,因为得看具体需求)

4.1锁的特点


  • 锁的基本操作 加锁(获取锁)lock 解锁(释放锁) unlock

  • 互斥的 同一时刻只有一个线程能获取到同一把锁

  • 其他线程如果尝试获取同一把锁 就会发生阻塞等待

  • 一直等到某个线程释放了这把锁 此时剩下的想要这把锁的线程会重新竞争这把锁

4.2锁如何解决线程不安全问题?


public class ThreadTest {

static class Count {

public int count = 0;

synchronized public void increase() {

count++;

}

}

public static void main(String[] args) throws InterruptedException {

Count count = new Count();

Thread t = new Thread() {

@Override

public void run() {

for (int i = 0; i < 50000; i++) {

count.increase();

}

}

};

Thread t1 = new Thread() {

@Override

public void run() {

for (int i = 0; i < 50000; i++) {

count.increase();

}

}

};

t.start();

t1.start();

t.join();

t1.join();

System.out.println(count.count);

}

}

  • 如上述例子 在increase方法前加上锁(synchronized)
  1. 假设上述俩个线程 如果一开始 t 获取到锁把 锁 锁住了 此时线程 t1 爱去获取锁的时候就会发现锁已经锁死 t1 就会阻塞等待 直到 t 线程把锁释放了 线程2 才有可能获取到锁

  2. 有了锁 即使假如线程 t 执行了一半被调度走了 也没关系 因为 线程 t 还未释放锁 (一个线程只有把上锁的那部分代码全部执行完才会释放锁) 所以 t1 线程还是获取不到锁 这也就保证了上锁那部分代码的原子性

  • 还需要知道锁这个东西并不那么容易
  1. 使用的时候一定好主意正确的方式使用 不然会出现很多问题

  2. 一旦上锁 这个代码就和高性能无缘了 因为锁的等待时间是不可控制的 可能会等待很久很久

4.3理解synchronized(锁)


synchronized的底层是使用操作系统的mutex lock实现的。

  • 当线程释放锁时,JVM会把该线程对应的工作内存中的共享变量刷新到主内存中

  • 当线程获取锁时,JVM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量

synchronized用的锁是存在Java对象头里的。

  • synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入。

  • 加锁 相当于给当前对象加锁 (所谓加锁其实就是某个指定的对象来加锁) (因为在你new这个对象的时候,编译器会自动加一个对象头,这个对象头就包含一个具体的字段 这个字段就有一部分表示当前对象是不是一个加锁状态, 可以想象成一个Boolean 未加锁就是 false 加锁了就是 true)

  • 总而言之:

- 所以在加锁前必须明确给哪个对象加锁

4.4synchronized的使用


4.4.1synchronized 关键字写到普通方法前

  • 如下面例子:

当线程开始执行后进入method方法后 会锁test指向的对象中的, 出方法后 会释放test指向对象中的锁

public class ThreadTest4 {

static class Test {

synchronized public void methond() {

System.out.println(“haha”);

}

}

public static void main(String[] args) {

Test test = new Test();

Thread thread = new Thread() {

@Override

public void run() {

test.methond();

}

};

thread.start();

}

}

4.4.2synchronized写到静态方法前

表示锁当前类的类对象

类对象就是反射的实现依据, JVM运行时把.class文件加载到内存中获取的(类加载)

  • 如下面代码:

进入方法会锁 Test.class 指向对象中的锁;出方法会释放 Test.class 指向的对象中的锁

public class ThreadTest4 {

static class Test {

synchronized public void methon() {

System.out.println(“haha”);

}

}

public static void main(String[] args) {

Test test = new Test();

Thread thread = new Thread() {

@Override

public void run() {

test.methon();

}

};

thread.start();

}

}

4.4.3synchronized写到某个代码块之前

指定给某个对象加锁

  • 如下面代码

使用同一把锁 给俩个代码块上锁 如果线程 t 要输入一个数字(要进行IO操作)会进入阻塞 但是这个线程并没有把锁释放 那么线程 t1 也就不能正常运行了

import java.util.Scanner;

public class ThreadTest5 {

public static void main(String[] args) {

Object lock = new Object();

Thread t = new Thread() {

@Override

public void run() {

try {

Thread.sleep(5000);

} catch (InterruptedException e) {

e.printStackTrace();

}

synchronized (lock) {

System.out.println(“请输入一个数字:”);

Scanner scanner = new Scanner(System.in);

int num = scanner.nextInt();

System.out.println(“num” + “=” + num);

}

}

};

Thread t1 = new Thread() {

@Override

public void run() {

while (true) {

synchronized (lock) {

System.out.println(“我t1得到锁了”);

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数网络安全工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年网络安全全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上网络安全知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注网络安全获取)
img

写在最后

在结束之际,我想重申的是,学习并非如攀登险峻高峰,而是如滴水穿石般的持久累积。尤其当我们步入工作岗位之后,持之以恒的学习变得愈发不易,如同在茫茫大海中独自划舟,稍有松懈便可能被巨浪吞噬。然而,对于我们程序员而言,学习是生存之本,是我们在激烈市场竞争中立于不败之地的关键。一旦停止学习,我们便如同逆水行舟,不进则退,终将被时代的洪流所淘汰。因此,不断汲取新知识,不仅是对自己的提升,更是对自己的一份珍贵投资。让我们不断磨砺自己,与时代共同进步,书写属于我们的辉煌篇章。

需要完整版PDF学习资源私我

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img

12836696634)]
[外链图片转存中…(img-e54fjfdk-1712836696635)]
[外链图片转存中…(img-y4JyC4ee-1712836696635)]
[外链图片转存中…(img-U2RJNj9p-1712836696635)]
[外链图片转存中…(img-iJEuFVJZ-1712836696636)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上网络安全知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注网络安全获取)
[外链图片转存中…(img-svqtebSX-1712836696636)]

写在最后

在结束之际,我想重申的是,学习并非如攀登险峻高峰,而是如滴水穿石般的持久累积。尤其当我们步入工作岗位之后,持之以恒的学习变得愈发不易,如同在茫茫大海中独自划舟,稍有松懈便可能被巨浪吞噬。然而,对于我们程序员而言,学习是生存之本,是我们在激烈市场竞争中立于不败之地的关键。一旦停止学习,我们便如同逆水行舟,不进则退,终将被时代的洪流所淘汰。因此,不断汲取新知识,不仅是对自己的提升,更是对自己的一份珍贵投资。让我们不断磨砺自己,与时代共同进步,书写属于我们的辉煌篇章。

需要完整版PDF学习资源私我

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-lr3Sl4A8-1712836696636)]

  • 15
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值