java 【多线程基础 一】线程概念

一个进程里可以有一个线程,也可以有多个线程

什么是进程

进程是操作系统对⼀个正在运行的程序的⼀种抽象,换言之,可以把进程看做程序的⼀次运行过程; 同时,在操作系统内部,进程又是操作系统进行资源分配的基本单位。

进程就是正在执行的应用程序

应用程序有两种状态

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对象,仍然存在

后台线程

某个线程在执行过程中,不能阻止进程结束(虽然线程还在执行,但进程要结束了,此时这个线程会随着进程的结束而结束),这样的线程就是"后台线程".

前台线程

某个线程在执行过程中,能够阻止进程结束,这样的线程就是"前台线程".

线程的核心操作

  1. 创建线程 start

start 和 run之间的区别

start:调用系统函数,在系统内核中,创建线程,创建好的线程再来单独执行run。(此处的 start,会根据不同的操作系统,来分别调用不同的api)

run:描述线程要执行的任务,也可以称为"线程的入口"

调用 start ,不一定是main线程调用。任何线程都可以创建其他线程,如果系统资源充裕,可以任意创建线程。

一个Thread对象,只能调用一次 start,如果多次调用,会报以下异常

(一个Thread对象,只能对应系统中的一个线程)

  1. 线程的中断

|----------------------------------|---------------------------------|
| 方法 | 说明 |
| 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;

    
}
}
  1. 使用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 对象存在,但线程已经结束

  1. 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时,阻塞.

  1. 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自身的特性,那么我们只能从后两个条件入手。

针对第三个条件,我们可以采用避免锁嵌套的方法来避免,但一些特殊场景下,必须要多重加锁,因此避免锁嵌套的方法,也不是最好的解决方法。

针对第四个条件,我们可以采用给锁编号,并约定加锁顺序的形式来解决。

内存可见性

线程安全问题产生的原因多种多样,其中之一的原因就是内存可见性。

针对一个变量,一个线程修改,一个线程读取

相关推荐
繁依Fanyi18 小时前
从初识到实战 | OpenTeleDB 安装迁移使用指南
开发语言·数据库·python
小罗和阿泽18 小时前
java [多线程基础 二】
java·开发语言·jvm
悟空码字18 小时前
SpringBoot整合Zookeeper,实现分布式集群部署
java·zookeeper·springboot·编程技术·后端开发
橘颂TA18 小时前
线程池与线程安全:后端开发的 “性能 + 安全” 双维实践
java·开发语言·安全
bruce_哈哈哈18 小时前
go语言初认识
开发语言·后端·golang
色空大师18 小时前
服务打包包名设置
java·elasticsearch·maven·打包
xiaoyustudiowww18 小时前
fetch异步简单版本(Tomcat 9)
java·前端·tomcat
十五年专注C++开发18 小时前
VS2019编译的C++程序,在win10正常运行,在win7上Debug正常运行,Release运行报错0xC0000005,进不了main函数
开发语言·c++·报错c0x0000005