Fork me on GitHub

并发编程——基础拾遗

并发编程基础拾遗

在看并发的书或者看并发相关的博客时,会发现一些知识点会遗漏或者之前没有看到,这里去总结一下。

创建线程相关

main线程

我们通常会通过写main方法去创建多个线程,main线程是非守护线程,代表方法的入口。这个时候如果用jconsole去看的话,会发现这时也会启用很多后台线程:比如GC线程、计算引用线程、JMX线程。

线程生命周期

  • 新建状态
  • 可运行状态
  • 运行状态
  • 阻塞状态
  • 死亡状态

这些个状态之间的转换每次都记得不是很牢固。

当新建一个线程之后,这个线程处于新建状态,这时调用t.start()方法,线程就会进入可运行状态,这个状态下线程还没有真正的执行。当os分配给当前线程时间片的时候,线程会进入运行状态。当线程处于运行状态时,时间片用完或者调用Thread.yield()方法,线程则变回为可运行状态,同样若再获得os的时间片,线程将会再次进入运行状态。当正在运行状态的线程遇到Thread.sleep或者其他线程join的时候,会进入阻塞状态,直到sleep结束或者join线程结束之后线程进入可运行状态等待时间片的分配。(这里不能直接由阻塞状态直接变为运行状态)。当线程处于运行状态的时候,对象调用了o.wait方法,那么线程就会进入等待队列,等待被其他线程唤醒进入锁池,还有如果线程未持有对象的锁而被阻塞的时候也会进入到锁池,直到拿到对象锁标志之后,才能到可运行状态,再次等待时间片的分配。当线程的run方法结束,或者线程是一个守护线程是main线程结束,或者异常退出之后,线程进入到死亡状态。

start()方法和run方法的一个设计

start方法和run()方法是Thread类中的两个方法,我们启动一个线程的时候要调用start方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public synchronized void start() {

if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);

boolean started = false;
try {
start0();
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 */
}
}
}

在start方法里是调用了一个start0的native方法,这个方法其实是调用的run方法。

1
2
3
4
5
6
@Override
public void run() {
if (target != null) {
target.run();
}
}

这样的设计是体现的设计模式中的一个简单模板方法,在调用start方法的同时其实是调用你重写的run方法,其他的start0和一些线程状态的判断都在start方法中作为不变的部分,而变的run方法则供使用者去实现run方法。

Runnable接口相关

runnable接口的作用

Runnable接口创建线程,这种创建线程的方式可以将run方法的逻辑从new thread的控制中抽离出来,这里如果耦合在new Thread中,没有体现业务可执行逻辑和线程控制分离开,体现了面向对象的思想。其实这种方式体现的是一种策略模式,Runnable接口就像策略模式中的策略接口,实现Runnable接口的run方法的类都是一个线程运行的策略,同时Runnable设计成了函数式接口,也更加灵活。

Thread API相关

new Thread构造函数

Thread构造函数有多个参数的重载,这里去记录下相关的构造函数,并且每个构造函数中参数的含义。

new Thread()

创建线程对象Thread,默认有一个线程名,以Thread-开头,从0开始数。

new Thread(Runnable target)

如果在构造Thread的时候没有传递Runnable或者没有复写Thread的run方法,该Thread将不会调用任何东西,如果传递了Runnable接口的实例,或者复写了Thread的run方法,则会执行该方法的逻辑单元(逻辑代码)。

new Thread(ThreadGroup g)

在创建线程的时候,如果构造函数中未传入threadGroup,则thread会默认获取父线程的threadGroup作为该线程的threadGroup。此时子线程和父线程将会在同一个ThreadGropu中。这时候可以通过threadGroup提供的api去获取一些信息。

比如threadGroup.getActiveCount 获取活跃线程数。

比如threadGroup.enumerate(new Thread[]) 是将所有活跃线程枚举到一个数组中。

new Thread(long stackSize)

stackSize是平常用的比较少的,这里要清楚jvm的一个结构:(这里简单用图去展示了jmm和虚拟机栈中栈帧结构)。jmm的结构:

关于变量的存储,这里可以看一下下面代码中的注释:

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 NewThreadWithStackSize {

/**
* 注意这里不是局部变量,所以这里的int i是放入方法区的
*/
private int i = 0;

/**
* bytes这个变量存放在方法区中,里面存放的是对象在堆内存中的地址
*/
private byte[] bytes = new byte[1024];

private static int count = 0;

public static void main(String[] args) {

// main函数也是一个线程
// create a thread by jvm named 'main' and 创建一个虚拟机栈

// 局部变量 放入局部变量表中
int j = 0;
// arr在局部变量表中,对象还是放入堆内存中
int[] arr = new int[1024];
}
}

栈帧的结构:

这里可以做一个栈溢出的的例子,来为一个线程分配固定栈空间的大小,看看大概操作栈的次数(这里只是一个大概的数值)

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

private static int count = 0;

public static void main(String[] args) {

/**
* 指定stackSize的new Thread构造函数
*/
Thread thread = new Thread(null, new Runnable() {
@Override
public void run() {
try {
add(1);
} catch (Error r) {
r.printStackTrace();
System.out.println(count);
}
}

private void add(int i) {
count ++;
add(i+1);
}
}, "test-stackSize", 1<<23); // 1 右移23位 大概是8mb 给当前线程thread分配了8m的stackSize 进行递归栈溢出

thread.start();
}
}

可以看到count的数值是:

我们可以知道:构造Thread的时候传入stacksize表示该线程占用的stack大小,如果没有指定stacksize的大小,默认是0,0代表着会忽略该参数,该参数会被JNI函数去调用。需要注意:该参数有一些平台有效,有些平台则无效。可以通过jvm参数 -Xss10m 来设置stacksize的大小

-------------本文结束感谢您的阅读-------------

本文标题:并发编程——基础拾遗

文章作者:夸克

发布时间:2018年12月31日 - 17:12

最后更新:2022年07月01日 - 05:07

原始链接:https://zhanglijun1217.github.io/2018/12/31/并发编程——基础拾遗/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。