2024年最新【JavaEE多线程】线程安全、锁机制及线程间通信(1),【面试必会】

学习路线:

这个方向初期比较容易入门一些,掌握一些基本技术,拿起各种现成的工具就可以开黑了。不过,要想从脚本小子变成黑客大神,这个方向越往后,需要学习和掌握的东西就会越来越多以下是网络渗透需要学习的内容:
在这里插入图片描述

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以点击这里获取

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

//进入就针对当前对象“加锁”
synchronized public void increase(){
    count++;
}
//出来就针对当前对象“解锁”

synchronized关键字最主要有以下3种应用方式:

  • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁。(这里可以给实例对象的名称Test test=new Test()test,也可以给this对象代表当前实例,也可以给当前类的class对象作为锁)
  • 思考:通过加锁操作之后把并发执行=>串行执行了,此时多线程还有存在的意义吗?
  • 答:因为两个线程,可能有一部分代码是串行执行的,有一部分是并发执行的=>这仍然比纯粹的串行执行效率要高!

理解 “阻塞等待”.

针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁.

注意:

  • 上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这也就是操作系统线程调度的一部分工作.
  • 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则

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

synchronized进行加锁解锁,其实是以“对象”为维度进行展开的。

加锁目的是为了互斥使用资源。(互斥的修改变量)

使用synchronized的时候,其实是指定了某个具体的对象进行加锁,当synchronized直接修饰方法时,此时就相当于是针对this加锁(修饰方法相当于这段代码的简化写法)[不存在所谓的“同步方法”的概念]

class Counter{
    public int count=0;
    public void increace(){
        synchronized (this){//this就是下面调用的counter
            count++;
        }
    }
    public void increace2(){
        count++;
    }
    public synchronized static void func(){
        synchronized (Counter.class){

        }
    }
}
public class Demo {
    public static void main(String[] args) throws InterruptedException {
        Counter counter=new Counter();

        Thread t1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increace();//这里t1的counter和下面t2的counter进行锁竞争/锁冲突
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increace();//这里t2的counter和上面t1的counter进行锁竞争/锁冲突
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(counter.count);
    }
}

如果是两个线程针对同一个对象进行加锁,就会出现锁竞争/锁冲突一个线程能加锁成功,另一个线程阻塞等待

如果是两个线程针对不同对象进行加锁,就不会出现锁竞争/锁冲突,也就不存在阻塞等待的操作了

因此具体针对哪个对象加锁不重要,重要的是两个线程,是不是针对同一个对象加锁

  • 思考:如果接下来的代码里,一个线程加锁了,一个线程没加锁,此时是否还会存在线程安全问题
  • 答:单方面加锁等于没加锁,必须得多个线程都对同一个对象加锁,才有意义

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

synchronized有且只有一条规则:

当两个线程针对同一个对象加锁的时候,就会出现锁竞争/锁冲突。一个线程能先拿到锁,另一个线程就会阻塞等待(BLOCKED)。直到第一个线程释放了锁之后,第二个线程才可能获取到锁,才能继续往下执行。

刷新内存

synchronized 的工作过程:

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁
可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题

理解 "把自己锁死"

一个线程没有释放锁, 然后又尝试再次加锁.

// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待. 
lock();

按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无法进行解锁操作. 这时候就会 死锁.

这样的锁称为 不可重入锁.

Java 中的 synchronized 是 可重入锁, 因此没有上面的问题

代码示例

在下面的代码中,

  • increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前对象加锁的.
  • 在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释放, 相当于连续加两次锁)

这个代码是完全没问题的. 因为 synchronized 是可重入锁.

static class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
   }
    synchronized void increase2() {
        increase();
   }
}

在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息.

  • 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
  • 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)
synchronized使用范例

synchronized 本质上要修改指定对象的 “对象头”. 从使用角度来看, synchronized 也势必要搭配一个具体的对象来使用.

  1. 直接修饰普通方法:锁的 SynchronizedDemo 对象
public class SynchronizedDemo {
    public synchronized void methond() {
    }
}

  1. 修饰静态方法: 锁的 SynchronizedDemo 类的对象
public class SynchronizedDemo {
    public synchronized static void method() {
    }
}

  1. 修饰代码块: 明确指定锁哪个对象.

锁当前对象

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {//Test test=new Test()的test也行
            
       }
    }
}

锁类对象

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

volatile

volatile能保证内存可见性

volatile 修饰的变量, 能够保证 “内存可见性”.

在这里插入图片描述

代码在写入 volatile 修饰的变量的时候,

  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存

代码在读取 volatile 修饰的变量的时候,

  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本

代码示例

在这个代码中

  • 创建两个线程 t1 和 t2
  • t1 中包含一个循环, 这个循环以 flag == 0 为循环条件.
  • t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.
  • 预期当用户输入非 0 的值的时候, t1 线程结束.
static class Counter {
    public int flag = 0;
}
public static void main(String[] args) {
    Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        while (counter.flag == 0) {
            // do nothing
       }
        System.out.println("循环结束!");
   });
    Thread t2 = new Thread(() -> {
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入一个整数:");
        counter.flag = scanner.nextInt();
   });
    t1.start();
    t2.start();
}
// 执行效果
// 当用户输入非0值时, t1 线程循环不会结束. (这显然是一个 bug)
// 注意:这里直接在控制台是看不出的

t1 读的是自己工作内存中的内容.

当 t2 对 flag 变量进行修改, 此时 t1 感知不到 flag 的变化.

如果给 flag 加上 volatile

static class Counter {
    public volatile int flag = 0;
}
// 执行效果
// 当用户输入非0值时, t1 线程循环能够立即结束.
// 注意:这里直接在控制台同样是看不出的

volatile不保证原子性

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见性.

synchronized 也能保证内存可见性

synchronized 既能保证原子性, 也能保证内存可见性.

内存可见性问题:

  1. 编译器优化
  2. 内存模型
  3. 多线程
  • volatile保证的是内存可见性,不是原子性

内存可见性加锁描述了线程安全问题的典型情况和处理方式

wait 和 notify

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知.

但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.

完成这个协调工作, 主要涉及到三个方法 :

  • wait() / wait(long timeout): 让当前线程进入等待状态.
  • notify() / notifyAll(): 唤醒在当前对象上等待的线程.

注意: wait, notify, notifyAll 都是 Object 类的方法.

wait(等待)和notify(通知)就是一个用来协调线程顺序的重要工具

这两个方法都是Object提供的方法,随便找个对象都可以调用

当wait引起线程阻塞之后,可以使用interrupt方法把线程唤醒,打断当前线程的阻塞状态

wait()方法

给大家的福利

零基础入门

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

同时每个成长路线对应的板块都有配套的视频提供:

在这里插入图片描述

因篇幅有限,仅展示部分资料

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以点击这里获取

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值