这是深入浅出并发编程,详细讲解并发编程的方方面面,在以后Android面试中,遇到并发问题,不再担忧,同时这些知识,也适用于Java开发

第一章:线程基础

1.1什么是进程,线程,并发和并行,同步,异步?

进程:进程可以简单理解为一个应用,如QQ,微信,抖音,一般来说,一个应用就是一个进程,进程是资源分配的最小单位

线程:一个进程可以有多个线程,线程是最小调度单位,线程也可以理解为一串代码指令

并发:线程轮流使用cpu叫做并发

并行:在多核情况下,两个任务可能同时执行,叫做并行

1.2创建线程有几种方式?

两种方式:

  • 继承Thread
  • 使用Thread + Runnable

使用Thread是将线程和任务放在了一起,使用Runnable则将线程和任务分开,因为使用接口,比继承更加灵活

1
2
3
4
5
6
7
8
// 创建线程对象
Thread t = new Thread() {
public void run() {
// 要执行的任务
}
};
// 启动线程
t.start();
1
2
3
4
5
6
7
8
9
Runnable runnable = new Runnable() {
public void run(){
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();

另一种方式:FutureTask

1
2
3
4
5
6
7
8
 //方式三:使用FutureTask 可以返回结果
FutureTask futureTask = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 100;
}
});
new Thread(futureTask).start();

1.3线程常用的api方法?

方法名 说明 注意
start() 启动一个新线程 只能调用一次,调用多次,会抛出IllegalThreadStateException
run() 线程启动后会调用的方法
join() 插入线程 其他线程会等待该线程执行结束
join(long n) 插入线程,等待n 其他线程只等待n
getState() 获取线程状态 Java 中线程状态是用 6 个 enum 表示,分别为:NEW, RUNNABLE, BLOCKED, WAITING,TIMED_WAITING, TERMINATED
isInterrupted() 判断线程是否被打断 不会清除打断标记
interrupt() 打断线程 如果当前被打断的线程正处于阻塞状态,会抛出InterruptedException,并且清除打断标记(线程继续执行try catch后面的语句),如果是正在运行的线程,则会设置打断标记(但是不会打断线程的运行,只是做个标记)
interrupted() 判断线程是否被打断 同isInterrupted(),但是会清除打断标记
sleep(long n) 让当前线程休眠,让出时间片
yield 礼让线程,主动让出时间片 让出时间片给其他线程,然后自身进入可运行状态,后面再竞争时间片

面试题:start和run的区别?

回答:start方法会开启一个线程去执行run方法里面代码,run方法不会,仍然是在主线程去直接执行run方法而已

面试题:sleep和yield的区别?

回答:调用sleep后进入time waiting状态,时间到了后,重新竞争时间片,yield则是主动让出时间片,进入runnable状态,去竞争下一个时间片

线程优先级:这个就不讲了,不太重要的知识点,在cpu不忙的情况下有点用,cpu很忙的话,优先级没啥用处,调度器可能会忽略它

1.5守护线程

什么是守护线程呢?

守护线程:需要等其他非守护线程执行结束后,自身立刻结束,不管自身代码是否执行完毕

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class 守护线程 {

public static void main(String[] args) throws InterruptedException {

System.out.println("主线程开始执行");
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("守护线程工作中...");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("睡醒了,继续工作");
}
});
t1.setDaemon(true);
t1.start();

TimeUnit.SECONDS.sleep(2);
System.out.println("主线程执行结束");
}
}

如上代码中,当我们设置线程为守护线程,该线程有一些工作需要做,但是当主线程结束后,守护线程也会一起结束,不会执行任务了

1.6线程状态

从操作系统层面,有5种状态

  • 初始状态
  • 可运行状态
  • 运行状态
  • 阻塞状态
  • 终止状态

从Java层面,有6种状态

  • new 新建状态
  • Runnable(包括可运行,运行)
  • Block
  • Time Waiting
  • Waiting
  • terminated

第二章 线程安全

2.1Java内存模型

主要理解的是工作内存和主内存,工作内存是线程私有的,可以简单理解为栈,而共享内存呢,可以简单理解为堆。

主内存的共享变量,需要读取到工作内存的变量副本,然后送给CPU寄存器处理数据,正是由于多个线程同时处理同一个共享变量数据,而导致的线程安全问题

2.2并发编程三要素

也可以说是线程安全安全的三个问题

  • 原子性
  • 可见性
  • 有序性

原子性

指一个操作或多个操作要么全部执行,且执行的过程不会被任何因素打断,要么就都不执行,如:i++就不是原子性

实际案例:两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int counter = 0;

public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);

结果有正数,负数,零,接下来分析一下i++的JVM字节码指令:

1
2
3
4
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i

i–的JVM字节码指令:

1
2
3
4
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
isub // 自减
putstatic i // 将修改后的值存入静态变量i

在上面的代码中,问题就出在,因为不是原子性操作,当线程A的工作副本数据处理后,还没来得及写回主内存,时间片就用完,上下文切换,另一个线程B去主内存中读取共享变量(还是以前的值),处理完成后,写回主内存,然后上下文切换,线程A并不能感知到线程B修改了数据,即使说加了volatile,保证了可见性,但是没有保证原子性,这个时候,寄存器里处理好的中间变量,写回工作内存副本,再写回主内存共享变量,这个时候,就出错了!

解决原子性的办法:

  • 阻塞式解决方案:synchronized加锁 Lock
  • 非阻塞式的解决方案:原子变量

有一个概念:临界区

一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

我们加锁,就是对这个临界区进行加锁

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
/**
* synchronized 解决线程安全问题 原子性问题
*/
public class Test12 {

static int num = 0;

public static void main(String[] args) throws InterruptedException {

Thread t1 = new Thread(() -> {
for (int i = 0; i < 4000; i++) {
synchronized (Test12.class){
num++;
}
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 4000; i++) {
synchronized (Test12.class){
num--;
}
}
});

t1.start();
t2.start();
t1.join();
t2.join();


// TimeUnit.SECONDS.sleep(2);
System.out.println("结果:"+num);
}
}