Skip to content

并发编程

概念

进程和线程和程序

  1. 进程:
  2. 线程:
  3. 程序:

并发和并行

  1. 并发:
  2. 并行:

同步和异步

  1. 同步:需要等待结果返回的,则是同步
  2. 异步:不需要等待结果返回的,就可以继续运行的就是异步

临界区

  1. 一个程序运行多个线程本身是没有问题的
  2. 问题出在多个线程访问共享资源
    1. 多个线程读共享资源其实也没有问题
    2. 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  3. 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

竞态条件 Race Condition

  1. 多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

synchroized

  • 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
  • 作用范围
    • 作用方法:锁定对象的实例
    • 作用代码块:锁定对象的实例
    • 作用静态方法:锁定类

什么情况下线程不安全

  1. 当多线程操作一个非线程安全的集合
  2. 多线程下操作一个成员变量
  3. 在调用安全集合类时需要注意,单个方法是原子的,但是多个方法组合在一起就不是原子的了
java
Hashtable map = new Hashtable();

// 多线程  线程 1 线程 2 
new Thread(()->{
    if(map.get("key") == null) {
        map.put("key", value);
    }
}).start();
shell
t1 --> map.get("key") == null
t2 --> map.get("key") == null

t1 --> map.put("key", "2");
t2 --> map.put("key", "1");

// 最后结果不一致
  1. 不可变变量 String、Integer
    1. String 每次对 String 的操作都会创建一个新的字符串
    2. 因为其内部的状态不可以改变,因此它们的方法都是线程安全的
    3. String 设置 final 之后里面的方法不可以被重写,这样就保证了线程安全
  2. Spring 里面,如果 Bean 的作用域是默认值 singleton 则线程不安全
  3. Spring 切面编程,不安全,使用环绕通知
  4. 两个类之间的操作,如转账
    1. 锁类:
    2. 锁id:

多线程一定能提升效率吗?

  1. 如果处理器只有一个核心,多线程只会增加线程上下文切换的开销。
  2. 多核 CPU 可以并行计算,但是能否给应用程序带来显著的性能提升还是要看情况
  3. IO 操作不占用 CPU,只是我们一般拷贝文件用的是【阻塞IO】,这时相当于线程虽然不用 CPU,但是要等 IO 结束,没能充分利用线程。所以才有 非阻塞 IO 和异步 IO。

并发之共享模型

安全问题案例

  1. 通过两个线程,同时对变量 a 进行操作,最后结果会有所偏差
java
static volatile int a = 0;
public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            a++;
        }
    });
    Thread thread2 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            a--;
        }
    });
    thread.start();
    thread2.start();
    thread.join();
    thread2.join();

    System.out.println(a);
}

解决方案

应用之互斥

阻塞式解决方案:synchroized,Lock

注意

虽然java中互斥和同步都可以采用synchronized关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
  1. synchroized:实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
java
synchronized (对象) { // 线程1,线程2(blocked)
  // 临界区
}
java
static volatile int a = 0;
static volatile Object room = new Object();
public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            synchronized (room) {
                a++;
            }
        }
    });
    Thread thread2 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            synchronized (room) {
                a--;
            }
        }
    });
    thread.start();
    thread2.start();
    thread.join();
    thread2.join();
    System.out.println(a);
}
  1. 作用范围
  • 作用方法:锁定对象的实例
  • 作用代码块:锁定对象的实例
  • 作用静态方法:锁定类
非阻塞式的解决方案:原子变量

synchronize