java并发编程系列-第一篇   2019-02-27


1. 进程和线程的由来

1.1 进程的由来

操作系统中为什么会出现进程?

计算机制造的早期,是用来解决数学问题的,在最初的时候,计算机只能完成特定的指令。用户输入一个指令,计算机就执行一个操作。如果用户在思考或者输入数据的时候,计算机就会等待。很显然,这样的效率很低下,因为大部分情况下,计算机一直都会处于等待输入指令的状态。

后来,就有人想到,能不能把一系列需要执行的指令预先写下来,形成一个清单,一次性交给计算机。计算机呢,则不断的去读取指令来进行相应的操作。(这就是最早的批处理操作系统,用户可以将程序写到磁带上,然后计算机去逐条读取并执行,将结果输出到另一个磁带上。)

虽然,批处理操作系统能提高任务处理的便捷性,但仍然存在一个很大的问题:

假如有两个任务A和B,任务A在执行到一半的过程中,需要读取大量的数据输入(I/O操作),而此时CPU只能眼睁睁的干等着任务A读取完数据才能继续执行,这样就白白浪费了CPU资源。人们于是想,能否在任务A读取数据的过程中,让任务B去执行,当任务A读取完数据之后,让任务B暂停,然后让任务A继续执行?

但是原来每次都是只有一个程序在计算机里面运行,也就说内存中始终只有这一个程序的运行数据。所以会面临以下问题:

  1. 如果想要任务A执行I/O操作的时候,让任务B去执行,必然内存中要装入多个程序,那么如何处理呢?
  2. 多个程序使用的数据如何进行辨别?
  3. 当一个程序运行暂停后,后面如何恢复到它之前执行的状态?

为了解决这些问题,就发明了进程。

  • 对于问题1,用进程来对应一个程序。
  • 对于问题2,每个进程对应一定的内存地址空间,并且只能使用它自己的内存空间,各个进程间互不干扰。
  • 基于上面的设定,对于问题3,由于进程保存了程序每个时刻的运行状态,这样就为进程切换提供了可能。当进程暂时时,它会保存当前进程的状态(比如进程标识、进程的使用的资源等),在下一次重新切换回来时,便根据之前保存的状态进行恢复,然后继续执行。

这就是并发,能够让操作系统从宏观上看起来同一个时间段有多个任务在执行。换句话说,进程让操作系统的并发成为了可能。

注意,虽然并发从宏观上看有多个任务在执行,但是事实上,任一个具体的时刻,只有一个任务在占用CPU资源(当然是对于单核CPU来说的)。

1.2 线程的由来

在出现了进程之后,操作系统的性能得到了大大的提升。虽然进程的出现解决了操作系统的并发问题,但是人们仍然不满足,人们逐渐对实时性有了要求。从上面的内容我们知道一个进程在一个时间段内只能做一件事情,如果一个进程有多个子任务,只能逐个地去执行这些子任务。

然而对于一个监控系统来说,它不仅要把图像数据显示在画面上,还要与服务端进行通信获取图像数据,还要处理人们的交互操作。如果某一个时刻该系统正在与服务器通信获取图像数据,而用户又在监控系统上点击了某个按钮,那么该系统就要等待获取完图像数据之后才能处理用户的操作,如果获取图像数据需要耗费10s,那么用户就只有一直在等待。显然,对于这样的系统,人们是无法满足的。

那么可不可以将这些子任务分开执行呢?即在系统获取图像数据的同时,如果用户点击了某个按钮,则会暂停获取图像数据,而先去响应用户的操作(因为用户的操作往往执行时间很短),在处理完用户操作之后,再继续获取图像数据?

于是人们就发明了线程,让一个线程去执行一个子任务,这样一个进程就包括了多个线程,每个线程负责一个独立的子任务,这样在用户点击按钮的时候,就可以暂停获取图像数据的线程,让UI线程响应用户的操作,响应完之后再切换回来,让获取图像的线程得到CPU资源。从而让用户感觉系统是同时在做多件事情的,满足了用户对实时性的要求。

换句话说,进程让操作系统的并发性成为可能,而线程让进程的内部并发成为可能。

但是要注意,一个进程虽然包括多个线程,但是这些线程是共同享有进程占有的资源和地址空间的。进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位。

1.3 多线程并发

由于多个线程是共同占有所属进程的资源和地址空间的,那么就会存在一个问题:如果多个线程要同时访问某个资源,怎么处理?这个问题就是后序文章中要重点讲述的同步问题。

现在很多时候都采用多线程编程,那么是不是多线程的性能一定就由于单线程呢?这个问题的答案是:不一定,要看具体的任务以及计算机的配置。

比如说:对于单核CPU,如果是CPU密集型任务,如解压文件,多线程的性能反而不如单线程性能,因为解压文件需要一直占用CPU资源,如果采用多线程,线程切换导致的开销反而会让性能下降。

但是对于比如交互类型的任务,肯定是需要使用多线程的。

而对于多核CPU,对于解压文件来说,多线程肯定优于单线程,因为多个线程能够更加充分利用每个核的资源。

虽然多线程能够提升程序性能,但是相对于单线程来说,它的编程要复杂地多,要考虑线程安全问题。因此,在实际编程过程中,要根据实际情况具体选择。

为什么多线程是必要的?

个人觉得可以用一句话概括:开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。

为什么提倡多线程而不是多进程?

线程就是轻量级进程,是程序执行的最小单位。使用多线程而不是用多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程。

2. 几个重要的概念

2.1 同步和异步

同步和异步通常用来形容一次方法调用。

  • 同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为。
  • 异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者可以继续后续的操作。

关于异步目前比较经典以及常用的实现方式就是消息队列:在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库(消息队列也比数据库有更好的伸缩性),因此响应速度得到大幅改善。

2.2 并发 (Concurrency) 和并行(Parallelism)

并发和并行是两个非常容易被混淆的概念。它们都可以表示两个或者多个任务一起执行,但是偏重点有些不同。并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。而并行是真正意义上的 “同时执行”。

多线程在单核CPU的话是顺序执行,也就是交替运行(并发)。多核CPU的话,因为每个CPU有自己的运算器,所以在多个CPU中可以同时运行(并行)。

2.3 高并发

高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。

高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率 QPS(Query Per Second),并发用户数等。

2.4 临界区

临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。在并行程序中,临界区资源是保护的对象。

2.5 阻塞和非阻塞

非阻塞指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回,而阻塞与之相反。

3. 使用多线程常见的三种方式

注:前两种实际上很少使用,一般都是用线程池的方式比较多一点。

3.1 继承Thread类

1
2
3
4
5
6
7
public class MyThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("MyThread");
}
}
1
2
3
4
5
6
7
public class Run {
public static void main(String[] args) {
MyThread mythread = new MyThread();
mythread.start();
System.out.println("运行结束");
}
}

运行结果:

1
2
运行结束
MyThread

从上面的运行结果可以看出:线程是一个子任务,CPU以不确定的方式,或者说是以随机的时间来调用线程中的run方法。

3.2 实现Runnable接口

推荐实现Runnable接口方式开发多线程,因为Java单继承但是可以实现多个接口。

1
2
3
4
5
6
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("MyRunnable");
}
}
1
2
3
4
5
6
7
8
public class Run {
public static void main(String[] args) {
Runnable runnable=new MyRunnable();
Thread thread=new Thread(runnable);
thread.start();
System.out.println("运行结束!");
}
}

运行结果:

1
2
运行结束!
MyRunnable

3.3 使用线程池

使用线程池的方式也是最推荐的一种方式。

4. 实例变量和线程安全

线程类中的实例变量针对其他线程可以有共享和不共享之分。下面通过两个简单的例子来说明!

4.1 不共享数据的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//多个线程之间不共享变量线程安全的情况
public class MyThread extends Thread {

private int count = 5;

public MyThread(String name) {
super();
this.setName(name);
}

@Override
public void run() {
super.run();
while (count > 0) {
count--;
System.out.println("由" + MyThread.currentThread().getName() + "计算,count=" + count);
}
}

public static void main(String[] args) {
MyThread a = new MyThread("A");
MyThread b = new MyThread("B");
MyThread c = new MyThread("C");
a.start();
b.start();
c.start();
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
由C计算,count=4
由C计算,count=3
由C计算,count=2
由C计算,count=1
由C计算,count=0
由A计算,count=4
由B计算,count=4
由B计算,count=3
由B计算,count=2
由A计算,count=3
由A计算,count=2
由B计算,count=1
由B计算,count=0
由A计算,count=1
由A计算,count=0

可以看出每个线程都有一个属于自己的实例变量 count,它们之间互不影响。我们再来看看另一种情况。

4.2 共享数据的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//多个线程之间共享变量线程不安全的情况
public class SharedVariableThread extends Thread {
private int count = 5;

@Override
public void run() {
super.run();
count--;
System.out.println("由" + SharedVariableThread.currentThread().getName() + "计算,count=" + count);
}

public static void main(String[] args) {

SharedVariableThread mythread = new SharedVariableThread();
// 下列线程都是通过mythread对象创建的
Thread a = new Thread(mythread, "A");
Thread b = new Thread(mythread, "B");
Thread c = new Thread(mythread, "C");
Thread d = new Thread(mythread, "D");
Thread e = new Thread(mythread, "E");
a.start();
b.start();
c.start();
d.start();
e.start();
}
}

运行结果:

1
2
3
4
5
由A计算,count=4
由D计算,count=0
由E计算,count=1
由B计算,count=2
由C计算,count=2

可以看出这里已经出现了错误,我们想要的是依次递减的结果。为什么呢??

因为在大多数 jvm 中,count–的操作分为如下下三步:

  1. 取得原有 count 值
  2. 计算 i -1
  3. 对 i 进行赋值

所以多个线程同时访问时出现问题就是难以避免的了。

那么有没有什么解决办法呢?

答案是:当然有,而且很简单。给大家提供两种解决办法:

  1. synchronized关键字(保证任意时刻只能有一个线程执行该方法)
  2. 利用 AtomicInteger 类(JUC 中的 Atomic 原子类)。

注意:这里不能用volatile关键字,因为volatile关键字不能保证复合操作的原子性。

5. 一些常用方法

方法 描述
currentThread() 返回对当前正在执行的线程对象的引用。
getId() 返回此线程的标识符
setName(String name) 将此线程的名称更改为等于参数name
getName() 返回此线程的名称
setPriority(int newPriority) 更改此线程的优先级
getPriority() 返回此线程的优先级
setDaemon(boolean on) 将此线程标记为 daemon 线程或用户线程。
isDaemon() 测试这个线程是否是守护线程。
isAlive() 测试这个线程是否还处于活动状态(活动状态就是线程已经启动且尚未终止。线程处于正在运行或准备运行的状态。)。
sleep(long millis) 使当前正在执行的线程以指定的毫秒数 “休眠”(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性。
interrupt() 中断这个线程。
interrupted() 测试当前线程是否已经是中断状态,执行后具有将状态标志清除为 false 的功能
isInterrupted() 测试线程 Thread 对相关是否已经是中断状态,但部清楚状态标志
join() 在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。join() 的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行
yield() yield()方法的作用是放弃当前的CPU资源,将它让给其他的任务去占用 CPU 时间。注意:放弃的时间不确定,可能一会就会重新获得 CPU 时间片。

6. 如何停止一个线程

stop(),suspend(),resume()(仅用于与 suspend() 一起使用)这些方法已被弃用,所以此处不予讲解。

6.1 使用 interrupt() 方法

我们上面提到了interrupt() 方法,先来试一下 interrupt() 方法能不能停止线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyThread extends Thread {
@Override
public void run() {
super.run();
for (int i = 0; i < 5000000; i++) {
System.out.println("i=" + (i + 1));
}
}
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.start();
Thread.sleep(2000);
thread.interrupt();
} catch (InterruptedException e) {
System.out.println("main catch");
e.printStackTrace();
}
}
}

运行上述代码你会发现,线程并不会终止。

针对上面代码的一个改进:

interrupted() 方法判断线程是否停止,如果是停止状态则 break

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class InterruptThread2 extends Thread {
@Override
public void run() {
super.run();
for (int i = 0; i < 500000; i++) {
if (this.interrupted()) {
System.out.println("已经是停止状态了!我要退出了!");
break;
}
System.out.println("i=" + (i + 1));
}
System.out.println("看到这句话说明线程并未终止------");
}

public static void main(String[] args) {
try {
InterruptThread2 thread = new InterruptThread2();
thread.start();
Thread.sleep(2000);
thread.interrupt();
} catch (InterruptedException e) {
System.out.println("main catch");
e.printStackTrace();
}
}
}

运行结果:

1
2
3
4
5
i=344061
i=344062
i=344063
已经是停止状态了!我要退出了!
看到这句话说明线程并未终止------

for 循环虽然停止执行了,但是 for 循环下面的语句还是会执行,说明线程并未被停止。

6.2 使用 return 停止线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyThread extends Thread {

@Override
public void run() {
while (true) {
if (this.isInterrupted()) {
System.out.println("停止了!");
return;
}
System.out.println("timer=" + System.currentTimeMillis());
}
}
public static void main(String[] args) throws InterruptedException {
MyThread t=new MyThread();
t.start();
Thread.sleep(2000);
t.interrupt();
}
}

运行结果:

1
2
3
4
5
6
7
timer=1551270875684
timer=1551270875684
timer=1551270875684
timer=1551270875684
timer=1551270875684
timer=1551270875684
停止了!

当然还有其他停止线程的方法,后面再做介绍。

7. 线程的优先级

每个线程都具有各自的优先级,线程的优先级可以在程序中表明该线程的重要性,如果有很多线程处于就绪状态,系统会根据优先级来决定首先使哪个线程进入运行状态。但这个并不意味着低
优先级的线程得不到运行,而只是它运行的几率比较小,如垃圾回收机制线程的优先级就比较低。所以很多垃圾得不到及时的回收处理。

线程优先级具有继承特性,比如A线程启动B线程,则B线程的优先级和A是一样的。

线程优先级具有随机性,也就是说线程优先级高的不一定每一次都先执行完。

Thread 类中包含的成员变量代表了线程的某些优先级。如 Thread.MIN_PRIORITY(常数 1),Thread.NORM_PRIORITY(常数 5),
Thread.MAX_PRIORITY(常数 10)。

其中每个线程的优先级都在 Thread.MIN_PRIORITY(常数 1) 到 Thread.MAX_PRIORITY(常数 10) 之间,在默认情况下优先级都是 Thread.NORM_PRIORITY(常数 5)。

学过操作系统这门课程的话,我们可以发现多线程优先级或多或少借鉴了操作系统对进程的管理。

线程优先级具有继承特性测试代码:

1
2
3
4
5
6
7
8
public class MyThread1 extends Thread {
@Override
public void run() {
System.out.println("MyThread1 run priority=" + this.getPriority());
MyThread2 thread2 = new MyThread2();
thread2.start();
}
}
1
2
3
4
5
6
public class MyThread2 extends Thread {
@Override
public void run() {
System.out.println("MyThread2 run priority=" + this.getPriority());
}
}
1
2
3
4
5
6
7
8
9
public class Run {
public static void main(String[] args) {
System.out.println("main thread begin priority=" + Thread.currentThread().getPriority());
Thread.currentThread().setPriority(6);
System.out.println("main thread end priority=" + Thread.currentThread().getPriority());
MyThread1 thread1 = new MyThread1();
thread1.start();
}
}

运行结果:

1
2
3
4
main thread begin priority=5
main thread end priority=6
MyThread1 run priority=6
MyThread2 run priority=6

8. Java多线程分类

8.1 多线程分类

  • 用户线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
  • 守护线程:运行在后台,为其他前台线程服务. 也可以说守护线程是JVM中非守护线程的 “佣人”。

  • 特点:一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作

  • 应用:数据库连接池中的检测线程,JVM 虚拟机启动后的检测线程

最常见的守护线程:垃圾回收线程

8.2 如何设置守护线程?

可以通过调用Thead类的setDaemon(true)方法设置当前的线程为守护线程

注意事项:

  1. setDaemon(true)必须在start()方法前执行,否则会抛出IllegalThreadStateException异常
  2. 在守护线程中产生的新线程也是守护线程
  3. 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyThread extends Thread {
private int i = 0;

@Override
public void run() {
try {
while (true) {
i++;
System.out.println("i=" + (i));
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Run {
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.setDaemon(true);
thread.start();
Thread.sleep(5000);
System.out.println("我离开thread对象也不再打印了,也就是停止了!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

运行结果:

1
2
3
4
5
6
7
8
i=43
i=44
i=45
i=46
i=47
i=48
i=49
我离开thread对象也不再打印了,也就是停止了!


分享到:


  如果您觉得这篇文章对您的学习很有帮助, 请您也分享它, 让它能再次帮助到更多的需要学习的人. 您的支持将鼓励我继续创作 !
本文基于署名4.0国际许可协议发布,转载请保留本文署名和文章链接。 如您有任何授权方面的协商,请邮件联系我。

目录

  1. 1. 1. 进程和线程的由来
    1. 1.1. 1.1 进程的由来
    2. 1.2. 1.2 线程的由来
    3. 1.3. 1.3 多线程并发
  2. 2. 2. 几个重要的概念
    1. 2.1. 2.1 同步和异步
    2. 2.2. 2.2 并发 (Concurrency) 和并行(Parallelism)
    3. 2.3. 2.3 高并发
    4. 2.4. 2.4 临界区
    5. 2.5. 2.5 阻塞和非阻塞
  3. 3. 3. 使用多线程常见的三种方式
    1. 3.1. 3.1 继承Thread类
    2. 3.2. 3.2 实现Runnable接口
    3. 3.3. 3.3 使用线程池
  4. 4. 4. 实例变量和线程安全
    1. 4.1. 4.1 不共享数据的情况
    2. 4.2. 4.2 共享数据的情况
  5. 5. 5. 一些常用方法
  6. 6. 6. 如何停止一个线程
    1. 6.1. 6.1 使用 interrupt() 方法
    2. 6.2. 6.2 使用 return 停止线程
  7. 7. 7. 线程的优先级
  8. 8. 8. Java多线程分类
    1. 8.1. 8.1 多线程分类
    2. 8.2. 8.2 如何设置守护线程?