【JavaEE】线程安全

【JavaEE】线程安全

  • 一、引出线程安全
  • 二、引发线程安全的原因
  • 三、解决线程安全问题
      • [3.1 synchronized关键字(解决修改操作不是原子的)](#3.1 synchronized关键字(解决修改操作不是原子的))
        • [3.1.1 synchronized的特性](#3.1.1 synchronized的特性)
        • [3.1.1 synchronized的使用事例](#3.1.1 synchronized的使用事例)
      • [3.2 volatile 关键字(解决内存可见性)](#3.2 volatile 关键字(解决内存可见性))
  • 四、死锁
      • [4.1 可重入](#4.1 可重入)
      • [4.2 两个线程出现的死锁](#4.2 两个线程出现的死锁)
      • [4.3 哲学家就餐问题](#4.3 哲学家就餐问题)
      • [4.4 造成死锁的原因](#4.4 造成死锁的原因)

博客结尾有此篇博客的全部代码!!!

一、引出线程安全

举例:

java 复制代码
public class Demo1 {
    private static int count=0;
    public static void main(String[] args) throws InterruptedException {
        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="+count);//结果:56154
    }
}

这段代码运行结束发现结果不是理论值:100000,而是每次运行完出现一个新的数。

在计算机操作系统中,count++;在寄存器中分为三步:读取,加一,写回这三步。

假设:两个线程按照这样进行,那么得到的count的最终值就是理论值100000。

但往往事实不是这样这样的,CPU资源调度是随机的 !!!很有可能是这样的(这里只列举一种情况给大家示范一下):

首先t1线程和t2 线程分别从内存中读取count值,此时两个线程读取到的count值都是0,然后t1线程进行加一操作后写回到内存中,t2线程也是进行加一操作后写回到内存中,t2线程得到的count值将t1得到的count覆盖,这样count经过两个线程的加一操作之后值还是1!

二、引发线程安全的原因

  1. 【根本原因】操作系统对于线程的调度是随机的,抢占式执行
  2. 多个线程同时修改同一变量
  3. 修改操作不是原子的(事务中的原子性)
  4. 内存可见性,引起的线程不安全
  5. 指令重排序,引起的线程不安全

三、解决线程安全问题

  • 由于线程调度是随机的,这个不是我们可以左右的;
  • 我们确保多个线程不同时修改同一变量

主要带大家学习引发第三个和第四个引起线程安全的解决方法:

3.1 synchronized关键字(解决修改操作不是原子的)

引发线程安全第三个原因是:修改操作不是原子的;关键字:synchronized 将修改操作"锁"在一起(相当于将读取,加一,写回三个操作绑定在一起,三操作要么全部执行,要么全部不执行)

3.1.1 synchronized的特性
  1. 互斥
    synchronized 会起到互斥效果, 某个线程执⾏到某个对象的 synchronized 中时, 其他线程如果也执⾏到同⼀个对象 synchronized 就会阻塞等待.
    语法:
java 复制代码
synchronized(变量){
//修改操作
}

• 进⼊ synchronized 修饰的代码块, 相当于 加锁

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

java 复制代码
public class Demo2 {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (lock){
                count++;
                }
            }
        }); Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (lock){
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count="+count);//结果:count=10000
    }
}

当加上锁之后,count值就是10000!

这里需要注意的事:

synchronized(变量)里面的这个变量必须是相同的变量,否则就不会发生阻塞等待!!!

事例 :

java 复制代码
public class Demo3 {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Object lock1 = new Object();
        Object lock2 = new Object();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (lock1){
                    count++;
                }
            }
        }); Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                synchronized (lock2){
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count="+count);//结果:count=70738
    }

}
  1. 可重入
    可重入就是指一个线程连续针对一个对象加多次锁,不会出现"死锁"现象称为可重入。
java 复制代码
synchronized (block) {
	synchronized(block) {
	//代码
	} //右大括号}2
}  //右大括号}1

按理来说,在进入第一个synchronized的时候,加上了一把锁,此时已经是"锁定状态",当我们进入到第二个synchronized的时候要加锁,就发生"阻塞等待",就要等到第一个锁走到右大括号}1解完锁才能加,然而第一个锁走到右大括号}1解锁,又需要第二把锁创建走完到右大括号}2。

这是线程就卡死了,这就是死锁

Java大佬发现了这个问题,所以将synchronized设为可重入锁,这样就不会出现死锁的问题。

• 如果某个线程加锁的时候, 发现锁已经被⼈占⽤, 但是恰好占⽤的正是⾃⼰(这个锁是自己加的), 那么仍然可以继续获取到锁, 并让计数器⾃增.

• 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)

3.1.1 synchronized的使用事例
  1. 修饰代码块
    锁定任意对象

    锁住当前对象
java 复制代码
     public class SynchronizedDemo {
            public void method() {
                synchronized (this) {
                }
            }
        }
  1. 直接修饰普通⽅法
java 复制代码
public class SynchronizedDemo {
            public synchronized void methond() {
            }
        }
  1. 修饰静态⽅法
java 复制代码
public class SynchronizedDemo {
            public synchronized static void methond() {
            }
        }

3.2 volatile 关键字(解决内存可见性)

volatile可以保证内存可见性,只能修饰变量。并且volatile不能保证原子性

计算机运行代码/程序的时候,访问数据常常要从内存中访问(定义变量时变量就储存在内存中),然而CPU从内存中读取数据相比于从寄存器中读取数据要慢上很多(几千上万倍),CPU在进行读/写内存的时候速度就会降低。

为了解决这个问题,提高效率,编译器就可能会对代码优化,把一些本来要读取内存的操作,优化为读取寄存器,减少读取内存的次数。这就会导致内存可见性问题。

以我们接下来的代码为例------当CPU从自身寄存器中读取成千上万次发现count一直是0,此时编译器就将代码优化,让count一直等于0,所以接下来线程1中一直处于循环状态,尽管线程2中已经将count修改为1!

java 复制代码
public class Demo4 {

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Object lock1 = new Object();
        Thread t1 = new Thread(() -> {
            while (count == 0) {

            }
            System.out.println("循环结束");
        });
        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            count = 1;
        });
        t1.start();
        t2.start();
        System.out.println("main 线程结束");
    }
}

这里修改的方法:给线程1中的程序加入sleep,让它休眠时间大于线程2的休眠时间,这样它读取的count就是1,编译器就不会进行优化,循环就会结束!

java 复制代码
Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            while (count == 0) {

            }
            System.out.println("循环结束");
        });
        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            count = 1;
        });

volatile解决内存可见性问题:

java 复制代码
public class Demo5 {
    private volatile static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (count == 0) {

            }
            System.out.println("循环结束");
        });
        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            count = 1;
        });
        t1.start();
        t2.start();
        System.out.println("main 线程结束");
    }
}

四、死锁

死锁是一个非常严重的bug,它会让你的代码在执行到这块卡住!!!

4.1 可重入

java 复制代码
public class Demo6 {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (lock) {
                    synchronized (lock) {
                        count++;
                    }
                }
            }
            System.out.println("循环结束");
        });
        t1.start();
        t1.join();
        System.out.println(count);
    }
}

由于Java提出了可重入的概念,所以这段代码执行的到这里并没有卡住!但在C++中,没有引入可重入的概念,所以C++这里写出这样的代码就会出现死锁!!!

4.2 两个线程出现的死锁

假设t1线程先拿醋,t2线程先拿酱,两个线程都将醋和酱已经加上自己的锁了,然后t1线程尝试拿酱,t2线程尝试拿醋,此时就会出现死锁!!!

java 复制代码
public class Demo7 {

    public static void main(String[] args) throws InterruptedException {
        Object lock1 = new Object();
        Object lock2 = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("t1拿到醋了!!!");
                synchronized (lock2) {
                    System.out.println("t1拿到酱了!!!");
                }
            }
        });
        Thread t2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("t2拿到酱了!!!");
                synchronized (lock1) {
                    System.out.println("t2拿到醋了!!!");
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("main 线程结束!!!");
    }
}

如果将两个锁改成并列就不会出现死锁!

java 复制代码
     Thread t2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("t2拿到酱了!!!");
            }
            synchronized (lock1) {
                    System.out.println("t2拿到醋了!!!");
            }
        });

4.3 哲学家就餐问题

相当于是两个线程出现死锁的进阶(M个线程,N把锁):

5个哲学家(5个线程),5只筷子(5把锁),哲学家坐在圆桌边,桌上放有面条,每只筷子放在每个哲学家的中间。

每个哲学家,会做两件事:

  1. 思考人生.放下筷子,啥都不干
  2. 吃面条.拿起左右两侧的两根筷子,开始吃面条,

哲学家啥时候吃面条,啥时候思考人生,是随机的

哲学家吃面条啥时候吃完,也是随机的,

哲学家正在吃面条的过程中,会持有左右两侧的筷子。此时相邻的哲学家如果也想吃面条,就需要阻塞等待,

当出现极端情况,每个哲学家都想吃面条,都拿起自己左手边的筷子,并且不会在没吃到面条情况下放下筷子,这时就是死锁了。

4.4 造成死锁的原因

  • 互斥使用(锁的基本特性):当一个线程拿到一把锁后,另一个线程要拿到这把锁就要阻塞等待;
  • 不可抢占(锁的基本特性):当一把锁被线程拿到后,其他线程不能抢占,只能等线程自己释放锁;
  • 请求保持(代码结构):当一个线程拿到一把锁后,再去拿其它锁的时候,已经被拿到的锁不会被释放;
  • 循环/环路 等待(代码结构):阻塞等待的依赖关系形成环了。

此篇博客的全部代码!!!

相关推荐
Magnum Lehar4 分钟前
vulkan游戏引擎test_manager实现
java·算法·游戏引擎
sss191s5 分钟前
校招 java 面试基础题目及解析
java·开发语言·面试
异常君9 分钟前
MySQL 中 count(*)、count(1)、count(字段)性能对比:一次彻底搞清楚
java·mysql·面试
wkj00135 分钟前
QuaggaJS 配置参数详解
java·linux·服务器·javascript·quaggajs
代码搬运媛36 分钟前
React 中 HTML 插入的全场景实践与安全指南
安全·react.js·html
异常君1 小时前
MyBatis 中 SqlSessionFactory 和 SqlSession 的线程安全性深度分析
java·面试·mybatis
crud1 小时前
Spring Boot 使用 spring-boot-starter-validation 实现优雅的参数校验,一文讲透!
java·spring boot
Dcs1 小时前
常见 GC 垃圾收集器对比分析
java
程序员岳焱1 小时前
Java高级反射实战:15个场景化编程技巧与底层原理解析
java·后端·编程语言
程序员小假1 小时前
说一说 Netty 中的心跳机制
java·后端