进程是操作系统资源分配的基本单位,

线程是处理器(CPU)任务调度和执行的基本单位
线程依赖于进程,一个进程可能对应多个线程。

并发与并行

1、并发(Concurrent):指两个或多个事件在同一时间间隔内发生,即交替做不同事的能力,多线程是并发的一种形式。例如垃圾回收时,用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

2、并行(Parallel):指两个或者多个事件在同一时刻发生,即同时做不同事的能力。例如垃圾回收时,多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

同时能处理的最大线程数取决于CPU内核数量,
有些机器还有逻辑处理器,

能同时处理任务的线程最大数量为CPU内核与逻辑处理器的较大值。

如内核:4,逻辑处理器:8
那么CPU正在能同时处理的任务数量为8。

但为何我们写代码的时候,设置多线程运行时为何感觉可以同时设置很多个线程数量呢。

这个就跟CPU的时间轮转片调度算法有关了

在早期的时间片轮转法中,系统将所有的就绪进程按先来先服务的原则,排成一个队列,每次调度时,把CPU分配给队首进程,并令其执行一个时间片.时间片的大小从几ms到几百ms.当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片.这样就可以保证就绪队列中的所有进程,在一给定的时间内,均能获得一时间片的处理机执行时间.

所以我们在多线程运行时,超过内核数量的线程任务在执行时,其实是在切换运行的。
只是因为CPU的时间片粒度很小,所以在切换时,很多都是无感的

线程的启动

java中的程序天生的多线程的,启动线程有两种方式。

  • 继承Thread类

  • 实现Runnable接口

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
public class Test {

public static void main(String[]args) {
MyThread thread = new MyThread();
thread.start();
MyThread2 thread2 = new MyThread2();
new Thread(thread2).start();
}

static class MyThread extends Thread {

@Override
public void run() {
super.run();
System.out.println("extends Thread");
}
}

static class MyThread2 implements Runnable {
@Override
public void run() {
System.out.println("implements Runnable");
}
}
}

线程的终止

线程启动了,如何终止呢?
Thread类中提供了stop、suspend、resume等函数,但都是已经遗弃的,不提倡使用,
在API29时,调用内部就是直接抛出异常UnsupportedOperationException。

为何遗弃,因为suspend函数,只会终止当前线程,而不会释放线程已经占有的资源(比如锁),而是占有资源进入睡眠状态,这样容易引发死锁问题。
而stop函数也是同理,在终止线程时,不会保证线程资源能够正常释放
所以这些函数都已经被遗弃,不提倡使用。

正确的中断线程的方法,是调用Thread中的interrupt().
interrupt()函数不会强制中断线程,而只是改变了一个线程的标志位,线程通过检测自身的标志位是否被置为true来判断是否需要继续执行。

可以调用isInterrupted()来获取该标志位,判断线程任务是否应该中断,
还可以调用Thread中的静态函数interrupted()来判断,但是interrupted()函数调用时,会将标志位重新置于false。

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
class Test {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
try {
Thread.sleep(300);
thread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

static class MyThread extends Thread {
@Override
public void run() {
super.run();
while (!currentThread().isInterrupted()) {
System.out.println("线程正常运行");
}
// while (!Thread.interrupted()) {
// System.out.println("线程正常运行");
// }
System.out.println("线程已经被终止 flag ---" + currentThread().isInterrupted());
}
}
}

打印结果可以看得出isInterrupted()与interrupted()的区别。

run() start()

我们new一个Thread实例对象,只是创建了一个对象,并没有与操作系统的真正的线程挂钩,
只有在执行start函数时,才是真正启动了一个线程,执行了其中的run方法

start()的调用,让线程进入就绪队列等待分配cpu,分到cpu后执行run()方法,start方法不能重复调用,否则抛出异常

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
29
30
31
32
33
34
35
36
37
38
39
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
// Android-changed: Replace unused threadStatus field with started field.
// The threadStatus field is unused on Android.
if (started)
throw new IllegalThreadStateException();

/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);

// Android-changed: Use field instead of local variable.
// It is necessary to remember the state of this across calls to this method so that it
// can throw an IllegalThreadStateException if this method is called on an already
// started thread.
started = false;
try {
// Android-changed: Use Android specific nativeCreate() method to create/start thread.
// start0();
nativeCreate(this, stackSize, daemon);
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}

主要逻辑在这个native函数中nativeCreate()

Thread中其他函数

yield

yield(): 使当前让出cpu占有权,但让出时间不可限定,也不会让出锁资源,而且执行yield()的线程也不一定持有锁,我们可以在释放锁后执行这个方法。
执行yield()后让出cpu,但也可能在下一个时间片重新获取cpu。

join

join() :将指定线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行,比如在线程B中执行了A线程的join函数,
直到A线程执行完毕,才会执行B线程。

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
29
30
31
32
class Test {
public static void main(String[] args) {
Thread a = new Thread() {
@Override
public void run() {
super.run();
System.out.println("这是a线程在执行");
try {
Thread.sleep(100);
System.out.println("这是a线程在休眠结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};

Thread b = new Thread(){
@Override
public void run() {
super.run();
try {
a.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("这是b线程在执行");
}
};
a.start();
b.start();
}
}

线程的同步

synchronized

锁,内置锁,具体详情可以查看:https://wangchongwei.github.io/blog/2020/09/Synchronized%E8%AF%A6%E8%A7%A3.html

volatile

只保证可见性,不能保证原子性

ThreadLocal

在每个线程存在副本,各个线程数据互不干扰。
具体详情可以查看: https://wangchongwei.github.io/blog/2020/08/java-ThreadLocal%E8%A7%A3%E6%9E%90.html

ForkJoin

ForkJoin 是采用分而治之的思维,将一个大任务分解为若干个相互独立的子任务(异步),
达到提高运算效率。
我们做一个简单的运算 计算1-100000的累加

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class TestJoinTask {

public static void main(String[] args) {
long currentTime = System.currentTimeMillis();
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Integer> task = pool.submit(new MyForkJoinTask(1, 100000));

try {
int result = task.get();
System.out.println("result = " + result);
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程名" + Thread.currentThread().getName() + " 执行完毕" + " time = " + (System.currentTimeMillis() - currentTime));
}

/**
* 传入的范型Integer是返回结果类型
*/
static class MyForkJoinTask extends RecursiveTask<Integer> {

private int startValue;

private int endValue;

private int limitValue = 100;

public MyForkJoinTask(int startValue, int endValue) {
if(startValue > endValue) {
throw new RuntimeException("startValue < endValue");
}
this.startValue = startValue;
this.endValue = endValue;
}

@Override
protected Integer compute() {
if(endValue - startValue <= limitValue) {
System.out.println("线程名" + Thread.currentThread().getName() + " 执行计算");
// 两个值在限制值内 进行计算
int sum = 0;
for(int i = startValue; i <= endValue; i ++) {
sum += i;
}
return sum;
}
MyForkJoinTask task1 = new MyForkJoinTask(startValue, (endValue + startValue) / 2);
task1.fork();
MyForkJoinTask task2 = new MyForkJoinTask((endValue + startValue) / 2 + 1, endValue);
task2.fork();
return task1.join() + task2.join();
}
}
}

如上所示,我们将一个从0 - 100000的累加任务分解为500个子任务:每200位数累加

在日志打印中,我们可以看到,有多个线程在执行任务,也就是说,分解的任务其实都是提交到线程池中执行,

守护线程

守护线程依附与其他线程,当其他线程结束时,守护线程自动退出

在守护线程中的 finally 中代码可能不会执行