JavaEE(系列6) -- 多线程(解决线程不安全系列1-- 加锁(synchronized)与volatile)

news/2024/9/12 12:08:52

首先我们回顾一下上一章节引起线程不安全的原因

本质原因:线程在系统中的调度是无序的/随机的(抢占式执行)

  

1.抢占式执行

2.多个线程修改同一个变量.

        一个线程修改一个变量=>安全

        多个线程读取同一个变量=>安全

        多个线程修改不同的变量=>安全

3.修改操作,不是原子的.(最小不可分割的单位)

例如:对一个变量进行自增操作可分为3步:1.load 2.add 3.save

其中一个操作对应单个CPU指令,是原子的.如果跟上述自增这个操作,对应三步,也就对应多个CPU指令,大概率就不是原子的.

4.内存可见性(本章内容进行讲解)

5.指令重排序(本章内容进行讲解)

目录

1. 解决线程抢占式执行  -- 加锁

如何进行加锁呢?

Synchronized的用法(其他) 

2. 内存可见性

3. 指令重排序


1. 解决线程抢占式执行  -- 加锁

我们学过的join不能防止线程抢占执行吗?

这个思想是一个办法,不过如果这么搞,就不需要多线程了,直接一个线程串行执行。

多线程的初心:进行并发编程,更好地利用多核CPU.

那么如何保证自增这个操作,是一个原子的呢?--->加锁

举例:

生活中常见的例,去公共厕所.

 上厕所,打开门进去,把门锁了。上完厕所,解锁,打开门离开.

锁的核心操作有两个

1.加锁

2.解锁

         一旦某个线程加锁了之后,其他线程也想加锁,就不能直接加上了,就需要阻塞等待,一直等到拿到锁的线程释放锁了为止。

记得,线程调度,是抢占式执行的

当1号释放锁之后,等待的2和3和4,谁能抢先一步拿到锁,那是不确定的了。图中就是3号老铁抢到了.

此处的“抢占式执行”导致了线程之间的调度是“随机”的。

如何进行加锁呢?

synchronnized是java中的关键字,直接使用这个关键字来实现加锁效果.

具体如下图所示

package threading;
class Counter{
    private int count=0;
    public void add(){
        synchronized (this){//加锁
            count++;
        }
 
    }
    public int get(){
        return count;
    }
}
public class ThreadDemo10 {
    public static void main(String[] args) throws InterruptedException{
        Counter counter=new Counter();
        //搞两个线程,两个线程分别对这个counter自增5w次
        Thread t1=new Thread(()->{
            for (int i = 0; i <50000 ; i++) {
                counter.add();
            }
        });
        t1.start();
        Thread t2=new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.get());
    }
}

运行结果:

此时来说明一个点:

join是完全让两个线程变成串行的,而加锁是将两个线程的一小部分变成串行,而不加锁的部分还是并发执行的.

加锁之前:

在上述代码中,一个线程做的工作大概是这些:

1.创建i

2.判定i<50000

3.调用add

4.count++

5.add返回

6.i++

其中只有虽然都是并行的,但是因为自增操作不是原子性的,导致线程之间出现抢占式执行.

加锁之后:

其中只有count++是串行的,(抢到锁的先自增完,剩下的再自增)剩下的12356两个线程仍然是并发的。

在保证线程安全的前提下,同时还能让代码跑的更快一些,更好地利用下多核cpu。

无论如何,加锁都可能导致阻塞。代码阻塞,对于程序的效率肯定还是会有影响的。此处虽然是加了锁,比不加锁要慢些,肯定是比串行快,比不加锁算的准。

Synchronized的用法(其他) 

1.直接修饰普通成员方法  ==> 以this为锁对象进行加锁

2.修饰静态成员方法 ==> 以类对象为锁对象

 等价于下面==>

 

2. 内存可见性

首先给出一个结论:

所谓的内存可见性就是多线程环境下,编译器对于代码优化,产生了误判,从而引起了bug,进一步导致了我们代码的bug。

下面给出一个例子

package threading;
 
import java.util.Scanner;
 
public class ThreadDemo11 {
    public 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(()->{
            Scanner scanner=new Scanner(System.in);
            System.out.println("请输入一个整数");
            flag=scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

上述代码:我们创建了一个新的线程,里面写了一个循环,我们期待通过t2线程改变标志位,使得标志位变为非0,然后终止t1线程的循环.

运行代码,当我们多次输入1的时候,线程t1循环没有终止. 

出现的原因:

t1的这个循环,步骤是这样的,load从内存中读取数据到寄存器,cmp比较寄存器的值是否为0,

此时load的开销非常的大,要一直重复上述操作.读取内存虽然比读硬盘来的快,但是读寄存器,比读内存又要快。此时,编译器就做了一个非常大胆的操作,把load就给优化掉了。只有第一次执行load才真正的执行了后续循环都只cmp,不load(相当于是复用之前寄存器中的load过的值).这是编译器优化的手段,是一个非常普遍的事情,能智能地调整你的代码执行逻辑,保证程序结果不变地前提下,语句变化,通过一些列操作,让整个程序执行的效率大大提升。编译器对于“程序结果不变”单线程下判定是非常准确的。但是多线程不一定,可能导致调整后,效率提高,结果变了。

那么我们如何针对,这个操作进行修改呢?

1.可以让读寄存器的这个速度稍微慢下来,此时编译器就不会进行优化,也就会每次进行重新load这个操作,那么就会读取到真正的修改后标志位的值. 

public class ThreadDemo12 {
    public static int flag = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while (flag == 0){
                try {
                    Thread.sleep(10);
                    //加了sleep就让循环执行的很慢,编译器就不会进行优化.load
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("循环结束!t1结束");
        });
        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            flag = scanner.nextInt();
        });

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

    }
}

 运行结果:我们可以看到,我们成功的将线程t1的循环停止下来了.

 那么除了上述操作,我们还可以使用volatile关键字来进行修改.

被volatile修饰的变量,此时编译器就会禁止上述优化,能够保证每次都是从内存重新读取数据。

 注意:

3. 指令重排序

这就是volatile的另一个作用了,防止指令重排序

什么事指令重排序呢?

指令重排序:也是编译器优化的策略,调整了代码执行的顺序,让程序更高效。前提也是保证整体逻辑不变。谈到优化,都要保证调整之后的结果和之前是不变得。单线程下容易保证,多线程就不好说了。

举例:

就拿房子装修来说:

A买了一个新房子(精装修)

B买了一个新房子(毛坯房)

那么A这个过程就是

1.交钱 2.房地产装修 3.交付钥匙

那么B这个过程就是

1.交钱 3.交付钥匙 2.房地产装修

最后AB都拿到了一样的房子(假设装修队是一样的),但是这个过程是不一样的.

 上述伪代码

t1中的语句大体可以分为三个操作:

1.申请内存空间——交钱

2.调用构造方法(初始化内存的数据)——装修

3.把对象的引用赋值给s(内存地址的赋值)——拿到钥匙

如果是单线程环境,此处就可以指令重排序:

1肯定先执行,2和3谁先执行,谁后执行,都可以。

  

那么如果两个线程按照两种不同的方式执行呢?

如果t1按照 1 3 2的顺序执行,当t1执行完1 3 之后,即将执行2的时候,t2开始执行。由于t1的3已经执行过了,这个引用已经非空了。t2开始调用s.learn()。但是由于t1还没有初始化,learn的结果是什么不知道了,(也就是相当于A拿到了毛坯房的钥匙,这不就坏了吗),就出现了bug。

这个代码难以演示,因为大部分情况是正确的。

上述情况,不好演示,因为大部分是正确的,但是也会发生,那么使用volatile关键字修饰这个对象进行修饰,就会保证不会受到指令重排序的影响.


http://lihuaxi.xjx100.cn/news/1142750.html

相关文章

Java反射简单介绍_01

文章目录 1. 什么是反射2. Java中类加载的三个阶段3. 反射机制提供的相关类4. Java中获取Class类的三种方式5. Class类提供的功能5.1. 获取Field类方法5.2. 获取Method类方法5.3. 获取Constructor类方法5.4. Class中其他方法 1. 什么是反射 Java中的反射主要是体现在运行期间,…

使用onSaveInstaceState保存活动信息

工程地址&#xff1a;https://github.com/MADMAX110/Stopwatch 上次实现了一个Android秒表应用&#xff0c;在模拟器里运行这个应用&#xff0c;应用没有任何问题&#xff0c;但是在真实设备上运行这个应用时&#xff0c;旋转设备的方向&#xff0c;秒表会自动归零。下面分析一…

apk自动签名工具

序言 因为360加固&#xff0c;自动签名需要开通VIP&#xff0c;每次加固完了都得手动签名。所以写了个工具。实现通过配置文件配置&#xff0c;拖拽APK自动签名。 支持&#xff1a;V1 V2 V3 V4 签名。通过分析清单文件&#xff0c;自动选择版本。 效果 使用 1.下载jar包 au…

Linux scp 命令详解

文章目录 scp补充说明语法选项参数实例 scp 加密的方式在本地主机和远程主机之间复制文件 补充说明 scp命令 用于在Linux下进行远程拷贝文件的命令&#xff0c;和它类似的命令有cp&#xff0c;不过cp只是在本机进行拷贝不能跨服务器&#xff0c;而且scp传输是加密的。可能会…

uni-app对缓存进行封装-此篇博客会持续更新

仅仅做个笔记&#xff0c;自己经常用此缓存&#xff0c;做了大部分封装支持缓存时间以及过期自动删除 此篇博客会持续更新 //---------------------------------------------2022-10-05----------------------------------------------------------- class Cache {construct…

经典神经网络(3)Vgg-Net及其在Fashion-MNIST数据集上的应用

经典神经网络(3)VGG_使用块的网络 1 VGG的简述 1.1 VGG的概述 VGG-Net 是牛津大学计算机视觉组和DeepMind公司共同研发一种深度卷积网络&#xff0c;并且在2014年在ILSVRC比赛上获得了分类项目的第二名和定位项目的第一名。通过使⽤循环和⼦程序&#xff0c;可以很容易地在任…

Android Studio报错:Could not resolve com.android.tools.build:gradle:8.0.0

一、报错信息 Android Studio 新建项目会报以下错误&#xff1a; Could not resolve com.android.tools.build:gradle:8.0.0.完整版报错信息如下&#xff1a; A problem occurred configuring root project My Application. > Could not resolve all files for configura…

如何管理好团队的工时表?

工时表管理对所有团队来说都是一项具有挑战性的任务。它是确保每个团队成员高效工作并获得最大时间的关键工具。团队工时表是任何项目经理武器库中的一个重要工具。它们提供了对团队表现的宝贵见解。 一个成功的工时表管理系统对于希望最大限度提高生产力和利润的团队成员是必…