JUC
juc
synchronized
synchronized(锁的是对象不是代码)是可重入锁,在同一个线程的一个类中有两个方法都被锁了的情况下,第一个调用第二个方法不会再申请锁,这样叫可重入锁,如果还要继续申请锁的话会造成死锁的情况。
synchronized为什么叫重量级锁,因为这个锁是虚拟机想操作系统(OS)申请的,操作时间长繁琐所以叫重量级,但是现在更新的锁,实现了锁升级这个概念,在只有一个线程的情况下使用偏向锁(不需要获取锁),如果有线程争用的话升级为自旋锁,自旋10次还没有获取锁的话升级为重量级锁。
不要用String常量 integer Long,String会锁到别的对象,integer值修改了就会变成另一个对象。
什么时候用重量级,什么时候用自旋?
在线程多的情况下,并且业务时间长用重量级,线程少业务时间短用自旋。
ReentrantLock
支持公平锁:抢锁的时候需要进行排队。初始化时参数为true就可以设置。
加锁的过程可以被打断。lock.trylock();尝试加锁lock.interruptibly();
锁上面的队列可以指定任意数量:区分了不同条件下的等待队列
volitile(可见性)
保证线程可见性:基于cpu的缓存一致性mesi
禁止指令重排序:cpu执行操作的时候以前是一个一个命令处理,在现在为了提高效率采取了一个并发的处理命令的形式,所以现在执行操作不一定是按之前顺序来的,这个禁止重排序可以使命令按照开始的的顺序来执行。
单例模式
单例模式:保证在一个工程之中的某个对象的某个实例只有一个。
分为懒汉式和饿汉式两种写法,懒汉式是不管你调不调用总是给你生成一个对象(使用static final和instance初始化本类的对象,并且将无参构造方法设为private在通过getinstance这个方法 给外部获取对象的实例),就是我可以不用但是你不可以没有,饿汉式就是饿了才吃,即在被调用的时候才生成对象的实例(使用static 和instance创建对象但不初始化,将无参构造方法设为private在通过getinstance这个方法中先判断instance对象是不是为空,为空的话初始化对象,不为空的话返回原对象,那么怎么写一个线程安全的单例模式呢,在getinstance这个方法上加上锁synchronized但是直接在这个方法上加锁效率不能达到最高,我们可以只在获取对象的那段代码中加锁,但是在高并发的状态下我们还是无法保证线程的安全性了,所以我们在获取对象的那段代码内再判断一次instance是不是为空,这种方法叫做双重检查,这样就可以避免在高并发的状态下创建出多个对象的问题,那为什么不省略外边的那层判断呢,因为外面这层判断可以筛选出符合要求的线程并且减少锁的竞争,那我们要不要加volitile呢,要加,因为在超高的并发下,指令是重排序的,数据不一定能保证安全,加上可以保证万无一失)。
CAS
CAS(无锁优化自旋):通过原子类来保证在多线程下数据一致性。cas相当于一个方法里面包含三个参数分别是当前值、期望值、以及要改的值
CAS的ABA问题:在进入原子类操作之前数据可能被修改过(基本类型影响不大,引用类型可能会出现并非操作原来的那个对象的情况,就是你和你的前女友复合,中间你有了别的女人),通过加版本号(也就是乐观锁)可以解决。
cas在比较交换的时候的原子性问题:在比较并交换的时候是怎么保证原子性的?在java中调用了compareandswap这个方法,而这个方法是c++写的,在c++中使用了汇编指令cmpxchg这个指令并且前面有一个lock指令这个lock是在多核竞争的情况下就加锁的,而这个cmpxchg这个指令也不一定原子性的,在多核情况下也有可能被其他核打断,所以lock指令才是保证原子性的原因。
CAS底层是靠unsafe这个类来直接操作jvm中的内存的。
AQS
AQS是使用voaltile和一个共享变量state实现的,将线程封装成一个node节点加入到双向链表中(CLH),因为要获取节点前后的信息所以使用双向的,底层通过CAS来改变state值。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。
计算机的组成
操作系统通过系统调度器,来调度任务给cpu执行。
一核包含一个cpu,cpu包含alu(计算单元),registers(寄存器),一个alu对应一组registers就可以执行一个线程,对应两组registers就可以执行两个线程,这叫超线程。
超线程技术把多线程处理器内部的两个逻辑内核模拟成两个物理芯片,让单个处理器就能使用线程级的并行计算,进而兼容多线程操作系统和软件。超线程技术充分利用空闲CPU资源,在相同时间内完成更多工作。
虽然采用超线程技术能够同时执行两个线程,当两个线程同时需要某个资源时,其中一个线程必须让出资源暂时挂起,直到这些资源空闲以后才能继续。因此,超线程的性能并不等于两个CPU的性能。而且,超线程技术的CPU需要芯片组、操作系统和应用软件的支持,才能比较理想地发挥该项技术的优势。
alu访问registers的速度,比访问内存快将近100倍,为了充分利用数据,在registers和内存之间加了几层缓存,目前工业实践最完美的就是加三层缓存(L1、L2、L3)。
为什么要三层缓存呢,在多核cpu里面是一个这样的结构,读取一个数据要去内存读,先放到共享内存里边,然后再一层层往上存,之后就可以读cpu的本地缓存而不必每次都读内存了。但是这个读取数据并不是每次只读一个字节,这就涉及到程序的局部性原理,每次其实都会读取相邻空间的数据,一次读64bytes的数据(缓存行,实践得出一次读64是效果最好的)。
缓存对齐:两个线程读同一个数据的时候,为了保证自己操作的数据不跟另一个线程操作的数据处于同一个缓存行,所以使用缓存对齐这个东西来避免(在本数据前后加满到64个字节的数据)。
cpu从内存读取数据的话如果xy处于同一个缓存行,在底层的缓存一致性协议的制约下,如果有一个cpu改变了x的值,那势必另一个cpu再去读这一行数据时这行数据就失效了,那就必须去内存再去重新获得这个数据,如果另一个cpu修改是y这个数据那么在两个cpu之间为了保证缓存一致性的协议将会频繁的去更新数据这样效率就会很低,如果在xy两个数据之间填充数据使其不在一行那就可以解决这个问题了,这种填充的方法叫做缓存对齐。
程序真的是有序执行的吗?
在多线程之中,运行的代码不一定是有序的。在不影响单线程最终结果的情况下,每条语句被执行的先后都不一样。
为什么要乱序?
因为要增加效率,在不影响单线程的最终一致性的条件下,可以先执行较快的不影响其他事的代码。
什么情况下可以乱序?
as、if、serial(看上去像序列化执行)
对象的创建过程
先申请内存,对象有多大就申请多大的内存。如果有全局变量的话,会先赋一个初始值,然后调用构造方法给那个全局变量赋值,最后将变量指向这个内存。由于代码转为汇编语言的情况下new一个对象并不是原子性的,而是分为几条指令执行的,这时第四条和第七条就可能不按顺序执行,这时如果有另外一个线程打印m的值话是会打印出0这个结果。所以不要在构造方法里面启动线程,可以初始化但不要启动。
如何禁止指令重排序?
cpu底层是加上内存屏障指令来保证某些指令必须先执行。
jvm 底层c++使用fence方法里面的lock add指令实现指令重排序,将CPU线程锁住使得别的无法访问。
浙公网安备 33010602011771号