前言:上一篇说到线程的实现方式,停止/复位,状态之间的转换,这次我们就来讲解为什么 wait(),notify(),notifyAll()方法在Object类中,而sleep()在Thread中以及什么是线程的安全, 我们直接开始
(一).wait(),notify(),notifyAll()方法为什么在Object类中?
1.因为在我们Java中每个对象都有一把monitor的锁,由于每个对象都可以上锁,这就要求在每一个对象头有一个用来保存锁信息的位置,这个锁是对象级别的,不是线程级别的,wait(),notify(),notifyAll()这三个都是属于锁级别的操作,他们的锁属于对象,所以把他们定义在Object类中,因为Object类是所有对象的父类。
2. 如果把wait(),notify(),notifyAll()定义在Thread类中会有很大的局限性,比如一个线程可以持有多把锁,wait()定义在Thread类中,那么我们如何一个线程持有多把锁了,又如何知道线程等待的是那个对象的锁了,既然我们是让当前线程去等待某个对象的锁,自然要通过操作对象来实现,而不是操作线程。
(二).wait()和sleep()方法的异同
相同之处:1.它们都可以让线程阻塞2.并且都可以响应interrupt中断,并抛出InterruptException异常
不同点:1.wait()方法必须在synchronized保护的代码块中,sleep不需要
2.sleep()必须要定义一个时间,到了时间会自动恢复,而wait()如果没有定义时间则会永久等待,除非通过notify/notifyall方法去唤醒,并不会主动恢复
3.wait()在Object类中,sleep()在Thread类中
线程安全
相信小伙伴们在日常工作中,经常会听到线程安全这四个字,那么线程安全到底是什么? 我们一起来了解到底什么是线程安全
线程安全总共分为三个问题: 原子性,可见性,有序性
原子性:是指一个操作是不可中断的,即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。下面我们来看一段代码
这段代码最后输出的值按理说是20000,因为线程1启动线程给i加了一万的值,线程2亦是如此,但是最终的结果真的是20000吗?答案是不确定,可能是20000,也可能是14444,也可能是19980,这是为什么了?
因为在多线程的情况下,CPU的调度是以时间片为单位进行分配的,每个线程得到一定量的时间片,但是如果线程的时间片耗尽,它将会被暂停执行并让出CPU资源给其他线程,这样就有可能发生线程的安全问题。比如i++操作,表面上看只有一行代码,但实际上他并不是一个原子操作。
可见性:说到可见性,我们必须得从硬件方面说起,大家都知道一台计算机当中最核心的组件是CPU,内存,以及I/O设备,这三者在处理速度方面CPU最快,内存次之,最后是I/O设备,而我们为了提升性能。CPU从单核变为多核,当然还有一些其他的变化,在本文就不详细介绍,而我们为了最大化利用CPU去提升性能,引入了CPU高速缓存,什么是CPU高速缓存,大家自行去查找,而我们引入了CPU高速缓存就产生了一个问题,缓存的一致性问题,什么是缓存一致性问题? 首先我们有CPU高速缓存,每个CPU的处理过程就是将用到的数据缓存在CPU高速缓存中,在CPU进行计算时,直接从CPU高速缓存读取数据并且计算完成之后再写入到缓存中。在整个运算完成之后,在把缓存中的数据同步到主内存,但是在多个CPU情况下,每个线程可能会运行到不同的CPU中并且每个线程都拥有自己的高速缓存,同一份数据可能会被缓存到多个CPU中,如果在不同CPU中运行的不同线程看到同一份内存的缓存值不一样就会存在缓存一致性问题。为了解决这个问题,在CPU层面做了很多事情,提供了两种解决方案
1.总线锁
总线锁简单来说就是在多个cpu下,当其中一个处理器对共享内存进行操作的时候会在总线上发出LOCK信号,这个信号使得其他处理器无法通过总线访问到共享内存,总线锁定把CPU和内存之间的通信给锁定了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以开销比较大,那么我们要怎么去优化了?在这个方面我们只需要保证被多个CPU缓存的同一份数据是一致的就行,所以我们引入了缓存锁。
2.缓存锁
缓存锁的核心机制是基于缓存一致性协议 ,他为了达到数据访问的一致,需要各个处理器在访问缓存时遵循一些协议,最常见的就是MESI协议,具体可自行百度,但是MESI协议也会给我们带来一些问题,就是说各个CPU缓存行的状态是通过信息来传递的,比如CPU0要对一个在缓存中共享变量进行操作,首先需要发送一个失效的信息给缓存了这个数据的其他CPU,并且需要他们的回执,而我们的CPU0在这段时间就会处于阻塞状态,为了避免阻塞时间带来的资源浪费,我们又引入了Store Buffers。
引入了Stroe Buffers的情况比之前稍微好一点,但是我们的CPU又会出现乱序问题,也会带来可见性问题,所以在我们的硬件层面是无法去优化的,在这个时候我们在CPU层面提供了内存屏障,什么是内存屏障?
内存屏障:内存屏障就是将Store Buffers中的指令写入到内存,从而使得其他访问共享内存的线程的可见性。他的作用是可以防止CPU对内存的乱序访问来保证共享数据在多线程并行执行的可见性。
那么我们如何加这个内存屏障了?在之前说到过使用volatile关键字去保证程序的可见性和有序性,那么他就会生成一个Lock的汇编指令,这个指令就相当于实现了一种内存屏障。说到内存屏障不得不说到JMM内存模型。
JMM内存模型:可见性问题的根本原因是缓存和重排序,而JMM模型实际上就是提供了合理的禁用缓存以及重排序,所以JMM模型的作用就是解决可见性和有序性,那么JMM模型是如何去解决可见性和有序性的,这些方法大家都很熟悉如:volatile,synchronized,final,那么JMM如何解决顺序的一致性问题了,说到这里就会产生线程安全的第三个原因,有序性
有序性:为了提高程序中的执行性能,我们的编译器和处理器都会对其进行优化,就会产生重排序的问题,编译器的重排序,JMM提供了禁止特定类型的编译器重排序,处理器重排序,JMM会要求编译器生成指令时插入内存屏障,当然并不是所有的程序都会有重排序的问题,解决同上。下面我们就说一下JMM中的Happen-Before原则。
我们来看一下JMM中有哪些方法会建立Happen-Before原则
1.程序顺序规则
2.volatile变量规则
3.传递性规则
4.start规则
5.join规则
6.synchronize规则(监视器锁)
具体可自行百度查询,今天的文章就到这里啦,有问题,欢迎指正,谢谢大家!