Java并发编程01-并发基础
date: 2020-03-23 00:50:25
一、 基本概念
并发:一个处理器处理多个任务,逻辑上的同时发生。
并行:多个处理器同时处理多个任务,物理上的同时发生。
二、Java 线程基础
1. 线程
进程与线程
进程: 程序运行的一个实例。
线程:程序的执行单元,是程序使用 CPU 的基本单位。一个进程可以包含多个线程。
2. 线程实现/创建的方式
- 继承 Thread 类
- 实现 Runnable 接口
- ExecutorService、Callable、Future 有返回值线程
- 基于线程池的方式
3. 线程的生命周期
当线程启动后,不可能一直霸占着 CPU,CPU 需要在多个线程中来回切换,于是线程也会多次的在运行、阻塞状态之间切换。

1.新建(New)
当使用 new 关键字创建了一个线程后,该线程就处于新建状态。此时 JVM 为其分配内存,并初始化其成员变量。
2.就绪(Runnable)
当线程对象调用了 start() 方法后,该线程就处于就绪状态。JVM 为其创建方法调用栈和程序计数器,等待 CPU 调度执行。
3.运行(Running)
处于就绪状态的线程获得了 CPU,开始执行 run() 方法的线程执行体,则该线程就处于运行状态。
4.阻塞(Blocked)
线程因为某种原因,放弃了 CPU 的使用权,暂时停止运行,则该线程处于阻塞状态。
阻塞后的线程,需要进入就绪状态后,才有机会再次运行。
阻塞的三种情况:
-
等待阻塞(wait -> 等待队列)
运行的线程执行 o.wait() 方法时,JVM 会把该线程放入到等待队列(Waitting Queue)中。
-
同步阻塞(lock -> 锁池)
运行的线程在获取对象的同步锁时,若该锁正在被其它线程占用,则 JVM 会把该线程放入锁池(Lock Pool)中。
-
其它阻塞(sleep/join)
运行的线程执行了 Thread.sheep(long ms) / t.join() 方法时,或者发出 IO 请求时,JVM 会把该线程置于阻塞状态。 当 sleep() 状态超时、join() 等待线程终止或者超时、IO 处理完毕时,该阻塞线程重新转入就绪状态。
5.死亡(Dead)
线程执行结束后,就是死亡状态。
线程的结束方式:
-
正常结束
run() / call() 方法执行完成后,线程正常结束。
-
异常结束
线程抛出一个未捕获的 Exception 或者 Error。
-
调用 stop() 方法
调用 Thread.stop() 方法可以强行结束线程。 该方法不推荐使用,容易导致死锁。
-
使用退出标志推出线程
使用一个变量来控制循环。
最直接的方法就是设一个 boolean 类型的标志,并通过设置这个标志为 true 或 false 来控制 while 循环是否退出,代码示例:public class MyThread extends Thread { public volatile boolean exit = false; public void run() { while (!exit){ //do something } } }注意:使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。
-
Interrupt() 方法结束线程
-
线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获 InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。
-
线程未处于阻塞状态:使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。
代码示例:
public class MyThread extends Thread { public void run() { while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出 try{ Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出 }catch(InterruptedException e){ e.printStackTrace(); break;//捕获到异常之后,执行 break 跳出循环 } } } }
-
4. 线程控制
1.start() 与 run()
2.sleep() 与 wait()
3.interrupt() 与 stop()
4.线程加入(join)
等待该线程执行完毕
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程开始执行...");
Thread t = new Thread(() -> {
System.out.println("子线程开始执行...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
t.join();
System.out.println("主线程执行结束...");
}
执行结果一定为:
主线程开始执行...
子线程开始执行...
子线程执行结束...
主线程执行结束...
5.线程礼让(yield)
暂停当前正在执行的线程对象,并执行其他线程,让多个线程的执行更和谐,但是不能靠它保证一人一次。
程序示例:
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
Thread.yield();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
Thread.yield();
}
}).start();
}
6.等待/通知机制(wait、notify)
wait() 方法:该方法用来使得当前线程进入等待状态,直到接到通知或者被中断打断为止。在调用 wait() 方法之前,线程必须要获得该对象的对象级锁;换句话说就是该方法只能在同步方法或者同步块中调用,如果没有持有合适的锁的话,线程将会抛出异常 IllegalArgumentException。调用 wait() 方法之后,当前线程则释放锁。
notify() 方法:该方法用来唤醒处于等待状态获取对象锁的其他线程。如果有多个线程则线程规划器任意选出一个线程进行唤醒,使其去竞争获取对象锁,但线程并不会马上就释放该对象锁,wait() 所在的线程也不能马上获取该对象锁,要程序退出同步块或者同步方法之后,当前线程才会释放锁,wait() 所在的线程才可以获取该对象锁。
程序示例,通过notify()方法去唤醒一个休眠中的线程的执行:
public class N16_Wait_Notify {
private volatile int signal;
public void set(int signal) {
this.signal = signal;
}
public int get() {
return this.signal;
}
public static void main(String[] args) {
N16_Wait_Notify n16 = new N16_Wait_Notify();
// wait, notify 必须在同步代码块中,并且是锁的实例,不是当前线程的实例
new Thread(() -> {
System.out.println("线程A开始执行...");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (n16) {
System.out.println("线程A修改信号值为1...");
// n16.set(1);
n16.notify();
}
}).start();
new Thread(() -> {
// 这样写,虽然也能达到通知等待的效果,但是一直占用CPU资源,损耗性能
// 即使循环体使用sleep休眠,休眠时间不好控制,也不是优雅的方式
// while (n16.get() != 1) {
// }
synchronized (n16) {
// while (n16.get() != 1) {
try {
n16.wait(); // 阻塞当前
} catch (InterruptedException e) {
e.printStackTrace();
}
// }
System.out.println("线程B开始执行...");
}
}).start();
}
}
7.notify与notifyAll的区别
5. 守护线程(Daemon)
定义:又称“服务线程”、“后台线程”,为用户线程提供公共服务,在没有用户线程可服务时会自动离开。
生命周期:只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
优先级:低
设置:t.setDaemon(true)
注意:
- 在Daemon线程中产生的线程也是Daemon线程
- 必须在start()之前设置
6. 线程调度
线程调度
线程有两种调度模型:分时调度、抢占式调度。
Java 使用的是抢占式调度模型:优先让优先级高的线程使用 CPU,优先级高的线程获取 CPU 时间片也相对多一些。 如果线程的优先级相同,会随机选择一个。
注意:设置了优先级也只是提高了获得 CPU 的几率。并不能保证哪个线程在某个时刻一定会抢到。
线程优先级
Java 线程中,通过一个整型成员变量 priority 来控制优先级,优先级范围为 0~10,在线程构建时可以通过 setPriority(int n) 方法来修改优先级,默认优先级为 5。
7. 线程变量 ThreadLocal
ThreadLocal,即线程变量,是一个线程实例为 key,任意对象为 value 的 Map 存储结构。通过 get()/set() 方法来获取/设置值,通过实现 initialValue() 方法来赋初始值。
程序示例:
public class N17_ThreadLocal {
private ThreadLocal<Integer> count = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public Integer getNext() {
Integer value = count.get();
count.set(value + 1);
return value;
}
public static void main(String[] args) {
N17_ThreadLocal n = new N17_ThreadLocal();
new Thread(() -> {
while (true) {
System.out.println(Thread.currentThread().getName() + " " + n.getNext());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true) {
System.out.println(Thread.currentThread().getName() + " " + n.getNext());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
while (true) {
System.out.println(Thread.currentThread().getName() + " " + n.getNext());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
三、Java 内存模型
Java 内存模型(JMM),线程之间的通信由 JMM 控制。JMM 定义了线程和主内存之间的抽象关系:
线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。
本地内存是 JMM 的一个抽象概念,不真实存在。它覆盖了缓存、写缓冲区、寄存器以及其它硬件和编译器优化。
Java 内存模型的抽象结构示意如下图:

从上图看,如果线程 A 与线程 B 之间要通信的话,必须要经过以下步骤:
- 线程 A 把本地内存 A 中更新的共享变量刷新到主内存中;
- 线程 B 到主内存中去读取线程 A 之前已经更新过的共享变量
可见,JMM 通过控制主内存与每个线程的本地内存之间的交互,来为 Java 程序员提供内存可见性保证。
四、并发的优缺点
优点:
- 资源利用率高
- 程序响应更快
缺点:
- 安全性:多个线程变量共享会存在问题
- 活跃性:需要考虑死锁等问题
- 性能:线程过多导致 CPU 切换开销大,消耗性能。线程需要内存空间,增加内存消耗。

浙公网安备 33010602011771号