Java中线程的状态和线程安全问题

目录

线程的状态

NEW:安排了工作,但还没有开始行动

RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作

BLOCKED: 这几个都表示排队等着其他事情

WAITING: 这几个都表示排队等着其他事情

TIMED_WAITING: 这几个都表示排队等着其他事情

TERMINATED: 工作完成了

线程安全

怎么去加锁呢

线程不安全的原因

1.【根本原因】线程是抢占执行的

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

3.针对变量的操作不是原子的

4.内存可见性

5.指令重排序

synchronized的使用方式

1.直接修饰普通方法

2.修饰一个代码块

3.修饰一个静态方法


提到线程状态,首先我们来说说进程状态,我们所知的进程状态有就绪和堵塞,这里的状态就决定了系统按照啥样的态度去调度这个进程,但更常见的情况下,一个进程包含了多个线程,所以,所谓的状态,其实是与线程绑定的,接下来我们就进入正题,来说说线程的状态。

线程的状态

在Java的Thread类中对于线程的状态,有进一步的细分,线程的状态是一个枚举类型:Thread.State

NEW:安排了工作,但还没有开始行动

RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作


BLOCKED: 这几个都表示排队等着其他事情


WAITING: 这几个都表示排队等着其他事情


TIMED_WAITING: 这几个都表示排队等着其他事情


TERMINATED: 工作完成了

那么这几个线程状态间存在什么关系呢,下面给大家提供一个线程的状态转换简图

Java中线程的状态和线程安全问题_第1张图片

线程安全

首先我们是如何区分线程安不安全的呢?

首先,我们要知道在操作系统调度线程的时候是随机的(抢占式执行),而正是这个随机性,才导致出了很多问题,当我们因为这样随机性的调度线程引入了bug,这样我们称之为线程不安全

反之,这样调度没有引入bug,则称之为线程安全

下面,我们来用一个典型的线程不安全的案例来说明下:

创建两个线程,让两个线程对同一个变量自增50000次,看最后这个值会是多少,绝大多数朋友感觉会是100000,那我们接下来动手看看这个值是多少

Java中线程的状态和线程安全问题_第2张图片

Java中线程的状态和线程安全问题_第3张图片

通过这段代码我们完成了上述的要求,接下来我们来运行下看看结果 

 

 我们接连运行了三次,发现每次的值都不一样,并且每次都是小于100000,大于50000的,那这是什么原因呢?那我们需要了解这个count++ 到底做了什么

原来,在CPU的角度上来看,count++ 可以分为三个步骤

1.把内存中的count值加载到CPU的寄存器中。(load)

2.给寄存器上的count值++。(add)

3.将寄存器上的值写回到内存上。(save)

这样正因前面说了线程是抢占式执行,而且count++执行分为三个命令,这样就充满了随机性,下面我们来说说可能出现的结果

Java中线程的状态和线程安全问题_第4张图片

当我们按照这个进行时count最后得到的值会是2,这个是正常执行的,没有bug

Java中线程的状态和线程安全问题_第5张图片

 而当是这样执行的时候,最后会发现count只是为1,相当于+了两次,但只有一次生效了

还有种种类似的情况进而导致了最后我们得到的结果不为100000,那既然遇到了这样的问题,我们就需要去想办法去解决他,而解决这样的办法的方法就是给他加锁,通过加锁,就让每次++必须三个步骤执行完了,才能下一个++。

加锁就相当于我们生活中去ATM取钱,进去后完成一系列取钱所需要的步骤,外面的人才能进来,取钱中途,其他人是不能插入的

 那可能会有兄弟想,拿着加锁了,不就相当于串行了,那这和单线程不没有啥区别嘛,确实,加锁了确实和单线程一样,但是,在我们实际开发中,一个线程要做的事远不止于此,要做的任务很多很多,而我们只需要把会引起线程不安全的任务加锁即可,其他还是正常并行并发。

怎么去加锁呢

在Java中加锁的方法有很多种,我们最常使用的是synchronized这个关键字,有点长,大家要记一下了,给方法直接加上synchronized,此时进入方法就会自动加锁,当执行完这个方法后会自动解锁,当一个线程加锁后,另一个线程想尝试触发锁,就会进入阻塞状态,阻塞状态会一直持续到加锁线程释放锁为止。

Java中线程的状态和线程安全问题_第6张图片

Java中线程的状态和线程安全问题_第7张图片

当我们给++方法加锁后发现最后的答案就是100000了。

 

那么有人会想,那不直接把所有的线程全部加锁就行了,那这样就和单线程没啥区别了,多线程的并发能力就形同虚设了,那么啥样的线程会有线程不安全的的可能呢?那么我们就需要去了解下线程不安全的原因。

 

 

线程不安全的原因

1.【根本原因】线程是抢占执行的

这个原因是线程不安全的根本原因,万恶之源,线程调度是充满随机性的,但面对这个原因,我们也没有解决办法。

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

如果是多个线程对多个不同的变量进行修改,那倒没啥事,或者多个线程对一个变量只进行读操作,也没有啥事,但如果像我们上面举的例子,对他进行++操作,那么就会导致线程不安全。

3.针对变量的操作不是原子的

这个就和第二个原因类似,加锁操作,就是把好几个操作打包成原子的

4.内存可见性

这个是什么呢,我们来举个例子大家就明白了,一个线程对其疯狂执行读操作,另一个线程在合适的时候,对他进行一次读操作

Java中线程的状态和线程安全问题_第8张图片

 当isQuit为0的时候就会一直循环,而当main线程接收到一个不为0的数字时就会结束循环,当我们输入1时按理来说会输出“结束”和“搞起”,那我们来看看是不是这样呢?

Java中线程的状态和线程安全问题_第9张图片

但结果并没有向我们想的那样,他还是在一直循环,没有退出循环的意思 ,那我们来慢慢说下其中的原因。

原来这是Java编译器进行代码优化产生的效果,当t 这个线程在循环读时,读取内存的速度相比于读取寄存器的速度慢了很多,就会非常低效,而且main线程迟迟没有修改,t线程独到的内容又时始终一样,这是,t线程就有了个大胆的想法,就不再从内存中读取数据了,而是直接从寄存器中读取数据,这时当我们输入一个退出循环的数时t线程已经不在内存中读取数据了,就感知不到了

这里我们就要提出一个关键字来用于出现内存可见性的问题了,volatile来保证内存可见性,但我们用synchronized也可以保证内存可见性。

那我们来谈下synchronized关键字和volatile关键字的用法和区别

1.使用synchronized关键字

synchronized不光能保证指令的原子性,同时也能保证内存的可见性,被synchronized包裹起来的代码,编译器就不敢亲易像前面那样直接读取寄存器上的值,不读内存中的,相当于手动禁用了优化。

2.使用volatile关键字

volatile和原子性无关,但是他能保证原子性,禁止编译器做出上面说的优化,编译器每次执行判定相等,都会冲内存中读取值。

Java中线程的状态和线程安全问题_第10张图片

Java中线程的状态和线程安全问题_第11张图片 

 这时我们看到就可以继续从内存中读取,保证了内存的可见性

5.指令重排序

指令重排序也是编译器优化的一种,就是当我们写代码时,一些代码谁在前谁在后都无所谓,但是编译器就会智能的调整代码的前后顺序,如果是单线程的程序,编译器统称判定优化很准,但是当我们使用多线程的时候,编译器就可能参生误判,这时,我们就又需要用到synchronized,他不光能保证原子性,同时还能保证可见性 ,还能禁止指令重排序,几乎是万能的了。

既然synchronized这么常见,那我们来讲讲synchronized的使用方式

synchronized的使用方式

1.直接修饰普通方法

当我们使用synchronized的时候,本质上是针对某个对象进行加锁,此时这个对象就是this

加锁操作就是在设置this的对象头的标志位,当两个线程尝试对一个对象进行加锁的时候,才有竞争,如果两个线程对不同的对象加锁,那么就不存在竞争了。

那么什么是对象头呢?

在Java中,每个类都是继承自Object,每个new出来的实例,里面一方面包含了你自己安排的属性,另一方面包含了“对象头”,对象的一些元数据,当我们new出一个对象时,这个对象会有一块额外的内存空间,这额外的内存空间就是“对象头”,我们说的加锁操作,就是给这个对象的对象头里设置了一个标志位。

2.修饰一个代码块

需要显示指定针对那个对象加锁,(Java中任意对象都可以作为锁对象),这种随手拿个对象都能作为锁对象的用法,是Java中非常有特色的设定(在其他的语言中都没有这么弄,正常的语言都是有专门的的锁对象的)。

3.修饰一个静态方法

相当于针对当前类的类对象进行加锁

你可能感兴趣的