并发编程概述

内容纲要

并发编程基本概念

程序

人为编写或自动生成的代码,保存在文件中,程序本身是静态的。
如果要运行程序,需将程序加载到内存,通过编译器或解释器翻译为计算机理解的方式运行。

编程语言:汇编语言、C/C++、Java语言、Python语言和Go语言等。

进程与线程

操作系统启动一个程序时,会启动一个进程。
如:

  • 启动一个Java程序时,会创建一个JVM进程;
  • 启动一个Python程序时,会创建一个Python进程;
  • 启动一个Go程序时,会创建一个Go进程;

进程,是操作系统进行资源分配的最小单位,在一个进程中可以创建多个线程。
线程,是比进程粒度更小的,能够独立运行的基本单位。也是CPU调度的最小单元被称为轻量级进程。多个线程各自拥有独立的局部变量、线程堆栈和程序计数器等,能够访问共享的资源。

进程和线程的区别

(1)进程是操作系统分配资源的最小单位,线程是CPU调度的最小单元。

(2)一个进程中可以包含一个或多个线程, 一个线程只能属于一个进程。

(3)进程与进程之间是互相独立的,进程内部的线程之间并不完全独立,可以共享进程的堆内存、方法区内存和系统资源。

(4)进程上下文的切换要比线程的上下文切换慢很多。

(5)进程是存在地址空间的,而线程本身无地址空间,线程的地址空间是包含在进程中的。

(6)某个进程发生异常不会对其他进程造成影响,某个线程发生异常可能会对所在进程中的其他线程造成影响。

线程组

线程组可以同时管理多个线程。在实际的应用场景中,如果系统创建的线程比较多,创建的线程功能也比较明确,就可以将具有相同功能的线程放到一个线程组中。

如何使用线程组

如:创建了一个线程组threndGroup,两个线程thread1thread2,将thread1thread2放到threadGroup 中。

/**
 * 测试线程组的使用
 */
public class ThreadGroupTest {
    public static void main(String[] args) {
        // 创建线程组threadGroup
        ThreadGroup threadGroup = new ThreadGroup("threadGroupTest");

        // 创建Thread1,并在构造方法中传入线程组和线程名称
        Thread thread1 = new Thread(threadGroup, ()-> {
            String groupName = Thread.currentThread().getThreadGroup().getName();
            String threadName = Thread.currentThread().getName();
            System.out.println(groupName + "-" + threadName);
        }, "thread1");

        // 创建Thread2,并在构造方法中传入线程组和线程名称
        Thread thread2 = new Thread(threadGroup, ()-> {
            String groupName = Thread.currentThread().getThreadGroup().getName();
            String threadName = Thread.currentThread().getName();
            System.out.println(groupName + "-" + threadName);
        }, "thread2");

        thread1.start();
        thread2.start();
    }
}

输出:
threadGroupTest-thread1
threadGroupTest-thread2

用户线程与守护线程

用户线程是最常见的线程。
例如,在程序启动时,JVM调用程序的main()方法就会创建一个用户线程。

/**
 * 测试用户线程
 */
public class ThreadUserTest {
    public static void main(String[] args) {
        Thread threadUser = new Thread(() -> {
            System.out.println("我是用户线程");
        }, "threadUser");

        threadUser.start();
    }
}

守护线程是一种特殊的线程,这种线程在系统后台完成相应的任务,例如,JVM中的垃圾回收线程、JIT编译线程等都是守护线程。

在程序运行的过程中,只要有一个非守护线程还在运行,守护线程就会一直运行。只有所有的非守护线程全部运行结束,守护线程才会退出。

在编写Java程序时,可以手动指定当前线程是否是守护线程。方法也比较简单,就是调用Thread对象的setDeamon()方法,传入true 即可。

创建了一个线程threadDeamon,并将其设置为守护线程。

/**
 * 测试守护线程
 */
public class ThreadDaemonTest {
    public static void main(String[] args) {
        //创建threadDaemon线程实例
        Thread threadDaemon = new Thread(() -> {
            System.out.println("我是守护线程");
        }, "threadDaemon");

        //将线程设置为守护线程
        threadDaemon.setDaemon(true);
        //启动线程
        threadDaemon.start();
    }
}

并行与并发

并行:指当多核CPU中的一个CPU核心执行一个线程时,另一个CPU核心能够同时执行另一个线程,两个线程之间不会相互抢占CPU资源,可以同时运行。

并行的执行流程

并发指在一段时间内CPU处理了多个线程,这些线程会抢占CPU的资源,CPU资源根据 时间片周期在多个线程之间来回切换,多个线程在一段时间内同时运行,而在同一时刻实际上不是同时运行的。

并发的执行流程

并行、并发的区别

(1)
并行指多个线程在一段时间的每个时刻都同时运行;
并发指多个线程在一段时间内(而非每个时刻)同时运行。

(2)
并行执行的多个任务之间不会抢占系统资源;
并发执行的多个任务会抢占系统资源。

(3)
并行只有在多核CPU或者多CPU的情况下才会发生,在单核CPU中只可能发生串行执行或者并发执行。

同步与异步

同步与异步主要是针对一次方法的调用来说的。

以同步方式调用方法时,必须在方法返回 信息后,才能执行后面的操作。

以异步方式调用方法时,不必等方法返回信息,就可以执行
后面的操作,当完成被调用的方法逻辑后,会以通知或者回调的方式告知调用方。

共享与独享

共享指多个线程在运行过程中共享某些系统资源;
独享指一个线程在运行过程中独占某些系统资源。

例如,在Java程序运行的过程中,JVM中的方法区和堆空间是线程共享的,而栈、本地方法栈和程序计数器是每个线程独占的、独享的。

临界区

临界区一般表示能够被多个线程共享的资源或数据,但是每次只能提供给一个线程使用。

临界区资源一旦被占用,其他线程就必须等待。

在并发编程中,临界区一般指受保护的对象或者程序代码片段,可以通过加锁的方式保证每次只有一个线程进入临界区,从而达到保护临界区的目的。

阻塞与非阻塞

阻塞与非阻塞用来描述多个线程之间的相互影响。

例如,在并发编程中,多个线程抢占一个临界区资源,如果其中一个线程抢占成功,那么其他的线程必须阻塞等待。在占用临界区资源的线程执行完毕,释放临界区资源后,其他线程可以再次抢占临界区资源。

如果占用临界区资源的线程一直不释放资源,其他线程就会一直阻塞等待。

非阻塞指线程之间不会相互影响,所有的线程都会继续执行。

例如,著名的高性能网络编程框架Netty,内部就大量使用了异步非阻塞的编程模型。

并发编程的风险

并发编程优点:可以充分利用多核CPU的计算能力,通过对业务进行拆分并以多线程并发的方式执行,来提升应用的性能。

并发编程风险:包括安全性问题、活跃性问题和性能问题。

安全性问题

编写安全的多线程程序是比较困难的,如果处理不当,就会出现意想不到的后果,甚至会出现各种诡异的Bug,导致程序不能按照最初的设想执行。

例如,下面的代码就存在安全性问题。

/**
 * 测试线程的不安全性
 */
public class UnSafevalue {
    private long value;

    public long nextValue() {
        return value++;
    }
}

上述代码的本意是每当调用同一个UnSafevalue对象的nextvalue()方法时,value的值都会加1并返回。但是在多线程并发的情况下,当多个线程调用同一个UnSafevalue对象的nextvalue()方法时,不同线程可能返回相同的value值。也就是说,上面的代码不是线程安全的,具有安全性问题。

如何让上述代码变得安全呢?
可以在nextvalue()方法上添加一个synchronized关键字,为其添加一个同步锁,如下所示。

/**
 * 测试线程的不安全性
 */
public class UnSafevalue {
    private long value;

    public synchronized long nextValue() {
        return value++;
    }
}

此时,当多个线程调用同一个Safevalue对象的nextvalue()方法时,每次调用value的值都会加1并返回,解决了线程安全的问题。

活跃性问题

性能问题

并发编程中的锁

悲观锁与乐观锁

公平锁与非公平锁

可重入锁与不可重入锁

可终端锁与不可中断锁

读/写锁

自旋锁

死锁、饥饿与活锁

总结

Leave a Comment

您的电子邮箱地址不会被公开。 必填项已用*标注

close
arrow_upward