浅谈java线程状态与线程安全解析

目录
  • 1.线程的几种状态
    • 1.1 线程的状态
    • 1.2 线程状态的转移
  • 2.有关线程安全问题
    • 2.1 一个简单的例子
    • 2.2 造成线程不安全的原因

1.线程的几种状态

1.1 线程的状态

以下就是我们线程所有的状态和意义:

NEW 已经创建Thread但未创建线程
RUNNABLE 可工作的. 又可以分成正在工作中和即将开始工作
BLOCKED 等待锁(阻塞状态)
WAITING 调用wati方法(阻塞状态)
TIMED_WAITING 调用sleep方法(阻塞状态)
TERMINATED 系统线程执行完毕已销毁,但Thread还存在

注意:

BLOCKED 表示等待获取锁, WAITING 和 TIMED_WAITING 表示等待其他线程发来通知.

TIMED_WAITING 线程在等待唤醒,但设置了时限; WAITING 线程在无限等待唤醒

1.2 线程状态的转移

各线程之间的转移关系可以简化成下图:

 关于yield方法:

在多线程中我们存在一个yield方法可以让线程在就绪队列中重新”排队“,不改变线程状态。相当于你去帮别人排队,但是轮到你了那个人还没回来,你就就让原本排在你后面的人换到你的位置上,但你仍然处于排队状态。这种”大公无私“的行为可以类比到我们的yield方法帮助我们理解。

public class Demo{
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() { while (true) {
                System.out.println("张三");
                // 先注释掉, 再放开
                //Thread.yield();
            }
            }
        }, "t1");
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("李四");
                }
            }
        }, "t2");
        t2.start();

    }
}

可以看到:

1. 不使用 yield 的时候, 张三李四大概五五开

2. 使用 yield 时, 张三的数量远远少于李四

结论: yield 不改变线程的状态, 但是会重新去排队.

2.有关线程安全问题

2.1 一个简单的例子

// 创建两个线程, 让这俩线程同时并发的对一个变量, 自增 5w 次. 最终预期能够一共自增 10w 次.
class Counter {
    // 用来保存计数的变量
    public int count;

    public void increase() {
        count++;
    }
}

public class Demo {
    // 这个实例用来进行累加.
    // public static Counter counter = new Counter();

    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count: " + counter.count);
    }
}

大家先看到以上的代码,意思很简单,用两个线程对同一个变量进行自增操作,运行的结果如下

看起来不太对,我们再试一次

结果有了变化,但仍然不是我们想要的结果,是什么导致了5w+5w<10w呢?其中一个原因就是线程的随机调度和改操作不具有原子性。 这些概念我们下面会详细讲,这里我们先简单了解一下。

首先我们的自增操作在cpu内其实分为三步:

1.LOAD:cpu从内存中读取数据到寄存器

2.ADD:在寄存器内实现自增

3.SAVE:将寄存器的数据写回内存中

而我们已经知道cpu对于线程调度我们可以理解为是随机的,所以会有很多种可能,比如下图

其中纵轴代表运行时间,这里我们可以看到两个线程相当于互不影响,线程1完成自增操作后又将数据写回内存由线程2再去操作,这种情况下是没有问题的。但是也可能是下面的一种情况

此时线程1还没有将自增后的数据写回内存而线程2就已经将要修改的数据读入了寄存器,此时相当于线程2读到了那个还未自增的数据,相当于两个线程对同一个数进行了自增,所以此时相当于只自增了一次。其实情况还有很多,这里我们仅举例比较经典的例子。所以这也能够解释为什么结果大于5w而小于10w了。

2.2 造成线程不安全的原因

2.2.1 操作系统的随机调度/抢占式运行

这种是操作系统内核就已经决定的,我们无能为力。类似于我们上一个例子,就是因为线程的随机调度和操作不具有原子性造成的。

2.2.2 操作不具有原子性

什么是原子性

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入 房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性 的。转换成代码我们可以理解成只具有一条指令的操作。

当然这个问题我们可以通过加锁操作解决(以后会提到)。

一条 java 语句不一定是原子的,也不一定只是一条指令,比如我们上面提到的自增操作。

不保证原子性会给多线程带来什么问题

如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。 这点也和线程的抢占式调度密切相关. 如果线程不是 "抢占" 的, 就算没有原子性, 也问题不大.

2.2.3 多个线程修改同一个变量

1.一个线程修改变量没事

2.多个线程同时一个变量也没事

3.多个线程同时修改不同变量也没有问题

唯独需要注意多个线程修改同一个变量,如果不加以处理可能会造成我们之前讲到的例子的问题

2.2.4 内存可见性问题

jvm中规定了java的内存模型

线程之间的共享变量存在 主内存 (Main Memory).

每一个线程都有自己的 "工作内存" (Working Memory) .

当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.

当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.

正是因为这种机制,所以可能会出现下面的问题:

由于每个线程有自己的工作内存, 这些工作内存中的内容相当于同一个共享变量的 "副本". 此时修改线程 1 的工作内存中的值, 线程2 的工作内存不一定会及时变化.通俗的讲就是 线程1针对工作内容修改了数据,而线程2此时并不一定能够及时同步修改的数据,所以可能会引发各种问题。

2.2.5 指令重排序

所谓指令重排序是指jvm针对我们的代码,可能会在保证逻辑不变的情况下去调整指令执行的顺序以达到运行效率更高的效果。这种情况在单线程的情况下可以很好实现,而在多线程的情况下就可能会出现bug,导致程序逻辑改变。比如对于下面这行代码:

Test t=new Test();

它其实总共有三个步骤:

1.创建内存空间

2.往这个内存空间构造一个对象

3.将这个内存引用赋给t

在单线程的情况下2,3互换并不会有上面影响,但假如在多线程情况下我们按1,3,2来执行,当执行到3时t为非null,此时线程2读取t,但是却发现是一个无效对象。

到此这篇关于浅谈java线程状态与线程安全解析的文章就介绍到这了,更多相关java线程状态与线程安全内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 浅谈JAVA 线程状态中可能存在的一些误区

    BLOCKED 和 WAITING 的区别 BLOCKED 和 WAITING 两种状态从结果上来看,都是线程暂停,不会占用 CPU 资源,不过还是有一些区别的 BLOCKED 等待 Monitor 锁的阻塞线程的线程状态,处于阻塞状态的线程正在等待 Monitor 锁进入 synchronized   Block 或者 Method ,或者在调用 Object.wait 后重新进入同步块/方法.简单的说,就是线程等待 synchronized 形式的锁时的状态 下面这段代码中, t1 在等待

  • 浅谈java线程状态与线程安全解析

    目录 1.线程的几种状态 1.1 线程的状态 1.2 线程状态的转移 2.有关线程安全问题 2.1 一个简单的例子 2.2 造成线程不安全的原因 1.线程的几种状态 1.1 线程的状态 以下就是我们线程所有的状态和意义: NEW 已经创建Thread但未创建线程 RUNNABLE 可工作的. 又可以分成正在工作中和即将开始工作 BLOCKED 等待锁(阻塞状态) WAITING 调用wati方法(阻塞状态) TIMED_WAITING 调用sleep方法(阻塞状态) TERMINATED 系统线

  • 浅谈Java线程间通信之wait/notify

    Java中的wait/notify/notifyAll可用来实现线程间通信,是Object类的方法,这三个方法都是native方法,是平台相关的,常用来实现生产者/消费者模式.先来我们来看下相关定义: wait() :调用该方法的线程进入WATTING状态,只有等待另外线程的通知或中断才会返回,调用wait()方法后,会释放对象的锁. wait(long):超时等待最多long毫秒,如果没有通知就超时返回. notify() :通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提

  • 浅谈java常用的几种线程池比较

    1. 为什么使用线程池 诸如 Web 服务器.数据库服务器.文件服务器或邮件服务器之类的许多服务器应用程序都面向处理来自某些远程来源的大量短小的任务.请求以某种方式到达服务器,这种方式可能是通过网络协议(例如 HTTP.FTP 或 POP).通过 JMS 队列或者可能通过轮询数据库.不管请求如何到达,服务器应用程序中经常出现的情况是:单个任务处理的时间很短而请求的数目却是巨大的. 构建服务器应用程序的一个简单模型是:每当一个请求到达就创建一个新线程,然后在新线程中为请求服务.实际上对于原型开发这

  • 浅谈Java关闭线程池shutdown和shutdownNow的区别

    目录 前言 项目环境 1.线程池示例 2.shutdown 3.isShutdown 4.isTerminated 5.awaitTermination 6.shutdownNow 7.shutdown 和 shutdownNow 的区别? 前言 本章分为两个议题 如何正确关闭线程池 shutdown 和 shutdownNow 的区别 项目环境 jdk 1.8 github 地址:https://github.com/huajiexiewenfeng/java-concurrent 本章模块:

  • 浅谈java线程中生产者与消费者的问题

    一.概念 生产者与消费者问题是一个金典的多线程协作的问题.生产者负责生产产品,并将产品存放到仓库:消费者从仓库中获取产品并消费.当仓库满时,生产者必须停止生产,直到仓库有位置存放产品:当仓库空时,消费者必须停止消费,直到仓库中有产品. 解决生产者/消费者问题主要用到如下几个技术:1.用线程模拟生产者,在run方法中不断地往仓库中存放产品.2.用线程模拟消费者,在run方法中不断地从仓库中获取产品.3  . 仓库类保存产品,当产品数量为0时,调用wait方法,使得当前消费者线程进入等待状态,当有新

  • 浅谈Java线程池是如何运行的

    异步编程工具在Android开发中目前最被推荐的就是Kotlin协程,在引入Kotlin协程机制前,除了响应式扩展(RxJava)兼任异步编程工具外,Java API中线程与线程池就是最重要异步编程手段.而对于Android平台的Kotlin协程实现来说,依然使用的是线程池来作为任务执行的载体,所以可以将Android平台的Kotlin协程简单的理解是对线程池的一种高度封装. Executors.newFixedThreadPool(10).asCoroutineDispatcher() Dis

  • 浅谈Java线程池的7大核心参数

    前言 java中经常需要用到多线程来处理一些业务,我不建议单纯使用继承Thread或者实现Runnable接口的方式来创建线程,那样势必有创建及销毁线程耗费资源.线程上下文切换问题. 同时创建过多的线程也可能引发资源耗尽的风险,这个时候引入线程池比较合理,方便线程任务的管理. java中涉及到线程池的相关类均在jdk1.5开始的java.util.concurrent包中,涉及到的几个核心类及接口包括: Executor.Executors.ExecutorService.ThreadPoolE

  • 浅谈Java中ArrayList线程不安全怎么办

    ArrayList线程不安全怎么办? 有三种解决方法: 使用对应的 Vector 类,这个类中的所有方法都加上了 synchronized 关键字 就和 HashMap 和 HashTable 的关系一样 使用 Collections 提供的 synchronizedList 方法,将一个原本线程不安全的集合类转换为线程安全的,使用方法如下: List<Integer> list = Collections.synchronizedList(new ArrayList<>());

  • 浅谈Timer和TimerTask与线程的关系

    1. Timer是一个定时器,它可以根据指定的时间,指定的执行周期来执行固定的任务TimerTask,例子如下: Timer与线程的关系,在Timer源代码中可现如下代码: 1. 以下为Timer的默认构造方法,起调用了自身的一个有参构造函数: 2.有参构造函数代码如下: 3. 有参构造函数设置了当前线程的名字并启动该线程,线程代码如下: 4. TimerThread类的源代码如下: 由以上代码可以看出Timer其实就是一个Thread类的子类. TimerTask与线程的关系: 1. 以下为T

随机推荐