并行与并发:
并行:不同的线程互不干扰
并发:不同的线程存在资源竞争
线程的七种状态:新建、就绪、运行、死亡、阻塞、等待、睡眠
一个核心同一时刻只能运行一个线程。
由操作系统进行调度,让CPU核心 轮流执行线程
Thread x1 = new Thread() {
@Override
public void run() {
for(;;) {
System.out.println("======");
}
}
};
x1.start();//进入就绪态
任何的任务不能绕过操作系统
操作系统从就绪队列中选中已就绪的任务交给CPU运行,时间片截止后回到队列(这里的队列属于不公平队列)
任何程序都不会立即执行,而是进入就绪态。立即执行会占用一个核心。
和人交互的优先级会更高
.start()仅仅是进入就绪态,而不是立即执行
方法什么时候能执行根据硬件决定,与语言无关
时间片是随机的,操作系统给每个线程分配时间片。
主线程创建子线程之后,所有线程是独立的, 先进入就绪状态的不一定会先执行,只是被执行的概率大
上下文切换:
如果分配的时间片( 一般是几毫秒到几十毫秒,不同的操作系统不一样)不够则会把 当前状态记录下来重新回到就绪态,下次再轮到执行时则从内存中读取,从上次执行到的地方接着执行。相反如果分配的时间大于线程执行时间的话则会立刻让出CPU
读取的过程会对cpu造成额外的损耗(ms级)
从保存到再加载的过程就是一次上下文切换。
start()会把start里存的信息放到操作系统的就绪队列中,以便随时调用
一个线程读取另一个线程的变量,相当于在前面加 final
在 浪费严重的情况下使用多线程可以增加运行效率
举例:io访问网络,访问硬盘时。IO密集(等待的时间比运行的时间长很多)
t1.join();//后面的代码块必须在t1线程执行完之后才能继续执行
单核情况下执行两个一万次for循环串行是比并行快的。
使用Lmbench3可以测量上下文切换的时长
使用vmstat可以测量上下文切换的次数
Lmbench3是一个性能分析工具
如何减少上下文切换次数:无锁并发编程、CAS算法,使用最少线程和使用协程
重要的几个指令:grep,awk
dump:快照,查看一瞬间的信息
synchronized()只能对引用类型加锁,基本数据类型会报错
sleep()线程如果进入睡眠状态,会立刻让出CPU且不在就绪队列
锁的范围:锁后的大括号执行完才会释放锁,即使让出cpu也不会释放锁
wait():让出CPU并且释放锁
线程竞争资源失败后会进入阻塞队列,但是不会释放锁
如何避免死锁?
synchronized读和写安全,volatile读安全写不安全
可见性的意思是当一个线程修改一个共享变量时,另一个线程能读到这个修改的值
指令在总线上必须排队传输---->内存在同一时刻只能被一个指令指挥
高速缓存内存越大,计算机的性能一定越好
设置三层缓存,支持多核CPU,也提升了核心的利用率。但是也带来了并发问题:相互覆盖
内存通过总线交给CPU的数据,进入CPU之后先给L3,再传给L2,再传给L1,再传给寄存器,然后交给CPU
数据从高速缓存中传回内存的时机是不确定的,同时高速缓存中会清除数据
内存屏障:按照约定,一种表示可以访问,一种表示不可以访问
缓冲行:缓存中可以分配的最小存储单位(64字节)
原子操作:不可中断的一个或一系列操作(中断后需要全部还原重新执行)
volatile并不一定能保证数据的准确性:两个线程同时操作volatile修饰的变量,从高速缓存区写回到内存后,会把该变量在高速缓存区中对应的数据清除,这样线程需要重新访问更新后的数据,从而保证数据的准确。但是如果两个同时进入总线的指令队列,此时不会清除指令队列中的数据,所以仍出现了覆盖。
程序的执行过程:线程
线程的不断执行:依赖方法的不断入栈出栈
内存为正在运行的程序开辟内存空间
线程之间相互独立
锁需要加在被争抢的资源上面
一个完整的操作不能分开加锁
锁加在方法上:
- 非静态方法:锁的是方法的调用者
- 静态方法:锁的是类
锁加在代码块上:锁住的是传入的对象
跨线程时,A线程内的变量,在其余线程是final类型的(只可读,不能修改指向)
volatile(只能修饰变量)只能保证读准确,但是写会存在覆盖,不能保证数据被正确修改
synchronized保证了写后读(一个线程对资源写(更新)完之后,其余线程才能读取数据),所以保证了数据的准确性。
synchronized如果修饰非静态方法,调用该方法的对象也会被锁住
synchronized不一定保证数据准确,一定要保证读并且写完之后再释放锁(即写后读),才能保证数据准确
多线程,多进程,多服务器,集群并发操作,只要是写后读,最终计算结果一定是准确的(语言无关)
进程与进程之间不允许访问对方的私有属性
无论是哪种语言,最终都翻译成C语言,由C语言直接驱动
缓存行(一个基本单元是64字节)只填充此线程的数据,避免了读取其他线程数据的操作,提高了运算效率
public synchronized static m1(){}对象锁锁的是对象,类锁锁的是类。(不加锁的方法不受影响)
但是synchronized (x1){ }此时x1的一切都不可读不可写
抛异常时会释放锁
- 同步是指对某个资源轮流操作必须等其余释放资源之后才能操作
- 异步同时操作互不干扰
锁的范围,由开始标记和结束标记决定
字宽、地址最终都受程序的位数(程序在一个时钟周期处理的位数)影响
对象锁的对象头:
- 32/64位:存储对象的hashcode值或锁信息
- 32/64位:
锁的升级是指synchronized的三种状态:偏向锁,轻量级锁和重量级锁(只能升级不能降级)
- 偏向锁:适用于只有一个线程
- 轻量级锁:只用于线程竞争不激烈
- 重量级锁:适用于线程竞争激烈的情况下
当几个线程同时竞争统一资源时,其中一个线程拿到资源后加上了锁,其余线程竞争失败就会进入阻塞队列。即使不进入阻塞队列时间片轮转到也会因为无法调用资源而浪费CPU。
一个资源对应着一个它的阻塞队列
CAS 比较并且交换--必须调用操作系统提供的CAS内核函数(原子性):线程操作资源的同时记录读取资源时刻的状态,如果更新时状态一致则改变,不一致则会重复进行读取并操作。
流水线技术:减少了半导体切换次数,加快工作效率。但是指令顺序会发生变化
保证原子操作:
- 通过总线锁保证原子性
- 通过缓存锁保证原子性
CAS操作都需要一个for()循环,直到比较成功退出循环。
并发剧烈的情况下synchronized锁合适,低并发的情况下CAS合适
面试点:ABA:经过两次修改又回到了原来的值(通过加版本号解决:每次修改版本号+1)
比较版本号并且更新也是原子操作
线程可见性:一个线程修改了共享变量,其他线程能及时看到修改后准确的数据
final:减少指令重排序
进程由线程组成,进程拥有的功能线程全都有,现成拥有的功能进程不一定有
线程间如何通信:共享内存和消息传递(方法调用)
进程间如何通信
引用类型的静态值在堆中,对象在方法区
指令重排序会导致线程可见性