参考: https://mp.weixin.qq.com/s/Xv8Ho0Q2gHTi5UQXSIxamQ
本文讲的并发编程,并非指分布式技术中的高并发,采用水平扩展或者垂直拆分实现,将并发流量分到不同的物理服务器上。而是将服务器的服务性能最大化,提升cpu的利用率,从而加快响应速度和增大吞吐量,主要技术包括多线程。
并发编程,绕不开的三个核心问题:分工、同步、互斥。
分工,指的是如何高效地拆解任务并分配给各个线程;
同步,指的是线程之间如何协作;
互斥,则是保证同一时刻只允许一个线程访问共享资源;
并发编程是围绕着信号量(Semaphore-pv原语) 和管程(monitor)的,这2者区别在于Semaphore可以允许多个线程访问一个临界区:new Semaphore(size)。 java更倾向于管程技术。这个我们并不陌生,synchronized关键字、wait、notify、nofityAll方法就是管程的重要组成部分。 另外,管程采用的是MESA模型。
我们遇到的很多并发编程,无外乎就是多个线程同时读写同一数据产生的bug。可以最终归为安全性问题,比如方法和类不是线程安全的,导致了程序没有按照我们所期望的那样执行。产生这些并发bug的源头 是可见性、原子性和有序性的问题。为了解决这三者问题,你需要知道了解 java内存模型(如图1)、互斥锁方案、 happens-before规则。
图1 JMM
可见性问题:
真实案例:假设2核,线程A修改了cpu1上的某变量M,线程B修改了cpu2上的该变量M值,这个时候线程A和B对变量M的修改是相互不可见的,这就导致了结果不是你的预期值。
导致原因:高速/本地缓存
解决方法:synchronized、volatile
原子性问题:
真实案例:int a = 0; 开10个线程,执行 a++; 最终a是小于10的
导致原因:线程切换
解决方法:synchronized、Lock等互斥锁
有序性问题:
真实案例:
单例模式
public class Singleton {
private Singleton() { }
private static Singleton instance;
public Singleton getInstance(){
if(instance==null){
synchronized (Singleton.class){
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}
看起来没问题,但是实际上可能导致的NullPointerException
导致原因:编译优化导致的指令重排,1.分配对象的内存空间;2.设置instance指向刚分配的内存地址 3.初始化对象;在步骤2之后,3之前,虽然instance!=null,但是实际去用的时候报错了。
解决方法:synchronized、volatile( private volatile static Singleton instance;)
上面提到的互斥锁,它是解决并发问题的核心工具,但它可能会因为竞争共享资源带来死锁问题,
死锁4个同时条件:互斥,占有且等待,不可抢占,循环等待
当然,我们只要破坏其中之一,即可避免死锁:
互斥,因为共享资源同一时刻只能被一个线程占用,这个不可避免。
占有且等待,可以一次性锁全部共享资源,就不存在等待了(方法有点粗暴~~~)。
不可抢占,类似tryLock方案,等待资源超时就释放已占有资源。
循环等待,synchronized配合wait,notify,notifyAll实现等待-通知机制。或者,给资源id排序,利用顺序锁。
实现并发主要技术是多线程,这里说下java中线程的生命周期(跟通用线程略有点点不同)
-
初始化状态:new
-
可运行/运行状态 runnable
-
休眠状态 blocked、waiting、timed_waiting
-
终止状态 terminated
线程的状态之间是可以相互转换的,从而达到并发效果:
new->runnable : extends Thread 、 implements Runnable
runnable->blocked: 等待synchronized隐式锁,此时别的线程正在执行synchronized块
blocked->runnable:获得了synchronized隐式锁后
runnable->waiting: obj.wait() 、ThreadA.join() 、 LockSupport.park
waiting->runnable: notify、nofityAll、ThreadA执行完、LockSupport.unpark
runnable->terminated: run()执行完、ThreadA.interrupt + isInterrupt自检
多线程既然对提升服务器性能这么有用,不过并不是越多越好,因为线程间切换是需要时间的,有个计算线程数公式是这样子的:
最佳线程数 = cpu核数 * (1 + io耗时/cpu耗时)
如果是大批量浮点型计算,1核单线程即可
如果是纯文件读写型,那么线程数可以设置较大。
上面讲到很多概念和理论,也许你会觉得并发编程很难,其实
java sdk中,有专门的并发包:java.util.concurrent,例如
分工: Fork/Join 框架;
同步: CountDownLatch;
互斥: ReentrantLock。
如果我们想要熟练使用,还是得花费很大功夫去好好理解和学习。
下列章节会介绍一些concurrent中常见的并发类和方法,从实践出发。