计算机简史图解(终于有大佬把34)
计算机简史图解(终于有大佬把34)
2024-11-22 01:34:14  作者:心微微一动  网址:https://m.xinb2b.cn/sport/mbg416401.html
计算机的历史算盘和机械计算机

有很多民族自豪感爆棚的兄弟会把算盘当成计算机的起源,还有爆破天的兄弟会把阴阳当成二进制0和1的起源,我觉得这件事儿就有点儿不靠谱了

如果非要追究计算机的鼻祖,那就得讲讲17世纪前欧洲的故事,最早的计算机其实是计算器,就是算数用的,在欧洲工业工业革命的时候,大量的工业模具需要计算,欧洲又没有中国传统的计算器 - 算盘,就催生了很多科学家发明自己的计算器(对,就是计算器,就是以前菜市场还在使用的那种,还不能称之为现在的计算机),这其中有个NB的人物,这个人叫布莱士帕斯卡,我们的压强单位(帕,千帕,百帕,兆帕)等等,就是以这哥们儿的名字命名,还有,计算机语言里面有一种叫做Pascal,就是为了纪念他。


就是这么个NB的人物,发明了最早的机械计算器,长这样儿:


经过后人的逐步改进,机械计算机的最后发展堪称精美,有长这样儿的:


还有长成这样儿的:


还有更NB的:


机械计算机改进者中有个人值得一提,他就是德国百科全书式的天才,17世纪的亚里士多德 -- 莱布尼茨!


莱伯尼兹这个人又是个大牛,他既懂物理又懂数学(物理数学不分家),著名的微积分基本定理,牛顿莱布尼茨公式,就是莱布尼茨发明的,当然这里面是牛顿跟莱布尼茨或者说英国跟欧洲大陆的恩怨情仇。简单说,莱布尼茨发表论文创立微积分公式,牛顿当时是英国皇家学会的老大,话语权影响力比较大。牛顿说莱布尼茨发表的公式是参考了牛顿三年前的笔记,莱布尼茨嗓门不够响,争不过牛顿,所以没有办法,后人就把这个公式称为牛顿莱布尼茨公式。

一个人想在社会取得回报或者想发挥巨大作用,就必须要明白这个社会的运行机制,通过这件事儿,大家应该明白话语权的(传媒、笔杆子)重要性,如果还不能理解,参考美国把上个世纪的美国病毒命名为西班牙病毒这件事儿,当然最近又想把新冠病毒扣在我们脑袋上,就是因为他把控了话语权。衍生出来你应该明白的是,历史是个任人打扮的小姑娘,你看到的,你听到的,都是别人想让你看到和听到的,所以你要进行深度的思考,他是谁?为什么这么说?他说的是真的吗?对我有没有什么企图?多问自己几个为什么,你会慢慢从白痴成为智者。

扯远了,还说回来莱布尼茨,他除了改进机械计算机以外,还有一个重要的发明,那就是大名鼎鼎的二进制!(这里终于跟现代IT技术关联起来了)据说二进制的发明是参考中国古代的阴阳太极图而创作出来的,对此,我觉得倒是真的有可能。因为莱布尼茨有一本著名的著作,叫做《论中国人的自然哲学》,说明这个人对中国是有研究的。而且,他发明了二进制以后,还通知了当时的康熙大帝,因为他认为康熙大帝是个数学迷(对此我深表怀疑)。

当然,机械计算机又大又笨重,早就被现代的电子计算机所取代,不过说句题外话,机械计算机也有电子计算机所不具备的优点,就是结实耐用,几百年都不坏,而且,还不用电 ,谁要是大学食堂里面打饭收费做计算的时候来这么一台,那绝对是学妹眼中最酷的仔!

顺便也来一张现代电子计算机的鼻祖(当然,第一台电子计算机这件事儿也是见仁见智,美国嗓门大,所以现在资料大多认为1946诞生于美国宾夕法尼亚大学的“ENIAC”是世界上第一台电子计算机),它长这样儿:


这是个庞然大物,它大概占地一个别墅,跟一辆前苏联虎式坦克一样重,每个小时耗电150度,但是,每秒钟的计算量仅区区的5000次,要知道现在手机上的芯片的计算速度可以达到每秒10 0000 0000 0000次。不过就是这样一台还比上菜市场计算器的东西,开启了20世纪最NB的数字化革命!从此之后,计算机行业飞速发展,造就了现在所谓的信息化大革命。

严格讲,这台机器应该称作电子管计算机,因为,这里面用的零件全部都是电子管,电子管如果开关的速度太快,很容易就会坏掉,据说这台机器每天都会有电子管冒烟儿,工程师在寻找和修复每一个电子管中疲于奔命,想象一天24小时,计算时间仅有半小时,剩下的23个半小时都是在寻找和修复坏掉的点,这是多么让人抓狂的一件事。如果你不能理解这件事儿,想象一下一个灯泡每秒不停地开关5000次,它会不会坏掉。而且,电子管还有很严重的发热问题,需要把风扇进行紧密的排布,这也是一个工艺难题。

不过,幸运的是,在这台又笨重毛病又多的计算机问世的第二年,也就是1947年,美国贝尔实验室研究发明了晶体管,和电子管相比,晶体管体积又小,耗电还低,最重要每秒开关几十万上亿次都不带坏的,从这一刻开始,计算机革命才真正的进入了突飞猛进的时代。

这堂课,我们要讲的就是计算机的原理。

CPU的原理

为什么讲线程要讲CPU?因为线程和CPU有一对一的对应关系!(超线程除外)

当然,现代的计算机的核心,也就是芯片,是由10 0000 0000 零件构成,我没有办法带你走遍这里面的每一个细节,不过,作为高级语言的程序员,我会带你走到足够深的深度,让你能够深入理解你写的每一行代码到底在计算机内部是怎么转换成电信号,从而被精密执行的。这一点很重要,因为这会给你带来“通透感”(原谅我找不到更好的形容词,现在很多程序员是没有经过科班训练的,是根据业务进行速成的,对这样的小伙伴儿来说,你写的代码虽然可以工作,但是它对你是一个黑盒子,你看不到代码背后的一切,从而也就无法进行更深入的理解和更准确的调优,总之,我个人非常喜欢这种通透感,我不喜欢一个技术对我来说是黑盒,是秘密,希望你也能理解和享受这种通透感)

好吧,让我们揭开代码背后的神秘世界吧。

还要从一个故事谈起。

我小时候最喜欢的女同学叫小芳,长得好看又善良,我们俩情投意合,每天放学后都约会共同进步,童年的时候山青水白,鸟语花香,环境特别好,我们的年纪都很小,我爱谈天她爱笑,有一回并肩坐在桃树下,风在林梢鸟在叫,不知怎么就睡着了,梦里花落知多少...

不要打断我,让我陷在美好的回忆中不可自拔一会儿。

只不过后来大人发现了我们的联系,用他们自带的污秽的思想,认为我们的关系是污秽的,是不纯洁的,我们当时还没有罗密欧与朱丽叶,梁山伯与祝英台这样的觉悟,不懂得以死相争,所以就被双方家长棒打鸳鸯,各自关了禁闭。

不过这个难不倒刚刚学了电学的我,我们就设立了这样入门级别的电路:


我还发明了灯泡语言:

亮亮 = 放亮灭 = 学灭亮 = 等灭灭 = 我

当然你会发现如果只有两个信号的组合,就最多表示四个字,如果想沟通更顺畅,我只要增加信号的组合长度就可以了,比如三个信号,我就可以表示八个字

亮亮亮 = 放亮亮灭 = 学亮灭亮 = 等亮灭灭 = 我灭灭亮 = 一灭灭灭 = 起灭亮灭 = 电灭亮亮 = 影

如果想交流的更加复杂,我可以增加更长的信号组合,比如我如果用16个长度的信号,就可以表示2^16个汉字,这个数字是65536,要知道,我们日常的汉字常用的话也就4000个左右,整个康熙字典的总字数也仅仅47000个,我用灯泡信号的长度仅需要16个信号长,就足矣涵盖中文的交流了。

思考题:如果仅需要覆盖日常交流(4000个汉字),我需要的信号组合的长度至少是多少?

灯泡语言有些复杂,我结合莱布尼茨的二进制,用1来代表灯泡亮(通电),用0来代表灯泡灭(断电),这样我和小芳就有了自己的通信语言,比如下面这句话,你猜我说了什么?

111 110 001 000 = (? )把答案写到括号里。

话说到这里,不知道大家有没有发现,我发明了一种汉字编码,就是把特定的汉字用0和1的组合表示出来,注意,汉字的编码并不是只有一种方式,完全有可能发生的是,在一种的编码方式中,111代表'我',而在另外一种编码方式中111代表'中',如果我们在解析一段编码的用错了编码格式,就会出现平时经常遇见的'乱码'问题。

思考题:A编码中,111 = 我 110 = 你,B编码中 111 = 沙 110 = 雕,那么下面这段话究竟代表什么呢?110 111 110

再有了第一个电路的基础之上,我有设计了下面的电路:


这里就有了输入和输出的概念了

输入1

输入2

输出

0

0

0

0

1

0

1

0

0

1

1

1

可以用这样的符号表示:


也可以有这样的电路:




加法器

输入1

输入2

加和输出

进位输出

0

0

0

0

0

1

1

0

1

0

1

0

1

1

0

1







时钟


ram 保存信号






程序 - 自动化(时钟信号


荐书

《编码 隐匿在计算机软硬件背后的语言》《Code: The Hidden Language of Computer Hardware and Software》

程序的执行


进程与线程

一个程序,读入内存,全是0和1构成

从内存读入到CPU计算,这个时候要通过总线

怎么区分一段01的数据到底是数据还是指令?

总线分类为三种:控制线 地址线 数据线

一个程序的执行,首先把可执行文件放到内存,找到起始(main)的地址,逐步读出指令和数据,进行计算并写回到内存。

什么是进程?什么是线程?

一个程序进入内存,被称之为进程?一个QQ.exe可以运行多份儿吗?

同一个进程内部:有多个任务并发执行的需求(比如,一边计算,一边接收网络数据,一边刷新界面)

能不能用多进程?可以,但是毛病多,最严重的毛病是,我可以很轻易的搞死别的进程

线程的概念横空出世:共享空间,不共享计算

进程是静态的概念:程序进入内存,分配对应资源:内存空间,进程进入内存,同时产生一个主线程

线程是动态的概念:是可执行的计算单元(任务)

一个ALU同一个时间只能执行一个线程

同一段代码为什么可以被多个线程执行?

线程的切换

保存上下文,保存现场

问题:是不是线程数量越多,执行效率越高?(初级)展开:调度算法怎么选?(难)

问题:单核CPU多线程执行有没有意义?(初级)

问题:对于一个程序,设置多少个线程合适?(线程池设定多少核心线程?)(中高级)

线程调度器算法(平均时间片、CFS(考虑权重))


CPU的并发控制关中断缓存一致性协议

CPU的速度和内存的速度(100 :1)

这里的速度值得是ALU访问寄存器的速度比访问内存的速度快100倍

为了充分利用CPU的计算能力,在CPU和内存中间引入缓存的概念(工业上的妥协,考虑性价比)

现在的工业实践,多采用三级缓存的架构

缓存行:一次性读取的数据块

程序的局部性原理:空间局部性 时间局部性

如果缓存行大:命中率高,但读取效率低。如果缓存行小:命中率低,但读取效率高。

工业实践的妥协结果,目前(2021)的计算机多采用64bytes (64 * 8bit)为一行

由于缓存行的存在,我们必须有一种机制,来保证缓存数据的一致性,这种机制被称为缓存一致性协议。

系统屏障

程序真的是按照“顺序”执行的吗?

CPU的乱序执行

Disorder这个程序,证明乱序执行的确存在

为什么会乱序?主要是为了提高效率(在等待费时的指令执行的时候,优先执行后面的指令)

线程的as-if-serial

单个线程,两条语句,未必是按顺序执行

单线程的重排序,必须保证最终一致性

as-if-serial:看上去像是序列化(单线程)

会产生的后果

多线程会产生不希望看到的结果

ThisEscape(this 溢出问题)

推荐《Effective Java》- 不要在构造方法中启动线程!

哪些指令可以互换顺序

hanppens-before原则(JVM规定重排序必须遵守的规则)

JLS17.4.5 (不需要记住)

•程序次序规则:同一个线程内,按照代码出现的顺序,前面的代码先行于后面的代码,准确的说是控制流顺序,因为要考虑到分支和循环结构。

•管程锁定规则:一个unLOCK操作先行发生于后面(时间上)对同一个锁的lock操作。

volatile变量规则:对一个volatile变量的写操作先行发生于后面(时间上)对这个变量的读操作。

•线程启动规则:Thread的start( )方法先行发生于这个线程的每一个操作。

•线程终止规则:线程的所有操作都先行于此线程的终止检测。可以通过Thread.join( )方法结束、Thread.isAlive( )的返回值等手段检测线程的终止。

•线程中断规则:对线程interrupt( )方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupt( )方法检测线程是否中断

•对象终结规则:一个对象的初始化完成先行于发生它的finalize()方法的开始。

•传递性:如果操作A先行于操作B,操作B先行于操作C,那么操作A先行于操作C

禁止编译器乱序使用内存屏障阻止指令乱序执行

内存屏障是特殊指令:看到这种指令,前面的必须执行完,后面的才能执行

intel : lfence sfence mfence(CPU特有指令)

JVM中的内存屏障

所有实现JVM规范的虚拟机,必须实现四个屏障

LoadLoadBarrier LoadStore SL SS

volatile的底层实现

volatile修饰的内存,不可以重排序,对volatile修饰变量的读写访问,都不可以换顺序

1: volatile i

2: ACC_VOLATILE

3: JVM的内存屏障

屏障两边的指令不可以重排!保障有序!

happends-before

as - if - serial4:hotspot实现

bytecodeinterpreter.cpp

int field_offset = cache->f2_as_index(); if (cache->is_volatile()) { if (support_IRIW_for_not_multiple_copy_atomic_cpu) { OrderAccess::fence(); }

orderaccess_linux_x86.inline.hpp

inline void OrderAccess::fence() { if (os::is_MP()) { // always use locked addl since mfence is sometimes expensive #ifdef AMD64 __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory"); #else __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory"); #endif } }

LOCK 用于在多处理器中执行指令时对共享内存的独占使用。它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效。另外还提供了有序的指令无法越过这个内存屏障的作用。

面试题

DCL单例要不要加volatile?(难)

总线/缓存锁

lock;

荐书 黄俊老师的书《深入理解JAVA并发原理》(出版中)操作系统的并发控制信号量与P-V原语互斥量自旋锁读写锁中断控制与内核抢占顺序锁rcu锁JAVA启动线程的5种方法new MyThread().start()new Thread(r).start()new Thread(lamda).start()ThreadPoolFuture Callable and FutureTask常见的线程方法

我们来认识几个线程的方法

sleep() yield() join()

package com.mashibing.juc.c_000;public class T03_Sleep_Yield_Join { public static void main(String[] args) {//testSleep();//testYield(); testJoin(); } static void testSleep() { new Thread(()->{ for(int i=0; i<100; i ) { System.out.println("A" i); try {Thread.sleep(500);//TimeUnit.Milliseconds.sleep(500) } catch (InterruptedException e) {e.printStackTrace(); } } }).start(); } static void testYield() { new Thread(()->{ for(int i=0; i<100; i ) { System.out.println("A" i); if(i == 0) Thread.yield(); } }).start(); new Thread(()->{ for(int i=0; i<100; i ) { System.out.println("------------B" i); if(i == 0) Thread.yield(); } }).start(); } static void testJoin() { Thread t1 = new Thread(()->{ for(int i=0; i<100; i ) { System.out.println("A" i); try {Thread.sleep(500);//TimeUnit.Milliseconds.sleep(500) } catch (InterruptedException e) {e.printStackTrace(); } } }); Thread t2 = new Thread(()->{ try { t1.join(); } catch (InterruptedException e) { e.printStackTrace(); } for(int i=0; i<100; i ) { System.out.println("A" i); try {Thread.sleep(500);//TimeUnit.Milliseconds.sleep(500) } catch (InterruptedException e) {e.printStackTrace(); } } }); t1.start(); t2.start(); }}

线程状态

小节说明:

本节重要程度:中 (帮助理解线程问题,保障知识完整性,面试很少考)本节难度:低

JAVA的6中线程状态:

NEW : 线程刚刚创建,还没有启动RUNNABLE : 可运行状态,由线程调度器可以安排执行包括READY和RUNNING两种细分状态WAITING: 等待被唤醒TIMED WAITING: 隔一段时间后自动唤醒BLOCKED: 被阻塞,正在等待锁TERMINATED: 线程结束

如下图:


线程状态测试代码:

package com.mashibing.juc.c_000_threadbasic;import com.mashibing.util.SleepHelper;import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.LockSupport;public class T04_ThreadState { static class MyThread extends Thread { @Override public void run() { System.out.println("2: " this.getState()); for (int i = 0; i < 10; i ) { try {Thread.sleep(100); } catch (InterruptedException e) {e.printStackTrace(); } System.out.print(i " "); } System.out.println(); } } public static void main(String[] args) throws Exception { Thread t1 = new MyThread(); System.out.println("1: " t1.getState()); t1.start(); t1.join(); System.out.println("3: " t1.getState()); Thread t2 = new Thread(() -> { try { LockSupport.park(); System.out.println("t2 go on!"); TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } }); t2.start(); TimeUnit.SECONDS.sleep(1); System.out.println("4: " t2.getState()); LockSupport.unpark(t2); TimeUnit.SECONDS.sleep(1); System.out.println("5: " t2.getState()); final Object o = new Object(); Thread t3 = new Thread(()->{ synchronized (o) { System.out.println("t3 得到了锁 o"); } }); new Thread(()-> { synchronized (o) { SleepHelper.sleepSeconds(5); } }).start(); SleepHelper.sleepSeconds(1); t3.start(); SleepHelper.sleepSeconds(1); System.out.println("6: " t3.getState()); }}

线程的打断

小节说明:

重要程度:中(面试不多)

小节难度:低

interrupt相关的三个方法:

//Thread.java public void interrupt() //t.interrupt() 打断t线程(设置t线程某给标志位f=true,并不是打断线程的运行)public boolean isInterrupted() //t.isInterrupted() 查询打断标志位是否被设置(是不是曾经被打断过)public static boolean interrupted()//Thread.interrupted() 查看“当前”线程是否被打断,如果被打断,恢复标志位

interrupt() :实例方法,设置线程中断标志(打扰一下,你该处理一下中断)isInterrupted():实例方法,有没有人打扰我?interrupted():静态方法,有没有人打扰我(当前线程)?复位!interrupt和sleep() wait() join()

sleep()方法在睡眠的时候,不到时间是没有办法叫醒的,这个时候可以用interrupt设置标志位,然后呢必须得catch InterruptedException来进行处理,决定继续睡或者是别的逻辑,(自动进行中断标志复位)

interrupt是否能中断正在竞争锁的线程

package com.mashibing.juc.c_000_threadbasic;import com.mashibing.util.SleepHelper;public class T09_Interrupt_and_sync { private static Object o = new Object(); public static void main(String[] args) { Thread t1 = new Thread(()-> { synchronized (o) { SleepHelper.sleepSeconds(10); } }); t1.start(); SleepHelper.sleepSeconds(1); Thread t2 = new Thread(()-> { synchronized (o) { } System.out.println("t2 end!"); }); t2.start(); t2.interrupt(); }}

interrupt()不能打断正在竞争锁的线程synchronized()

如果想打断正在竞争锁的线程,使用ReentrantLock的lockInterruptibly()

package com.mashibing.juc.c_000_threadbasic;import com.mashibing.util.SleepHelper;import java.util.concurrent.locks.ReentrantLock;public class T11_Interrupt_and_lockInterruptibly { private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { Thread t1 = new Thread(()-> { lock.lock(); try { SleepHelper.sleepSeconds(10); } finally { lock.unlock(); } System.out.println("t1 end!"); }); t1.start(); SleepHelper.sleepSeconds(1); Thread t2 = new Thread(()-> { System.out.println("t2 start!"); try { lock.lockInterruptibly(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } System.out.println("t2 end!"); }); t2.start(); SleepHelper.sleepSeconds(1); t2.interrupt(); }}

优雅的结束线程

小节说明:

本节内容的重要程度:中(面试有可能被问)

小节难度:低

结束线程的方法:

自然结束(能自然结束就尽量自然结束)stop() suspend() resume()volatile标志不适合某些场景(比如还没有同步的时候,线程做了阻塞操作,没有办法循环回去)打断时间也不是特别精确,比如一个阻塞容器,容量为5的时候结束生产者,但是,由于volatile同步线程标志位的时间控制不是很精确,有可能生产者还继续生产一段儿时间interrupt() and isInterrupted(比较优雅)线程组

(不重要,暂忽略。)

ThreadGroups - Thread groups are best viewed as an unsuccessful experiment , and you may simply ignore their existence! - Joshua Bloch one of JDK designers

并发编程的特性可见性有序性原子性线程的原子性

从一个简单的小程序谈起:

package com.mashibing.juc.c_001_sync_basics;import java.util.concurrent.CountDownLatch;public class T00_IPlusPlus { private static long n = 0L; public static void main(String[] args) throws Exception { Thread[] threads = new Thread[100]; CountDownLatch latch = new CountDownLatch(threads.length); for (int i = 0; i < threads.length; i ) { threads[i] = new Thread(() -> { for (int j = 0; j < 10000; j ) {//synchronized (T00_IPlusPlus.class) {n ;//} } latch.countDown(); }); } for (Thread t : threads) { t.start(); } latch.await(); System.out.println(n); }}

一些基本概念

race condition => 竞争条件 , 指的是多个线程访问共享数据的时候产生竞争

数据的不一致(unconsistency),并发访问之下产生的不期望出现的结果

如何保障数据一致呢?--> 线程同步(线程执行的顺序安排好),

monitor (管程) ---> 锁

critical section -> 临界区

如果临界区执行时间长,语句多,叫做 锁的粒度比较粗,反之,就是锁的粒度比较细

具体: 保障操作的原子性(Atomicity)

悲观锁:悲观的认为这个操作会被别的线程打断(悲观锁)synchronized(上一个小程序)乐观锁:乐观的认为这个做不会被别的线程打断(乐观锁 自旋锁 无锁)cas操作CAS = Compare And Set/Swap/Exchange package com.mashibing.juc.c_018_00_AtomicXXX; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; public class T01_AtomicInteger { //int count1 = 0; AtomicInteger count = new AtomicInteger(0); void m() { for (int i = 0; i < 10000; i ) //if count1.get() < 1000 count.incrementAndGet(); //count1 } public static void main(String[] args) { T01_AtomicInteger t = new T01_AtomicInteger(); List<Thread> threads = new ArrayList<Thread>(); for (int i = 0; i < 100; i ) { threads.add(new Thread(t::m, "thread-" i)); } threads.forEach((o) -> o.start()); threads.forEach((o) -> { try { o.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); System.out.println(t.count); } }

我们平时所说的"上锁",一般指的是悲观锁

上锁的本质

上锁的本质是把并发编程序列化

package com.mashibing.juc.c_001_sync_basics;import com.mashibing.util.SleepHelper;public class T00_01_WhatIsLock { private static Object o = new Object(); public static void main(String[] args) { Runnable r = () -> { //synchronized (o) { //打开注释试试看,对比结果 System.out.println(Thread.currentThread().getName() " start!"); SleepHelper.sleepSeconds(2); System.out.println(Thread.currentThread().getName() " end!"); //} }; for (int i = 0; i < 3; i ) { new Thread(r).start(); } }}

同时保障可见性

注意序列化并非其他程序一直没机会执行,而是有可能会被调度,但是抢不到锁,又回到Blocked或者Waiting状态(sync锁升级)

一定是锁定同一把锁(抢一个坑位)

package com.mashibing.juc.c_001_sync_basics;import com.mashibing.util.SleepHelper;public class T00_02_SingleLockVSMultiLock { private static Object o1 = new Object(); private static Object o2 = new Object(); private static Object o3 = new Object(); public static void main(String[] args) { Runnable r1 = () -> { synchronized (o1) { System.out.println(Thread.currentThread().getName() " start!"); SleepHelper.sleepSeconds(2); System.out.println(Thread.currentThread().getName() " end!"); } }; Runnable r2 = () -> { synchronized (o2) { System.out.println(Thread.currentThread().getName() " start!"); SleepHelper.sleepSeconds(2); System.out.println(Thread.currentThread().getName() " end!"); } }; Runnable r3 = () -> { synchronized (o3) { System.out.println(Thread.currentThread().getName() " start!"); SleepHelper.sleepSeconds(2); System.out.println(Thread.currentThread().getName() " end!"); } }; new Thread(r1).start(); new Thread(r2).start(); new Thread(r3).start(); }}

什么样的语句(指令)具备原子性?

CPU级别汇编,需要查询汇编手册!

Java中的8大原子操作:(了解即可,无需背过)

lock:主内存,标识变量为线程独占unlock:主内存,解锁线程独占变量read:主内存,读取内存到线程缓存(工作内存)load:工作内存,read后的值放入线程本地变量副本use:工作内存,传值给执行引擎assign:工作内存,执行引擎结果赋值给线程本地变量store:工作内存,存值到主内存给write备用write:主内存,写变量值JVM中的两中锁

重量级锁(经过操作系统的调度)synchronized早期都是这种锁(目前的实现中升级到最后也是这种锁)

轻量级锁(CAS的实现,不经过OS调度)(无锁 - 自旋锁 - 乐观锁)

CAS深度剖析

CAS的ABA问题解决方案 - Version

CAS操作本身的原子性保障

AtomicInteger:

public final int incrementAndGet() { for (;;) { int current = get(); int next = current 1; if (compareAndSet(current, next)) return next; } }public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }

Unsafe:

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

运用:

package com.mashibing.jol;import sun.misc.Unsafe;import java.lang.reflect.Field;public class T02_TestUnsafe { int i = 0; private static T02_TestUnsafe t = new T02_TestUnsafe(); public static void main(String[] args) throws Exception { //Unsafe unsafe = Unsafe.getUnsafe(); Field unsafeField = Unsafe.class.getDeclaredFields()[0]; unsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) unsafeField.get(null); Field f = T02_TestUnsafe.class.getDeclaredField("i"); long offset = unsafe.objectFieldOffset(f); System.out.println(offset); boolean success = unsafe.compareAndSwapInt(t, offset, 0, 1); System.out.println(success); System.out.println(t.i); //unsafe.compareAndSwapInt() }}

jdk8u: unsafe.cpp:

cmpxchg = compare and exchange set swap

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) UnsafeWrapper("Unsafe_CompareAndSwapInt"); oop p = JNIHandles::resolve(obj); jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); return (jint)(Atomic::cmpxchg(x, addr, e)) == e;UNSAFE_END

jdk8u: atomic_linux_x86.inline.hpp 93行

is_MP = Multi Processors

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) { int mp = os::is_MP(); __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)": "=a" (exchange_value): "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp): "cc", "memory"); return exchange_value;}

jdk8u: os.hpp is_MP()

static inline bool is_MP() { // During bootstrap if _processor_count is not yet initialized // we claim to be MP as that is safest. If any platform has a // stub generator that might be triggered in this phase and for // which being declared MP when in fact not, is a problem - then // the bootstrap routine for the stub generator needs to check // the processor count directly and leave the bootstrap routine // in place until called after initialization has ocurred. return (_processor_count != 1) || AssumeMP; }

jdk8u: atomic_linux_x86.inline.hpp

#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

最终实现:

cmpxchg = cas修改变量值

lock cmpxchg 指令

硬件:

lock指令在执行的时候视情况采用缓存锁或者总线锁

两种锁的效率

不同的场景:

临界区执行时间比较长 , 等的人很多 -> 重量级

时间短,等的人少 -> 自旋锁

synchronized如何保障可见性


JVM中的线程和OS线程对应关系

JVM 1:1 -> LOOM -> M:N (golang)

synchronized锁升级过程

(内容太多,见另一篇文档)

常见的锁类型悲观锁 乐观锁自旋锁(CAS)读写锁排他锁 共享锁分段锁死锁 活锁数据库的行锁 表锁 间隙锁 ...偏向锁可重入锁题外话:有没有程序天生就是线程安全的?

有没有一门编程语言天生安全,目前有一门RUST,但是由于语言难度较大,同时缺乏强有力的团队推广,目前并不是很流行,对RUST有了解兴趣的,参考马士兵老师《RUST》

一些大厂难题线程唤醒问题(阿里)样例代码

public class Test { private final static Object LOCK = new Object(); public void startThreadA() { new Thread(() -> { synchronized (LOCK) { System.out.println(Thread.currentThread().getName() ": get lock"); //启动线程b startThreadB(); System.out.println(Thread.currentThread().getName() ": start wait"); try { //线程a wait LOCK.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() ": get lock after wait"); System.out.println(Thread.currentThread().getName() ": release lock"); } }, "thread-A").start(); } private void startThreadB() { new Thread(() -> { synchronized (LOCK) { System.out.println(Thread.currentThread().getName() ": get lock"); //启动线程c startThreadC(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() ": start notify"); //线程b唤醒其他线程 LOCK.notify(); System.out.println(Thread.currentThread().getName() ": release lock"); } }, "thread-B").start(); } private void startThreadC() { new Thread(() -> { System.out.println(Thread.currentThread().getName() ": thread c start"); synchronized (LOCK) { System.out.println(Thread.currentThread().getName() ": get lock"); System.out.println(Thread.currentThread().getName() ": release lock"); } }, "thread-C").start(); } public static void main(String[] args) { new Test().startThreadA(); } }

输出结果:

thread-A: get lock thread-A: start wait thread-B: get lock thread-C: thread c start thread-B: start notify thread-B: release lock thread-A: get lock after wait thread-A: release lock thread-C: get lock thread-C: release lock

问题:

为什么每次运行,线程A总是优先于线程C获取锁

分析:

在Hotspot源码中,我们知道synchronized关键字是通过monitor_enter和monitor_exit字节来实现的,最终用于阻塞线程的对象为ObjectMonitor对象,该对象包含三个关键字段:*WaitSet、*cxq、*EntryList。*WaitSet用于保存使用wait方法释放获得的synchronized锁对象的线程,也即我们调用wait函数,那么当前线程将会释放锁,并将自身放入等待集中。而cxq队列用于存放竞争ObjectMonitor锁对象失败的线程,而_EntryList用于也用于存放竞争锁失败的线程。那么它们之间有何区别呢?这是由于我们需要频繁的释放和获取锁,当我们获取锁失败那么将需要把线程放入竞争列表中,当唤醒时需要从竞争列表中获取线程唤醒获取锁,而如果我们只用一个列表来完成这件事,那么将会导致锁争用导致CPU资源浪费且影响性能,这时我们独立出两个列表,其中cxq列表用于竞争放入线程,而entrylist用于单线程唤醒操作。具体策略是这样的:

线程竞争锁失败后CAS放入cxq列表中线程释放锁后将根据策略来唤醒cxq或者entrylist中的线程(我们这里只讨论默认策略)默认策略下优先唤醒entrylist列表中的线程,因为唤醒线程对象的操作是单线程的,也即只有获取锁并且释放锁的线程可以操作,所以操作entrylist是线程安全的如果entrylist列表为空,那么将会CAS将cxq中的等待线程一次性获取到entrylist中并开始逐个唤醒

在hotspot中我们称这种算法为电梯算法,也即将需要唤醒的线程一次性从竞争队列中放入entrylist唤醒队列。

那么这时我们就可以分析以上代码为何总是唤醒线程A了,我们先看线程执行顺序,首先启动线程A,随后线程A启动线程B,B线程需要获取对象锁从而创建线程C,我们看到当线程A调用wait方法将自己放入等待集中后,将会唤醒线程B,随后线程B创建并启动了线程C,然后等待C开始执行,由于此时对象锁由线程B持有,所以线程C需要放入cxq竞争队列,随后B从睡眠中醒来,执行notify方法,该方法总是唤醒了线程A而不是C,也即优先处理等待集中的线程而不是cxq竞争队列的线程。那么我们通过notify方法来看看实现原理。Notify便是Wait操作的反向操作,所以这里很简单,无非就是将线程从等待集中移出并且唤醒。源码如下。

JVM_ENTRY(void, JVM_MonitorNotify(JNIEnv* env, jobject handle)) Handle obj(THREAD, JNIHandles::resolve_non_null(handle)); // 直接调用ObjectSynchronizer::notify ObjectSynchronizer::notify(obj, CHECK); JVM_END

这里直接跟进ObjectSynchronizer::notify。源码如下。

void ObjectSynchronizer::notify(Handle obj, TRAPS) { if (UseBiasedLocking) { // 如果使用偏向锁,那么取消偏向锁 BiasedLocking::revoke_and_rebias(obj, false, THREAD); } markOop mark = obj->mark(); if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) { // 如果是轻量级锁,那么直接返回,因为wait操作需要通过对象监视器来做 return; } ObjectSynchronizer::inflate(THREAD, obj())->notify(THREAD); }

可以看到最终调用了ObjectSynchronizer的notify方法来唤醒。源码如下。

void ObjectMonitor::notify(TRAPS) { CHECK_OWNER(); if (_WaitSet == NULL) { // 如果等待集为空,直接返回 return ; } int Policy = Knob_MoveNotifyee ; // 移动策略,这里默认是2 Thread::SpinAcquire (&_WaitSetLock, "WaitSet - notify") ; // 首先对等待集上自旋锁 // 调用DequeueWaiter将一个等待线程从等待集中拿出来 ObjectWaiter * iterator = DequeueWaiter() ; if (iterator != NULL) { if (Policy != 4) { // 如果策略不等于4那么将线程的状态修改为TS_ENTER iterator->TState = ObjectWaiter::TS_ENTER ; } iterator->_notified = 1 ; // 唤醒计数器 Thread * Self = THREAD; iterator->_notifier_tid = Self->osthread()->thread_id(); ObjectWaiter * List = _EntryList ; if (Policy == 0) { // 如果策略为0,那么头插入到entrylist中 if (List == NULL) { // 如果entrylist为空,那么将当前监视器直接作为_EntryList 头结点 iterator->_next = iterator->_prev = NULL ; _EntryList = iterator ; } else { // 否则头插 List->_prev = iterator ; iterator->_next = List ; iterator->_prev = NULL ; _EntryList = iterator ; } } else if (Policy == 1) { // 如果策略为1,那么插入entrylist的尾部 if (List == NULL) { iterator->_next = iterator->_prev = NULL ; _EntryList = iterator ; } else { ObjectWaiter * Tail ; for (Tail = List ; Tail->_next != NULL ; Tail = Tail->_next) ; Tail->_next = iterator ; iterator->_prev = Tail ; iterator->_next = NULL ; } } else if (Policy == 2) { // 如果策略为2,那么如果entrylist为空,那么插入entrylist,否则插入cxq队列 if (List == NULL) { iterator->_next = iterator->_prev = NULL ; _EntryList = iterator ; } else { iterator->TState = ObjectWaiter::TS_CXQ ; for (;;) { ObjectWaiter * Front = _cxq ; iterator->_next = Front ; if (Atomic::cmpxchg_ptr (iterator, &_cxq, Front) == Front) { break ; } } } } else if (Policy == 3) { // 如果策略为3,那么直接插入cxq iterator->TState = ObjectWaiter::TS_CXQ ; for (;;) { ObjectWaiter * Tail ; Tail = _cxq ; if (Tail == NULL) { iterator->_next = NULL ; if (Atomic::cmpxchg_ptr (iterator, &_cxq, NULL) == NULL) { break ; } } else { while (Tail->_next != NULL) Tail = Tail->_next ; Tail->_next = iterator ; iterator->_prev = Tail ; iterator->_next = NULL ; break ; } } } else { // 否则直接唤醒线程,让线程自己去调用enterI进入监视器 ParkEvent * ev = iterator->_event ; iterator->TState = ObjectWaiter::TS_RUN ; OrderAccess::fence() ; ev->unpark() ; } } Thread::SpinRelease (&_WaitSetLock) ; // 释放等待集自旋锁 }

这里有一个方法DequeueWaiter() 将线程从等待集中取出来,这里的notify读者都知唤醒一个,很多人都说随机唤醒一个,那么我们这里来看看唤醒算法是什么。源码如下。

inline ObjectWaiter* ObjectMonitor::DequeueWaiter() { ObjectWaiter* waiter = _WaitSet; // 很简单对吧,直接从头部拿 if (waiter) { // 如果waiter不为空,那么从等待集中断链 DequeueSpecificWaiter(waiter); } return waiter; } inline void ObjectMonitor::DequeueSpecificWaiter(ObjectWaiter* node) { ObjectWaiter* next = node->_next; if (next == node) { // 如果只有一个节点,那么直接将等待集清空即可 _WaitSet = NULL; } else { // 否则双向链表的断链基础操作 ObjectWaiter* prev = node->_prev; next->_prev = prev; prev->_next = next; if (_WaitSet == node) { _WaitSet = next; } } // 断开连接后,也需要把断下来的节点,next和prev指针清空 node->_next = NULL; node->_prev = NULL; }

那么读者应该可以明显的看到,底层对于唤醒操作是从等待集的头部选择线程唤醒。

总结

通过源码我们看到,为何总是唤醒线程A,这是用于当线程C竞争不到锁时,被放入了cxq队列,而此时entrylist为null,线程A在等待集waitset中,当我们调用notify方法时,由于移动策略默认是2,这时会从等待集的头部将线程A取下,放入到entrylist中,当notify执行完毕后,在执行后面的monitor_exit字节码时将会优先从entrylist中唤醒线程,这就导致了A线程总是被优先执行。

线程执行完isAlive方法返回true问题(谷歌)样例代码:

public class ThreadAliveTest { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { System.out.println("t1 start"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1 end"); }); t1.start(); Thread t2 = new Thread(() -> { synchronized (t1) { System.out.println("t2 start"); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1 isAlive:" t1.isAlive()); } }); t2.start(); } }

输出结果:t1 start t2 start t1 end t1 isAlive:true

问题:

为什么线程结束了,isAlive方法还返回true

分析:

我们首先看看执行流程,线程T1启动后将会睡眠2秒,随后2秒后执行结束,随后线程T2启动,T2首先获取到T1的对象锁,然后睡眠5秒,随后调用T1的isAlive方法判定线程是否存活,那么为什么会输出true呢?我们还得先看看isAlive方法如何实现的。我们来看源码。

public final native boolean isAlive();

首先看到isAlive方法由JNI方法实现。我们来看Hotspot源码。

JVM_ENTRY(jboolean, JVM_IsThreadAlive(JNIEnv* env, jobject jthread)) JVMWrapper("JVM_IsThreadAlive"); oop thread_oop = JNIHandles::resolve_non_null(jthread); return java_lang_Thread::is_alive(thread_oop); JVM_END

我们看到首先通过resolve_non_null方法将jthread转为oop对象thread_oop,随后调用java_lang_Thread的is_alive方法来判断是否存活,我们继续跟进。

bool java_lang_Thread::is_alive(oop java_thread) { JavaThread* thr = java_lang_Thread::thread(java_thread); return (thr != NULL); } JavaThread* java_lang_Thread::thread(oop java_thread) { return (JavaThread*)java_thread->address_field(_eetop_offset); }

我们看到最后是通过获取java thread对象,也即java的Thread类中的eetop属性,如果该属性为null,那么表明线程已经销毁,也即返回false,如果eetop还在那么返回true,表明线程存活。那么什么是eetop呢?我们还得从线程创建方法入手。

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread)) JVMWrapper("JVM_StartThread"); JavaThread *native_thread = NULL; bool throw_illegal_thread_state = false; // 非法线程状态标识 { // Threads_lock上锁,保证C 的线程对象和操作系统原生线程不会被清除。当前方法执行完,也就是栈帧释放时,会释放这里的锁,当然肯定会调用析构函数,而这个对象的析构函数中调用unlock方法释放锁 MutexLocker mu(Threads_lock); if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) { // 如果线程不为空,则表明线程已经启动,则为非法状态 throw_illegal_thread_state = true; } else { // 本来这里可以检测一下stillborn标记来看看线程是否已经停止,但是由于历史原因,就让线程自己玩了,这里就不玩了 // 取得线程对象的stackSize的大小 jlong size = java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread)); // 开始创建C Thread对象和原生线程对象,使用无符号的线程栈大小,所以这里不会出现负数 size_t sz = size > 0 ? (size_t) size : 0; // 创建JavaThread,这里的thread_entry为传入的运行地址,也就是启动线程,需要一个入口执行点,这个函数地址便是入口执行点 native_thread = new JavaThread(&thread_entry, sz); // 如果osthread不为空,则标记当前线程还没有被使用 if (native_thread->osthread() != NULL) { native_thread->prepare(jthread); } } } // 如果throw_illegal_thread_state不为0,那么直接抛出异常 if (throw_illegal_thread_state) { THROW(vmSymbols::java_lang_IllegalThreadStateException()); } // 原生线程必然不能为空,因为线程是由操作系统创建的,所以没有OS线程,空有个JavaThread类有啥用0.0 if (native_thread->osthread() == NULL) { delete native_thread; // 直接用C 的delete释放内存 THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(),"unable to create new native thread"); } Thread::start(native_thread); // 一切准备妥当,开始启动线程 JVM_END

我们看到首先创建了JavaThread对象,该对象内部创建了OSThread对象,我们这么理解:JavaThread代表了C 层面的Java线程,而OSThread代表了操作系统层面的线程对象。随后调用了native_thread->prepare(jthread)方法为启动线程做准备。我们关注该方法。

void JavaThread::prepare(jobject jni_thread, ThreadPriority prio) { // 包装当前Java线程对象 Handle thread_oop(Thread::current(), JNIHandles::resolve_non_null(jni_thread)); // 将Java层面的线程Oop对象与JavaThread C 层面的对象关联 set_threadObj(thread_oop()); java_lang_Thread::set_thread(thread_oop(), this); // 设置优先级 if (prio == NoPriority) { prio = java_lang_Thread::priority(thread_oop()); } Thread::set_priority(this, prio); // 将JavaThread类放入到全局线程列表中 Threads::add(this); }

我们注意看 java_lang_Thread::set_thread方法。我们跟进它的源码。

void java_lang_Thread::set_thread(oop java_thread, JavaThread* thread) { // 将JavaThread C 层面的线程对象设置为Java层面的Thread oop对象的eetop变量 java_thread->address_field_put(_eetop_offset, (address)thread); }

这下我们知道了eetop变量即使JavaThread对象的地址信息。在了解完eetop如何被设置之后我们得继续看,eetop什么时候被取消。当Java线程执行完Runnable接口的run方法最后一个字节码后,将会调用exit方法。该方法完成线程对象的退出和清理操作,我们重点看ensure_join方法。

void JavaThread::exit(bool destroy_vm, ExitType exit_type) { ... ensure_join(this); ... }

我们继续跟进ensure_join的源码实现。

static void ensure_join(JavaThread* thread) { // 封装Java Thread线程oop对象 Handle threadObj(thread, thread->threadObj()); // 获取Java Thread线程oop对象锁 ObjectLocker lock(threadObj, thread); // 清除未处理的异常信息 thread->clear_pending_exception(); // 将状态修改为TERMINATED java_lang_Thread::set_thread_status(threadObj(), java_lang_Thread::TERMINATED); // 将Java Thread线程oop对象与JavaThread C 对象解绑 java_lang_Thread::set_thread(threadObj(), NULL); // 唤醒所有阻塞在线程对象的线程 lock.notify_all(thread); // 如果以上代码期间发生异常,那么清理挂起的异常 thread->clear_pending_exception(); }

我们看到最终由ensure_join方法中的java_lang_Thread::set_thread(threadObj(), NULL),将eetop变量设置为null,当执行完这一步时,我们再通过isAlive方法判断线程是否存活时,将返回false,否则返回true。而我们看到在操作该变量时需要获取线程对象锁。我们来看ObjectLocker的构造函数和析构函数的实现。

ObjectLocker::ObjectLocker(Handle obj, Thread* thread, bool doLock) { _dolock = doLock; _thread = thread; if (_dolock) { // 获取Java Thread线程oop对象锁 ObjectSynchronizer::fast_enter(_obj, &_lock, false, _thread); } } ObjectLocker::~ObjectLocker() { if (_dolock) { // 释放Java Thread线程oop对象锁 ObjectSynchronizer::fast_exit(_obj(), &_lock, _thread); } }

我们看到当我们创建ObjectLocker对象时,会在构造函数中获取到线程对象锁,而当ensure_join方法执行完毕后,将会调用ObjectLocker的析构函数,在该函数中释放线程对象锁。

总结:

这下我们就可以通过以上知识来分析为何isAlive方法在线程执行完毕后仍然返回true了,这是用于isAlive方法通过判断Java线程对象的eetop变量来判定线程是否存活,而当我们线程执行完毕后将会调用exit方法,该方法将会调用ensure_join方法,在该方法中将eetop甚至为null,但是由于赋值前需要获取到Java线程的对象锁,而该对象的对象锁已经由线程T2持有,这时当前线程将会阻塞,从而造成eetop变量没有被清除,从而导致isAlive方法在T1线程执行完毕后仍然返回true。读者也可以看看java Thread的源码,join函数也是通过对Thread对象获取锁然后调用isAlive来判定线程是否结束的,这就意味着如果我们用别的线程持有了Java Thread的对象锁,那么这时调用join方法的线程也是会被阻塞的。

  • 元宵煮多久能熟(煮元宵的方法)
  • 2024-11-22煮元宵的方法新鲜的元宵煮7分钟左右即就能熟,速冻元宵需要煮10分钟才能熟煮元宵之前先轻轻捏一下,等到锅中的水沸腾后再下锅,如果是新鲜的汤圆,煮至浮出水面即可,如果是速冻汤圆,浮出水面后要再加入一点冷水,第二次上浮。
  • 微信群主怎么看是谁(微信看群主是谁怎么做)
  • 2024-11-22微信看群主是谁怎么做要是想看群主是谁,那首先让我们打开手机从桌面找到“微信”APP并点击打开当我们进入微信之后,找到左下角“微信”按钮并点击一下,然后我们就进入了显示消息界面我们完成了上个步骤,接着滑动屏幕找到要查看的群。
  • 卡西欧方块联名(模玩资讯CASIOBABY-G)
  • 2024-11-22模玩资讯CASIOBABY-G著名的手表品牌CASIO,为了庆祝旗下相当受到欢迎的人气商品系列「BABY-G」已经问世25周年,特别与大家最喜欢的「精灵宝可梦」(ポケットモンスター)里面的最受欢迎的「皮卡丘」(ピカチュウ)合作推出。
  • 脖子紧与甲亢有关系吗(脖子大就一定是甲亢吗)
  • 2024-11-22脖子大就一定是甲亢吗脖子肿大可能是颈部器官病、或者皮下积液造成的脖子肿大给人最深刻的印象是甲亢或者甲状腺结节实际上,很多疾病都会导致颈部肿大颈部肿大可能是多种不同原因的信号,可能有下列症状:全身症状:疼痛、发热、盗汗、呼。
  • 头盔玻璃起雾怎么办(有什么办法能让头盔前面不挂水)
  • 2024-11-22有什么办法能让头盔前面不挂水现在很多人都喜欢骑那种非常酷的摩托车、重型机车,搭配上帅气酷炫的头盔,别提有多拉风了但是如果遇到下雨的话,想耍酷可能就会受到限制,因为下雨的时候头盔玻璃很容易挂水,留水珠,这种很影响开车视野,非常影响。
  • 手机充不上电怎么恢复(看完你就知道了)
  • 2024-11-22看完你就知道了检查手机充电口:查看手机上的充电口有没有进入异物,例如卫生纸,细小颗粒等如果有,用绣花针轻轻的将充电口里的异物取出即可更换充电器:充电器和手机接触不良,此时只需要重新将充电器插头插入手机或者更换数据线。
  • 红人馆女装店岑巩(红人馆脱衣舞娘Dita的别墅)
  • 2024-11-22红人馆脱衣舞娘Dita的别墅“能去你家住一晚吗?”有人家里豪华,有人家里清净,有人家里被子没叠,有人家里是仙境大家好像都看出赵小棠家装修豪华别致,其实传奇舞娘Dita的家,才是从进门的每一步都精心设计过可以称其为Dita的移动城。
  • 韭菜种植技术详解(韭菜传统栽培方法费时又耗工)
  • 2024-11-22韭菜传统栽培方法费时又耗工当前韭菜种植一般为大田直播或土地平畦育苗移栽,但存在以下问题:①直播虽然可节省工作量,但韭菜种子小,苗小,出苗、生长慢,整齐度差,且易发生草荒,田间养护很不容易;②若采用传统土地平畦育苗移栽,育苗时可。
  • 卡娃搞笑(当代卡娃迷惑行为大赏)
  • 2024-11-22当代卡娃迷惑行为大赏这个世界,有一类娃,叫卡娃有一种野到不行的迷惑行为,叫做卡娃的迷惑行为那到底什么是卡娃?卡娃顾名思义,就是哪里都能卡自动扶梯能卡,魔鬼身材门能卡,蒸笼孔能卡,痰盂也能卡,手指头能卡,眼皮也能卡各种奇葩。
  • 面部刮痧有什么好处(面部刮痧有哪些好处)
  • 2024-11-22面部刮痧有哪些好处面部刮痧的好处比较多,比如可以减少皱纹、紧致皮肤、疏通经络,促进肌肤的新陈代谢,还能够间接调节脏腑,改善耳鸣耳聋、鼻炎症状但是面部不宜长期刮痧,刮痧的手法也不宜过重,以免损伤皮肤此外,面部有皮肤破损的。