long类型的赋值是原子性吗?
32位操作系统每次读取最长的是4个字节,32bit,所以,在32位操作系统上,超过32bit的数据是需要分俩次读取的。
那么,在读取long,double类型数据时,有可能在读取第一次时,别的线程对其进行修改,所以才会出现非原子性的操作 。
在java中,除单纯的引用类型和非long,double的基本类型变量的赋值才是是原子性的。
原子通知上面的时间咋设置呢?
在原子通知中,时间可以通过设置“时钟”来实现。具体来说,可以使用系统提供的时钟服务来获取当前时间,并将其与需要设置的时间进行比较,计算出时间差,然后将时间差以特定的格式展示在通知中。
例如,可以使用Java中的Date和SimpleDateFormat类来实现时间的格式化和比较。同时,还可以使用定时任务来定时更新通知中的时间显示,以保证时间的准确性。通过这种方式,就可以实现在原子通知中显示指定时间的功能。
cas工具是什么意思?
CompareAndSwap:比较并交换;
CAS的判断思路:认为变量V的值应该是A,如果是的话就修改为B,如果不是A,就不修改,避免多线程同时修改出错;
Java中是如何利用CAS实现原子操作的?
AtomicInteger加载Unsafe工具,用来直接操作内存数据;
用Unsafe来实现底层操作;
用volatile修饰value字段,保证可见性;
Java如何解决可见性和有序性的问题?
首先需要了解,为什么会有「可见性」和「时序性」问题,然后我们来看Java是如何解决这两个问题的。
「可见性」和「时序性」问题
导致「可见性」和「时序性」问题的原因有如下几个:
抢占式任务执行:现代CPU执行多任务方式是「抢占式」,它的总控制权在操作系统手中,操作系统会轮流给需要CPU执行的任务分配执行时间片,超过时间后,操作系统会剥夺当前任务的 CPU 使用权,把它排在队列的最后,最后分配时间片……
存储速度差异:各存储执行速度的不同,离CPU越近,存储速度越快,相对的容量就越小。执行程序所需要的数据不可能一次性全部都加载到寄存器中,所以有load与store的过程,影响了所谓的「可见性」
指令重排:大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待。通过乱序执行的技术,处理器可以大大提高执行效率。除了处理器,常见的Java运行时环境的JIT编译器也会做指令重排序操作,即生成的机器指令与字节码指令顺序不一致。
解决方法
解决思路很简单,就是把多线程强制单线程执行。
解决方法无非两种:
内存屏障
锁
先看下JVM的内存模型,我们基于这个模型来简单说明下
内存屏障
内存屏障在Java中通过volatile关键字体现。volatile会在适当的地方添加下面四种内存屏障。
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
内存屏障只保证可见性,不保证时序性。也就是说内存屏障只是解决了线程A修改的内容能立刻被线程B读到。
锁
Java中锁按性质分可以分悲观锁和乐观锁。悲观锁基于锁指令实现,乐观锁基于CAS实现。
通过monitorenter和monitorexit两个指令实现悲观锁,这两个指令之间的指令不得重排,且独占。假设线程A和线程B同时执行一段代码,线程A先通过monitorenter获取到了锁,那么在线程A执行monitorexit之前,线程B都只能等待。
CAS即CompareAndSet,Java通过自旋以及CPU层级的指令实现。具体可参考JUC实现。假设有一个变量c,初始值为3。线程A和线程B同时修改这个变量,A,B都同时获取到了变量c的值,A首先进行修改,将值改成了4。B尝试修改,但是发现c的值现在是4而不是3,所以进行自旋等待,然后重新执行修改操作,将4改成了5。
ThreadLocal
最后说下ThreadLocal。ThreadLocal即本地线程变量,也就是将公共的变量直接拿到线程内使用,其中的修改对外不影响。谈不上解决了「可见性」和「时序性」。只是保证了当前线程内的修改不影响其它线程,其它线程的修改也不影响当前线程。
线程安全有三大特性:原子性,可见性,有序性,只有三大特性都满足的时候才能保证线程安全,三大特性详细描述如下:
1,原子性:通常是指代码执行的效果,要么全部执行成功,要么全部失败;
2,可见性:线程中的本地内存中的变量值,应该立即同步到主内存中,让其他线程可见;
3,有序性:保证代码执行的串行性;
java中的原子性操作主要是使用sun.misc.Unsafe包下的compareAndSwap方法实现,这是sun包下native方法,使用c++代码实现,在CPU层级来 保证底层指令的原子性,或者使用加锁的方式!
但是CAS也会存在问题,具体参见我的另一个回答,这不做 详细讨论!
可见性无法保证主要是因为,内存的存取速度跟CPU相比存在差距,所以在CPU和主内存之间,引入了缓存(线程本地内存)的概念,来增加CPU 的计算性能,副作用就是导致线程操作共享数据时,无法保证中间数据是最新的(有可能还在别的线程的本地内存汇总)!
有序性无法保证是因为基于性能的考虑,编译器和处理器会对操作指令进行重排序,在单线程之中不存在问题,但如果是多线程就可能存在 判断错误的情况!
编译器的重排序也是规则的,java中先天的有序性组成了happens-before原则,如果happens-before原则不能推导出指令的执行次序,则指令 就不是有序的,happens-before八大原则为:程序次序规则(单线程中的顺序性,多线程无法保证),锁定规则(同一个锁先解锁,后加锁) ,volatile变量规则(先写后读),传递规则(类似A早于B,B早于C,则A早于C),线程启动原则(start方法是线程最先执行的方法),线程 中断规则,线程终结规则,对象终结规则!happens-before八大原则规定了指令执行的有序性
由此可以看出,线程不安全的原因十有八九都是追求性能惹的祸!
通常代码在满足三大特性的时候,就能保证线程安全,java中保证线程安全的方式有很多,包括加锁和不加锁,下面来逐一谈下:
1,加锁:比如synchronized(JDK中自带的关键字,JMM规定获取锁的时候,必须清空工作内存中的变量值,保证需要 获取变量的时候,只能从主内存中获取;释放锁的时候,必须把最新值写入到主内存中,这样来保证数据的可见性和原子性)!
还有基于AQS实现的reentrantLock,ReentrantReadWriteLock等都是加锁!
2,不加锁:
①,使用volatile+CAS操作,volatile使用内存屏障来保证变量的可见性和指令有序性,CAS保证原子性,最终实现线程安全,在jdk中Atomic 打头的几个类,都是用了这种方式,实现线程安全!如下图:
②,使用ThreadLocal,每个线程都维护一份数据到自己的本地内存中,相当于没有共享资源的竞争,所以也不会有线程安全问题;
后面会有更多的JAVA开发技术或者遇到的面试问题进行持续分享,希望能帮到更多的朋友,敬请关注。。

