解决什么问题
1.为了让代码运行得更快,单纯依靠更快的硬件已无法满足要求,我们需要利用多核,充分发掘应用程序并发的潜力。
2.并发优势:解决问题的速度往往比单核串行程序快得多。
3.缺点:数据一致性。共享数据缺乏原子操作,带来数据不同步的问题。
概念
并发:同一个时刻,应对多件事情的能力。
并行:同一个时刻,处理多件事情的能力。
例子:班级制作一批红花。老师收到任务后,让5位同学挺身而出,接受了这个制作任务(并发)。任务完成后,老师边听汇报边批改作业(并行)。
并发程序要求
及时性(分布式部署响应)
高效(慢请求不影响其他请求)
容错(两个任务不互相影响,串行就不行)
简单(多分模块)
并发模型
线程:虽有很多不足之处,但是是基础
函数式:消除可变状态,线程安全
clojuer之道
actor: 共享内存模型,基于消息传递
CSP通信顺序模型
CPU:数据级并行计算
Lambda架构:类似map reduce
简单粗暴:线程
线程与锁:简单,程序容易出错,难以维护(禁忌:直接调用底层的Thread)
锁
互斥
竞态/死锁
共享变量
public static void main(String[] args) throws InterruptedException {
class Counter{
private int count = 0;
public void increment( ) { count++; }
public int getCount() { return count; }
}
final Counter counter = new Counter();
class CountingThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
}
}
CountingThread t1 = new CountingThread();
CountingThread t2 = new CountingThread();
t1.start();t2.start();
t1.join();t2.join();
System.out.println(counter.getCount());
}
// 结果:程序跑出来的结果是不确定的,但至少大于10000,少于20000
// 原因:并发竞态,导致资源没写就被其他线程读到
// 解决方法:加锁,public synchronized void increment( ) { count++; }
乱序
static boolean answerReady = false;
static int answer = 0;
static Thread t1 = new Thread(){
public void run() {
answerReady = true;
answer = 42;
};
};
static Thread t2 = new Thread(){
public void run() {
if (answerReady) {
System.out.println("answer is " + answer);
} else {
System.out.println("not found answer");
}
};
};
public static void main(String[] args) {
try {
t1.start();t2.start();
t1.join();t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 结果:1.输出answer is 42;2.输出not found answer;3.输出answer is 0;
// 原因:并发乱序执行,1编译器静态优化打乱代码执行顺序。2JVM动态化也能打乱顺序。3硬件乱序执行来优化性能
// 解决:if (answerReady) {
Thread.sleep(100);
System.out.println("answer is " + answer);
}
// 针对此类优化显然有副作用,因此最好不这么写代码
多锁
public static void main(String[] args) {
class Thread_test extends Thread{
public void run() {
while (true) {
System.out.println("线程"+this.getName()+"饿了找餐具");
// 做点事情
if(knife_key ){
knife_key = false;
if(dish_key ){
dish_key = false;
// 用餐
try {
System.out.println("线程"+this.getName()+"在吃饭");
Thread.sleep(new Random().nextInt(100));
System.out.println("线程"+this.getName()+"在吃饭");
} catch (InterruptedException e) {
e.printStackTrace();
}
// 做完开锁
knife_key = true;
dish_key = true;
// 休息
try {
System.out.println("线程"+this.getName()+"在休息");
Thread.sleep(new Random().nextInt(5000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
};
};
}
try {
Thread_test t1 = new Thread_test();t1.setName("客户:t1");
Thread_test t2 = new Thread_test();t2.setName("客户:t2");
Thread_test t3 = new Thread_test();t3.setName("客户:t3");
Thread_test t4 = new Thread_test();t4.setName("客户:t4");
t1.start();
t2.start();
t3.start();t4.start();
t1.join();
t2.join();
t3.join();t4.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 结果:当客户进餐时间加长或者客户增多时,每个客户都处于寻找餐具的状态
// 原因:死锁
// 方法:避免长时间持有锁,有顺序的持有锁
全局锁
锁惟一且确定
外星方法
长时间占用资源进行耗时操作
可以数据保护性复制,采用监听器同步
如何避免
- 对所有共享变量的访问都需要同步化
- 按照全局顺序持有锁
- 持有锁避免使用外星方法
- 持有锁时间尽可能短
课后思考
- java内存模型是如何保证对象初始化是线程安全的?是否必须通过加锁才能在线程之间安全地公开对象?
- 了解反模式“双重检查锁模式(double-checked locking)”
- 我们进行调试时,应该如何增大重现死锁的几率?
本人拙见非标准答案
- JMM模型使得堆内存在线程之间是共享的。即java对象建立到消亡是存于堆内存中,完全可以直接在内存中操作,不需要线程之间同步化,即也不需要加锁
- 反模式的双上锁,第二层锁针对变量是否初始化上锁,即针对内存中的共享变量的直接检查,防止线程之间变量未共享,就获取了变
- 1.增加并发线程数量。2.延长锁定时间。3.超时同步数据。