线程的历史-----是一部对于cpu性能压榨的历史
单进程人工切换
– 纸带机
多进程批处理
– 多个任务批量执行
多进程并行处理
– 把程序写在不同的内存位置上来回切换
多线程
– 一个程序内部不同任务的来回切换
– selector - epoll
纤程/协程
– 绿色线程,用户管理的(而不是OS管理的)线程
什么是进程?什么是线程?
什么是进程:资源分配的基本单位(静态概念)
什么是线程:资源调度的基本单位(动态概念)
通俗说:一个程序中不同的执行路径
常见问题
程序是什么?--> QQ.exe PowerPoint.exe 可执行文件
进程是什么?--> 程序启动 进入内存 资源分配的基本单位
线程是什么?--> 程序执行的基本单位
程序如何开始运行?--> CPU 读指令 - PC(存储指令地址) ,读数据 Register ,计算, 回写, -> 下一条
线程如何进行调度?--> linux 线程调度器(OS)操作系统
线程切换的概念是什么?--> Context Switch CPU保存现场 执行新线程,恢复现场,继续执行原线程这样的一个过程
单线程与多线程
计算机的组成
线程切换
两个线程不同时间段使用cpu资源,T2线程使用cpu时,T1线程先把数据记录到缓冲,然后让出cpu使用权,等到下一次获得cpu使用权时,再从缓存中拿出数据,继续执行。
线程之间的切换通过操作系统调度
常见线程问题
1.单核CPU设定多线程是否有意义?
有意义,如果A线程阻塞(如等待IO),则可以让出cpu资源给B线程,即使是单核,多线程也能提高cpu的利用率
2.工作线程是不是设置的越大越好?
并不是,如果线程设置的非常大,则每个线程获得的cpu执行时间片段很少,从而导致cpu不断的在进行线程间的上下文切换,而上下文切换也是需要消耗cpu资源的,反而导致cpu利用率不高
3.工作线程数(线程池中线程数量)设置多少合适
一般使用下面这个公式计算
创建线程的5种方法
方法1---继承Thread
方法2---实现Runnable
方法3---Lambda
方法4---线程池
方法5---Future Callable FutureTask
static class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello MyThread!");
}
}
static class MyRun implements Runnable {
@Override
public void run() {
System.out.println("Hello MyRun!");
}
}
static class MyCall implements Callable<String>{
@Override
public String call() throws Exception {
System.out.println("Hello MyCall");
return "success";
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//方法1---继承Thread
new MyThread().start();
//方法2---实现Runnable
new Thread(new MyRun()).start();
//方法3---Lambda
new Thread(()->{
System.out.println("Hello Lambda!");
}).start();
//方法4---线程池
ExecutorService service = Executors.newCachedThreadPool();
service.execute(() ->{
System.out.println("hello ThreadPool");
});
Future<String> f = service.submit(new MyCall());
String s=f.get();
System.out.println(s);
service.shutdown();
//方法5---Future Callable FutureTask
FutureTask<String> task = new FutureTask<>(new MyCall());
Thread t = new Thread(task);
t.start();
System.out.println(task.get());
}
线程的状态
1. NEW : 线程刚刚创建,还没有启动 2. RUNNABLE :可运行状态,由线程调度器可以安排执行 包括READY和RUNNING两种细分状态 3. WAITING: 等待被唤醒 4. TIMED WAITING: 隔一段时间后自动唤醒 5. BLOCKED: 被阻塞,正在等待锁 6. TERMINATED: 线程结束
线程的打断(interrupt)
1. interrupt() :实例方法,设置线程中断标志(打扰一下,你该处理一下中断)
2. isInterrupted():实例方法,有没有人打扰我?
3. interrupted():静态方法,有没有人打扰我(当前线程)?复位!
interrupt和sleep() wait() join()
sleep()方法在睡眠的时候,不到时间是没有办法叫醒的,这个时候可以用interrupt设置标志位,然后呢必须得catch InterruptedException来进行处理,决定继续睡或者是别的逻辑,(自动进行中断标志复位)
interrupt是否能中断正在竞争锁的线程?
1.interrupt()不能打断正在竞争锁的线程synchronized(),ReentrantLock的lock()
2.如果想打断正在竞争锁的线程,使用ReentrantLock的lockInterruptibly()
如何优雅的控制线程的结束
1. 自然结束(能自然结束就尽量自然结束)
2. stop() (太粗暴了,直接干掉,直接释放锁导致数据不一致)suspend() resume() (这两哥们不会释放锁,会产生死锁也被废掉了)
3. volatile标志
1). 不适合某些场景(比如还没有同步的时候,线程做了阻塞操作,没有办法循环回去)
2). 打断时间也不是特别精确,比如一个阻塞容器,容量为5的时候结束生产者,
但是,由于volatile同步线程标志位的时间控制不是很精确,有可能生产者还继续生产一段儿时间
4. interrupt() and isInterrupted(比较优雅)
5. 如果要做到精确控制,多长时间和while循环多少次,就需要业务线程+锁的形式来控制
并发编程的三大特性
可见性(visibility)
有序性(ordering)
原子性(atomicity)
可见性
多线程提高效率,本地缓存数据,造成数据修改不可见,
要想保证可见,要么触发同步指令,要么加上volatile,被修饰的内存,只要有修改,马上同步涉及到的每个线程
volatile 应用类型(包括数组)只能保证引用本身的可见性,不能保证内部字段的可见性
volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized
三级缓存
缓存行对齐
缓存行64个字节是CPU同步的基本单位,缓存行隔离会比伪共享效率要高
缓存一致性协议和volatile无关
有序性
CPU的乱序执行
为什么会乱序?主要是为了提高效
线程的as-if-serial
单个线程,两条语句,未必是按顺序执行
单线程的重排序,必须保证最终一致性
as-if-serial:看上去像是序列化(单线程)
会产生的后果
多线程会产生不希望看到的结果
如何保证有序性?
内存屏障是特殊指令:看到这种指令,前面的必须执行完,后面的才能执行
所有实现JVM规范的虚拟机,必须实现四个屏障
volatile修饰的内存,不可以重排序,对volatile修饰变量的读写访问,都不可以换顺序
JVM内存屏障
LoadLoad屏障:
对于这样的语句Load1; LoadLoad; Load2,
在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:
对于这样的语句Store1; StoreStore; Store2,
在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:
对于这样的语句Load1; LoadStore; Store2,
在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:
对于这样的语句Store1; StoreLoad; Load2,
在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
LOCK 用于在多处理器中执行指令时对共享内存的独占使用。 它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效。 另外还提供了有序的指令无法越过这个内存屏障的作用。
原子性
基本概念:
race condition => 竞争条件 , 指的是多个线程访问共享数据的时候产生竞争
数据的不一致(unconsistency),并发访问之下产生的不期望出现的结果
如何保障数据一致呢?--> 线程同步(线程执行的顺序安排好),
monitor (管程) ---> 锁
critical section -> 临界区
如果临界区执行时间长,语句多,叫做 锁的粒度比较粗,反之,就是锁的粒度比较
保障操作的原子性
悲观的认为这个操作会被别的线程打断(悲观锁)synchronized(上一个小程序)
乐观的认为这个做不会被别的线程打断(乐观锁 自旋锁 无锁)cas操作
上锁的本质:
上锁的本质是把并发编程序列化 ,同时保障可见性
注意序列化并非其他程序一直没机会执行,而是有可能会被调度,但是抢不到锁,又回到Blocked或者Waiting状态(sync锁升级)
一定是锁定同一把锁(抢一个坑位)
JVM中的两种锁:
重量级锁(经过操作系统的调度)synchronized早期都是这种锁(目前的实现中升级到最后也是这种锁)
轻量级锁(CAS的实现,不经过OS调度)(无锁 - 自旋锁 - 乐观锁)
两种锁使用场景:
临界区执行时间比较长 , 等的人很多 -> 重量级
时间短,等的人少 -> 自旋锁
CAS深度解析
CAS的ABA问题解决方案 - Version解决(数值类型和布尔类型)
CAS操作本身的原子性保障
应用:java的atomic包
最终实现:lock cmpxchg 指令
对象结构
1)前两行为markword,第三行记录的是这个对象的指针,第四行补偿行(将整个对象补偿成8字节的整数倍)
2)前面一部分为普通对象
3)后一部分为加上synchronize对象
4)红框的不同,表示加锁是由markword控制的
4)markword记录了锁信息,JC信息,HashCode信息
markword-64
锁升级过程
偏向锁、轻量级锁:用户态
重量级锁:内核态
偏向锁什么时候升级为轻量级锁?
有人竞争时就升级
轻量级锁什么时候升级为重量级锁?
竞争加剧:有线程超过10次自旋
为什么有自旋锁还需要重量级锁?
自旋是消耗cpu资源,如果锁时间长,或者自旋线程多,cpu会被大量消耗
重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗cpu资源
偏向锁是否一定比自旋锁效率高?
不一定,在明确知道会有多个线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁
JVM启动过程,会有很多线程竞争(明确),先不开启偏向锁,过段时间再开启
锁重入
synchronize是可重入锁
重入次数必须记录,因为要解锁几次必须得对应
偏向锁、轻量级锁-->线程栈-->LR+1
重量级锁-->objectmonitor字段上