并发编程基本概念
程序
人为编写或自动生成的代码,保存在文件中,程序本身是静态的。
如果要运行程序,需将程序加载到内存,通过编译器或解释器翻译为计算机理解的方式运行。
编程语言:汇编语言、C/C++、Java语言、Python语言和Go语言等。
进程与线程
操作系统启动一个程序时,会启动一个进程。
如:
- 启动一个Java程序时,会创建一个JVM进程;
- 启动一个Python程序时,会创建一个Python进程;
- 启动一个Go程序时,会创建一个Go进程;
进程,是操作系统进行资源分配的最小单位,在一个进程中可以创建多个线程。
线程,是比进程粒度更小的,能够独立运行的基本单位。也是CPU调度的最小单元被称为轻量级进程。多个线程各自拥有独立的局部变量、线程堆栈和程序计数器等,能够访问共享的资源。
进程和线程的区别
(1)进程是操作系统分配资源的最小单位,线程是CPU调度的最小单元。
(2)一个进程中可以包含一个或多个线程, 一个线程只能属于一个进程。
(3)进程与进程之间是互相独立的,进程内部的线程之间并不完全独立,可以共享进程的堆内存、方法区内存和系统资源。
(4)进程上下文的切换要比线程的上下文切换慢很多。
(5)进程是存在地址空间的,而线程本身无地址空间,线程的地址空间是包含在进程中的。
(6)某个进程发生异常不会对其他进程造成影响,某个线程发生异常可能会对所在进程中的其他线程造成影响。
线程组
线程组可以同时管理多个线程。在实际的应用场景中,如果系统创建的线程比较多,创建的线程功能也比较明确,就可以将具有相同功能的线程放到一个线程组中。
如何使用线程组
如:创建了一个线程组threndGroup
,两个线程thread1
和thread2
,将thread1
和thread2
放到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并返回,解决了线程安全的问题。