1.并发编程概念及问题
- 上下文切换:任务从保存到再加载的过程就是一次从上下文的切换
- 串行与并发的耗时比较:当并发执行超过百万次时,速度比串行慢,因为线程有创建和上下文切换的开销
- 上下文切换次数和时长:每一秒切换1000多次
1.1如何减少上下文切换
- 方法有无锁并发编程、CAS算法、使用最少线程、使用协程
- 无锁并发编程:最直接的方法就是避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据
- CAS算法:Java的Atomic包使用CAS算法来更新数据,而不需要加锁(乐观锁无锁) (Compare and Swap,即比较再交换。CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做)
- 使用最少线程:避免创建不需要的线程
- 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换(参考资料:https://www.cnblogs.com/zingp/p/5911537.html)
插入部分锁知识
- 共享锁(S锁、读锁):如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。
- 排他锁(X锁、写锁):如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。
- 共享锁下其它用户可以并发读取,查询数据。但不能修改,增加,删除数据。资源共享.
1.2死锁
1.3资源限制
- 硬件资源限制资源有贷款的上传/下载限制、硬盘读写速度和cpu的处理速度
- 软件资源限制有数据库的连接数和socket连接数等
2.volatile的应用
volatile在多处理器开发中保持了共享变量的可见性。可见性的意思是当一个线程修改一个共享变量时,另一个线程能读到这个修改的值。它不会引起线程的上下文切换和调度
2.1volatile的定义
Java编程语言允许线程范文共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排它锁单独获得这个变量。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。
2.2volatile的原理
有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,引发两件事情
- 将当前处理器缓存行的数据写回到系统内存
- 这个写回内存的操作会使在其他的CPU里缓存了该内存地址的数据无效
3.synchronized的实现原理与应用
synchronized实现同步的基础:Java中的每一个对象都可以作为锁
- 对于普通的同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类的Class对象
- 对于同步方法块,锁是synchronized括号里配置的对象(synchroinzed(this){})
3.1synchronized实现原理
jvm基于进入和退出monitor对象来实现方法同步和代码块同步
3.2Java对象头
synchronized用的锁是存在java对象头里的,java对象头是一个记录实例数据的对象,包括了hashcode或锁信息(锁信息默认包括是否是偏向锁、锁标志位)、存储到对象类型数据的指针、数组的长度(如果是数组)
- 如果对象是数组类型,则用3个字宽存储对象头
- 如果对象是非数组类型,则用2个字宽存储对象头
3.3锁的升级与对比
- 偏向锁的获得:访问同步块——检查对象头中是否存储了线程1——如果没有则使用CAS替换MarkWord(锁信息)——成功后则将对象头MarkWord中的线程ID指向自己——执行同步体
- 偏向锁的撤销:访问同步块——检查对象头中是否存储了线程2——如果没有则使用CAS替换MarkWord(锁信息)——不成功后则开始撤销偏向锁——线程1暂停线程——解锁,将线程ID设为空——恢复线程
- 轻量级加锁:访问同步块——分配空间并复制MarkWord到栈——CAS修改MarkWord——如果成功则MarkWord替换为轻量级锁——执行同步体——如果失败则自旋升级为重量级锁
- 轻量级解锁:CAS替换MarkWord回到对象头——失败则释放锁并唤醒等待的线程
锁 | 优点 | 缺点 | 适用场景 |
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的消耗 | 适用于只有一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间,同步块执行速度非常快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步执行速度较长 |
4.原子操作的实现原理
原子操作指的是不可被中断的一个或一系列操作
4.1处理器如何实现原子操作
- 第一个机制通过总线锁保证原子性:使用处理器提供的一个LOCK信号,当一个处理器在总线上输出此信号时,其他处理器的请求被阻塞住,那么该处理器可以独占共享内存
- 第二个机制是通过缓存锁定来保证原子性:总线锁定会将其他处理器的请求被阻塞住,所以总线的开销大。通过缓存一致性机制保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效、
4.2处理器不适用缓存锁定情况
- 当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,则处理器会调用总线锁定
- 有些处理器不支持缓存锁定
4.3Java实现原子操作
解决方案一使用循环CAS实现原子操作:JVM中自旋CAS实现的基本思路是循环进行CAS操作直到成功为止,实现的方式就是AtomicBoolean、AtomicInteger、AtomicLong的操作。不过CAS实现原子操作也带来了三个问题
- ABA问题:由于一个值可能从A到B再到B,这样子并不能嗅探到值得变化,实际却是变化了。解决思路是在变量面前追加一个版本号,例如1A——2B——3A(JDK的Atomic包里提供了一个类AtomicStampedReference的compareAndSet来解决ABA问题)
- 循环时间开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销(如果有Pause指令则效率会提升)
- 只能保证一个共享变量的原子操作:多个共享变量操作,循环CAS无法保证操作的原子性。解决方法是把多个共享变量合并成一个共享变量来操作(JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个对象放在一个对象里来进行CAS操作)
/**
* Gets the current value.
*
* @return the current value
*/
public final V get() {
return value;
}
/**
* Sets to the given value.
*
* @param newValue the new value
*/
public final void set(V newValue) {
value = newValue;
}
解决方案二使用锁机制实现原子操作:JVM内部实现了很多种锁机制,偏向锁、轻量级锁、互斥锁。除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS的方式来释放锁。