IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> PHP知识库 -> Java并发基础(一):多个线程读写同一共享变量是否存在并发问题? -> 正文阅读

[PHP知识库]Java并发基础(一):多个线程读写同一共享变量是否存在并发问题?

目录

一、经典面试题

二、内存模型概念

2.1、CPU内存模型

2.2、JVM内存模型

三、基于内存模型一步一步分析缓存一致性问题

四、解决并发问题的方法有哪些?

4.1、局部变量

4.2、不可变对象

4.3、ThreadLocal

4.4、CAS原子类

4.5、Synchronized/ReentrantLock加锁


一、经典面试题

有没有在JAVA笔试或面试中遇见过这样的题目:统计服务器某个接口的访问次数。

public class AccountCount {

    int accessCount;

    public void access(){
        accessCount ++;
    }
}

问题:多线程调用access()方法时,接口访问次数统计的结果是否能保证准确呢?

自定义线程中的实例变量针对其他线程有共享和不共享之分。

在这里插入图片描述

1.1、多个线程共享同一个变量,产生线程安全问题。

public class AccountCount extends Thread{
    int accessCount;

    public void access(){
        accessCount ++;
        System.out.println(Thread.currentThread().getName() + accessCount);
    }

    @Override
    public void run() {
        access();
    }

    public int getAccount() {
        return accessCount;
    }

    public static void main(String[] args) {

        AccountCount accountCount = new AccountCount();
        new Thread(accountCount,"A").start();
        new Thread(accountCount,"B").start();
        new Thread(accountCount,"C").start();
        new Thread(accountCount,"D").start();
        new Thread(accountCount,"E").start();
        new Thread(accountCount,"F").start();
    }

}
B2
E5
F6
A6
D6
C4

2)、不共享数据时每个线程都拥有自己作用域的变量,且多个线程之间相同变量名的值也不相同

public class AccountCount extends Thread{
    int accessCount;

    public void access(){
        accessCount ++;
        System.out.println(Thread.currentThread().getName() + accessCount);
    }

    public AccountCount(String accessCount) {
        this.setName(accessCount);
    }

    @Override
    public void run() {
        access();
    }

    public int getAccount() {
        return accessCount;
    }

    public static void main(String[] args) throws InterruptedException {
        new AccountCount("A").start();
        new AccountCount("B").start();
        new AccountCount("C").start();
        new AccountCount("D").start();
        new AccountCount("E").start();
        new AccountCount("F").start();
    }
}
A1
B1
E1
D1
F1
C1

二、内存模型概念

????????我们的程序是运行在Java虚拟机上面的,Java虚拟机本身有自己的内存模型, Java的内存模型和计算机的CPU内存模型又有很多相同之处。

2.1、CPU内存模型

????????计算机在执行程序的时候,每条指令都是在CPU中执行的,而在CPU执行指令的过程中会涉及到数据的读取和写入操作,而在计算机运行过程中所有的数据都是存放在主存中的(比如一台普通的4C8G机器,这个8G就是指主存的容量),CPU则是从主存中读取数据进行运行。

????????由于CPU执行速度非常快,比计算机主存的读取和写入的速度快了很多,这样就会导致CPU的执行速度大大下降。

????????因此,每个CPU都会自带一个高速缓冲区,在运行的时候,会将需要运行的数据从计算机主存先复制到CPU的高速缓冲区中,然后CPU再基于高速缓冲区的数据进行运算,运算结束之后,再将高速缓冲区的数据刷新到主存中。这样CPU的执行指令的速度就可以大大提升。

2.2、JVM内存模型

????????JVM启动之后,操作系统会为JVM进程分配一定的内存空间,这部分内存空间就称为“主内存”。另外Java程序的所有工作都由线程来完成,而每个线程都会有一小块内存,称为“工作内存”即线程栈。?Java中的线程在执行的过程中,会先将数据从主内存(指堆内存)中复制到线程的工作内存,然后再执行计算,执行计算之后,再把计算结果刷新到“主内存”中。

三、基于内存模型一步一步分析缓存一致性问题

????????假设现在有两个线程同时访问了这个接口的access()方法,两个线程都执行了accessCount++,在内存中是怎么样执行的呢?

首先我们要明白,计算机需要执行accessCount++ 这个语句,需要分为以下3个步骤:

  1. 从主存中读取accessCount的值

  2. 将accessCount的值进行加1

  3. 将accessCount的值写回主存中

先来看看第1步,假设两个线程同时来执行accessCount()方法

上面图中在第1个步骤的时候,线程1和线程2都会把accessCount的值从主存中复制到线程所属的工作内存中,两个线程此时得到的accessCount的值都是0。

接着两个线程执行第2步操作:将accessCount的值进行加1:

5.jpg

图中,线程1和线程2都进行了第2步的计算,然后线程1得到的结果是 accessCount=1,线程2得到的结果也是accessCount=1。

接着两个线程都到了第三步:将accessCount的值写回主存中:

6.jpg

线程1和线程2都计算完之后就会将计算结果刷新回主存,特别注意一下图中红框的内容,这是两个线程把计算结果刷新回主存的步骤,两个红框中操作的执行顺序不分先后(在实际运行情况,两个操作的顺序是随机的,可能是线程1先刷新,也可能是线程2先刷新),但是这不影响结果,因为无论是线程1还是线程2,写回主存的结果都是accessCount=1。

但是实际上,我们观察到是2个线程都执行了一次access()方法,按照预期来说accessCount的值应该是等于2才对。

这种多个线程访问同一个对象时,调用这个对象的方法得到不正确的结果,这种问题称为线程安全问题。

四、解决并发问题的方法有哪些?

解决并发问题方法分为两类:无锁和有锁。

无锁分为:局部变量、不可变对象、ThreadLocal、CAS原子类。

有锁分为:synchronized关键字 和 reentranLock可重入锁。

4.1、局部变量

善用局部变量可以避免出现线程安全问题,因为局部变量仅仅存在于每个线程的工作内存中。

public void test(){
    int i = 0;
    i++;
    System.out.println(i);
}

只有当每个线程都执行到int i =0时,会在各自线程栈中创建该变量。

4.2、不可变对象

不可变对象是指一经创建,则对外的状态就不会改变的对象。如果一个对象的状态不变,无落多少个线程,对其如何操作,都不改变。例如字符串对象就是不可变对象。String s = "a",指字面值a是不可变的,而引用s可以变。

4.3、ThreadLocal

ThreadLocal本质是在每个线程有自己的一个副本,每个线程的副本互不影响。

?一个命令为“I”的ThreadLocal类,他会在每个线程都有一个Integer对象,虽然每个线程都会在主内存把Integer对象拷贝到工作内存,但是拷贝的不是一个对象。

4.4、CAS原子类

CAS(Compare And Swap)比较交换。

对CAS的理解,CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

CAS比较与交换的伪代码可以表示为:

do{

备份旧数据;

基于旧数据构造新数据;

}while(!CAS( 内存地址,备份的旧数据,新数据 ))

?其中内存值V是56,旧的预期值CPU1和CPU2中的值,要修改的新值为最新值。

? ? ? ? 在Java中,以Atomic为前缀的一系列类都采用CAS思想。

Atomic使用的是无锁的CAS操作,基于乐观锁,并发性能比较高,可以多个线程同时执行,保证线程安全。

Atomic简单使用:首先声明一个AtomicInteger的成员变量,然后再atomicAdd()方法中调用incrementAndGet()。

private AtomicInteger counter = new AtomicInteger(0);

    public void actomicAdd(){
        counter.incrementAndGet();
    }

atomicInterger源码分析:内部有一个Unsafe实例,Unsafe类提供硬件级别的原子操作,因为Java无法直接访问到操作系统底层的硬件,故Java使用native方法进行扩展,其中Unsafe类就是一个操作入口。Unsafe提供几种功能:CAS操作、内存的分配和释放、挂起和恢复线程、定位对象字段内存地址、修改对象的字段值等。

其中对于Unsafe类的CAS的操作,主要调用了unsafe的getAndInt()方法:

?首先通过var5获取旧值,然后调用compareAndSwapInt()通过CAS操作对数据比较交换,如果操作失败进行while循环直到成功。

4.5、Synchronized/ReentrantLock加锁

Synchronized和ReentrantLock都采用悲观锁策略。

Synchronized是语言层面

ReentrantLock是编程方式实现

public class ReentratLockTest {
    
    private int count = 0;
    private ReentrantLock reentrantLock = new ReentrantLock();
    
    public void lockMethod(){
        reentrantLock.lock();
        try {
            add();
        } finally {
            reentrantLock.unlock();
        }
    }
    
    public synchronized void lockMethod2(){
        add();
    }
    
    private void add(){
        count++;
    }
}

加锁原理:首先两个线程争抢同一把锁,假如线程1获取到锁,而线程二没获取锁就会进入等待队列,等到线程1执行完代码逻辑,会去通知线程2,此时线程2重新尝试获取锁,假如线程2获取成功,则执行代码。

?

  PHP知识库 最新文章
Laravel 下实现 Google 2fa 验证
UUCTF WP
DASCTF10月 web
XAMPP任意命令执行提升权限漏洞(CVE-2020-
[GYCTF2020]Easyphp
iwebsec靶场 代码执行关卡通关笔记
多个线程同步执行,多个线程依次执行,多个
php 没事记录下常用方法 (TP5.1)
php之jwt
2021-09-18
上一篇文章      下一篇文章      查看所有文章
加:2021-09-19 07:45:54  更:2021-09-19 07:46:25 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/15 7:02:07-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码