前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >并发编程基础

并发编程基础

原创
作者头像
kwan的解忧杂货铺
发布2024-08-07 21:16:48
610
发布2024-08-07 21:16:48
举报
文章被收录于专栏:基础

一.并发基础

1.什么是并行和并发?

并行和并发都是多任务处理的概念,但它们的含义不同。

并行是指两个或多个任务在同一时刻执行,即在同一时刻有多个任务在同时进行。在计算机领域,多核 CPU 可以实现并行处理,即多个 CPU 内核同时执行不同的任务。在并行处理中,任务之间相互独立,不需要等待其他任务的完成。

并发是指两个或多个任务在同一时间段内执行,即在同一时间段内有多个任务在交替进行。在计算机领域,单核 CPU 可以通过轮流执行各个任务来实现并发处理。在并发处理中,任务之间可能会相互影响,需要考虑任务的顺序和优先级,也需要考虑任务之间的同步和通信问题。

简单来说,如果是多个任务同时执行,就是并行;如果是多个任务交替执行,就是并发。并行处理通常需要多核 CPU 来支持,可以提高处理速度;而并发处理可以在单核 CPU 上实现,但需要考虑任务之间的同步和通信问题。

2.什么是活锁?

假设有两个线程,线程 1 和线程 2,它们都需要资源 A/B,假设线程 1 占有了 A 资源,线程 2 占有了 B 资源;

由于两个线程都需要同时拥有这两个资源才可以工作,为了避免死锁,1 号线程释放了 A 资源占有锁,2 号线程释放了 B 资源占有锁;此时 AB 空闲,两个线程又同时抢锁,再次出现上述情况,此时发生了活锁。

简单类比,电梯遇到人,一个进的一个出的,对面占路,两个人同时往一个方向让路,来回重复,还是堵着路。如果线上应用遇到了活锁问题,恭喜你中奖了,这类问题比较难排查。

3.单线程创建方式

单线程创建方式比较简单,一般只有两种方式:

  • 继承 Thread 类
  • 实现 Runnable 接口;

需要注意的问题有:

  • 不管是继承 Thread 类还是实现 Runable 接口,业务逻辑写在 run 方法里面,线程启动的时候是执行 start()方法;
  • 开启新的线程,不影响主线程的代码执行顺序也不会阻塞主线程的执行;
  • 新的线程和主线程的代码执行顺序是不能够保证先后的;
  • 对于多线程程序,从微观上来讲某一时刻只有一个线程在工作,多线程目的是让 CPU 忙起来;
  • 通过查看 Thread 的源码可以看到,Thread 类是实现了 Runnable 接口的,所以这两种本质上来讲是一个;

单线程创建举例:

代码语言:java
复制
public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
        }
    }
}
代码语言:java
复制
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }
    }
}
代码语言:java
复制
public static void main(String[] args) {
  Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
      for (int i = 0; i < 10; i++) {
        System.out.println(i);
      }
    }
  });
  thread.start();
}

4.终止线程运行的情况?

线程调度器选择优先级最高的线程运行。但是,如果发生以下情况,就会终止线程的运行:

  • 线程体中调用了 yield()方法,让出了对 CPU 的占用权;
  • 线程体中调用了 sleep()方法,使线程进入睡眠状态;
  • 线程由于 I/O 操作而受阻塞;
  • 另一个更高优先级的线程出现;
  • 在支持时间片的系统中,该线程的时间片用完。

5.如何减少上下文切换

减少上下文切换的方法有无锁并发编程、CAS 算法、使用最少线程和使用协程。

  • 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的 ID 按照 Hash 算法取模分段,不同的线程处理不同段的数据。
  • CAS 算法。Java 的 Atomic 包使用 CAS 算法来更新数据,而不需要加锁。
  • 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多多线程来处理,这样会造成大量线程都处于等待状态。
  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

6.什么是死锁?

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去

如上图例子:线程 1 己经持有了资源 2,它同时还想申请资源 1,线程 2 已经持有了资源 1,它同时还想申请资源 2,所以线程 1 和线程 2 就因为相互等待对方已经持有的资源,而进入了死锁状态。

7.写出死锁代码?

死锁的产生必须具备以下四个条件。

  • 互斥条件:一个资源同时只能有一个线程占有.其他线程只能等待.
  • 请求并持有条件:当前线程已经获取到一个资源,又获取其他资源,其他资源被别的线程占有,当前线程等待,但是不释放持有资源.
  • 不可剥夺条件:占有资源期间,不能被其他线程剥夺,只能自己释放.
  • 环路等待条件:等待资源形成环形链.a 被 A 占有,b 被 B 占有,A 想获取 b,B 想获取 a
代码语言:java
复制
//死锁代码
public class DeadLockDemo {
  private static String A = "A";
  private static String B = "B";

  public static void main(String[] args){
    new DeadLockDemo().deadLock();
  }

  private void deadLock(){
    Thread t1 = new Thread(new Runnable(){
      @Override
      public void run(){
        //线程1获取A的锁
        synchronized (A){
          try {
            Thread.currentThread().sleep(2000);
          } catch (InterruptedException e){
            e.printStackTrace();
          }
          synchronized (B){
            System.out.println("1");
          }
        }
      }
    });
    Thread t2 = new Thread(new Runnable(){
      @Override
      public void run(){
        //线程2获取B的锁
        synchronized (B){
          //A对象已经被线程1持有
          synchronized (A){
            System.out.println("2");
          }
        }
      }
    });
    t1.start();
    t2.start();
  }
}

8.如何避免死锁呢?

要想避免死锁,只需要破坏掉至少一个构造死锁的必要条件即可,目前只有请求并持有条件和环路等待条件是可以被破坏的。考虑死锁产生的条件:互斥访问、占有并保持、循环等待。针对以上几点,可以:资源一次性分配、占有时可被打断、考虑资源分配顺序。

  • 避免一个线程同时获取多个锁。
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
  • 尝试使用定时锁,使用 locktryLock(timeou t)来替代使用内部锁机制。
  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。

如上题代码中,在线程 B 中获取资源的顺序和在线程 A 中获取资源的顺序保持一致,其实资源分配有序性就是指,假如线程 A 和线程 B 都需要资源 1,2,3,..., n 时,对资源进行排序,线程 A 和线程 B 只有在获取了资源 n-1 时才能去获取资源 n.

代码语言:java
复制
public class DeadLockRelessDemo {
  private static String A = "A";
  private static String B = "B";

  public static void main(String[] args){
    new DeadLockRelessDemo().deadLock();
  }

  private void deadLock(){
    Thread t1 = new Thread(new Runnable(){
      @Override
      public void run(){
        //线程1获取A的锁
        synchronized (A){
          try {
            Thread.currentThread().sleep(2000);
          } catch (InterruptedException e){
            e.printStackTrace();
          }
          synchronized (B){
            System.out.println("1");
          }
        }
      }
    });
    Thread t2 = new Thread(new Runnable(){
      @Override
      public void run(){
        //线程2也获取A的锁
        synchronized (A){
          synchronized (B){
            System.out.println("2");
          }
        }
      }
    });
    t1.start();
    t2.start();
  }
}

9.线程,进程,协程的区别?

进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位.

线程则是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。操作系统在分配资源时是把资源分配给进程的,但是 CPU 资源比较特殊,它是被分配到线程的,因为真正要占用 CPU 运行的是线程,所以也说线程是 CPU 分配的基本单位。在 Java 中,当我们启动 main 函数时其实就启动了一个 JVM 进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程

一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。

  • 进程:静态分配内存资源的最小单位
  • 线程:动态执行任务的最小单位
  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。协程也叫纤程.

线程是在内核态调用的,协程在用户态调用,避免了上下文切换

10.继承 Thread 类的优劣?

使用继承方式的好处是,在 run()方法内获取当前线程直接使用 this 就可以了,无须使用 Thread.currentThread()方法;不好的地方是 Java 不支持多继承,如果继承了 Thread 类,那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码, Runable 则没有这个限制。

代码语言:java
复制
public class DemoTest extends Thread {
  // private int tickets = 20;
  private volatile int tickets = 20;

  @Override
  public void run(){
    synchronized (this){
      while (tickets > 0){
        System.out.println(Thread.currentThread().getName()+"卖出一张票"+ tickets);
        tickets--;
      }
    }
  }

  public static void main(String[] args){
    //实际上一共卖出了80张票,每个线程都有自己的私有的非共享数据。都认为自己有20张票
    DemoTest test4 = new DemoTest();
    DemoTest test5 = new DemoTest();
    DemoTest test6 = new DemoTest();
    DemoTest test7 = new DemoTest();
    test4.setName("一号窗口:");
    test5.setName("二号窗口:");
    test6.setName("三号窗口:");
    test7.setName("四号窗口:");
    test4.start();
    test5.start();
    test6.start();
    test7.start();
  }
}

11.缓存一致性协议?

CPU 多级缓存,切换后如何保证一致性?

image-20240126160137155
image-20240126160137155

一种处理一致性问题的办法是使用 Bus Locking(总线锁)。当一个 CPU 对其缓存中的数据进行操作的时候,往总线中发送一个 Lock 信号。这个时候,所有 CPU 收到这个信号之后就不操作自己缓存中的对应数据了,当操作结束,释放锁以后,所有的 CPU 就去内存中获取最新数据更新。但是用锁的方式总是避不开性能问题。总线锁总是会导致 CPU 的性能下降。所以出现另外一种维护 CPU 缓存一致性的方式,MESI。

MESI 是保持一致性的协议。它的方法是在 CPU 缓存中保存一个标记位,这个标记位有四种状态:

  • M: Modify,修改缓存,当前 CPU 的缓存已经被修改了,即与内存中数据已经不一致了
  • E: Exclusive,独占缓存,当前 CPU 的缓存和内存中数据保持一致,而且其他处理器并没有可使用的缓存数据
  • S: Share,共享缓存,和内存保持一致的一份拷贝,多组缓存可以同时拥有针对同一内存地址的共享缓存段
  • I: Invalid,实效缓存,这个说明 CPU 中的缓存已经不能使用了

CPU 的读取遵循下面几点:

  • 如果缓存状态是 I,那么就从内存中读取,否则就从缓存中直接读取。
  • 如果缓存处于 M 或 E 的 CPU 读取到其他 CPU 有读操作,就把自己的缓存写入到内存中,并将自己的状态设置为 S。
  • 只有缓存状态是 M 或 E 的时候,CPU 才可以修改缓存中的数据,修改后,缓存状态变为 M。

12.如何实现线程安全的单例模式?

安全的单例模式

1.使用 volatile 禁止重排序

2.可以重排序,但重排序对其他线程不可见

image-20220418162112331
image-20220418162112331

初始化一个类,包括执行这个类的静态初始化和初始化在这个类中声明的静态字段。根据 Java 语言规范,在首次发生下列任意一种情况时,一个类或接口类型 T 将被立即初始化。

  1. T 是一个类,而且一个 T 类型的实例被创建。
  2. T 是一个类,且 T 中声明的一个静态方法被调用。
  3. T 中声明的一个静态字段被赋值。
  4. T 中声明的一个静态字段被使用,而且这个字段不是一个常量字段。
  5. T 是一个顶级类(Top Level Class,见 Java 语言规范的 7.6),而且一个断言语句嵌套在 T 内部被执行。
代码语言:java
复制
public class InstanceFactory {
   private static class InstanceHolder {
   public static Instance instance=new Instance();

   public static Instance getinstance(){
   return InstanceHolderinstance; //这里将导致InstanceHolder类被初始化
}

13.Finally 不执行的情况

在 Java 的finally块中通常执行的是一些必须要在退出try块时执行的代码,不受异常是否抛出的影响。然而,有几种情况下finally块不会执行:

  1. System.exit() 或 JVM 崩溃:
    • 当程序调用System.exit()方法时,Java 虚拟机会立即退出,不再执行任何未完成的代码,包括finally块。
    • 如果在finally块中执行了一个会使 JVM 崩溃的操作(如死循环或异常死锁等),则finally块不会完成执行。
  2. 线程无法停止:
    • 如果在finally块中有一个无法停止的线程(如Thread.stop()方法),finally块可能无法完成执行。
  3. 处理器或虚拟机崩溃:
    • 在某些极端情况下,如处理器错误或 Java 虚拟机崩溃,finally块可能不会执行。
  4. 在 try 块中出现死循环:
    • 如果在try块中出现一个无限循环,finally块将不会执行,因为代码永远不会跳出try块。
  5. System.exit() 或 Runtime.halt() 在 finally 块中被调用:
    • 如果在finally块中调用了System.exit()Runtime.halt()来退出 JVM,那么 JVM 将立即终止,finally块的执行也将被中断。
  6. 守护线程中的 finally:当只剩下守护线程在运行时,JVM 认为它已经完成了所有需要执行的任务,于是会立即退出,不会等待守护线程的finally块执行完毕。这意味着,如果守护线程中有finally块,当 JVM 退出时,这些finally块可能不会被执行。

总之,finally块在大多数情况下都会执行,但在某些极端情况下,如程序终止或 JVM 崩溃等情况下,finally块可能不会被执行。因此,在使用finally块时,应该确保其中的代码是可靠的,不依赖于异常处理或 JVM 的状态。

14.Thread 类中的方法

java.lang.Thread 类是 Java 中用于多线程编程的关键类,它提供了管理线程的各种方法。以下是 Thread 类中常用的一些方法:

  1. start(): 启动线程,使其进入可运行状态。一旦调用了 start() 方法,线程会在适当的时机被调度执行,而不是立即执行。
  2. run(): 线程的主要执行代码应该放在这个方法中。当线程被启动并进入运行状态时,run() 方法会被调用。
  3. sleep(long millis): 使当前线程休眠指定的毫秒数。在这段时间内,线程不会执行任何操作。
  4. join(): 等待调用此方法的线程结束。如果在线程 A 中调用了线程 B 的 join() 方法,线程 A 将会等待直到线程 B 执行完毕。
  5. interrupt(): 中断线程,给线程设置中断状态。可以在其他线程中调用该方法来中断目标线程。
  6. isInterrupted(): 检查线程是否被中断,但不会清除中断状态。
  7. currentThread(): 静态方法,返回当前正在执行的线程对象。
  8. setName(String name): 设置线程的名字,便于识别和调试。
  9. setPriority(int priority): 设置线程的优先级,用于调度决策。优先级范围是 1(最低)到 10(最高)。
  10. yield(): 提示调度器当前线程愿意放弃 CPU 使用权,但不保证会被采纳。
  11. isAlive(): 检查线程是否处于活动状态(已启动但尚未终止)。
  12. getState(): 返回线程的状态,如 NEW、RUNNABLE、BLOCKED、WAITING 等。
  13. setDaemon(boolean on): 设置线程是否为守护线程。守护线程在非守护线程全部结束后会自动终止。
  14. getId(): 返回线程的唯一标识符。

这些方法只是 Thread 类中的一部分,它们允许你控制线程的创建、运行、暂停、中断等操作。在多线程编程中,了解如何使用这些方法非常重要,以确保线程能够按照预期进行协调和执行。

15.Object 类中的方法

java.lang.Object 是 Java 中所有类的根类,因此它的方法在所有对象中都可用。以下是一些 Object 类中常用的方法:

  1. toString(): 返回对象的字符串表示。默认情况下,返回的是对象的类名和哈希码的组合。可以在子类中覆盖这个方法以返回更有意义的字符串。
  2. equals(Object obj): 用于比较对象是否相等。默认情况下,比较的是对象的引用地址。应该在子类中覆盖这个方法,根据业务逻辑来判断对象是否相等。
  3. hashCode(): 返回对象的哈希码,是一个整数。用于在哈希表等数据结构中进行快速查找。通常需要与 equals 方法一起覆盖。
  4. getClass(): 返回对象的运行时类(Class 对象),该对象包含有关类的信息。
  5. wait(), notify(), notifyAll(): 用于线程之间的同步与通信。wait() 使线程等待,直到另一个线程调用该对象的 notify()notifyAll() 方法,唤醒等待线程。
  6. finalize(): 在对象被垃圾回收之前调用。通常不建议使用,因为现代 Java 已经提供更好的资源管理方式(如 try-with-resources 语句块)。
  7. clone(): 用于创建对象的浅拷贝(复制引用,不复制对象本身)。需要实现 Cloneable 接口,并在子类中覆盖该方法来实现深拷贝。
  8. finalize(): 已过时,曾经是在对象被垃圾回收之前调用的方法。现在不推荐使用,因为更好的资源管理方式已经可用。

这些只是 Object 类中的一些方法。在实际编程中,你会经常使用其中的一些方法,特别是 toString()equals()hashCode() 等。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一.并发基础
    • 1.什么是并行和并发?
      • 2.什么是活锁?
        • 3.单线程创建方式
          • 4.终止线程运行的情况?
            • 5.如何减少上下文切换
              • 6.什么是死锁?
                • 7.写出死锁代码?
                  • 8.如何避免死锁呢?
                    • 9.线程,进程,协程的区别?
                      • 10.继承 Thread 类的优劣?
                        • 11.缓存一致性协议?
                          • 12.如何实现线程安全的单例模式?
                            • 13.Finally 不执行的情况
                              • 14.Thread 类中的方法
                                • 15.Object 类中的方法
                                领券
                                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档