当前位置: 首页 > article >正文

【JavaEE】多线程 (2) --线程安全

目录

1. 观察线程不安全

2. 线程安全的概念

3. 线程不安全的原因

4. 解决之前的线程不安全问题

5. synchronized 关键字 - 监视器锁 monitor lock

5.1 synchronized 的特性

5.2 synchronized 使⽤⽰例


1. 观察线程不安全

package thread;
public class ThreadDemo19 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        //创建两个线程,每个线程都针对上面的count变量循环自增5w次
        Thread t1 = new Thread(()-> {
            for(int i = 0; i<50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(()-> {
            for(int i = 0; i<50000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

执行上面的代码,我们发现结果并不是100000, 并且多次运行, 每次的结果都有所不同: 

这就是线程不安全的一个例子. 

2. 线程安全的概念

想给出⼀个线程安全的确切定义是复杂的,但我们可以这样认为:

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

3. 线程不安全的原因

线程调度是随机的

  •  这是线程安全问题的罪魁祸⾸
  • 随机调度使⼀个程序在多线程环境下, 执⾏顺序存在很多的变数.
  • 程序猿必须保证在任意执⾏顺序下 , 代码都能正常⼯作.

修改共享数据

多个线程修改同⼀个变量

上⾯的线程不安全的代码中, 涉及到多个线程针对 count 变量进⾏修改. 此时这个 count 是⼀个多个线程都能访问到的 "共享数据"

原⼦性

什么是原⼦性

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

那我们应该如何解决这个问题呢?是不是只要给房间加⼀把锁,A 进去就把⻔锁上,其他⼈是不是就进不来了。这样就保证了这段代码的原⼦性了。 有时也把这个现象叫做同步互斥,表⽰操作是互相排斥的。

⼀条 java 语句不⼀定是原⼦的,也不⼀定只是⼀条指令

⽐如刚才我们看到的 count++,其实是由三步操作组成的:

1. 从内存把数据读到 CPU

2. 进⾏数据更新

3. 把数据写回到 CPU

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

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

这点也和线程的抢占式调度密切相关. 如果线程不是 "抢占" 的, 就算没有原⼦性, 也问题不⼤.

可⻅性

可⻅性指, ⼀个线程对共享变量值的修改,能够及时地被其他线程看到

4. 解决之前的线程不安全问题

使用 synchronized 关键字将一条指令的多个操作, 打包成一个原子的操作.

下面是使用 synchronized 来解决上面代码的问题:

如果两个线程, 针对不同的对象加锁, 也会存在线程安全问题.

如果一个线程加锁, 一个线程不加锁, 是否会存在线程安全问题?

针对加锁操作的一些混淆理解

把count 放到一个Test t对象中, 通过类方法add 来进行修改, 加锁的时候锁对象写作 this

package thread;
class Test {
    public int count = 0;
    public void add() {
        synchronized (this) {
            count++;
        }
    }
}
public class ThreadDemo20 {
    public static void main(String[] args) throws InterruptedException {
        Test t = new Test();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                t.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                t.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + t.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 重新竞争, 并不遵守先来后到的规则.

2) 可重⼊

synchronized 同步块对同⼀条线程来说是可重⼊的,不会出现⾃⼰把⾃⼰锁死的问题;

理解 "把⾃⼰锁死"

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

// 第⼀次加锁, 加锁成功

lock();

// 第⼆次加锁, 锁已经被占⽤, 阻塞等待.

lock();

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

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

Java 中的 synchronized 是可重⼊锁, 因此没有上⾯的问题.

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 锁的是什么. 两个线程竞争同⼀把锁, 才会产⽣阻塞等待.

两个线程分别尝试获取两把不同的锁, 不会产⽣竞争

6.由内存可见性引起的线程安全问题

这个代码中, 预期通过 t2 线程输入整数, 只要输入的整数不为 0 , 就可以使 t1 线程结束:

package thread;
import java.util.Scanner;
public class ThreadDemo23 {
    private static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while(flag == 0) {
            }
            System.out.println("t1 线程结束");
        });

        Thread t2 = new Thread(()->{
            System.out.println("请输入flag的值:");
            Scanner scanner = new Scanner(System.in);
            flag = scanner.nextInt();
        });

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

 但是, 当我启动线程, 输入一个非0的值, t1 并没有结束:

解决方法一: 避免快速重复读取内存, 防止编译器触发优化: 

解决方法二: 使用 volatile 关键字:

volatile 关键字 

volatile 能保证内存可⻅性

volatile 修饰的变量, 能够保证 "内存可⻅性".

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

• 改变线程⼯作内存中volatile变量副本的值

• 将改变后的副本的值从⼯作内存刷新到主内存

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

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

• 从⼯作内存中读取volatile变量的副本

前⾯我们讨论内存可⻅性时说了, 直接访问⼯作内存(实际是 CPU 的寄存器或者 CPU 的缓存), 速度⾮常快, 但是可能出现数据不⼀致的情况. 加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了.

volatile 不保证原⼦性

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

代码⽰例 这个是最初的演⽰线程安全的代码.

• 给 increase ⽅法去掉 synchronized

• 给 count 加上 volatile 关键字.

static class Counter {
    volatile public int count = 0;
    void increase() {
        count++;
    }
}
public static void main(String[] args) throws InterruptedException {
    final Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
        }
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 50000; i++) {
            counter.increase();
        }
    });
    t1.start();
    t2.start();
    t1.join();
    t2.join();
    System.out.println(counter.count);
}

此时可以看到, 最终 count 的值仍然⽆法保证是 100000.


http://www.kler.cn/a/156545.html

相关文章:

  • 蓝桥杯竞赛单片机组备赛【经验帖】
  • 服务jar包增加高斯数据库驱动jar包
  • STM32 51单片机设计半导体制冷片温控设计
  • Vim 编辑器学习笔记
  • 【教程】Ubuntu设置alacritty为默认终端
  • 自动化工具 Gulp
  • Elasticsearch 相似度评分模型介绍
  • JVM 运行时内存篇
  • ubuntu使用SSH服务远程登录另一台设备
  • 并发编程笔记
  • qiankun: 关于ElementUI字体图标加载不出来的问题
  • C++ Easyx 让圆球跟随鼠标移动
  • 【Node.js】解决npm报错:RequestError: unable to verify the first certificate
  • 手动安装配置dapr环境
  • 人工智能发展史
  • 算法leetcode|91. 解码方法(rust重拳出击)
  • 网易区块链,网易区块链赋能赣州脐橙数字藏品,数字指纹解决方案
  • 【Altera】Cyclone10 FPGA DDR3使用
  • 预约系统源码解析:打造智能定制化预约服务的技术奇迹
  • SQL Sever Unicode字符串简单介绍
  • 抽象类与接口
  • localForage使用 IndexedDB / WebSQL存储
  • 3D模型渲染导致电脑太卡怎么办?
  • 比特币上的有状态多重签名
  • 09 C++ 中的循环(while循环、do-while循环、for循环及嵌套循环)
  • Android 源码编译