一个进程里可以有一个线程,也可以有多个线程
什么是进程
进程是操作系统对⼀个正在运行的程序的⼀种抽象,换言之,可以把进程看做程序的⼀次运行过程; 同时,在操作系统内部,进程又是操作系统进行资源分配的基本单位。
进程就是正在执行的应用程序
应用程序有两种状态
1.没有运行时,是一个exe文件,存在于硬盘上
2.运行时,exe会被加载到内存中,并且cpu执行其中的指令

执行进程中的指令,需要硬件资源的

多进程编程虽然能够解决利用多核心编程的问题,但多进程编程每次创建进程/销毁编程,开销都比较严重(需要分配硬件资源,还有时间).
所以为了解决 进程 开销比较大的问题,就出现了"线程".
线程可以理解为轻量的进程,创建线程就是基于创建进程所申请的资源基础上,省去了"分配资源"的过程,也省去了"释放资源的过程"。
所以我们可以得到以下总结
线程是系统"调度执行"的基本单位
进程是系统"资源分配"的基本单位
多进程编程,是一个典型的并发编程
并发和并行
并行:从微观角度看,多个核心,每个核心都可以执行一个线程,这些核心之间的执行过程是"同时执行"
并发:一个核心,也可以按照"分时复用",来切换多个线程.从微观上看,多个线程是"一个接一个"执行。但由于调度速度很快,从宏观上,看起来就好像"同时执行"一样
举个例子
当你吃饭吃到一半时,电话突然响了
如果你吃完饭再去接电话,那么说明你既不并发也不并行
如果你停下吃饭,去接电话,那么说明你可以并发
如果你边吃饭,边接电话,那么说明你可以并行
多进程和多线程的区别
我们以读书为例子
如果我们要在一定的时间内看完50本书
为了提高效率,我们有两种方案,
方案一 牺牲成本,节约空间(增加房间)
方案二 节约成本,牺牲空间(增加人)
方案一(多进程)
创建一个新的进程,申请系统资源(分配房间,桌子,椅子等)

方案二(多线程)
同一个房间,同一个桌子,椅子。
这样看书的效率,和方案一一样也会大大增加看书的进度,但与方案一相比,方案二节约了大量的资源
在此处,线程是指人看书的过程,而不只是单纯增加人数就可以看做多线程。

但如果人数增加过多时

当人数增多后,就没有办法提升效率,反而会增加竞争资源,甚至有可能发生争抢(或掀桌,撕书现象),这样会大大降低效率。
从线程角度解释,当一个进程中有多个线程(超出CPU核心数)时,由于多个线程使用的是同一份内存资源,一旦发生冲突,就可能使程序出现问题. 如果问题处理不当,可能会使整个进程都崩溃,进而其他线程也会随之崩溃。
总结:
1.进程包括线程
一个进程中可以有多个线程,但最少有一个线程
2.进程是系统资源分配的基本单位
线程是系统调度执行的基本单位
3.同一个进程中的线程之间,共用同一份系统资源(内存,硬盘,网络带宽等....)
尤其是"内存资源",就是代码中定义的变量/对象...
编程中,多个线程,可以共用同一份变量.
4.线程是当下实现并发编程的主流方式,通过多线程,就可以充分利用好 多核CPU.
5.多个线程之间,可能会相互影响.线程安全问题,一个线程抛出异常,可能会使其他线程一起崩溃
6.多个线程之间,一般不会相互影响,一个进程崩溃了,不会影响到其他进程("进程的隔离性")
在Java代码中,编写多线程
线程,本身是操作系统提供的概念.
一共有五种方法
1.继承Thread,重写run方法,通过Thread实例 start 启动线程
java
class My_Thread extends Thread{
//创建的新线程需要执行的操作
@Override
public void run() {
while(true){
System.out.println("hello,Thread");
}
}
}
public class Demo12 {
public static void main(String[] args) {
My_Thread my_thread = new My_Thread();
//创建线程
my_thread.start();
while(true){
System.out.println("hello,main");
}
}
}
调用start会在进程内部,创建出一个新的线程,新的线程会自动执行run中的代码
注意:上述代码中,run方法,并没有主动的调用,但最终也执行了。
像run这种,主动定义,但没有主动调用,最终被调用的方法,叫做"回调函数".
由于while循环中打印的过于快速,我们可以使用sleep方法来减缓代码执行速度(sleep是Thread中的静态方法)
java
class MyThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println("Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class Demo1 {
public static void main(String[] args) {
MyThread t = new MyThread();
//创建线程
t.start();
while(true){
System.out.println("main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

每个进程中最少有一个线程,这个线程就是主线程(调用main方法产生).
调用start方法之后,新的线程产生,此时主线程和新线程各自并发/并发在CPU上执行.

多个线程之间,谁先在CPU上调度执行,此过程是"不确定的",这个调度顺序取决于操作系统的"调度器"。因此我们把这个过程叫做"抢占式执行".
我们可以借助jconsole,更直观的观察到线程的详细情况
注:jconsole存在于 本地电脑安装JDK的bin目录中

2.实现Runnable,重写run
java
class My_Runnable implements Runnable{
@Override
public void run() {
System.out.println("hello,Thread");
}
}
public class Demo13 {
public static void main(String[] args) {
My_Thread my_thread = new My_Thread();
Thread t = new Thread(my_thread);
t.start();
System.out.println("hello,main");
}
}
3.继承Thread,重写run,使用匿名内部类实现
本质和1)相同
java
public class Demo14 {
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
System.out.println("hello,Thread");
}
};
t.start();
System.out.println("hello,main");
}
}
4.实现 Runnable,重写,使用匿名内部类实现
本质和2)相同
java
public class Demo15 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello,Thread");
}
}
);
t.start();
System.out.println("hello,main");
}
}
5.基于 lambda 表达式,创建线程
java
public class Demo16 {
public static void main(String[] args) {
Thread t = new Thread(()->{
System.out.println("hello,Thread");
});
t.start();
System.out.println("hello,main");
}
}
Thread类
线程常见构造方法
|-------------------------------------|------------------------|
| 方法 | 说明 |
| Thread() | 创建线程对象 |
| Thread(Runnable target) | 使用Runnable 对象创建线程对象 |
| Thread(String name) | 创建线程对象,并命名 |
| Thread(Runnable target,String name) | 使用Runnable对象创建线程对象,并命名 |
其中第三、第四构造方法,可以给线程起名字。
如果不起名字默认Thread-0、Thread-1......

Thread 常见属性
|--------|---------------|
| 属性 | 获取方法 |
| ID | getId() |
| 名称 | getName() |
| 状态 | getState() |
| 优先级 | getPriority() |
| 是否后台进程 | isDaemon() |
| 是否存活 | isAlive() |
| 是否被中断 | isInterrupted |
ID式线程的唯一标识,不同线程不会重复
状态表示线程当前所处的情况(阻塞 / 就绪)
优先级高的线程理论上更容易被调度
关于后台线程,JVM会在一个进程的所有非后台线程结束后,才会结束运行.
是否存活,简单的理解是run方法是否运行结束了
代码中,创建的Thread对象的生命周期,和系统中实际线程的生命周期可能不同,可能会出现Thread对象仍然存在,但是内核中的线程不存在的情况
1)调用start方法之前,系统中,还没创建线程
2)线程的run执行完毕后,线程就结束了,但Thread对象,仍然存在
后台线程
某个线程在执行过程中,不能阻止进程结束(虽然线程还在执行,但进程要结束了,此时这个线程会随着进程的结束而结束),这样的线程就是"后台线程".
前台线程
某个线程在执行过程中,能够阻止进程结束,这样的线程就是"前台线程".
线程的核心操作
- 创建线程 start
start 和 run之间的区别
start:调用系统函数,在系统内核中,创建线程,创建好的线程再来单独执行run。(此处的 start,会根据不同的操作系统,来分别调用不同的api)
run:描述线程要执行的任务,也可以称为"线程的入口"
调用 start ,不一定是main线程调用。任何线程都可以创建其他线程,如果系统资源充裕,可以任意创建线程。
一个Thread对象,只能调用一次 start,如果多次调用,会报以下异常
(一个Thread对象,只能对应系统中的一个线程)

- 线程的中断
|----------------------------------|---------------------------------|
| 方法 | 说明 |
| public void interrupt() | 中断对象关联的线程,如果线程正在阻塞,则线程的中断状态将被设置 |
| public static void interrupted() | 判断当前线程是否中断。该方法可以清楚线程的中断状态 |
| piublic boolean isInterrupted() | 判断当前线程是否中。线程的中断状态不影响此方法的使用 |
1.自定义变量作为标志位
java
public class Demo17 {
private static boolean isQuit = false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(!isQuit) {
System.out.println("hello,Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("线程执行结束");
});
t.start();
Thread.sleep(2000);
System.out.println("main 线程尝试终止 t 线程");
isQuit = true;
}
}

- 使用Thread.Interrupted()或Thread.currentThread().isInterrupted()
Thread 内部包含了一个boolean 类型的变量作为线程是否被中断的标记
初始情况下,变量为false
但若有其他线程,调用interrupt方法,会设置上述标志位
java
public class Demo18 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
//获取线程的引用
Thread currentThread = Thread.currentThread();
while (!currentThread.isInterrupted()) {
System.out.println("hello,Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
Thread.sleep(3000);
//在主线程中,控制 t 线程终止,设置上述标志位
t.interrupt();
}
}
currentThread() 是 Thread 类的静态方法.调用此方法就能获取到调用此方法的线程实例(作用类似this)
但执行代码时,出现了以下异常
由于catch中 默认代码再次抛出异常,但再次抛出的异常,没有再次被catch,那么进程就直接异常终止

根本原因是 sleep/wait/join 等阻塞的方法被唤醒之后,会清空刚才设置的interrupted标志位,导致代码一直在循环
因此,要想结束循环,结束线程,需要在 catch 中加 return / break .
线程等待
操作系统,针对多个线程的执行,是一个"随机调度,抢占式执行"的过程。而线程等待,就是在确定两个线程的"结束顺序".
具体逻辑:让后结束的线程,等待先结束的线程即可,此时后结束的线程就会进入阻塞,一直到先结束的线程,真的结束了,阻塞才接触
假设现在有线程a,b
在a线程中调用 b.join 意思就是让a线程等待b线程,直到b线程结束,然后a再继续执行.
java
public class Demo19 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
for(int i = 0; i < 3 ; i++){
System.out.println("这是t线程");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("t线程结束");
});
t.start();
System.out.println("主线程开始等待");
//main线程开始等待 t线程
try {
t.join();
}catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("主线程等待结束");
}
}


任何线程之间都是可以互相等待的,线程等待不止可以存在于两个线程之间,也可以同时等待多个其他线程,或若干线程之间互相等待.
创建t1,t2线程,并让t2等待t1,主线程等待t1、t2
java
public class Demo20 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for(int i = 0; i < 5; i++ ){
System.out.println("这是t1线程");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("t1线程执行结束");
});
Thread t2 = new Thread(()->{
for(int i = 0; i < 3; i++){
System.out.println("这是t2线程");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
try {
//t2线程 等待 t1线程
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("t2线程结束");
});
t1.start();
t2.start();
System.out.println("main线程开始等待");
t1.join();
t2.join();
System.out.println("main线程等待结束");
}
}
我们来观察以下代码,先不考虑t1.join(),那么在t1,t2线程同时创建,同时执行逻辑时,那么应该t2线程先结束。因为 t1 要打印5次,而 t2 只需打印3次

t1,t2线程创建之后,和主线程一起同时执行逻辑,但由于主线程中的 t1.join,t2.join ,那么主线程先等待t1,t2线程执行逻辑。然后当 t2 打印三次之后又遇到了 t1.join,那么t2 开始等待 t1。所以出现了 t1线程执行结束,t2 线程结束,main线程结束的顺序。

其他线程等待
|-----------------------------------------|-----------------------|
| 方法 | 说明 |
| public void join() | 等待线程结束 |
| public void join(long millis) | 等待线程结束,最多等待 millis 毫秒 |
| public void join(long millis,int nanos) | 更高精度,可以具体到纳秒 |
获取当前线程引用
|--------------------------------------|-------------|
| 方法 | 说明 |
| public static Thread currentThread() | 返回当前线程对象的引用 |
java
public class Demo21 {
public static void main(String[] args) {
Thread t = Thread.currentThread();
System.out.println(t.getName());
}
}
这也说明了,main 主线程的存在

线程状态
既然说到线程状态,那么我们不得不说一下它的"爸爸"进程的进程状态
进程状态:
就绪:正在 cpu 上执行,或者随时可以去 cpu 上执行
阻塞:暂时不能参与 cpu 执行
线程的六种状态
1. NEW
当前 Thread 对象存在,但没有分配线程(即还没调用start)
2. TERMINATED
当前 Thread 对象存在,但线程已经结束
- RUNNABLE
就绪状态:正在 cpu 上运行 或 随时可以去 cpu 上执行
4. BLOCKED
因为 锁 竞争,引起的阻塞
5. TIMED_WAITING
有时间限制的线程等待
6. WAITING
没有时间的线程等待
线程安全问题
因为多个线程并发执行,引起的bug,这样的bug被称为"线程安全问题"或"线程不安全"
如果还不是很清楚,没问题,请看下面的代码
java
public class Demo22 {
private static int count;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for(int i = 0; i < 50000 ; i++){
count++;
}
});
Thread t2 = new Thread(()->{
for(int i = 0; i < 50000 ; i++){
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
在上面的代码中,t1,t2各自的逻辑中都count自增 50000次,因此正常情况下,结果应该输出为100000.

我们可以看到,程序执行了三次,三次的结果都不相同。此种结果与逻辑目标不同的情况就归为"线程安全问题".
但为什么会出现上述情况呢?
一条java语句不一定是原子的,也不一定只是一条指令
count++操作,虽然我们看来是一个语句,但其实在 cpu 视角来看,是3个指令
1)把内存中的数据,读取到 cpu 寄存器中 (load)
2)把 cpu 寄存器里的数据 + 1 (add)
3)把寄存器的值,写回内存 (save)
load,add,save 是cpu 中指令集的指令.不同架构的cpu有不同的指令集,在此只是为了方便介绍。
由于 cpu 调度执行线程时是抢占式执行,随机调度。说不定在执行某个指令时就会调走,因此count++ 是三个指令,可能会出现 cpu 执行了其中的一个指令或两个指令就调走的情况

但上述的执行顺序,只是一个可能的调度顺序.由于调度过程是"随机"的,因此会产生其他的执行顺序。


以上执行顺序都是没有保证原子性,导致此次结果不是100000的情况.
什么是原子性
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。 那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。

有时也把这个现象叫做同步互斥,表示操作是互相排斥的。
那么如何解决上面的线程不安全问题呢
java
public class Demo22 {
private static int count;
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(()->{
for(int i = 0; i < 50000 ; i++){
//添加synchronized 关键字,给count++操作 上"锁"
synchronized (locker){
count++;
}
}
});
Thread t2 = new Thread(()->{
for(int i = 0; i < 50000 ; i++){
//添加synchronized 关键字,给count++操作 上"锁"
synchronized (locker){
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}

具体内容逻辑:

由于,t1 和 t2 都是针对 locker 对象加锁。t1 先加锁,于是 t1 就继续执行{ }中的代码,t2 后加锁,发现locker对象已经被加锁了,于是 t2 只能排队等待。

synchronized关键字
synchronized的特性
1)互斥
synchronized 会起到互斥效果,某个线程执行到某个对象的 synchronized 中时,其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
进入 synchronized 修饰的代码块,相当于加锁
退出 synchronized 修饰的代码块,相当于解锁

2)可重入
synchronized 代码块对应同一条线程来说是可重入的,不会出现死锁的情况;

加锁的时候,需要判定当前这个锁,是否是 被占用状态。
java在synchronized中引入计数器,记录该线程加锁几次,后续解锁时,可以在正确位置进行解锁。
可重入锁,就是在锁中,记录当前是哪个线程持有锁,后续加锁时都会进行判定
死锁:
1)一个线程没有释放锁,然后又尝试再次加锁

synchronized 不会 出现上述情况,只是借此例子解释死锁情况。
2)两个线程,两把锁
线程1 线程2 锁A 锁B
1)线程1 先针对 A 加锁,线程2 针对 B 加锁
2)线程1 不释放锁A的情况下,再对 B 加锁. 同时,线程2 不释放 B 的情况下对A 加锁
java
public class Demo24 {
private static Object locker1 = new Object();
private static Object locker2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
synchronized (locker1){
System.out.println("t1 加锁 locker1成功");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2){
System.out.println("t1 加锁 locker2成功");
}
}
});
Thread t2 = new Thread(()->{
synchronized (locker2){
System.out.println("t2 加锁 locker2 成功");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker1){
System.out.println("t1 加锁 locker1 成功");
}
}
});
t1.start();
t2.start();
}
}
此时就只打印以下语句

我们在jconsole看一下线程具体情况


说明这两个线程 t1、t2 都在第二次 synchronized时,阻塞.
- N个线程,M个锁
哲学家就餐问题
synchronized 使用示例
1)修饰代码块:明确指定锁哪个对象
java
public class Demo23 {
private Object locker = new Object();
public void method(){
synchronized (locker){
}
}
}
锁当前对象
java
public class Demo23 {
public void method(){
synchronized (this){
}
}
}
2)直接修饰普通方法
相当于针对 this 加锁
java
public class Demo23 {
public synchronized void method(){
}
}

3)修饰静态方法
相当于针对 对应的类对象 加锁
java
public class Demo23 {
public synchronized static void method(){
}
}
理解锁对象的作用
可以把任意的 Object/Object 子类的对象,作为锁对象
锁对象是谁不重要,重要的是,两个线程的锁对象是否是同一个
是同一个,才会出现 阻塞 / 锁竞争
不是同一个,不会出现 阻塞 / 锁竞争
如何解决死锁问题?
那我们需要先知道死锁是如何产生的
死锁的四个必要条件
1.互斥的 [锁的基本特性]
2.不可抢占 [锁的基本特性]
3.请求和保持 [代码结构]
4.循环等待 [代码结构]
前两个条件我们很难解决,因为是synchronized自身的特性,那么我们只能从后两个条件入手。
针对第三个条件,我们可以采用避免锁嵌套的方法来避免,但一些特殊场景下,必须要多重加锁,因此避免锁嵌套的方法,也不是最好的解决方法。
针对第四个条件,我们可以采用给锁编号,并约定加锁顺序的形式来解决。
内存可见性
线程安全问题产生的原因多种多样,其中之一的原因就是内存可见性。
针对一个变量,一个线程修改,一个线程读取