并发编程-基础拾遗

1,059 阅读9分钟

本文是对并发编程基础知识总结,不包含高级用法,旨在巩固基础,增强对知识点的记忆。所以可以看做是一个手稿,适合熟悉基本概念的同学阅读。

基础概念

1)CPU 核心数和线程数的关系

核心数:线程数=1:1 ;使用了超线程技术后=1:2

2)CPU 时间片轮转机制

又称 RR 调度,会导致上下文切换

3)什么是进程和线程?

进程:程序运行资源分配的最小单位,进程内部有多个线程,会共享这个进程的资源

线程:CPU 调度的最小单位,必须依赖进程而存在。

4)为什么使用并发编程?

好处:充分利用 cpu 的资源、加快用户响应的时间,程序模块化,异步化

问题:线程共享资源,存在冲突;容易导致死锁;启用太多的线程,有搞垮机器的可能

你必须知道的几个概念

1)同步和异步?

同步方法一旦调用开始,调用者必须等到方法返回后才可以继续后续的行为。

异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者可以立刻继续后续的操作。而异步方法通常会在另外一个线程中执行。

2)并发和并行?

严格来说:并行的多个任务是真的在同时执行,真正的并行只可能出现在多个 CPU 的系统中(如多核 CPU)。

对于并发而言,多个任务是在交替执行,系统不断的在任务之间切换,但对外部观察者而言看起来是在并行。另外,并发更关注在单位时间内可以处理事情的能力 。

3)临界区?

临界区用来表示一种公共资源或者共享数据,可以被多个线程使用,但是每一次都只能有一个线程使用它。一旦临界区资源被占用,其他线程只能等待。在并行程序中,临界区资源是保护的对象,如果保护不得当,出现的结果可能都不是线程需要的。

4)阻塞和非阻塞?

阻塞和非阻塞通常用来形容多线程的相互影响,比如一个线程占用了临界区资源,那么其他需要这个资源的线程都必须在临界区等待,即出现线程挂起现象,这种情况就是阻塞。如果占用资源的线程一直不释放资源,其他阻塞在临界区的线程会一直等待。

非阻塞意思与之相反。

5)死锁、活锁、饥饿?

死锁恐怕是并发编程中最糟糕的情况了,通常是因为两个线程互相占用对方需要的资源,而都不进行释放,导致彼此之间相互等待对方释放资源,产生了无限期等待的情况,如果没有外力介入,这种等待将一直存在。

Tips:如果想避免死锁,可以使用无锁函数,还可使用可重入锁,通过重入锁的中断或者限时等待都可以有效规避死锁的问题。

活锁:线程之间互相谦让,为对方让出资源(两个人一起用电梯,一个人想进来,一个人想出去,不停的为对方让位置)。出现这种情况,资源不停的在线程间跳动,导致没有一个线程可以同时拿到所有资源正常执行,这种情况就是活锁。

饥饿:可能是因为线程优先级低或者关键的资源一直被占用,导致自己一直得不到执行。饥饿现象没有死锁那么严重,可能在未来一段时间内解决。

6)并发级别

  • 阻塞

    (synchronized)

  • 无饥饿

    (对非公平锁允许高优先级线程插队,低优先会饥饿)

  • 无障碍

    (多个线程同时进入临界区,进行修改,如果出现问题则回滚重试;一种可行的实现是依赖一致性标记,操作之前读取保存这个标记,操作完成后检查标记是否被更改过。如果不一致则重试)

  • 无锁

    (要求:一个线程可以在有限步内完成操作。无限循环 CAS,由于重试次数过多某些运气差的线程会出现饥饿现象)

  • 无等待

    (要求:所有线程可以在有限步内完成操作。一种典型的实现就是 RCU[Read Copy Update],基本思想是对数据的读不加控制。但是在写数据时,先取得原始数据的副本,接着修改副本数据,这也是为什么读可以不加控制的原因,修改完成在合适的时机写回数据)

认识 Java 里的线程

1)线程的状态?

有五种:新建、就绪、阻塞、运行、死亡

当调用wait()、sleep()时进入阻塞状态,在 wait 状态的线程被notify()或notifyAll()时重新进入就绪态。当阻塞状态的线程被中断时会抛出InterruptedException异常。

2)启动线程的三种方式?

  • 实现 runnable 接口
  • 继承 Thread 类
  • 实现 Callable 接口(有返回值)

3)如何让线程安全停止?

任务执行完成会自然终止或者出现未知异常会停止。

  • 强制

JDK 提供了一些类强制停止的方法,可以近似理解为结束任务管理器中的进程。但由于这些设计并非良好,所以已不再建议使用:

stop(),resume(),suspend()已不建议使用,stop()会导致线程不会正确释放资源,suspend()容易导致死锁。

  • 协作式停止

使用线程的interrupt()方法。调用一个线程的 interrupt()方法中断一个线程,并不是强行关闭这个线程,只是跟这个线程打个招呼,将线程的中断标志位置为 true,线程是否中断,由线程本身决定。

isInterrupted()判定当前线程是否处于中断状态。

方法里如果抛出 InterruptedException,线程的中断标志位会被复位成 false,如果确实是需要中断线程,要求我们自己在 catch 语句块里再次调用 interrupt()。

static 方法Thread.interrupted()判定当前线程是否处于中断状态,同时中断标志位改为 false,使用时需要特别注意。

一个设计良好的响应中断范式:

public class RunnableTask implements Runnable {

 @Override
 public void run() {

  // 如果线程本身就是需要轮询执行的可以使用这种方式
  while (!Thread.currentThread().isInterrupted()) {
   System.out.println("RunnableTask working...");
   try {
    TimeUnit.SECONDS.sleep(1);
   } catch (InterruptedException e) {
    e.printStackTrace();
    Thread.currentThread().interrupt();
    // 静态方法,是否中断状态,注意:此方法会重新将标记为 置为 false
//    System.out.println(Thread.interrupted());
//    Thread.currentThread().interrupt();
   }
  }

  // 如果本身只执行一次,那么在需要终中断的时候判定标志位
//  if (!Thread.currentThread().isInterrupted()) {
//   System.out.println("RunnableTask working...");
//   try {
//    TimeUnit.SECONDS.sleep(1);
//   } catch (InterruptedException e) {
//    e.printStackTrace();
//    Thread.currentThread().interrupt();
//   }
//  }
 }

}

4)其他概念

run()和start():run 方法就是普通对象的普通方法,只有调用了start()后,Java 才会将线程对象和操作系统中实际的线程进行映射,再来执行 run 方法。

yield():高风亮节让出 cpu 的执行权,将线程从运行转到可运行状态,但是下个时间片,该线程依然有可能被再次选中运行。

线程的优先级

取值为 1~10,缺省为 5,但线程的优先级不可靠,不建议作为线程开发时候的手段;因为有可能在 Windows 下操作系统支持,但切换为 Linux 后则无法正常工作了。

守护线程

和主线程共死,finally 不能保证一定执行。

join():一般用于控制线程执行的顺序,可以理解为插队。thread.join();即代表thread线程将在当前线程任务之前执行。实现原理为不停检查 join 线程是否存活,如果存活则一直等待wait(0),如果结束则调用notifyAll()。一般用法是将某个线程传递进具体的任务run()中调用join()需要注意的是:由于join()机制本身是依靠wait()、notify()实现的,所以使用时应避免直接使用等待通知,避免异常。

5)线程间的共享

synchronized 内置锁,可以锁对象也可以锁类

  • 对象锁,锁的是类的对象实例。

  • 类锁,锁的是每个类的的 Class 对象,每个类的的 Class 对象在一个虚拟机中只有一个,所以类锁也只有一个。

synchronized 使用时如果锁定的方法是 static 的,也是类锁。需要注意的是:在发生异常时会释放当前持有的锁。

  • volatile

适合于只有一个线程写,多个线程读的场景,因为它只能确保可见性。

  • ThreadLocal

线程变量。可以理解为是个 map,类型 Map<Thread, Object>

6)线程间的协作

使用等待通知机制,这里有一套标准范式:

等待方:

1.获取对象的锁; 2.循环里判断条件是否满足,不满足调用 wait 方法 3.条件满足执行业务逻辑

通知方:

1.获取对象的锁 2.改变条件 3.通知所有等待在对象的线程(注意:应当在临界区结束时进行通知,一般是代码块的最后)

notify 和 notifyAll 应该用谁?应该尽量使用 notifyAll,使用 notify 因为有可能发生信号丢失的的情况,因为它是随机选择一个持有相同锁的线程。

7)调用yield()、sleep()、wait()、notify()等方法对锁有何影响?

线程在执行yield()以后,持有的锁是不释放的。

sleep()方法被调用以后,持有的锁是不释放的。

调动方法之前,必须要持有锁。调用了wait()方法以后,锁就会被释放(由虚拟机自动执行),当wait()方法返回的时候(即收到通知继续执行),线程会重新持有锁。

调动方法之前,必须要持有锁,调用notify()、notifyAll()方法本身不会释放锁,一般会在退出synchronized区时自动释放。一般会写在退出区大括号的上一行,保证逻辑上的连续性。