学习笔记系列开头惯例发布一些寻亲消息
链接:https://baobeihuijia.com/bbhj/contents/3/197325.html
-
多线程访问共享资源冲突
-
临界区:一段代码块存在对共享资源的多线程读写操作,称这段代码块为临界区
-
竞态条件:多个线程在临界区内执行,由于代码的执行序列不同导致结果无法预测,称发生了竞态条件
-
-
一些线程安全的例子(什么时候加锁什么时候不加):
-
局部变量是线程安全的(每个线程创建的栈内部独立拥有)
-
局部变量引用的对象:如果用的对象的公共变量,需要加锁,因为对象在堆内
-
局部变量引用的对象:如果用的是对象方法内的局部变量**(虽然这种情况下每个线程在堆内单独拥有对象使用权,但是如果子类重写方法,新加了线程,那么也会出现两个线程共享局部变量的情况,所以尽量用final或者private将父类保护起来)**
-
线程安全类:
- string:在定义string类的时候,就已经声明了final,所以不会出现子类继承string,并且修改string的读写逻辑为多线程,所以对于单独一个string来说,我们的读写是安全的,因为只有有一个线程进行读写,但是多个方法的组合之间可以插入不同的别的线程,所以不是原子或者说不是安全的。
- string:在定义string类的时候,就已经声明了final,所以不会出现子类继承string,并且修改string的读写逻辑为多线程,所以对于单独一个string来说,我们的读写是安全的,因为只有有一个线程进行读写,但是多个方法的组合之间可以插入不同的别的线程,所以不是原子或者说不是安全的。
-
-
重量级锁——synchronized【父母考核:我听父母的,父母来决定我的owner,当追求者很多时,采用这种方案】
-
加锁room,别的线程会进入block,直到本线程释放锁,才会唤醒别的block线程,在这个过程中就算时间片轮完该线程,其他线程也无法唤醒
-
用对象锁的形式保证了临界区代码的原子性,避免多个线程一起执行同一段临界区代码,如果这个锁没有被用,那么就可以执行这段代码
-
锁住对象和锁住类对象是不同的,并不互斥(也就是说,在执行锁住对象的时候,当这轮cpu片时间耗尽,cpu也会去执行锁住类对象的操作,类对象锁不会被阻塞)
# 方法一 synchronized(对象) # 方法二 class Room { private int counter = 0; // 等价于 synchronized(this) public synchronized void increment() { counter++; } public synchronized void decrement() { counter--; } public synchronized int getCounter() { return counter; } } # 方法三 class Test{ // 等价于synchronized(Test.class) public synchronized static void test(){ } }
-
Monitor
-
对象头
- 普通对象:32bit的Mark Word(25位hashcode,每个对象都有自己的哈希码,4位的GC分代,锁状态标志)+32bit的Klass Word(类型指针)
-
数组对象:32bit mark word + 32bit klass word + 32bit的 数组长度
-
-
synchronized的原理:
- 每次线程遇到synchronized(obj),会去检查obj是否指向操作系统中的一个monitor,如果没有指向那就将obj对象的mask word 修改为monitor的地址,并将锁标志位从01改为10,将该线程置为monitor的Owner
- 在此期间如果别的线程执行遇到synchronized(obj),那么就会索引到monitor,发现已经有别的owner,进入entrylist 变为BLOCKED状态
- 当前owner执行完成后,随机唤醒一个BLOCKED线程,最后退出该对象时将自己的markword重置为hashcode等
-
重量级锁的线程自旋优化
- 为了避免线程阻塞,发生上下文切换,从而自旋等待解锁
- 适合多核cpu,单核的cpu没有自旋的意义,因为没有额外的cpu可以来执行自旋(需要等你结束我才能执行,但是我还总要中途打扰你问你好了没)
- 自旋失败:几次自旋重试后还是得不到锁就进行阻塞
-
-
轻量级锁(操作系统自动创建,不需要我们显式定义)【私定终生,我来决定我的owner】
-
加锁:线程中创建一个LOCK RECORD 00表示轻量级锁,与对象头交换mask信息(类似于一种密码机制,只有当前线程完成后,才会把密码信息还给对象,别人来访问交换时这个对象已经是加锁状态)
-
锁重入:当前线程已经拿到锁了,但是又执行了一遍synchronized(obj),这时会新建一个LOCK RECORD 00栈帧,数据这里会存储null,说明其他栈帧已经拿到锁了
-
解锁:如果是null,直接清除即可,不是null则需要将mask word恢复给对象
-
锁膨胀(追求者很多产生了竞争,发现决定权给到父母后,就需要与父母沟通)
- 线程加轻量级锁失败,进入锁膨胀流程,变成重量级锁(修改原始密码为monitor地址,owner置为当前线程,后续线程进入阻塞)
- 原来线程的轻量级锁解锁失败(因为密码匹配不对,对象的密码现在是monitor地址),需要进入重量级锁的解锁流程,即找到monitor对象,将owner置为null,并唤醒阻塞线程
-
-
偏向锁(只有一个线程一直重复用偏向——》有别的线程也来用该对象就会撤销变为轻量级锁——》锁之间有竞争交互就用重量级)
-
原始:将对象的mask word与新建锁栈帧的头部信息交换(锁重入都会尝试交换信息,造成资源浪费)
-
偏向:将对象的mask word修改为线程ID,锁重入就不会新建锁栈帧
-
当前线程id释放后,线程id还是不会改变,该对象已经从属于该主线程
-
ban掉偏向锁的方法
- 当一个可偏向的对象调用了hashcode就会ban掉偏向锁
- 当对象已经偏向一个线程了,另一个线程也来用该对象,但是不竞争,就会变为轻量级(线程id部分变为锁记录指针),如果竞争交互就是重量级
- 调用wait,notify也会撤销,因为wait,notify只有重量级有
-
批量重偏向
- 超过一定阈值数量(20)的对象都在撤销偏向锁,就会认为偏向设置的线程有误,因此会将所有的对象偏向锁改为别的线程
- 撤销的性能消耗比较大
- 批量执行
-
批量撤销
- 撤销超过阈值(40)的对象都在撤销偏向锁换轻量级(说的是所有线程的总数),偏向就要求整个类原始对象和新对象不再偏向,直接轻量级
-
-
JIT即时编译进行逃逸分析
-
sleep和wait的区别(状态都是timewaiting)
-
sleep是线程的方法,执行时不会放弃锁:相当于屋子内的人把门锁死,直到醒来检查一下资源是否满足
-
wait是对象A的方法,会放弃锁,需要notify叫醒:屋子内的人发现缺资源就出来在门外等着A.wait(),等到资源满足执行notify【这种方法可以通过资源的识别需要通过while条件来判断】
-
synchronized(lock){ while(资源不满足){ lock.wait(); } // 干活 } synchronized(lock){ lock.notifyAll() }
-
-