【JavaEE多线程】线程安全、锁机制及线程间通信

这篇具有很好参考价值的文章主要介绍了【JavaEE多线程】线程安全、锁机制及线程间通信。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


线程安全

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

线程安全问题的原因

  1. [根本原因]多个线程之间的调度顺序是“随机”的,操作系统使用“抢占式”执行的策略来调度线程

  2. 多个线程同时修改同一个遍历,容易产生线程安全问题

    • 3个条件
    1. 多个
    2. 修改
    3. 同一个
  3. 进行的修改,不是“原子的”,如果修改操作能按照原子的形式完成,就不会有线程安全问题(原子,即不可再分)

  4. 内存可见性,引起的线程安全问题

  5. 指令重排序,引起的线程安全问题

  • 以上五个原因,只有第3个原因能想办法修正
  • 通过“加锁”的方式,把一组操作给打包成一个“原子”的操作。此处的原子,就是通过锁,进行“互斥”,我这个线程工作的时候,其他线程无法工作
  1. 原子性:

    什么是原子性

    我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。

    那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。

    有时也把这个现象叫做同步互斥,表示操作是互相排斥的。

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

  2. 可见性: 一个线程对共享变量值的修改,能够及时地被其他线程看到

  3. 代码顺序性:

    什么是代码重排序

    一段代码是这样的:

    1. 去前台取下 U 盘
    2. 去教室写 10 分钟作业
    3. 去前台取下快递

​ 如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做指令重排序

编译器对于指令重排序的前提是 “保持逻辑不发生变化”. 这一点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.

synchronized 关键字-监视器锁monitor lock

代码中的锁就是让多个线程,同一时刻,只有一个线程能使用这个变量

synchronized的特性

互斥

synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.

  • 进入 synchronized 修饰的代码块, 相当于 加锁
  • 退出 synchronized 修饰的代码块, 相当于 解锁
//进入就针对当前对象“加锁”
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 修饰的变量, 能够保证 “内存可见性”.

【JavaEE多线程】线程安全、锁机制及线程间通信,JavaEE多线程,java-ee,安全,java

代码在写入 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()方法

wait在执行的时候,会做三件事:

  1. 解锁。object.wait,就会尝试针对object对象解锁
  2. 阻塞等待
  3. 当被其他线程唤醒之后,就会尝试重新加锁,加锁成功,wait执行完毕,继续往下执行其他逻辑。

wait要解锁前提是先能加上锁

  • 核心解决思路:先加锁,在synchronized里头再wait,这样子的wait就会一直阻塞到其他线程进行notify了

注意事项

  1. 要想让notify能够顺利唤醒wait,就需要确保wait和notify都是使用同一个对象调用的。
  2. wait和notify都需要放到synchronized之内的。虽然notify不涉及“解锁操作”,但是Java也强制要求notify要放到synchronized中。(系统的原生api中就没有这个要求)
  3. 如果进行notify的时候,另一个线程并没有处于wait状态,此时,notify相当于“空打一炮”,不会有任何副作用

代码示例: 观察wait()方法使用

public static void main(String[] args) throws InterruptedException {
    Object object = new Object();
    synchronized (object) {
        System.out.println("等待中");
        object.wait();
        System.out.println("等待结束");
   }
}

这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了另外一个方法唤醒的方法notify()

线程可能有多个,比如可以有n个线程进行wait一个线程负责notify,notify操作只会唤醒一个线程。具体是唤醒了哪个线程?是随机的!

wait和sleep的区别:

  • sleep有个明确的时间,到达时间自然就会被唤醒,也能提前唤醒,使用interrupt
  • wait默认是个死等,一直等到其他线程notify,wait也能被interrupt提前唤醒

notify()方法

notify 方法是唤醒等待的线程.

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

代码示例: 使用notify()方法唤醒线程

  • 创建 WaitTask 类, 对应一个线程, run 内部循环调用 wait.
  • 创建 NotifyTask 类, 对应另一个线程, 在 run 内部调用一次 notify
  • 注意:WaitTask 和 NotifyTask 内部持有同一个 Object locker. WaitTask 和 NotifyTask 要想配合就需要搭配同一个 Object.
static class WaitTask implements Runnable {
    private Object locker;
    public WaitTask(Object locker) {
        this.locker = locker;
    }
    @Override
    public void run() {
        synchronized (locker) {
            while (true) {
                try {
                    System.out.println("wait 开始");
                    locker.wait();
                    System.out.println("wait 结束");
               } catch (InterruptedException e) {
                    e.printStackTrace();
               }
           }
       }
    }
}
static class NotifyTask implements Runnable {
    private Object locker;
    public NotifyTask(Object locker) {
        this.locker = locker;
    }
    @Override
    public void run() {
        synchronized (locker) {
            System.out.println("notify 开始");
            locker.notify();
            System.out.println("notify 结束");
       }
    }
}
public static void main(String[] args) throws InterruptedException {
    Object locker = new Object();
    Thread t1 = new Thread(new WaitTask(locker));
    Thread t2 = new Thread(new NotifyTask(locker));
    t1.start();
    Thread.sleep(1000);
    t2.start();
}

notifyAll()方法

notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.

如果就想唤醒某个指定的线程,就可以让不同的线程使用不同的对象来进行 wait,想唤醒谁,就可以使用对应的对象来notify

范例:修改 NotifyTask 中的 run 方法, 把 notify 替换成 notifyAll

public void run() {
    synchronized (locker) {
        System.out.println("notify 开始");
        locker.notifyAll();
        System.out.println("notify 结束");
   }
}

**注意:**虽然是同时唤醒 3 个线程, 但是这 3 个线程需要竞争锁. 所以并不是同时执行, 而仍然是有先有后的执行.

wait和sleep的区别(面试题):文章来源地址https://www.toymoban.com/news/detail-853304.html

  1. sleep有个明确的时间,到达时间自然就会被唤醒,也能提前唤醒,使用interrupt
  2. wait默认是个死等,一直等到其他线程notify,wait也能被interrupt提前唤醒
  3. wait 需要搭配 synchronized 使用,sleep 不需要.
  4. wait 是 Object 的方法 sleep 是 Thread 的静态方法.

到了这里,关于【JavaEE多线程】线程安全、锁机制及线程间通信的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【JavaEE基础学习打卡03】Java EE 平台有哪些内容?

    📜 本系列教程适用于Java Web初学者、爱好者,小白白。我们的天赋并不高,可贵在努力,坚持不放弃。坚信量最终引发质变,厚积薄发。 🚀 文中白话居多,尽量以小白视角呈现,帮助大家快速入门。 🎅 我是 蜗牛老师 ,之前网名是 Ongoing蜗牛 ,人如其名,干啥都慢,所以

    2024年02月12日
    浏览(47)
  • 【JavaEE基础学习打卡02】是时候了解Java EE了!

    📜 本系列教程适用于 Java Web 初学者、爱好者,小白白。我们的天赋并不高,可贵在努力,坚持不放弃。坚信量最终引发质变,厚积薄发。 🚀 文中白话居多,尽量以小白视角呈现,帮助大家快速入门。 🎅 我是 蜗牛老师 ,之前网名是 Ongoing蜗牛 ,人如其名,干啥都慢,所

    2024年02月12日
    浏览(48)
  • java 线程安全问题 三种线程同步方案 线程通信(了解)

    线程安全问题指的是,多个线程同时操作同一个共享资源的时候,可能会出现业务安全问题。 下面代码演示上述问题,先定义一个共享的账户类: 在定义一个取钱的线程类 最后,再写一个测试类,在测试类中创建两个线程对象 某个执行结果: 为了解决前面的线程安全问题,

    2024年02月09日
    浏览(45)
  • <JavaEE> TCP 的通信机制(四) -- 流量控制 和 拥塞控制

    目录 TCP的通信机制的核心特性 五、流量控制 1)什么是“流量控制”? 2)如何做到“流量控制”? 3)“流量控制”的作用 六、拥塞控制 1)什么是“拥塞控制”? 2)如何做到“拥塞控制”? 3)“拥塞控制”的作用 4)“流量控制”和“拥塞控制”下的窗口大小如何取值

    2024年02月03日
    浏览(42)
  • JavaEE 初阶篇-生产者与消费者模型(线程通信)

    🔥博客主页: 【 小扳_-CSDN博客】 ❤感谢大家点赞👍收藏⭐评论✍   文章目录         1.0 生产者与消费者模型概述         2.0 在生产者与消费者模型中涉及的关键概念         2.1 缓冲区         2.2 生产者         2.3 消费者         2.4 同步机制         2.5 线程间通

    2024年04月28日
    浏览(42)
  • JavaEE:多线程(2):线程状态,线程安全

    目录 线程状态 线程安全 线程不安全 加锁 互斥性 可重入  死锁 死锁的解决方法  Java标准库中线程安全类 内存可见性引起的线程安全问题 等待和通知机制 线程饿死 wait notify 就绪:线程随时可以去CPU上执行,也包含在CPU上执行的线程 阻塞:这个线程暂时不方便去CPU上执行

    2024年01月23日
    浏览(46)
  • 【JavaEE】HTTPS及其安全机制

    目录 1、什么是HTTPS  2、HTTPS的基本工作过程 2.1、使用对称密钥进行加密 2.2、使用非对称密钥进行加密 2.3、中间人攻击 2.4、证书 HTTPS是在HTTP协议的基础上引入了一个加密层(SSL) 。HTTP协议内容都是按照文本的方式传输的,这就导致在传输过程中会出现一些被篡改的情况。

    2024年02月16日
    浏览(22)
  • 【Java EE初阶六】多线程案例(单例模式)

            单例模式是一种设计模式,设计模式是我们必须要掌握的一个技能;         设计模式是软性的规定,且框架是硬性的规定,这些都是技术大佬已经设计好的;         一般来说设计模式有很多种,且不同的语言会有不同的设计模式,(同时 设计模式也可

    2024年02月03日
    浏览(42)
  • 【Java EE初阶八】多线程案例(计时器模型)

            计时器类似闹钟,有定时的功能,其主要是到时间就会执行某一操作,即可以指定时间,去执行某一逻辑(某一代码)。         在java标准库中,提供了Timer类,Timer类的核心方法是schedule( 里面包含两个参数,一个是要执行的任务代码,一个是设置多久之后

    2024年01月21日
    浏览(46)
  • JavaEE之多线程编程:4. 线程安全(重点!!!)

    下面我们来举个例子: 我们大家都知道,在单线程中,以下的代码100%是正确的。 但是,两个线程,并发的进行上述循环,此时逻辑可能就出现问题了。 上述这样的情况就是非常典型的线程安全问题。这种情况就是bug!! 只要实际结果和预期的结果不符合,就一定是bug。 想

    2024年01月25日
    浏览(42)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包