Java并发编程的艺术-读书笔记
多线程不一定比单线程快
当累加操作少于百万次时,单线程执行的速度会比多线程执行的速度快,因为线程有创建和上下文切换的开销
vmstat的cs表示每秒上下文切换的次数
如何减少多线程上下文切换次数
使用无锁并发编程,CAS算法,使用最少线程和使用协程
死锁
死锁样例:
public class DeadLockDemo {
/** A锁 */
private static String A = "A";
/** B锁 */
private static String B = "B";
public static void main(String[] args) {
new DeadLockDemo().deadLock();
}
private void deadLock() {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (A) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
System.out.println("1");
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (B) {
synchronized (A) {
System.out.println("2");
}
}
}
});
t1.start();
t2.start();
}
}
现实场景中,比如t1拿到锁之后,因为一些异常情况没有释放锁(死循环)。又或者是t1拿到一个数据库锁,释放锁的时候抛出了异常,没释放掉,就会出现死锁
避免死锁的常用办法
- 避免一个线程同时获取多个锁。
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
volatile的应用
volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的可见性
可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值
volatile定义与实现原理
java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。
Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的
volatile会增加Lock前缀指令
Lock前缀指令会引起处理器缓存回写到内存
一个处理器的缓存回写到内存会导致其他处理器的缓存无效
synchronized
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步
代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的
锁的4种状态
无锁,偏向锁,轻量级锁,重量级锁
锁可以升级不能降级,目的是为了提高获得锁和释放锁的效率
单例模式中的双重检测锁
public class UnsafeLazyInitialization {
private static Instance instance;
public static Instance getInstance() {
if (instance == null) // 1:A线程执行
instance = new Instance(); // 2:B线程执行
return instance;
}
static class Instance {
}
}
这样初始化对象时会出现指令重排,导致取得instance对象时没有初始化完成
解决办法有两个,一个是禁止指令重排序,二是让指令重排序不让外面知道
1.instance类加上volatile关键字
2.利用JVM在类的初始化阶段(在class被加载后,且被线程使用之前),会执行类的初始化
在执行类的初始化期间,jvm会获取一个锁,这个锁可以同步多个线程对同一个类的初始化
队列同步器
AbstractQueuedSynchronizer
用来构建锁或者其他同步组件的基础框架
在锁的实现中聚合同步器,利用同步器实现锁的语义
独占锁
独占锁就是在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁