Java多线程(二)线程安全

线程安全

线程安全(风险)

线程不安全的原因:

解决线程不安全:

synchronized

内存刷新

可重入

[volatile 关键字](#volatile 关键字)

[wait 和 notify](#wait 和 notify)

wait()

[notify ()](#notify ())

wait与sleep的区别:


线程安全(风险)

某个代码在多线程的环境下执行,然后出现bug,其本质原因在于线程调度是不确定的。

比如:(代码有问题)

java 复制代码
public class test3 {
     static int count=0;
    public   static void sum(){
        count++;
    }
    public static void main(String[] args) throws InterruptedException {
        long time= System.nanoTime();
        Thread t1=new Thread(()->{
                for (int i = 0; i < 10000; i++) {
                    sum();
                }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 10000; i++) {
                sum();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);

}

我们会发现这个代码出现了一个问题,与我们想要的预期不一致。即出现bug

其本质是,count++操作,本质是有三个CPU指令构成

1.load,把内存中的数据读到cpu寄存器中。

2.add,就是把寄存器中的值,进行+1操作

3.save,把寄存器中值写回内存中。

大家肯定学过数学,那么对于组合,肯定是有过了解的。那么我问个问题,现在线程有两个,分别对count进行++操作。对于cpu来说有几种组合方式?3*3共有9种,那么问题来了,我们只要唯一的结果,不需要这么结果可能。

这个是我随便选择的两种情况,画出来的。箭头向下,表示时间的执行顺序。可以看到,第一个,t1的load执行后,t2的load开始执行了,然后执行t2的add操作,和save操作,再然后才执行t1的add操作和save操作。所以出现了不同的结果。

  • 线程之间的共享变量存在 主内存 (Main Memory).
  • 每一个线程都有自己的 "工作内存" (Working Memory) .
  • 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
  • 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.

其实在进一步理解,可以理解为,两个线程对同一个变量,进行了相互作用。

线程不安全的原因:

1.抢占式执行(大部分的原因)

2.多个线程修改同一个变量(不安全)(而有几种情况是安全:

一个线程改同一个变量(安全),多个线程读同一个变量(安全),多个线程修改不同变量)

3.修改操作,不是原子性的。

4.内存可见性,引起的线程不安全。

5.指令重排序,引起的线程不安全。

那么如何是的让++操作不会被干扰呢?

解决线程不安全:

主要思路是:诺是我们有一种操作,将++操作封装起来,让t1的++操作结束,再让t2的++操作开始。而这种操作就是加锁操作。

简单理解就是,目前有一个厕所,但是有很多要上厕所,怎么办,抢呗,总不可能等着膀胱爆炸吧,等待厕所里的人出来,并且打开厕所门,然后一群人看谁快,谁快谁就先如厕。

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

synchronized

java中提供了一个关键字:synchronized,监视器锁monitor lock

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

  • 进入 synchronized 修饰的代码块, 相当于 加锁
  • 退出 synchronized 修饰的代码块, 相当于 解锁

synchronized用的锁是存在Java对象头里的,底层是使用操作系统的mutex lock实现的.

内存刷新

工作流程:

synchronized 的工作过程:

  1. 获得互斥锁

  2. 从主内存拷贝变量的最新副本到工作的内存

  3. 执行代码

  4. 将更改后的共享变量的值刷新到主内存

  5. 释放互斥锁

可重入

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

这个问题呢,在synchronized中不会出现。而可重入其实就是,有人厕所上完了,但是他很缺德,将门锁了,然后呢,他突然发现他包忘哪了,回去后他在厕所门口等着,结果等了半天,里面的人死活不出来。但是厕所里没人。

java 复制代码
static class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
   }
    synchronized void increase2() {
        increase();
   }
}
  • increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前对象加锁的.
  • 在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释放, 相当于连续加两次锁)

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

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

synchronized 使用:

  • 直接修饰普通方法 **:**锁的 SynchronizedDemo 对象
java 复制代码
public class SynchronizedDemo {
    public synchronized void methond() {
   }
}
  • 修饰静态方法 **:**锁的 SynchronizedDemo 类的对象
java 复制代码
public class SynchronizedDemo {
    public synchronized static void method() {
   }
}
  • 修饰代码块 **:**明确指定锁哪个对象.

(锁当前对象)

java 复制代码
public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            
       }
   }
}

(锁类对象 )

java 复制代码
public class SynchronizedDemo {
    public void method() {
        synchronized (SynchronizedDemo.class) {
       }
   }
}

两个线程竞争同一把锁, 才会产生阻塞等待

Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder

还有一些是线程安全的. 使用了一些锁机制来控制.

  • Vector (不推荐使用)
  • HashTable (不推荐使用)
  • ConcurrentHashMap
  • StringBuffer

volatile 关键字

volatile 能保证内存可见性

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

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

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

  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本
java 复制代码
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();
}

根据上面的代码,发现问题没有,无论我们在控制台中输入什么值,程序都不会结束。为什么呢,就像上面所说的那样,一个cup的寄存器中数据并没进行更新。另一个线程所拿到的数据没有进行跟换。其主要原因是计算机运算速度太快了。寄存器和缓存的速度都太快了,

使用特点:

1.volatile 不保证原子性

2.volatile 适用于一个线程读,一个线程写

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

改正后:

java 复制代码
static class Counter {
  volatile  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();
}

此时就可以结束进程了。

wait notify

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知. 但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.

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

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

wait 做的事情:

  1. 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  2. 释放当前的锁
  3. 满足一定条件时被唤醒, 重新尝试获取这个锁.

wait 结束等待的条件:

  1. 其他线程调用该对象的 notify 方法.
  2. wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  3. 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.
notify ()

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

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

wait和notify的使用(只能一个结束,一个开始)

java 复制代码
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();
}

notify方法只是唤醒某一个等待线程.。使用notifyAll方法可以一次唤醒所有的等待线程。虽然是同时唤醒多个线程, 但是这些线程需要竞争锁。 所以并不是同时执行, 而仍然是有先有后的执行。

wait与sleep的区别:

其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间。

  1. wait 需要搭配 synchronized 使用.,sleep 不需要。

  2. wait 是 Object 的方法 sleep 是 Thread 的静态方法。

相关推荐
煤泥做不到的!1 小时前
挑战一个月基本掌握C++(第十一天)进阶文件,异常处理,动态内存
开发语言·c++
F-2H1 小时前
C语言:指针4(常量指针和指针常量及动态内存分配)
java·linux·c语言·开发语言·前端·c++
苹果酱05671 小时前
「Mysql优化大师一」mysql服务性能剖析工具
java·vue.js·spring boot·mysql·课程设计
_oP_i2 小时前
Pinpoint 是一个开源的分布式追踪系统
java·分布式·开源
mmsx2 小时前
android sqlite 数据库简单封装示例(java)
android·java·数据库
bryant_meng2 小时前
【python】OpenCV—Image Moments
开发语言·python·opencv·moments·图片矩
武子康3 小时前
大数据-258 离线数仓 - Griffin架构 配置安装 Livy 架构设计 解压配置 Hadoop Hive
java·大数据·数据仓库·hive·hadoop·架构
若亦_Royi3 小时前
C++ 的大括号的用法合集
开发语言·c++
KevinRay_3 小时前
Python超能力:高级技巧让你的代码飞起来
网络·人工智能·python·lambda表达式·列表推导式·python高级技巧
Captain823Jack3 小时前
nlp新词发现——浅析 TF·IDF
人工智能·python·深度学习·神经网络·算法·自然语言处理