【JavaEE】线程入门:线程基础 + 安全机制一次讲透

🎁个人主页:User_芊芊君子

🎉欢迎大家点赞👍评论📝收藏⭐文章

🔍系列专栏:JavaEE

文章目录:

前言

Java 多线程是后端开发和面试的高频重点,很多同学刚接触时,会被线程概念、创建方式、线程安全这些知识点绕晕。其实从 "什么是线程" 到 "怎么保证线程安全",是一条完整的学习路径。今天就用直白的方式,把线程基础和核心安全机制梳理清楚,帮你快速入门多线程开发。

一、认识线程

1.什么是线程

一个线程就是一个"执行流"。一个方法中包含很多指令,执行流会执行这些指令,此时,这个执行流就可以放到一个cpu核心上执行,多个执行流之间,就会采用"并行""并发"的方式执行。

2.线程和进程的区别

  • 线程比进程更轻:
  • 创建线程的开销比创建进程小很多;
  • 销毁线程的开销比销毁进程小很多;

同时,使用多线程也能解决并发编程的问题,so,使用多线程就可以替代多进程的模型。

3.线程与进程的关系

  • 进程包含线程;
  • 线程是资源分配的基本单位,进程是调度执行的基本单位;
  • 每个进程都有自己独立的资源,一个进程的多个线程之间共用一份资源;
  • 进程与线程之间是隔离的,一个进程出问题,不影响别的进程。同一个进程的线程之间是共享资源的,好处是节省线程的创建和销毁所消耗的资源,坏处是容易冲突,一个线程出问题,容易把别的线程带走。

二、Thread类及常见方法

Java对操作系统的各种API进行了封装,Java的标准库,就提供了Thread类,封装了多线程的相关操作。

  • 使用jconsole命令观察线程
    先找到你jdk的路径


1.创建线程

方法一:继承Thread类

java 复制代码
class MyThread extends Thread{
    //重写run方法,run代表进程的入口,自动调用
    @Override
    public void run(){
        while(true){
            System.out.println("hello thread");
        }
    }
}

创建MyThread类的实例

java 复制代码
MyThread myThread = new MyThread();

调用start方法启动线程

java 复制代码
myThread.start();//调用操作系统的API,执行到创建线程逻辑,--》run

【注意】run 方法中的sleep只能try catch,不能throws,因为父类run()没有抛出异常,子类不能抛出父类没有的异常或者比父类多

方法二:实现Runnable接口

Java不支持多继承,所有需要引入接口

实现Runnable接⼝

java 复制代码
class MyRunnable implements Runnable{
    @Override
    public void run(){
        while(true){
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
java 复制代码
MyRunnable runnable = new MyRunnable();
        Thread t = new Thread(runnable);
java 复制代码
t.start();

方法三:继承Thread,重写run,使用匿名内部类(一次性)

java 复制代码
public static void main(String[] args) {
        //创建Thread的匿名子类
        Thread t = new Thread(){
            @Override
            public void run(){
                while(true){
                    System.out.println("hello thread");
                    try {
                        sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }

方法四:使用匿名内部类基于Runnable

java 复制代码
public static void main(String[] args) throws InterruptedException {
        //创建Runnable的子类
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        //定义子类的实例
        Thread t = new Thread(runnable);
        t.start();
        while (true){
            System.out.println("hello world");
            Thread.sleep(1000);
        }
    }

方法五:基于lambda表达式(最推荐的写法)

lambda表达式本质上是匿名函数,表示一个一次性的使用逻辑

java 复制代码
public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() ->{
        while (true){
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        });
        t.start();
        while (true){
            System.out.println("hello world");
            Thread.sleep(1000);
        }
    }

2.Thread其他用法

2.1 Thread常见构造方法

方法 说明
Thread() 创建线程对象
Thread(Runnable target) 使用Runnable对象创建线程对象
Thread(String name) 创建线程对象,并命名
Thread(Runnable target, String name) 使用Runnable对象创建线程对象,并命名
Thread(ThreadGroup group, Runnable target) 线程可以被用来分组管理,分好的组即为线程组

2.2 Thread常见属性

属性 获取方法
ID getId()
名称 getName()
状态 getState()
优先级 getPriority()
是否后台线程 isDaemon()
是否存活 isAlive()
是否被中断 isInterrupted()

(1)代码中创建的线程默认是前台线程,包括main线程,JVM自带的线程是后台线程

  • 前台线程:这个线程如果没执行完,此时进程不会结束,后台线程会阻止进程结束;
  • 后台线程:这个线程执行完或者没完,都不会影响进程的结束,后台线程不会阻止进程结束

(2)不存活:未开始执行/执行结束

  • 描述操作系统内核里是不是有这个线程。
  • 操作系统的线程执行完毕,不代表Thread对象销毁。Thread对象的生命周期可以比系统内部的线程更长

2.3 启动一个线程-start()

重写run方法创建一个线程后,并没有真正的运行,只有调用start()方法之后,才真正在操作系统底层创建出一个线程。(一个Thread对象不能被调用多次)

2.4 中断/终止线程

Java中,只能通过使线程的入口方法尽快return来让线程结束,没有强制终止的操作。(操作系统原生的api可以强制终止)

(1)使用标志位来实现
java 复制代码
private static boolean running = true;

    public static void main(String[] args) {
        Thread t = new Thread(()->{
            while (running){
                System.out.println("hello thread");
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("thread 结束");
        });

        t.start();
        Scanner scanner = new Scanner(System.in);
        System.out.println("输入任意内容,控制t线程结束");
        scanner.next();
        running = false;
    }

【注意】

lambad + 局部变量 = 不能修改

java 复制代码
private static boolean running = true;

so:这里running的定义不能在main里面,这就属于局部变量了

(2)使用 Interrupt方法
  • 可以唤醒阻塞方法;
  • 如果线程没有执行到阻塞操作,还可以设置内置标志位,进行线程结束的判定
java 复制代码
public static void main(String[] args) {
        boolean running = true;
        Thread t = new Thread(()->{
            //通过isInterrupted()判定线程是否被终止
            //此处不能直接使用t,因为t的初始化在最后执行,这个时候不能使用
            //使用Thread.currentThread()来替代
            while (!Thread.currentThread().isInterrupted()){
                System.out.println("hello thread");
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    //throw new RuntimeException(e);
                    //1.立即终止
                    //break;
                    //2.稍后处理
                    //先执行其他操作,然后break;
                    //3.不处理,忽略终止请求
                    continue;

                }
            }
            System.out.println("线程t终止");
        });
        t.start();
        System.out.println("输入任意内容,终止t线程");
        Scanner scanner = new Scanner(System.in);
        //终止t线程
        t.interrupt();
    }

2.5 线程等待

等待线程结束。因为线程的调度顺序是随机的,所以需要一些方法来对线程的顺序产生干预。(后结束的线程等待先结束的线程)

java 复制代码
private static int result = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            //在这个线程中从1加到1000,主线程打印结果
            int sum = 0;
            for (int i = 0;i<=1000;i++){
                sum+=i;
            }
            result = sum;
        });
        t.start();
        t.join();
        System.out.println(result);
    }
  • 由于main和t线程是并发执行,很可能main执行打印的时候t还没执行完毕。
  • sleep的话不好控制时间,定义太长造成浪费。若使用while还会使main被反复调度。
  • t.join() : 在main代码中调用的,就是main等待t执行完。也可以放在t中,就变成t等待main.

方法 说明
public void interrupt() 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位
public static boolean interrupted() 判断当前线程的中断标志位是否设置,调用后清除标志位
public boolean isInterrupted() 判断对象关联的线程的标志位是否设置,调用后不清除标志位

无参数则为"死等",一般使用带超时时间的版本,达到时间上限就不等了

2.6 获取线程引用

返回当前线程的引用

java 复制代码
public static Thread currentThread();

2.7 休眠线程

  • sleep核心是让线程阻塞(不参与调度),操作系统会计时,在这个时间内,将PCB从就绪队列放入阻塞队列,结束后放回去。
  • sleep(0):不休眠,但主动放弃了cpu的执行权
  • 获取到系统的时间戳(毫秒)
java 复制代码
public static void main(String[] args) throws InterruptedException {
        long beg = System.currentTimeMillis();
        Thread.sleep(1000);
        long end = System.currentTimeMillis();
        System.out.println(end-beg);
    }

三、线程的状态

状态 通俗解释 核心场景
NEW 安排了工作,还未开始行动 线程对象已创建,未调用start()
RUNNABLE 可工作的,分为正在工作中、即将开始工作 包含操作系统运行中就绪等待CPU调度两种情况
BLOCKED 排队等着其他事情 等待获取synchronized锁,阻塞
WAITING 排队等着其他事情 无限等待,如wait()join(),需手动唤醒
TIMED_WAITING 排队等着其他事情 限时等待,如sleep()wait(超时),时间到自动唤醒
TERMINATED 工作完成了 Thread对象还在,但系统中的线程已经销毁

java 复制代码
public static void main(String[] args) {
        Thread t = new Thread(()->{
            while (true){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        System.out.println(t.getState());
        t.start();
    }

四、线程安全

有些代码在单线程环境下执行没有问题,但在多线程环境下就会有bug。这就是线程不安全。

4.1 线程安全产生原因

1.【根本】线程调度是随机的,一个线程执行到任何一个指令都可能被从cpu上调度走;

2.多个线程同时修改同一个变量;解决:

  • 一个线程先操作,另一个后操作;
  • 多个线程同时读取这个变量,不操作;
  • 多个线程同时修改不同变量

3.【直接】针对变量的修改操作不是原子:count++有三个指令

eg:count++对应三个cpu指令

load:把内存中的数据加载到cpu寄存器里;

add:把cpu寄存器中的值+1;

save:把寄存器的值写回内存

线程的调度是随机的,一个线程在cpu上执行,随时可能被从cpu上调走,比如线程一执行到add时(但也是会执行完add这个指令,不会执行一半被调走),被线程二再进行操作,这样就存在了bug,如下图,本来应该是自增两次为2,结果却还是1

t1回到cpu时会恢复上下文(存档),切换出cpu时,把所有寄存器的值都保存到了PCB的内存中

java 复制代码
private static int count = 0;
    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 = "+count);
    }

结果应该是100000,实际却不一样。

4.2 加锁

  • 通过锁,一个变量只能被一个线程修改,这个过程中,其他线程只能等。

  • 锁不是让线程停止调度,加锁过程中,线程仍然可以被调度出cpu。而是只要持有锁,别人就不能用。

  • Java中,通过synchronized关键字,搭配代码块的方式实现的。

java 复制代码
synchronized(锁对象){
   count++;
}
  • 锁对象可以是任何对象(String,HashMap,Object...)
  • 锁对象的用途:用来判定多个线程之间是否存在"锁竞争"(两个线程针对同一个对象加锁,此时,一个线程先拿到锁,另一个线程只能阻塞等待,等待锁被释放)
java 复制代码
private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        //创建两个线程,针对同一个变量进行循环自增
        //定义一个锁对象,任意
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            for (int i = 0;i<50000;i++){
                synchronized (locker){
                    count++;
                }
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0;i<50000;i++){
                synchronized (locker){
                    count++;
                }
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = "+count);
    }
  • t1线程lock之后,t2lock时,锁就已经被占用,此时lock就会产生阻塞等待,此时线程进入BLOCKED状态,直到另一个线程解锁;
  • t1解锁之后,操作系统才会告诉t2锁空闲了,t2就被唤醒,可以参与调度了,调度到cpu上就可以拿到锁了,然后继续t2操作;
  • 通过锁把load,add,save变成了"串行执行",安全问题解决,但会影响性能

4.3 可重入锁

一个线程针对一把锁加两次或者多次,不会产生死锁,如下代码:打印在第二个锁中,但仍能打印出来。

java 复制代码
public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker){
                synchronized (locker){
                    System.out.println("thread");
                }
            }
        });
        t1.start();
    }
  • 可重入锁的判定逻辑:

记录哪个线程持有了这把锁,当收到加锁请求时,判定加锁的线程是不是已经持有了锁的线程,如果是,则无事发生,如果不是,产生锁竞争,产生阻塞

4.4 死锁的场景

  • 一个线程,一把锁;对锁加锁两次。(可重入锁已解决);
  • 两个线程,两把锁:

(1)线程1对锁A加锁,同时线程2对锁B加锁;

(1)线程1再对锁B加锁,线程2再对锁A加锁

  • M个线程,N把锁

4.5 死锁产生原因

  • 锁是"互斥"的;
  • 锁不可被抢占(线程1先获取到锁,线程2只能阻塞等待) ;
  • 保持再请求(一个线程,获取到锁A的情况下再去获取锁B);
  • 循环等待(多个线程等待锁的顺序出现循环)如下代码:

t1要想获取到lockerB,就需要等待t2释放lockerB;t2要想获取到lockerA,就需要等待t1释放lockerA。这样就进入了永久阻塞

java 复制代码
public static void main(String[] args) {
        Object lockerA = new Object();
        Object lockerB = new Object();

        Thread t1 = new Thread(()->{
            synchronized (lockerA){
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lockerB){
                    System.out.println("t1");
                }
            }
        });


        Thread t2 = new Thread(()->{
            synchronized (lockerB){
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lockerA){
                    System.out.println("t2");
                }
            }
        });
        t1.start();
        t2.start();
    }
  • 如何解决?

只要约定好多个线程按照固定的顺序加锁,就可以避免等待(比如:对锁进行编号,按照从小到大或从大到小的顺序)

4.6 Java标准库中的线程安全类

下面这些类是线程不安全的,可能涉及到多线程修改共享数据,又没有任何加锁措施

  • ArrayList;
  • LinkedList;
  • HashMap;
  • TreeMap;
  • HashSet;
  • TreeSet;
    = StringBuilder

下面的是线程安全的,使用一些锁机制来控制.核心方法中都有synchronized

  • Vector;
  • HashTable;
  • ConcurrentHashMap;
  • StringBuffer

4.7 内存可见性问题----volatile

(1)什么是内存可见性问题

编译器会自动对代码进行优化,使效率变高。如下代码,线程1在读取,线程2在修改。多线程环境中,会产生误判导致优化后的逻辑不一样。(大部分情况下逻辑都不会变化,多线程就有可能)

java 复制代码
private static boolean flag = true;
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            while (flag){
                //无操作
            }
            System.out.println("t1结束");
        });

        Thread t2 = new Thread(()->{
            //用户输入修改flag
            System.out.println("输入任意内容,终止线程t1");
            Scanner scanner = new Scanner(System.in);
            scanner.next();
            //通过修改flag让t1结束
            flag = false;
            System.out.println("flag = "+flag);
        });

        t1.start();
        t2.start();
    }
  • 线程1并没有结束,这就产生了bug;
  • 在线程1的while循环中,会对load(读取内存)进行优化,本来是读内存,改成了读cpu寄存器/cpu缓存。
  • 优化的原因:load读取内存,开销很大;循环中每次load的结果一样;线程1中没有修改操作

(2)volatile

给flag变量加上volatile修饰,编译器就会避免进行优化了。

java 复制代码
private  static volatile boolean flag = true;

4.8 wait/notify

线程的调度是随机的。我们就可以使用 wait / notify(等待 / 通知)来控制谁先执行,谁后执行,这两个方法都属于Object类

(1)wait

java 复制代码
Object object = new Object();
        synchronized (object){
            System.out.println("wait 之前");
            object.wait();//阻塞
            System.out.println("wait 之后");
        }

【注意事项:】

  • 先释放锁才能wait,所有就得先加上锁;
  • 加锁的锁对象和wait的对象必须一致;

(2)notify

notify也得搭配锁使用

java 复制代码
public static void main(String[] args) {
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker){
                System.out.println("t1 wait 之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("t1 wait 之后");
        });

        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入任意内容唤醒锁");
            scanner.next();
            synchronized (locker) {
                locker.notify();
            }
        });

        t1.start();
        t2.start();
    }

wait的好处: 避免"线程饿死"。

  • eg:线程1释放锁后,所有线程(包括线程1)开始拿到锁,并无顺序,可能每次都是线程1拿到,那其他的线程就会饿死。
  • wait 中,释放锁后,会先阻塞等待。(这两步是"原子操作"),notify之后,接触阻塞,重新获取锁,继续执行。
  • 如果拆分前两步,会产生bug

4.9 wait和sleep的区别(经典面试题)

(1)

  • wait 提供两个版本:死等和带有超时时间的;
  • sleep只有带时间版本

(2)

  • wait 可以通过notify唤醒,也可以通过Interrupt提前终止;
  • sleep 只能通过Interrupt提前终止;

(3)

  • wait 阻塞时会释放锁(强制要求搭配 synchronized使用);
  • sleep和锁无关。

五、总结

简单来说,线程是进程内的执行单元,Java 提供了多种创建线程的方式,其中 Lambda 写法最简洁高效;而线程状态流转、加锁机制、volatile 关键字、wait/notify 这些,则是解决线程安全问题的核心手段。吃透这些基础,不仅能写对并发代码,也能轻松应对面试中的多线程高频问题。

相关推荐
2301_815645381 小时前
网络与安全
网络·安全
Geometry Fu1 小时前
《物联网安全》第9章 无线网络安全
物联网·安全·web安全
未若君雅裁1 小时前
JMM、volatile 与 CAS:并发安全三大问题
java·开发语言
hai3152475431 小时前
# 矩阵算法·算子对齐工具 v6.1 — 技术规格与使用手册
java·开发语言·驱动开发·神经网络·spring·目标检测·矩阵
一只鹿鹿鹿1 小时前
网络安全和安防建设方案(doc文件)
大数据·运维·网络·物联网·安全
宋浮檀s1 小时前
春秋云境——CVE-2022-25488
网络·安全·web安全
安协技术部门1 小时前
安全光栅的优缺点:漫反射式VS对射式
安全·制造
网安小学生(兼顾数据库版)1 小时前
从37万事故损失到零事故:Swisscom如何用ONEKEY筑牢固件安全防线
安全
QZ166560951592 小时前
2026年教育行业API安全解决方案综合排名:高性能、可追踪、全流程成为选型关键
网络·安全·web安全