0%

并发源码-线程

线程

线程篇知识,主要包括并发编程方面,JUC并发包下的类的源码学习。

Thread

执行main方法后,就会开启一个jvm进程了,进程里,又有很多线程,main线程就是执行程序的第一个线程,然后我们又创建了一个子线程,来执行另外的任务。

1
2
3
4
5
6
7
8
9
10
11
12
public class HelloWorld {
public static void main(String[] args) {
new Thread() {

@Override
public void run() {
System.out.println("子线程。。");
}

}.start();
}
}

多个线程的执行,是没有先后顺序的,他们会争夺和抢占的CPU的时间,谁先抢到,谁就先执行。

并发编程,无非就是在多线程的情况下去操作同一份数据,或者不同的线程之间需要通信。

Thread Group

ThreadGroup就是线程组,可以把一堆线程放入一个组里,作为一个整体统一管理和设置,这个一般不怎么用。

每一个线程都是会属于一个线程组的,如果在创建线程的时候没有设置,默认就是父线程的线程组。

比如main线程创建的子线程,子线程的线程组就是main ThreadGroup。

默认线程会加入父线程的ThreadGroup,也可以手动创建ThreadGroup,ThreadGroup也有父ThreadGroup,ThreadGroup可以包裹一大堆的线程,然后统一做一些操作,比如统一复制、停止、销毁,等等。

enumerate():复制线程组里的线程

activeCount():获取线程组里活跃的线程

getName()、getParent()、list(),等等

interrupt():打断所有的线程

destroy():一次性destroy所有的线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ThreadGroupDemo {
public static void main(String[] args) {
// main线程的线程组
// 输出:main
System.out.println(Thread.currentThread().getThreadGroup().getName());
new Thread(() -> {
// 子线程默认加入父线程的group
// 输出:main
System.out.println(Thread.currentThread().getThreadGroup().getName());
}).start();

ThreadGroup custom = new ThreadGroup("custom");
// 构造方法可以指定线程组
new Thread(custom, () -> {
// 加入了指定的custom线程组
// 输出:custom
System.out.println(Thread.currentThread().getThreadGroup().getName());
}).start();
}
}

优先级设置

优先级一般是在1~10之间,默认优先级是5,一般不设置,默认就是5,因为设置了,CPU也不一定按照这个来执行。

源码

看源码,一般就先扫一眼变量,或者从构造方法开始看起

1
2
3
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}

我们经常会看到日志打印里,线程名字都是Thread-0,Thread-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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}

this.name = name.toCharArray();
// 因为线程还没有被创建,所以这里获取到的是父线程的对象,也就是main Thread
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
// 如果没有传ThreadGroup,就用父线程的ThreadGroup
if (g == null) {
if (security != null) {
g = security.getThreadGroup();
}
if (g == null) {
g = parent.getThreadGroup();
}
}
g.checkAccess();

if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}

g.addUnstarted();

this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;

/* Set thread ID */
tid = nextThreadID();
}

(1)创建你的线程,就是你的父线程

(2)如果你没有指定ThreadGroup,你的ThreadGroup就是父线程的ThreadGroup

(3)你的daemon状态默认是父线程的daemon状态

(4)你的优先级默认是父线程的优先级

(5)如果你没有指定线程的名称,那么默认就是Thread-0格式的名称

(6)你的线程id是全局递增的,从1开始

这是初始化的代码,接着看下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() {
// 启动后状态会发生变化,一个线程不能被启动2次
if (threadStatus != 0)
throw new IllegalThreadStateException();
// 初始化的时候分配的group
group.add(this);

boolean started = false;
try {
// 调用Native方法来启动线程,然后执行run方法()
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}

sleep

Thread.sleep(500),可以让线程停顿一段时间,然后恢复运行,在很多场景都可能会用到,比如在死循环中,通过sleep方法来达到一个定时执行的效果。

yield

这也是一个Native的方法,很少看到有人会用到,它的意思就是让出CPU执行时间,让别的线程先去执行一下。

join

mian线程里创建线程start后,就会并发执行了,如果要实现等待的效果,可以调用线程的join方法,会阻塞等待子线程的逻辑执行完成,main线程继续往下走

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread() {
@Override
public void run() {
...
}
};
thread1.start();

// main线程会阻塞等待,等待thread1执行完成后,继续执行main线程后面的内容
thread1.join();

method();
}

interrupt

这个方法,叫打断,实际上他并不会中断线程的执行,只是给线程设置一个标志位,然后isInterrupted就能返回true了。一个最常见的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ThreadInterruptDemo {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {

while (!Thread.currentThread().isInterrupted()) {
System.out.println("run..");
}
});
thread.start();

Thread.sleep(100);

thread.interrupt();
}
}

还有一种情况,就是一个正在sleep的线程,如果调用interrupt的话,也会中断睡眠,抛出一个java.lang.InterruptedException: sleep interrupted的异常。

线程的使用,基本上就是这些了,后面就是基于线程的并发编程。

并发编程的问题

在并发编程中,有三类问题,分别是可见性、原子性、有序性。

可见性

就是一个线程修改了变量值,另外一个线程读取到的还是原来的值的问题,在Java中一般用volatile关键字来解决,或者加锁。

原子性

比如i++的操作,他就是不保证原子性的,因为i++在底层是拆分成了多个指令,包含了读取,计算,写入,不同的线程在对同一个变量执行i++的时候,可能拿到的值是相同的,然后i++完成后都写入,导致最后的结果就不对了,比如i=1,2个线程i++开始读到的都是1,然后++完了,你以为最后应该是3了,其实都是2。

有人在会在网上说什么volatile是轻量级的锁,这是不对的,他并不能保证原子性,原子性只能通过加锁去解决,比如synchronize、lock,锁住变量,只能自己访问,操作串行化,保证多个操作之间的原子性。

有序性

有序性,就是指令重排序的问题,编译器和指令器,有时候会对代码进行优化,在前后逻辑不影响的情况下,他可能会优化代码的执行顺序,比如下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
flag = false;

//线程1
prepare();//准备资源
flag=true;
//线程1

//线程2
while(!flag){
Thread.sleep(1000);
}
execute();// 基于线程1准备的资源进行操作

那么重排序之后,有可能flag=true,就先执行了,可能就会导致线程2的代码, 执行异常。

基于happens-before保证有序性

指令重排序,不是乱排的,有个happens-before原则,只要符合happens-before原则的情况,就不能乱排。

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

满足这8个原则的情况下,才能对指令进行重排序。