多线程的优点
- 资源利用率更好(等待IO的时间)
- 程序设计在某些情况下更简单(一个线程对应一个任务)
- 程序相应更快(不用实时去相应)
多线程的代价
- 设计更复杂(访问共享数据的机制)
- 上下文切换的开销
- 增加资源消耗(每个线程本身占用内存资源)
并发编程模型
并发工作者
传入的作业会被分配到不同的工作者上。
- 优点:理解简单,可以通过增加工作者提高系统的并行度
- 缺点:共享状态复杂、工作者无状态、任务的顺序不确定
流水线模式
每个工作者只负责作业的部分工作,当完成了这部分工作时的工作者会将作业转发给下一个工作者
- 优点:无需共享状态、有状态的工作者、更好的硬件整合,合理的工作顺序
- 缺点:作业的执行分布在多个工作者上,追踪某个作业到底被什么代码执行困难。
- 函数式并行
Same-threading
Same-threading是由多个单线程系统组成,每个单线程系统之间不共享数据。
并行和并发
并发:应用同时处理多个任务,任务的开始不需要等另外一个任务结束。
并行:应用将一个任务分成多个小的子任务,每个子任务实例都是用一个CPU进行处理。
竞争条件(Race Condition)和关键区域(Critical Sections)
线程对关键区域的数据进行竞争,竞争的结果影响执行关键区域的结果
当多个线程对关键区域里面的相同资源进行操作时,会产生问题。其实只有多个线程进行写操作的时候才会产生问题。
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞争条件(race condition)
导致竞争条件发生的代码区称作临界区(critical section)
解决竞争条件的方法:
- synchronized
- lock
- atomic variable
在某些情况下可以尝试分解同步块的作用范围
public class TwoSums {
private int sum1 = 0;
private int sum2 = 0;
private Integer sum1Lock = new Integer(1);
private Integer sum2Lock = new Integer(2);
public void add(int val1, int val2){
synchronized(this.sum1Lock){
this.sum1 += val1;
}
synchronized(this.sum2Lock){
this.sum2 += val2;
}
}
}
线程安全和共享资源
线程安全(thread safe)
当多个线程同时调用的时候,代码是安全的。线程安全的代码不包含竞争条件。竞争条件只在多个线程更新共同资源的时候发生。
共享资源的安全性(shared resources)
- 局部变量:存储在每个线程的栈当中,是线程安全的。
public void someMethod(){
long threadSafeInt = 0;
threadSafeInt++;
}
- 局部对象引用:引用本身不共享,但是引用的对象是放在公共区域(堆)中的。如果对象不逃出创造它的方法,那么它也是线程安全的。
public void someMethod(){
LocalObject localObject = new LocalObject();
localObject.callMethod();
method2(localObject);
}
public void method2(LocalObject localObject){
localObject.setValue("value");
}
- 成员变量:成员变量随着对象存储在堆中,如果两个线程调用同一对象的一个方法来更新这个成员变量,那么这个方法不是线程安全的。
public class NotThreadSafe{
StringBuilder builder = new StringBuilder();
public add(String text){
this.builder.append(text);
}
}
public class MyRunnable implements Runnable{
NotThreadSafe instance = null;
public MyRunnable(NotThreadSafe instance){
this.instance = instance;
}
public void run(){
this.instance.add("some text");
}
public static void main(String[] args) {
NotThreadSafe sharedInstance = new NotThreadSafe();
new Thread(new MyRunnable(sharedInstance)).start();
new Thread(new MyRunnable(sharedInstance)).start();
}
}
如果两个线程调用add()方法的时候使用不同的实例对象,那么久不会产生竞争条件。
new Thread(new MyRunnable(new NotThreadSafe())).start();
new Thread(new MyRunnable(new NotThreadSafe())).start();
线程控制逃逸规则--判断对某些资源的访问是线程安全的
如果一个资源的创建,使用,销毁都在同一个线程内完成,且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。
即使对象本身是线程安全的,如果该对象中包含的其他资源(文件,数据库连接),整个应用也有可能不是线程安全的。
区分某个线程控制的对象是资源本身,还是仅仅到某个资源的应用很重要。
线程安全及不可变性
我们可以通过把共享的资源设为不可变的,使线程之间的共享资源永远不能发生改变,因此是线程安全的。
public class ImmutableValue{
private int value = 0;
public ImmutableValue(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
public ImmutableValue add(int valueToAdd){
return new ImmutableValue(this.value + valueToAdd);
}
}
但是即使对象是不可变的,但是指向该对象的引用仍然可以是线程不安全的。在使用不可变特性来保证线程安全的时候,特别需要注意。
public class Calculator{
private ImmutableValue currentValue = null;
public ImmutableValue getValue(){
return currentValue;
}
public void setValue(ImmutableValue newValue){
this.currentValue = newValue;
}
public void add(int newValue){
this.currentValue = this.currentValue.add(newValue);
}
}