目录
  1. 1. 聊聊并发
    1. 1.1. 什么是并发
    2. 1.2. 什么是并行
    3. 1.3. 并发与并行的区别
    4. 1.4. 多线程和多进程
    5. 1.5. 线程一定快吗?
  2. 2. 线程
    1. 2.1. 线程的实现
      1. 2.1.1. 继承Thread类
        1. 2.1.1.1. 线程初始化
        2. 2.1.1.2. daemon
        3. 2.1.1.3. 线程中断与销毁
      2. 2.1.2. 实现Runnable接口
      3. 2.1.3. 匿名内部类的方法
      4. 2.1.4. 带返回值的线程(并抛出异常)—实现Callable接口
      5. 2.1.5. 定时器
      6. 2.1.6. 使用线程池实现多线程
      7. 2.1.7. Spring实现多线程
      8. 2.1.8. 使用lambda表达式
    2. 2.2. 线程启动
    3. 2.3. 线程的状态
    4. 2.4. 线程优先级
      1. 2.4.1. 优先级的特性
        1. 2.4.1.1. 优先级的继承性
        2. 2.4.1.2. 优先级的规则性
        3. 2.4.1.3. 优先级的随机性
    5. 2.5. 线程中断
    6. 2.6. 线程调度
    7. 2.7. 安全的终止线程
    8. 2.8. 守护线程
      1. 2.8.1. 守护线程优先级
      2. 2.8.2. 守护线程示例
      3. 2.8.3. 为什么要用守护线程
Java并发编程基础

聊聊并发

什么是并发

  并发(Concurrent):是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行
  并发不是真正意义上的”同时进行”.只是CPU把一个时间段划分成几个时间片段(时间区间),然后在这几个时间区间之间来回切换,由于CPU处理的速度非常快,只要时间间隔处理得当,即可让用户感觉是多个应用程序同时在进行.如:打游戏和听音乐两件事情在同一个时间段内都是在同一台电脑上完成了从开始到结束的动作.那么就可以说听音乐和打游戏是并发的

什么是并行

  并行(Parallel):当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)

其实决定并行的因素不是CPU的数量,而是CPU的核心数量,比如一个CPU多个核也可以并行

并发与并行.jpg
所以:并发是在一段时间内宏观上多个程序同时运行,并行是在某一时刻,真正有多个程序在运行

并发与并行的区别

  • 并发:指的是多个事情,在同一时间段内同时发生了
  • 并行:指的是多个事情,在同一时间点上同时发生了
  • 并发的多个任务之间是互相抢占资源的
  • 并行的多个任务之间是不互相抢占资源的

只有在多CPU或者一个CPU多核的情况中,才会发生并行.否则看似同时发生的事情,其实都是并发执行的

多线程和多进程

  • 进程:运行中的程序,是资源分配的基本单位
  • 进程中包含多个线程,线程共享进程的资源
  • 线程是处理器调度的基本单位

线程一定快吗?

不一定,线程之间切换会浪费资源,远不如一个线程执行的快;多个线程为每个去执行;所以并发在百万以下时可以采用单线程,超过则采用多线程

线程

线程的实现

继承Thread类

线程初始化、中断以及源码讲解

线程初始化

例如无参构造:

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

  线程默认名称由Thread与nextThreadNum()从0开始++组成,也可由Thread构造方法初始指定
  构造初始化主要是init方法,具体如下:

1
2
3
4
5
6
7
8
/**
* ThreadGroup:是一种树状结构,底下可以放线程组也可以放线程
* Runnable:可以指定的线程任务
* stackSize:线程所需栈大小,未指定默认为0
*/
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
init(g, target, name, stackSize, null, true);
}

daemon

thread.setDaemon(true):设置守护线程
作用在程序中后台调用,做支持性工作,比如垃圾回收线程,扔在后台默默做些事
随着主线程的退出后默默在后台运行

线程中断与销毁

  thread1.stop(); — 很好用,但是已经过期了,使用这种方式在停止的时候线程所获取的锁及资源都没释放掉,只是让线程在无限期的等待下去,所以这种方式是非常不好的,不推荐使用
  thread1.interrupt(); — 推荐使用interrupt,但直接使用并没有让线程停止掉,这需要我们手动去处理,代码如下

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 ExtendsThread extends Thread {
public ExtendsThread(String name) {
super(name);
}

@Override
public void run() {
while (!interrupted()) {
System.out.println(getName()+"线程执行了");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
ExtendsThread thread1 = new ExtendsThread("first-thread");
ExtendsThread thread2 = new ExtendsThread("second-thread");
thread1.start();
thread2.start();

thread1.interrupt();// 会把中断标志改为是中断
}
}

实现Runnable接口

run方法分析:

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

target就是指定的Runnable接口的,所以就是我们实现的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @author Eric
* @create 2018 04 11 7:15
* 作为线程任务存在
*/
public class Runn implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("thread running ...");
}
}

public static void main(String[] args) {
Thread thread1 = new Thread(new Runn());
thread1.start();
}
}

初学阶段一般比较上述两种方式哪种比较好?
  好与坏没有绝对,而是根据业务需求来决定;但是可以在业务场景中去评论对应方式的好坏,java中推崇面向接口编程,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
26
27
28
--子类方式
new Thread() {
@Override
public void run() {
System.out.println("thread start ...");
}
}.start();

--构造方法方式
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread start ...");
}
}).start();

--两种都有执行子类的,子类run方法覆盖父类的,直接执行run
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("runnable start...");
}
}) {
@Override
public void run() {
System.out.println("sub start....");
}
}.start();

带返回值的线程(并抛出异常)—实现Callable接口

1
2
FutureTask implements RunnableFuture
RunnableFuture<V> extends Runnable, Future

即FutureTask是对线程任务的封装,可将thread4封装成一个FutureTask
将Task任务放到Thread中去执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Thread4 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("正在进行紧张的计算...");
Thread.sleep(3000);
return 1;
}

public static void main(String[] args) throws Exception {
Thread4 thread4 = new Thread4();
FutureTask<Integer> task = new FutureTask<>(thread4);
// task包装到Thread中去执行
Thread thread = new Thread(task);
thread.start();
System.out.println("我先干点别的");
// 取出返回结果
Integer result = task.get();
System.out.println("线程执行的结果为:"+result);
}
}

定时器

除了JDK给我们提供的Timer类外,还有第三方的定时任务的框架Quartz
利用Timer的schedule去定时执行任务,TimerTask也是继承Runnable接口实现run方法的线程任务

1
2
3
4
5
6
7
8
9
10
11
12
public class Thread5 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
// 实现定时任务
System.out.println("timertask is run");
}
}, 0, 1000);
}
}

实现起来比较简单,相对比较灵活,但是有一个非常大的问题是不可控,控制起来比较麻烦,当任务没有执行完毕或每次执行不同任务时没法对它定制化;如果是quartz框架则都帮我们实现好了

使用线程池实现多线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Thread6 {
public static void main(String[] args) {
//ExecutorService threadPool = Executors.newFixedThreadPool(10);
ExecutorService threadPool = Executors.newCachedThreadPool();
// 向线程池提交任务去运行
for (int i = 0; i < 10; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
threadPool.shutdown();
}
}

Spring实现多线程

Spring对并发的支持,Spring的异步任务

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
@Configuration
@ComponentScan("com.eric.thread.createthreadmode")
@EnableAsync
public class Config {
}
@Service
public class DemoService {
@Async
public void a() {
while (true) {
System.out.println("a");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

@Async
public void b() {
while (true) {
System.out.println("b");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
DemoService ds = ac.getBean(DemoService.class);
ds.a();
ds.b();
}
}

使用lambda表达式

1
2
3
4
5
6
7
8
9
10
11
12
public class Thread7 {
public int add(List<Integer> values) {
//values.parallelStream().forEach(System.out::println);//排序不一定
values.parallelStream().forEachOrdered(System.out::println);
return values.parallelStream().mapToInt(a -> a).sum();
}
public static void main(String[] args) {
List<Integer> values = Arrays.asList(10,20,30,40);
int result = new Thread7().add(values);
System.out.println(result);
}
}

线程启动

Thread的构造初始化方法:

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
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc,  boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}

this.name = name;
// 当前线程就是待创建线程的父线程
Thread parent = currentThread();

g.addUnstarted();

this.group = g;
// 将daemon、priority属性设置为父线程对应的属性
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 (inheritThreadLocals && parent.inheritableThreadLocals != null)
// 将父线程的inheritableThreadLocals复制过来
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

this.stackSize = stackSize;

/* 分配一个线程id */
tid = nextThreadID();
}

在上述init过程中,一个新构造的线程对象是由其parent线程来进行空间分配,而child线程继承了parent是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程.至此一个能够运行的线程对象初始化好了,在堆内存中等待着运行

在线程初始化完成后,调用start方法就可以启动这个线程,线程start()方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程.

注意:启动一个线程前,最好为这个线程设置线程名称,因为这样在使用jstack分析程序或进行问题排查时,方便问题定位

线程的状态

线程状态图.jpg

  • 新建:一个线程被创建还没有调用start()方法
  • 可运行Runnable:状态处于就绪队列,等待CPU调度
  • 运行Running:获取到系统时间片,被执行
  • wait:线程执行了没有timeout参数的wait()方法或者执行了没有timeout参数的join()或者LockSupport.park()方法,处于这种状态必须等待被其他线程唤醒,执行notify()/notifyall(),才会进入就绪状态
  • 限定wait:线程执行了有timeout参数的wait()方法,或者执行了有timeout参数的join()或者执行sleep()方法等,等时间一到自己进入就绪状态
  • 阻塞:处于阻塞状态的线程正在等待一个排它锁资源
  • 结束:run方法结束线程结束执行

线程优先级

  在操作系统中,线程可以划分优先级的,优先级较高的线程得到的CPU资源较多
  设置线程的优先级使用setPriority()方法
  在java中,线程的优先级可以分为1-10个等级,小于1或者大于10都会抛出java.lang.IllegalArgumentException异常,默认优先级为5

优先级的特性

优先级的继承性

  优先级是有继承性的,比如A线程启动B线程,则B线程的优先级和A线程的优先级是一样的
下面看一下设置优先级的例子:

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 ThreadPriorityDemo {
public static void main(String[] args) {
threadA threadA = new threadA();
threadA.start();
}

public static class threadA extends Thread {
@Override
public void run() {
super.run();
System.out.println("threadA 线程优先级为:" + this.getPriority());
threadB threadB = new threadB();
threadB.start();
}
}

public static class threadB extends Thread {
@Override
public void run() {
super.run();
System.out.println("threadB 线程优先级为:" + this.getPriority());

}
}
}

优先级的规则性

CPU将尽量把执行资源给优先级越高的线程,看一下优先级不一样的线程执行结果怎么样:

1
2


优先级的随机性

  这个就简单了,上面我介绍过优先级的规则性,优先级越高的线程最先执行完,但是这个规则性并不是绝对的,当两个线程的优先级比较接近时,谁也不知道哪个线程最先执行完,这也就是线程的随机性

线程中断

  中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作.中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的interrupt()方法对其进行中断操作
  线程通过检查自身的是否被中断来进行响应,线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位.如果该线程已经处于终结状态.即使该线程被中断过.在调用该线程对象的isInterrupted()时依旧会返回false
  从Java的API中可以看到,许多声明抛出InterruptedException的方法(例如Thread.sleep(long millis)方法)这些方法在抛出InterruptedException之前,Java虚拟机会先将该线程的中断标识位清除,然后抛出InterruptedException,此时调用isInterrupted()方法将会返回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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Interrupted {
public static void main(String[] args) throws InterruptedException {
// sleepThread不停的尝试睡眠
Thread sleepThread = new Thread(new SleepRunner(), "SleepThread");
sleepThread.setDaemon(true);
// busyThread不停的运行
Thread busyThread = new Thread(new BusyRunner(), "BusyThread");
busyThread.setDaemon(true);
sleepThread.start();
busyThread.start();
// 休眠5秒,让sleepThread和busyThread充分运行
TimeUnit.SECONDS.sleep(5);
sleepThread.interrupt();
busyThread.interrupt();
System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());
// 防止sleepThread和busyThread立刻退出
Thread.sleep(2000);
}

static class SleepRunner implements Runnable {
@Override
public void run() {
while (true) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

static class BusyRunner implements Runnable {
@Override
public void run() {
while (true) {
}
}
}
}

从结果可以看出:抛出InterruptedException的线程SleepThread,其中断标识位被清除了,而一直忙碌运作的线程BusyThread,中断标识位没有被清除

线程调度

假如我们的计算机只有一个CPU,那么CPU在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令.那么Java是如何对线程进行调用的呢?
线程有两种调度模型:

  • 分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片
  • 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些

  现在操作系统一般采用采用抢占式线程调度,抛弃了协同式线程调度,抢占式调度由操作系统控制线程占用CPU的时间,那么线程的执行时间是可控制的.虽然java的线程调度是系统自己完成的,我们可以通过设置线程的优先级来给系统一些”建议”.但是也不靠谱,因为java线程是要映射到操作系统的原生线程中的,我们知道在java中一共有10个等级的优先级,虽然操作系统也有优先级的概念但是不能一一对应,windows有六个优先级少于java,那么java线程的优先级映射到操作系统,必然要有优先级更改为相同的.而且操作系统在调度线程的时候也会根据”优先级推进器”来改变要调度的线程

所以java中我们不能根据优先级判断处于ready的线程会先执行哪一个

安全的终止线程

  而中断操作是一种简便的线程间交互方式,而这种交互方式最适合用来取消或停止任务.除了中断以外,还可以利用一个boolean变量来控制是否需要停止任务并终止该线程

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
public class Shutdown {
public static void main(String[] args) throws InterruptedException {
Runner one = new Runner();
Thread countThread = new Thread(one, "CountThread");
countThread.start();
// 睡眠1秒,main线程对CountThread进行中断,使CountThread能够感知中断而结束
TimeUnit.SECONDS.sleep(1);
countThread.interrupt();
Runner two = new Runner();
countThread = new Thread(two, "CountThread");
countThread.start();
// 睡眠1秒,main线程对Runner two进行取消,使CountThread能够感知on为false而结束
TimeUnit.SECONDS.sleep(1);
two.cancel();
}

private static class Runner implements Runnable {
private long i;
private volatile boolean on = true;
@Override
public void run() {
while (on && !Thread.currentThread().isInterrupted()){
i++;
}
System.out.println("Count i = " + i);
}
public void cancel() {
on = false;
}
}
}

main线程通过中断操作和cancel()方法均可使CountThread得以终止.这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是武断地将线程停止,因此这种终止线程的做法显得更加安全和优雅

守护线程

在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程);任何一个守护线程都是整个JVM中所有非守护线程的保姆

只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作.Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是GC(垃圾回收器),它就是一个很称职的守护者

User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了. 因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了

用户在编写程序时也可以自己设置守护线程.下面的方法就是用来设置守护线程的

1
2
3
4
5
6
Thread daemonTread = new Thread();  
// 设定daemonThread为守护线程,default false(非守护线程)
daemonThread.setDaemon(true);

// 验证当前线程是否为守护线程,返回true则为守护线程
daemonThread.isDaemon();

这里有几点需要注意:

  1. thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常.你不能把正在运行的常规线程设置为守护线程
  2. 在Daemon线程中产生的新线程也是Daemon的
  3. 不要认为所有的应用都可以分配给Daemon来进行服务,比如读写操作或者计算逻辑

    因为你不可能知道在所有的User完成之前,Daemon是否已经完成了预期的服务任务.一旦User退出了,可能大量数据还没有来得及读入或写出,计算任务也可能多次运行结果不一样.这对程序是毁灭性的.造成这个结果理由已经说过了:一旦所有User Thread离开了,虚拟机也就退出运行了

守护线程优先级

守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务
通过setDaemon(true)来设置线程为”守护线程”;将一个用户线程设置为守护线程的方式是在线程对象创建之前 用线程对象的setDaemon方法

守护线程示例

垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是
JVM上仅剩的线程时,垃圾回收线程会自动离开.它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源
生命周期:守护进程(Daemon)是运行在后台的一种特殊进程.它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件.也就是说守护线程不依赖于终端,但是依赖于系统,与系统”同生共死”.那Java的守护线程是什么样子的呢.当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;如果还有一个或以上的非守护线程则JVM不会退出

为什么要用守护线程

我们知道静态变量是ClassLoader级别的,如果Web应用程序停止,这些静态变量也会从JVM中清除.但是线程则是JVM级别的,如果你在Web应用中启动一个线程,这个线程的生命周期并不会和Web应用程序保持同步.也就是说,即使你停止了Web应用,这个线程依旧是活跃的.正是因为这个很隐晦的问题,所以很多有经验的开发者不太赞成在Web应用中私自启动线程

如果我们手工使用JDK Timer(Quartz的Scheduler),在Web容器启动时启动Timer,当Web容器关闭时,除非你手工关闭这个Timer,否则Timer中的任务还会继续运行!

Spring为JDK Timer和Quartz Scheduler所提供的TimerFactoryBean和SchedulerFactoryBean能够和Spring容器的生命周期关联,在 Spring容器启动时启动调度器,而在Spring容器关闭时,停止调度器.所以在Spring中通过这两个FactoryBean配置调度器,再从Spring IoC中获取调度器引用进行任务调度将不会出现这种Web容器关闭而任务依然运行的问题.而如果你在程序中直接使用Timer或Scheduler,如不进行额外的处理,将会出现这一问题

文章作者: Eric Liang
文章链接: https://ericql.github.io/2019/11/12/02-Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/01-%E5%BA%94%E7%94%A8%E7%AF%87/01-Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B%E5%9F%BA%E7%A1%80/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Eric Liang
打赏
  • 微信
  • 支付宝