1. Java多线程基本概念

多线程的优点

  • 资源利用率更好(等待IO的时间)
  • 程序设计在某些情况下更简单(一个线程对应一个任务)
  • 程序相应更快(不用实时去相应)

多线程的代价

  • 设计更复杂(访问共享数据的机制)
  • 上下文切换的开销
  • 增加资源消耗(每个线程本身占用内存资源)

并发编程模型

并发工作者

传入的作业会被分配到不同的工作者上。

并行工作者
  • 优点:理解简单,可以通过增加工作者提高系统的并行度
  • 缺点:共享状态复杂、工作者无状态、任务的顺序不确定
流水线模式

每个工作者只负责作业的部分工作,当完成了这部分工作时的工作者会将作业转发给下一个工作者

流水线工作者
  • 优点:无需共享状态、有状态的工作者、更好的硬件整合,合理的工作顺序
  • 缺点:作业的执行分布在多个工作者上,追踪某个作业到底被什么代码执行困难。
  1. 函数式并行

Same-threading

Same-threading是由多个单线程系统组成,每个单线程系统之间不共享数据。

几种线程系统之间的比较

并行和并发

并发:应用同时处理多个任务,任务的开始不需要等另外一个任务结束。

Concurrency

并行:应用将一个任务分成多个小的子任务,每个子任务实例都是用一个CPU进行处理。

Parallelism

竞争条件(Race Condition)和关键区域(Critical Sections)

线程对关键区域的数据进行竞争,竞争的结果影响执行关键区域的结果

当多个线程对关键区域里面的相同资源进行操作时,会产生问题。其实只有多个线程进行写操作的时候才会产生问题。

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞争条件(race condition)
导致竞争条件发生的代码区称作临界区(critical section)

解决竞争条件的方法:

  1. synchronized
  2. lock
  3. 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);
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容