打算记录自己的学习情况,尽量不摆烂,另外一件事要有始有终,要弄完
2024.7.29
1并发开始
上下文切换会记住当前的状态,程序回来继续在这一条指令执行,这个加载的过程很慢
学习多线程是为了什么?
不乱开线程进程,因为被操作系统调度后就注定分配时间片,五个人能做完的任务让50个人做,大部分人虽然“眼前有活”但是根本轮不上,也耗费大量资源
不要cpu打空转,能有机会被cpu执行却根本不发挥价值,白白损耗时间。sleep
不要有很多注水没用的任务,虽然cpu没有空转,大家都在忙,但是其实没有产出
不要上下文切换很快,上下文切换消耗cpu资源很大,不仅要做“重新加载保存的指令”,时间成本上,这点时间足够做一些事情了
线程的几种状态
就绪运行阻塞死亡,是线程基础状态
等待和睡眠,都是进程线程主动让出cpu。二者区别在于,等待让出cpu,也会释放锁,睡眠让出cpu但是不会释放锁
锁的简单理解
既然多线程的本质是线程分配时间片被cpu调度和使用。作为一种井然有序的状态,资源也应该被管理起来。因为线程执行后不管别的线程,抱起来资源就马上运行。防止很多意外发生,当某些资源被一个线程霸占后,就不让其他线程再有机会运行了。
1.1上下文切换
上下文切换次数要节制
1.1.1多线程和单线程的比较
public class Main {
private static final long count = 1000000000;
public static synchronized void main(String[] args) throws InterruptedException {
concurrency();
serial();
}
public static void concurrency() throws InterruptedException {
long start = System.currentTimeMillis();
Thread th = new Thread() {
@Override
public void run() {
int a = 0;
for(int i = 0; i<count; i++) {
a+=5;
}
}
};
th.start();
int b = 0;
for(long i = 0; i < count; i++) {
b--;
}
long time = System.currentTimeMillis() - start;
th.join();
System.out.println("concurrency :" + time + "ms, b = "+b);
}
private static void serial() {
long start = System.currentTimeMillis();
int a = 0;
for(int i = 0; i<count;i++) {
a+=5;
}
int b = 0;
for(long i = 0; i<count; i++) {
b--;
}
long time = System.currentTimeMillis() - start;
System.out.println("serial :" + time + "ms,b= " +b);
}
}
40亿我的电脑就执行不动了
后面就都是1ms了,笔者是2023买的电脑了。现在电脑都挺好的,总之数据量太大多线程不会占据优势(上下文切换)
join使用后,main线程会等待上面的语句执行完再执行。如果不加join,大概率会main线程先入栈出栈结束运行了
1.1.2jps测试工具
jps
linux还有ps -ef|grep 线程号 和 top
jstack
如果多个线程处理同一个变量,读跟写都保证不了
2024.7.22》》》》》》》》》》》》
2.1.1volatile的实现原理
volatile不会引起线程上下文的切换和调度
一致性更新:处理器修改完某个变量会告诉其他处理器让它们更新最新版本的值,保证信息的一致
排他锁:某个处理器获得锁,就不让其他处理器操作这个变量。就是synchronized保证写后读,这不是2.1的重点
volatile能保证,当一个处理器写了某个被volatile修改的数据,其他处理器能正确的读到被修改后的值,就是保证读的正确
(小明):那写呢?不保证了?
不保证了,所以要充分理解volatile和syronchized,让它们各司其职,只盲目使用syronchized就有性能的大问题
内存屏障:屏障屏障,反正就是不让访问。内存的某个变量根据操作系统的某个约定使得在做一些处理变量的时候不让别人访问。可能有一个int version = 1;如果1就让访问,0不让访问
反正就是不让访问了。我感觉呢,应该屏障也是通过锁了去那个变量的导线,导致后面所有想访问这个变量,都得等着这根线没人用才可以。嗯,这也是锁的底层原理
缓冲行,跟cache沾边。当多个处理器操作缓存时会走一根大导线。处理器发送任何指令都是电压传输,如果2个处理器同时发命令两个电压造成电压叠加必定会造成混乱,最好情况就是其中一个处理器的指令执行被吃了(这也很恐怖),如果两个电压抵消,两个处理器的指令白干活。所以通过一些方法(应该也是锁),我走这根线你就先别走了,你在我后面,等我执行完你再执行。
缓存行由多个缓存线组成,如果某个处理器操作某个缓存线,别的处理器你就先等会。排队才有规则性
原子操作:要么都成功要么都失败,打游戏辛苦几个小时最后一步没存档。都回滚到存档开始数据重新打。这里侧重于某些指令希望一块都执行完并且不能有错
缓存行填充,缓存拿需要的变量时会去内存读,但是会一口气加载一个缓存行,也就是缓存行有64bit但是想要的变量就8bit,它会把剩下的56bit根据那个变量地址后面加载56bit。这个策略可能是好事,比如long[] 数组一口气没准都加载进去了。但是如果有a变量和b变量都需要操作,且b变量的操作要在a变量操作后执行,那就坏了,b和a绑在一块完全没法弄。所以希望某些操作的变量的bit很大。比如boolean。它放进去64bit占满了。
缓存行填充就是读内存地址会一口气加载一个缓存行的大小,64bit,32bit。然后某些变量可能有顺序关系,所以进行优化把一些关键的变量bit变的很大甚至填满
缓存命中:缓存离cpu近,内存离cpu远,如果缓存就有内存的数据就操作缓存而不是操作内存数据
写命中:首先,由于缓存命中,cpu会更“青睐”缓存行,跟缓存行打交道。操作完的数据写回缓存行。写了蛮多数据后,根据程序缓存行会把数据给内存的数据,缓存行再等着cpu继续操作数据。这也是计算机的理想操作数据场景
写缺失:当缓存行想往回更新内存数据时,内存的那块变量区域没了。迷失的孩子找不到回去的路。这就是写缺失。比如内存变量是一个即将垃圾回收的没用变量,操作这个变量更新内存数据时,已经被jvm回收,‘死了’,那就写缺失了。或者无法更新数据,已经到了家门口但是家里人不给你开门,也会写缺失
可见性:多个线程对同一个变量进行修改,能读到其他处理修改之后正确的数据
可见性 = volatile干的活 = 一致性更新 = 某个变量被处理器写后,其他处理器会嗅探到新数据并重新读新数据的值。
保障读,不保障写
这里在讲啥呢?
录制_2024_07_22_21_53_12_901
哇趣,上传视频后简直糊到爆炸,家人们先凑活看吧。后面有办法我再补回来
这里都能看懂了,重点是理解清楚视频,这里的‘嗅探’,‘一致性’。都是刚刚讲的清楚明白的概念。
在这里,通过某个处理器修改更新值后,其他处理器马上放弃自己处理的值重新读值再处理。这也是有些写后读的思想(其实整本书全部都是为了实现写后读,就是怎么实现的了)
2.1.2volatile的优化
其实就是刚刚讲的缓存行填充,8bit的东西结果后面56bit全放进去了,万一中间有一些逻辑。那就完了。所以防止这种情况干脆把关键需要的变量放大甚至占满。
2.2synchronized原理
锁方法,锁的是实例化的对象。
锁静态方法,锁类
锁方法块,锁对于锁的那个对象
还有行代码 int[] arr = {0,1};
通过读取对象头的信息知道是不是要上锁,上什么锁。
知道要上锁后,就用monitor对象锁东西。分成monitorenter和monitorexit两个对象放到开始和结束
其实锁的关键就是某个处理器拿到锁的资源,其他资源根本不让读和写
java拿某个变量或者方法去栈里都是拷贝,锁就不让拷贝了
家人们,觉得写的好就用去大厂的无敌代码小手点个赞,俺看到了会觉得自己做到事是有意义的,会更有决心更完。评论收藏都可以
2024.7.25》》》》》》》》》》
并发量是逐步增加的
锁的升级还是蛮不好说的,等我回炉一波
Atomic类都是线程安全类