当前位置:

首页 > 编程开发 > Java中线程状态+线程安全问题+synchronized的用法是什么

Java中线程状态+线程安全问题+synchronized的用法是什么

java中的线程状态在操作系统层面,一个线程就两个状态:就绪和阻塞状态.但是java中为了在线程阻塞时能够更快速的知晓一个线程阻塞的原因,又将阻塞的状态进行了细化.NEW:线程对象已经创建好了,但是系统层面的线程还没创建好,或者说线程对象还没调用start()TERMINATED:系统中的线程已经销毁,但是代码中的线程对象还在,也就是run()跑完了,Thread对象还在RUNNABLE:线程位于就绪队列,随时都有可能被cpu调度执行TIMED_WAITING:线程执行过程中,线程对象调用了sleep()

本文目录

      java中的线程状态

      在操作系统层面,一个线程就两个状态:就绪和阻塞状态.

      但是java中为了在线程阻塞时能够更快速的知晓一个线程阻塞的原因,又将阻塞的状态进行了细化.

      Java中线程状态+线程安全问题+synchronized的用法是什么

      • NEW:线程对象已经创建好了,但是系统层面的线程还没创建好,或者说线程对象还没调用start()

      • TERMINATED:系统中的线程已经销毁,但是代码中的线程对象还在,也就是run()跑完了,Thread对象还在

      • RUNNABLE:线程位于就绪队列,随时都有可能被cpu调度执行

      • TIMED_WAITING:线程执行过程中,线程对象调用了sleep(),进入阻塞,休眠时间到了,就会回到就绪队列

      • BLOCKED:有一个线程将一个对象上锁(synchronized)之后,另一个线程也想给这个对象上锁,就会陷入BLOCKED状态,只有第一个线程将锁对象解锁了,后一个线程才有可能给这个对象进行上锁.

      • WAITING:搭配synchronized进行使用wait(),一旦一个线程调用了wait(),会先将所对象解锁,等到另一个线程进行notify(),之后wait中的线程才会被唤醒,当然也可以在wait()中设置一个最长等待时间,防止出现死等.

      线程安全问题案例分析

      多线程对同一变量进行写操作

      1. 概念:一串代码什么时候叫作有线程安全问题呢?首先线程安全问题的罪恶之源是,多线程并发执行的时候,会有抢占式执行的现象,这里的抢占式执行,执行的是机器指令!那一串代码什么时候叫作有线程安全问题呢?多线程并发时,不管若干个线程怎么去抢占式执行他们的代码,都不会影响最终结果,就叫作线程安全,但是由于抢占式执行,出现了和预期不一样的结果,就叫作有线程安全问题,出bug了!

      2. 典型案例:使用两个线程对同一个数进行自增操作10w次:

      public class Demo1 {
          private static int count=0;
          public static void main(String[] args) {
              Thread t1=new Thread(()->{
                  for(int i=0;i<50000;i++){
                      count++;
                  }
              });
              t1.start();
              Thread t2=new Thread(()->{
              t2.start();
              try {
                  t1.join();
                  t2.join();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println(count);
          }
      }
      //打印结果:68994

      显然预期结果是10w,但算出来就是6w多,这就是出现了线程安全问题.

      分析原因:

      仅针对每个线程的堆count进行自增的操作:首先要明白,进行一次自增的机器指令有三步:从主内存中把count值拿到cpu寄存器中->把寄存器中的count值进行自增1->把寄存器中的count值刷新到主内存中,我们姑且把这三步叫作:load->add->save

      我们假设就是在一个cpu上(画两个cpu好表示)并发执行两组指令(就不会出现同时load这样的情况了):

      Java中线程状态+线程安全问题+synchronized的用法是什么

      如出现上图的情况:

      Java中线程状态+线程安全问题+synchronized的用法是什么

      观察发现:两个线程都是执行了一次count++,但是两次++的结果却不如意,相当于只进行了一次自增,上述就是出现了线程安全问题了.

      并且我们可以预测出上述代码的结果范围:5w-10w之间!,为什么呢?

      上面两张图表示的是出现线程安全问题的情况,表现的结果就是两次加加当一次去用了,如果两个线程一直处于这样的状态(也是最坏的状态了),可不就是计算结果就是5w咯,那如果两个线程一直是一个线程完整的执行完load-add-save之后,另一个线程再去执行这样的操作,那就串行式执行了,可不就是10w咯.

      3.针对上述案例如何去解决呢?

      案例最后也提到了,只要能够实现串行式执行,就能保证结果的正确性,那java确实有这样的功能供我们使用,即synchronized关键字的使用.

      Java中线程状态+线程安全问题+synchronized的用法是什么

      也就是说:cpu1执行load之前先给锁对象进行加锁,save之后再进行解锁,cpu2此时才能去给那个对象进行上锁,并进行一系列的操作.此时也就是保证了load-add-save的原子性,使得这三个步骤要么就别执行,执行就一口气执行完.

      那你可能会提问,那这样和只用一个main线程去计算自增10w次有什么区别,创建多线程还有什么意义呢?

      意义很大,因为我们创建的线程很多时候不仅仅只是一个操作,光针对自增我们可以通过加锁防止出现线程安全问题,但是各线程的其他操作要是不涉及线程安全问题那就可以并发了呀,那此时不就大大提升了执行效率咯.

      4.具体如何加锁呢?

      此处先只说一种加锁方式,先把上述案例的问题给解决了再说.

      使用关键字synchronized,此处使用的是给普通方法加synchronized修饰的方法(除此之外,synchronized还可以修饰代码块和静态方法)

      class Counter{
          private int count;
          synchronized public void increase(){
              this.count++;
          }
          public int getCount(){
              return this.count;
          }
      }
      public class Demo2 {
          private static int num=50000;
          public static void main(String[] args) {
              Counter counter=new Counter();//此时对象中的count值默认就是0
              Thread t1=new Thread(()->{
                  for (int i = 0; i < num; i++) {
                      counter.increase();
                  }
              });
              t1.start();
      
              Thread t2=new Thread(()->{
                  for (int i = 0; i < num; i++) {
                      counter.increase();
                  }
              });
              t2.start();
      
              try {
                  t1.join();
                  t2.join();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
      
              System.out.println(counter.getCount());
          }
      }//打印10W

      内存可见性问题

      首先说明:这是有编译器优化导致的,其次要知道cpu读取变量时:先从主内存将变量的值存至缓存或者寄存器中,cpu计算时再在寄存器中读取这个值.

      当某线程频繁的从内存中读取一个不变的变量时,编译器将会把从内存获取变量的值直接优化成从寄存器直接获取.之所以这样优化,是因为,cpu从主内存中读取一个变量比在缓存或者寄存器中读取一个变量的值慢成千上万倍,如果每每在内存中读到的都是同一个值,既然缓存里头已经有这个值了,干嘛还大费周折再去主内存中进行获取呢,直接从缓存中直接读取就可以了,可提升效率.

      但是:一旦一个线程被优化成上述的情况,那如果有另一个线程把内存中的值修改了,我被优化的线程还傻乎乎的手里拿着修改之前的值呢,或者内存中的变量值被修改了,被优化的线程此时已经感应不到了.

      具体而言:

      public class Demo3 {
          private static boolean flag=false;
          public static void main(String[] args) {
              Thread t1=new Thread(()->{
                  while(!flag){
                      System.out.println("我是优化完之后直接读取寄存器中的变量值才打印的哦!");
                  }
              });
              t1.start();
      
              flag=true;
              System.out.println("我已经在主线程中修改了标志位");
          }
      }

      运行上述代码之后,程序并不会终止,而是一直在那打印t1线程中的打印语句.

      如何解决上述问题:

      引入关键字volatile:防止内存可见性问题,修饰一个变量,那某线程想获取该变量的值的时候,只能去主内存中获取,其次它还可以防止指令重排序,指令重排问题会在线程安全的单例模式(懒汉)进行介绍.具体:

      public class Demo3 {
          private static volatile boolean flag=false;
          public static void main(String[] args) {
              Thread t1=new Thread(()->{
                  while(!flag){
                      System.out.println("我是优化完之后直接读取寄存器中的变量值才打印的哦!");
                  }
              });
              t1.start();
      
              try {
                  Thread.sleep(1);//主线程给t1留有充足的时间先跑起来
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              flag=true;
              System.out.println("我已经在主线程中修改了标志位");
          }
      }
      //打印若干t1中的打印语句之后,主线程main中修改标志位之后,可以终止t1

      注意:上述优化现象只会出现在频繁读的情况,如果不是频繁读,就不会出现那样的优化.

      指令重排序问题

      生活案例:买菜

      Java中线程状态+线程安全问题+synchronized的用法是什么

      如果是傻乎乎的按照菜单从上到下的去买菜,从路线图可以看出,不必要的路是真的没少走.

      如果执行代码时,编译器认为某些个代码调整一下顺序并不会影响结果,那代码的执行顺序就会被调整,就比如可以把上面买菜的顺序调整成:黄瓜->萝卜->青菜->茄子

      单线程这样的指令重排一般不会出现问题,但是多线程并发时,还这样优化,就容易出现问题

      针对这样的问题,如果是针对一个变量,我们可以使用volatile修饰,如果是针对代码块,我们可以使用synchronized.

      synchronized的用法

      • synchronized起作用的本质

      • 修饰普通方法

      • 修饰静态方法

      • 修饰代码块

      synchronized起作用的本质

      因为我们知道java中所有类都继承了Object,所以所有类都包含了Object的部分,我们可以称这继承的部分是"对象头",使用synchronized进行对象头中的标志位的修改,就可以做到一个对象的锁一个时刻只能被一个线程所持有,其他线程此时不可抢占.这样的设置,就好像把一个对象给锁住了一样.

      修饰普通方法

      如前述两个线程给同一个count进行自增的案例.不再赘述.此时的所对象就是Counter对象

      修饰静态方法⚡️

      与普通方法类似.只不过这个方法可以类名直接调用.

      修饰代码块

      首先修饰代码块需要执行锁对象是谁,所以这里可以分为三类,一个是修饰普通方法的方法体这个代码块的写法,其次是修饰静态方法方法体的写法,最后可以单独写一个Object的对象,来对这个Object对象进行上锁.

      class Counter{
          private int count;
          public void increase(){
              synchronized(this){
                  count++;
              }
          }
          public int getCount(){
              return this.count;
          }
      }
      class Counter{
          private static int count;
          public static void increase(){
              synchronized(Counter.class){//注意这里锁的是类对象哦
                  count++;
              }
          }
          public int getCount(){
              return this.count;
          }
      }
      class Counter{
          private static int count;
          private static Object locker=new Object();
          public static void increase(){
              synchronized(locker){
                  count++;
              }
          }
          public int getCount(){
              return this.count;
          }
      }

      注意:java中这种随手拿一个对象就能上锁的用法,是java中一种很有特色的用法,在别的语言中,都是有专门的锁对象的.

      Conclusion

      java中的线程状态,以及如何区分线程安全问题 罪恶之源是抢占式执行多线程对同一个变量进行修改,多线程只读一个变量是没有线程安全问题的修改操作是非原子性的内存可见性引起的线程安全问题指令重排序引起的线程安全问题 synchronized的本质和用法

      1.java中的线程状态,以及如何区分
      2.线程安全问题

      • 罪恶之源是抢占式执行

      • 多线程对同一个变量进行修改,多线程只读一个变量是没有线程安全问题的

      • 修改操作是非原子性的

      • 内存可见性引起的线程安全问题

      • 指令重排序引起的线程安全问题

      3.synchronized的本质和用法

      本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系bd@zhengruan.com
      作者最新文章
      编程开发 Java
      相关文章 更多
      Java单元测试入门:使用JUnit 5编写第一个测试用例
      Java单元测试入门:使用JUnit 5编写第一个测试用例

      面向初学者的JUnit 5实用入门指南。从测试概念、Maven依赖配置,到手把手编写首个单元测试用例及断言验证,助你掌握Java单元测试的核心流程。

      Java变量未初始化报错解析:局部变量与成员变量默认值差异
      Java变量未初始化报错解析:局部变量与成员变量默认值差异

      深入解析Java中局部变量未初始化编译报错与成员变量自动赋予默认值的机制差异,涵盖JVM内存分配、编译期确定性赋值检查与实际排查修复方案。

      Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat
      Java获取当前时间并格式化输出:DateTimeFormatter与SimpleDateFormat

      详细讲解在Java中获取当前系统时间并进行格式化输出的两种核心方案:基于Java 8全新日期时间API的DateTimeFormatter与传统遗留的SimpleDateFormat,包含完整代码示例、线程安全机制对比及生产选型建议。

      Java中final与static final常量的定义及初始化规则
      Java中final与static final常量的定义及初始化规则

      深入解析Java中final变量与static final常量的定义、内存语义、初始化时机及合法赋值路径,涵盖编译期常量优化与实战避坑指南。

      Java抽象方法与接口方法的区别及选型建议
      Java抽象方法与接口方法的区别及选型建议

      深入对比Java中抽象类抽象方法与接口方法的语法演进、设计意图及运行期差异,并提供清晰的架构选型决策流程与实战范例。

      Java实现HTTP请求与JSON解析的完整代码示例
      Java实现HTTP请求与JSON解析的完整代码示例

      本文提供Java标准库HttpClient发起HTTP GET/POST请求并结合Jackson进行JSON数据反序列化与序列化的完整代码示例与工程实践指南。

      Java递归出现StackOverflowError的原因分析及栈空间优化方案
      Java递归出现StackOverflowError的原因分析及栈空间优化方案

      详细分析Java递归调用出现StackOverflowError的原因,结合代码说明线程栈、栈帧和递归深度之间的关系,并介绍终止条件排查、迭代改写、减少栈帧开销以及通过-Xss调整JVM栈空间的方法。

      Java空指针异常(NPE)的代码排查与修复实例
      Java空指针异常(NPE)的代码排查与修复实例

      针对Java中常见的NullPointerException(NPE),本文从堆栈日志定位、典型代码触发场景、具体修复代码对比到防御体系进行系统讲解,提供实用且易上手的排查与修复方案。

      selenium上传文件教程本地文件路径设置方法
      selenium上传文件教程本地文件路径设置方法

      深入解析Selenium处理文件上传的技术细节,涵盖标准HTML控件的路径直接注入法与非标准控件的系统级模拟方案,提供Windows与Linux环境下的路径设置最佳实践及常见异常排查。

      大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景
      大数据开发语言选型:Java对比Scala与Python的优缺点及适用场景

      针对大数据工程场景,系统对比Java、Scala与Python在执行性能、生态结合、开发效率和维护成本上的核心差异,给出清晰的架构选型依据与落地方案。

      查看更多
      精品专题 更多
      装机必备
      装机必备

      正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

      Windows
      Windows

      正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

      PDF教程
      PDF教程

      正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

      Mac软件 更多
      讯飞智作 网页版
      讯飞智作 网页版
      Windows/ macOS/ Linux

      讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

      火山引擎 网页版
      火山引擎 网页版
      Windows/ macOS/ Linux

      火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

      Aperty macOS版
      Aperty macOS版
      macOS

      Aperty 是 Skylum 推出的 AI 人像修图软件,支持自然肤质修饰、瑕疵处理、面部与身体调整、预设及批量同步。

      WINDOWS 更多
      讯飞智作 网页版
      讯飞智作 网页版
      Windows/ macOS/ Linux

      讯飞智作是一款基于语音合成技术、虚拟人驱动技术与数字人形象声音构建能力的 AI 数字人音视频创作平台。

      火山引擎 网页版
      火山引擎 网页版
      Windows/ macOS/ Linux

      火山引擎是字节跳动旗下企业级云与AI服务平台。聚焦大模型核心能力,依托字节自研豆包多模态大模型,凭借火山方舟一站式平台,提供模型精调、推理部署、智能应用搭建等全链路服务,结合高性能AI算力,助力各行业快速落地大模型数字化与智能化应用。

      PixTV 网页版
      PixTV 网页版
      Windows

      PixTV 是一款在浏览器中使用的 AI 视觉内容创作平台,提供图片、视频、音频、文字生成、AI 分镜、局部重绘、提示词反推和无限画布等工具,适合短剧、漫剧、广告及社交媒体内容制作。