JAVA并发:线程安全与Synchorinzed

2023-11-20

1. 什么是线程安全问题

线程的合理使用能够提升程序的处理性能,主要有两个方面,第一个是能够利用多核 cpu 以及超线程技术来实现线程的并行执行;第二个是线程的异步化执行相比于同步执行来说,异步执行能够很好的优化程序的处理性能提升并发吞吐量。同时也带来了很多麻烦。如:多线程对于共享变量访问带来的安全性问题
一个变量 i,假如一个线程去访问这个变量进行修改,这个时候对于数据的修改和访问没有任何问题。但是如果多个线程对于这同一个变量进行修改,就会存在一个数据安全性问题。

对于线程安全性,本质上是管理对于数据状态的访问,而且这个这个状态通常是共享的、可变的。共享,是指这个数据变量可以被多个线程访问;可变,指这个变量的值在它的生命周期内是可以改变的。若共享变量对于多线程来说只读不写并不存在线程安全问题。

public class SynchronizedDemo {

    private static int count = 0;

    private static void countIncr()  {
        try {
            TimeUnit.MILLISECONDS.sleep(10);
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                countIncr();
            }).start();
        }
        // 睡眠5秒,确保线程执行结束
        TimeUnit.SECONDS.sleep(5);
        System.out.println("count_result:" + count);
    }
}

通过结果发现count_result有时为98或100,不是一个固定不变的值,和我们期望的结果不一样。这就是多线程对共享变量的读写带来的安全性问题。

2. 多线程的数据安全性

2.1 如何保证数据安全性

问题的本质在于共享数据存在并发访问。如果我们能够有一种方法使线程并行变成串行去访问共享数据,这样就不存问题了。

我们可以通过加锁来保证共享数据的安全问题。锁是处理并发的一种同步手段,而如果需要达到前面我们说的一个目的,那么这个锁一定需要实现互斥的特性。

java提供加锁的方法就是Synchroinzed关键字

2.2 Synchroinzed

通过Synchroinzed解决前面例子出现的线程安全问题。

	// 加锁保证线程安全问题
    private synchronized static void countIncr()  {
        try {
            TimeUnit.MILLISECONDS.sleep(10);
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++;
    }

synchronized实现同步的基础:java中的每一个对象都可以作为锁。具体表现为以下3种形式:

  • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  • 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

2.3 锁是如何存储的

为了实现多线程的互斥性,那么这把锁需要哪些东西呢?

  • 锁需要一个东西来表示。
  • 需要记录锁的状态(获得锁是什么状态,无锁是什么状态)
  • 锁状态需要对多个线程共享

synchroinzed(lock)是基于lock对象来控制锁的,因此锁和这个lock对象有关。因此我们需要关注对象在JVM内存中是如何存储的。

从JVM规范中可以看出Synchroinzed在JVM的实现原理,JVM进入和退出Monior对象来实现方法同步和代码块同步,两者的实现细节不一样但是,方法的同步同样可以使用这两个指令来实现。

2.4 JAVA对象头

在 Hotspot 虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。

image-20200715232707602

Synchronized用的锁存在java对象头里的。

image-20200713230233355

java对象头里的Mark Word里默认存储对象的hashCode、分带年龄和锁标记位。32位JVM的Mark Word存储结构如下:

image-20200713230655649

在运行期间,Mark Word里面存储的数据会随着锁标志位的变化而变化。Mark Word可能变为存储以下4种数据

image-20200713230922739

可以看到,当对象状态为偏向锁时,Mark Word存储的是偏向的线程ID;当状态为轻量级锁时,Mark Word存储的是指向线程栈中Lock Record的指针;当状态为重量级锁时,Mark Word为指向堆中的monitor对象的指针。

2.5 用户态和内核态

平时我们所写的java程序是运行在用户空间的,因为我们的jvm对于操作系统来讲就是一个普通程序。用户空间的程序要执行读写硬盘、读写网络、读写内存等重要操作时必须经过操作系统内核来进行。

在JDK早期,Synchronized是重量级锁,每次申请锁都需要调用系统内核。需要从用户空间切换到内核空间,拿到锁后再将状态返回给用户空间。

2.6 CAS原理

2.6.1 什么是CAS

Compare and Swap,即比较再交换。jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁。JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。

2.6.2 CAS算法理解

CAS是一种无锁算法,CAS有3个操作数,内存值N,旧的预期值E,要修改的新值V。当且仅当预期值E和内存值N相同时,将内存值N修改为V。

存在ABA问题:一个线程把数据A变为了B,然后又重新变成了A。此时另外一个线程读取的时候,发现A没有变化,就误以为是原来的那个A。此问题可以加入版本号解决,每次更新内存值后加入一个版本号进行区分。

image-20200715233141405

2.7 锁的升级与对比

java 1.6后为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁、轻量级锁。锁一共有4种状态,从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。这几个状态会随着竞争情况逐渐升级。锁升级后不能降级。

匿名偏向:锁对象线程ID为空,偏向锁的标识为1。

image-20200715221031149

2.7.1 偏向锁

Hotspot的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,于是引入了偏向锁。

当一个线程访问加了同步锁的代码块时,会在对象头中存储当前线程的 ID,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了。

2.7.1.1 偏向锁的获取

  1. 首先获取锁对象的Markword,检查对象头中是否存储了当前线程的ID,如果存储了当前线程ID,表示当前线程已经获得了锁。

  2. 如果没有存储当前线程ID,锁对象处于可偏向状态(MarkWord中的偏向锁标识为1 且线程ID为空)。通过 CAS 操作,把当前线程的 ID写入到 MarkWord。

    • 如果 cas 成功,将对象头MarkWord的线程ID指向自己(变为T1|Epoch|1|01)。表示已经获得了锁对象的偏向锁,接着执行同步代码

      块。

    • 如果 cas 失败,说明有其他线程已经获得了偏向锁,这种情况说明当前锁存在竞争,需要撤销已获得偏向锁的线程,并且把它持有的锁升级为轻量级锁。

  3. 如果没有存储当前线程ID,锁对象处于已偏向状态(MarkWord中的偏向锁标识为1 且线程ID不为空)。当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁

  4. 如果没有存储当前线程ID,且偏向锁标识为0,通过 CAS 操作,将对象头MarkWord的线程ID指向自己。

2.7.1.2 偏向锁的撤销

偏向锁的撤销并不是把对象恢复到无锁可偏向状态(因为偏向锁并不存在锁释放的概念),而是在获取偏向锁的过程中,发现 cas 失败也就是存在线程竞争时,直接把被偏向的锁对象升级到被加了轻量级锁的状态。

  1. 原获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无锁状态并且争抢锁的线程可以基于 CAS 重新偏向但前线程。
  2. 如果原获得偏向锁的线程的同步代码块还没执行完,处于临界区之内,这个时候会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块。

image-20200715232215020

2.7.1.3 关闭偏向锁

参考博客(https://blog.csdn.net/Epoch_Elysian/article/details/105519837)

偏向锁默认是启用的,但是它在应用程序启动几秒后才激活。如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果你确定程序所有的锁通常情况下是处于竞争状态,可以通过JVM参数关闭偏向锁–XX:-UseBiasedLocking,那么程序默认会进入轻量级锁状态。

偏向锁为什么要延迟激活?

jvm在启动过程中是有大量的线程竞争资源的,这个时候启动偏向锁是没有意义的,所以延迟开启等待JVM启动。

openjdk提供了一个查看java对象布局的工具jol-core,来验证各个状态的MarkWord。注意关注锁标志位的变化

  1. JVM启动后创建对象

    此时偏向锁延迟开启还未启动,创建的对象为普通对象,加锁后直接变为轻量级锁。

    public class MarkWordDemo {
    
        public static void main(String[] args) throws InterruptedException {
            // 默认情况下偏向锁会延迟打开,此时偏向锁未启动
            Object object = new Object();
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
            System.out.println("-----------------");
            synchronized (object) {
                System.out.println(ClassLayout.parseInstance(object).toPrintable());
            }
        }
    }
    

    image-20200716002349395

  2. 延迟创建对象

    睡眠后创建对象,此时偏向锁已经开启,创建的对象为匿名偏向对象,加锁后为偏向锁。

    public class MarkWordDemo {
    
        public static void main(String[] args) throws InterruptedException {
            // 10后,偏向锁已启动
            TimeUnit.SECONDS.sleep(10);
            Object object = new Object();
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
            System.out.println("-----------------");
            synchronized (object) {
                System.out.println(ClassLayout.parseInstance(object).toPrintable());
            }
        }
    }
    

    image-20200716002725716

  3. 关闭延迟参数

    启动参数 -XX:BiasedLockingStartupDelay=0

    关闭偏向锁的延迟开启,创建的对象为匿名偏向对象,加锁后为偏向锁。结论和2相同

    public class MarkWordDemo {
    
        public static void main(String[] args) throws InterruptedException {
            // VM配置-XX:BiasedLockingStartupDelay=0关闭偏向锁的启动延迟
            Object object = new Object();
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
            System.out.println("-----------------");
            synchronized (object) {
                System.out.println(ClassLayout.parseInstance(object).toPrintable());
            }
        }
    }
    

    image-20200716003305913

  4. 关闭偏向锁

启动参数 -XX:-UseBiasedLocking,结论和1相同

public class MarkWordDemo {

    public static void main(String[] args) throws InterruptedException {
        // -XX:-UseBiasedLocking
        Object object = new Object();
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
        System.out.println("-----------------");
        synchronized (object) {
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
        }
    }
}

image-20200716003444083

2.7.2 轻量级锁

什么是轻量级锁

轻量级锁是当一个线程获取到该锁后,另一个线程也来获取该锁,这个线程并不会被直接阻塞,而是通过自旋来等待该锁被释放,所谓的自旋就是让线程执行一段无意义的循环。

为什么会引入轻量级锁呢

轻量级锁主要考虑到竞争线程并不多,并且持有对象锁的线程执行的时间也不长的这种情况,在未引入轻量级锁之前,如果一个线程刚刚被阻塞,这个锁就被其他线程释放,如果这种情况频繁发生,那么会因为频繁的阻塞以及唤醒线程给带来不必要的资源浪费。而在引入轻量级锁之后,在线程获取锁失败的情况下,线程并不会立即被阻塞,而是通过一段自旋的过程,来等待获取锁,因此就避免了频繁的阻塞与唤醒操作带来的资源浪费。

2.7.2.1 轻量级锁的加锁

  1. 当前线程在自己的栈桢中创建存储锁记录的空间 LockRecord.
  2. 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中官方称为Displaced mark 。
  3. CAS修改MarkWord
    • 成功:当前线程获得锁,并将markword修改为指向锁记录的指针
    • 失败:表示其他线程竞争锁,当前线程尝试使用自旋来获取锁。

2.7.2.2 轻量级锁的解锁

轻量级锁的锁释放逻辑其实就是获得锁的逆向逻辑,通过CAS 操作把线程栈帧中的 LockRecord 替换回到锁对象的MarkWord 中,如果成功表示没有竞争。如果失败,表示当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁。

image-20200717000707232

2.7.2.3 自旋锁

轻量级锁在加锁过程中,用到了自旋锁。所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。注意,锁在原地循环的时候,是会消耗 cpu 的,就相当于在执行一个啥也没有的 for 循环。所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短的时间就能够获得锁了。自旋锁的使用,其实也是有一定的概率背景,在大部分同步代码块执行的时间都是很短的。所以通过看似无意义的循环反而能提升锁的性能。但是自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而会消耗 CPU 资源。默认情况下自旋的次数是 10 次,可以通过 preBlockSpin 来修改。

在 JDK1.6 之后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。

如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

2.7.3 重量级锁

当轻量级锁膨胀到重量级锁之后,意味着线程只能被挂起阻塞来等待被唤醒了。

2.7.3.1 monitorenter与monitorexit

public class SynchronizedDemo {

    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized (object) {

        }
    }
}

javap -v SynchronizedDemo.class 获得字节码文件如下

image-20200716231713355

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权即尝试获取对象的锁。monitorexit 表示释放 monitor的所有权,使得其他被阻塞的线程可以尝试去获得这个监视器。

monitor 依赖操作系统的 MutexLock(互斥锁)来实现的, 线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能。

image-20200716232530657

任意线程对 Object(Object 由 synchronized 保护)的访问,首先要获得 Object 的监视器。如果获取失败,线程进入同步队列,线程状态变为 BLOCKED。当访问 Object 的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

2.7.3.2 ObjectMonitor

ObjectMonitor() {
      _header       = NULL;//markOop对象头
      _count        = 0;
      _waiters      = 0,   //等待线程数
      _recursions   = 0;   //重入次数
      _object       = NULL;
      _owner        = NULL;//指向获得ObjectMonitor对象的线程
      _WaitSet      = NULL;//处于wait状态的线程,会被加入到wait set;
      _WaitSetLock  = 0 ;
     _Responsible  = NULL ;
     _succ         = NULL ;
     _cxq          = NULL ;
     FreeNext      = NULL ;
     _EntryList    = NULL ;//处于等待锁block状态的线程,会被加入到entry set;
     _SpinFreq     = 0 ;
     _SpinClock    = 0 ;
     OwnerIsThread = 0 ;   // _owner is (Thread *) vs SP/BasicLock
     _previous_owner_tid = 0;// 监视器前一个拥有者线程的ID
  }

其中ObjectMonitor以下几个成员变量需要重点关注

  • _owner:指向获得ObjectMonitor对象的线程

  • _EntryList: 处于等待锁block状态的线程,会被加入到entry set

  • _WiatSet: 处于wait状态的线程,会被加入到wait set(调用同步对象wait方法)

多个线程同时访问一段同步代码时,首先会进入_EntryList集合,进行阻塞等待, 当线程获取到对象的monitor后进入owner区域,并把monitor中的_owner变量指向该线程,同时monitor中的计数器count自加一,若线程调用同步对象的wait()方法将释放当前持有的monitor,_owner变量重置为null,count自减一,同时该线程进入_WaitSet中等待唤醒,线程执行完同步代码块后,也将_Owner和count变量重置.

3. 线程间通信

多个线程间相互配合工作,需要依靠线程间通信。在 Object对象中 , 提供了wait/notify/notifyall,可以用于控制线程的状态。

image-20200716233756471

需要注意的是:这些方法都必须在 synchronized 同步关键字所限定的作用域中调用 , 否则会报错java.lang.IllegalMonitorStateException。

代码Demo如下:

public class WaitNotifyDemo {

    private static Object object = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            synchronized (object) {
                System.out.println("ThreadA---Start");
                try {
                    // 释放锁等待
                    object.wait();
                    System.out.println("ThreadA---Wait");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread threadB = new Thread(() -> {
            synchronized (object) {
                System.out.println("ThreadB---Start");
                // 唤醒ThreadA,需要注意的是(ThreadB需要执行完同步代码)
                object.notify();
                System.out.println("ThreadB---Wait");
            }
        });

        threadA.start();
        TimeUnit.SECONDS.sleep(2);
        threadB.start();
        threadA.join();
        threadB.join();
    }
}

输出如下:

ThreadA---Start
ThreadB---Start
ThreadB---Wait
ThreadA---Wait

image-20200716234542690

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

JAVA并发:线程安全与Synchorinzed 的相关文章

随机推荐

  • 自动注册appleid

    1 通过猴油注册脚本 用js填写表单 问题 由于apple官网采用了自己封装的mvvm框架 如果只是赋值的话 还不能把视图上的数据更新到model上 必须触发一下表单元素的input事件或者change事件完成model的更新 CSP网站安
  • 孤儿进程和僵尸进程

    作者 华清远见讲师 前段时间 由于研究经典面试题 把孤儿进程和僵尸进程也总结了一下 我们有这样一个问题 孤儿进程和僵尸进程 怎么产生的 有什么危害 怎么去预防 下面是针对此问题的总结与概括 一 产生的原因 1 一般进程 正常情况下 子进程由
  • vue 表格表头内容居中

    放入
  • elasticsearch的object类型和动态映射

    我们需要讨论的最后一个自然JSON数据类型是对象 object 在其它语言中叫做hash hashmap dictionary 或者 associative array 内部对象 inner objects 经常用于在另一个对象中嵌入一个实
  • node mysql高级用法_nodejs中mysql用法

    1 建立数据库连接 createConnection Object 方法 该方法接受一个对象作为参数 该对象有四个常用的属性host user password database 与php中链接数据库的参数相同 属性列表如下 host 连接
  • Xray使用教程

    简介 Xray是长亭科技开发的一款漏扫工具 支持多种扫描方式和漏洞类型 可自定义POC Proof of Concept 概念验证 即漏洞验证程序 俺是在 乌雲安全 看到了这个工具的使用 作为一个脚本小子初学者 这里做一下笔记 使用 web
  • NVDLA系列之C-model:cvif<99>

    NV NVDLA cvif cpp WriteRequest sdp2cvif void NV NVDLA cvif WriteRequest sdp2cvif uint64 t base addr uint64 t first base
  • 通过递归,实现数组转树

    一 为什么需要数组转树 当我们做后台管理系统时难免会遇到关于公司组织架构这样的模块 一个部门下会有好几个小部门 这时我们就可以运用树形图来更好地进行查看 下面简单举例 将数组 const arr id 1 pid 0 name 生鲜 id
  • linux安装分区详解lvm,Linux下LVM的配置详解

    LVM是Logical Volume Manager 逻辑卷管理器 的简写 它为主机提供了更高层次的磁盘存储管理能力 LVM可以帮助系统管理员为应用与用户方便地分配存储空间 在LVM管理下的逻辑卷可以按需改变大小或添加移除 另外 LVM可以
  • 【正点原子探索者STM32F407开发板例程连载+教学】第30章 SPI通信实验

    第三十章 SPI 实验 mw shl code c true 1 硬件平台 正点原子探索者STM32F407开发板 2 软件平台 MDK5 1 3 固件库版本 V1 4 0 mw shl code 本章我们将向大家介绍STM32F4的SPI
  • upload-labs通关(Pass-06~Pass-10)

    目录 Pass 06 Pass 07 Pass 08 Pass 09 Pass 10 Pass 06 上传sh php失败 burp中将抓到的包send to repeater 修改filename为sh xxx发现可以上传成功 说明是黑名
  • 线上常见问题排查之CPU过高

    目前应用程序基本是Java 所以需要登录docker容器内部执行jstack命令打印堆栈信息再分析 确认目标进程 执行top命令查看CPU占用情况 找出CPU占用高的进程ID PS 输入大写P即可按照CPU占比排序进程 即 Shift p
  • BMP图像读取数据

    我们在使用Windows的画图软件 画一张图 然后保存成24位位图BMP图像后 要对这幅图像进行一系列的格式转换之前 需要先将BMP里的数据提取出来 然后再保存成BGR888的图像 有两处细节要注意 第一 我们直接保存成BMP图片 不知道为
  • [项目管理-25]:高效沟通的利器,结构思考力与树形结构化表达

    作者主页 文火冰糖的硅基工坊 文火冰糖 王文兵 的博客 文火冰糖的硅基工坊 CSDN博客 本文网址 目录 前言 第1章 结构化思考力概述 1 1 非结构化思考力的问题与结构化思路力的好处 1 2 什么是结构化思路力 1 3 三大沟通层次 层
  • vue prop属性使用方法小结

    Prop 一 基本用法 Prop的基本用法很简单 作用是在子组件中接收父组件的值 父组件传值
  • Allegro如何调整丝印字号

    1 设置颜色管理器 先将所有的都关掉 如果要调节top层丝印 打开top层的焊盘和阻焊层 绿油层 打开板框 丝印和位号 2 选择 设置字号 在Edit中执行Change命令 Find中选择Text 在Options中勾选刚才修改的2号字体
  • JSON首字母大写问题

    一般如果json中的字段和类里的属性相同 则不需要修改 如果不同则可以在类的属性上添加 JsonProperty 或者 JSONField 但是如果JSON数据或者类中属性首字母大写的话 只能使用 JsonProperty 否则注入的值是n
  • Spring学习笔记总结

    第一章 引言 1 EJB存在的问题 2 什么是Spring Spring是一个轻量级的JavaEE解决方案 整合众多优秀的设计模式 轻量级 1 对于运行环境是没有额外要求的 开源 tomcat resion jetty 收费 weblogi
  • 浅谈音视频开发入门基础及进阶资源分享

    导言 音视频开发涉及的知识面比较广 知识点又相对独立琐碎 入门门槛相对较高 想要对音视频开发具有深入全面的了解 需要在行业深耕多年 本文将简单介绍音视频的采集 编解码 传输 渲染四个技术点并对涉及到的知识点和原理进行解释 希望你可以对音视频
  • JAVA并发:线程安全与Synchorinzed

    1 什么是线程安全问题 线程的合理使用能够提升程序的处理性能 主要有两个方面 第一个是能够利用多核 cpu 以及超线程技术来实现线程的并行执行 第二个是线程的异步化执行相比于同步执行来说 异步执行能够很好的优化程序的处理性能提升并发吞吐量