思考:我们在之前,学习的程序在没有跳转语句的前提下,都是由上至下依次执行,那现在想要设计一个程序,边打游戏边听歌,怎么设计?

要解决上述问题,需要使用多进程或者多线程来解决.


目录

一、什么是多线程

1、线程与进程

2、并发与并行

3、线程调度

二、如何创建&&启动线程

1、继承Thread类

2、实现Runnable接口

3、实现Callable接口

4、线程池-ThreadPoolExecutor

4.1、线程池的核心参数及作用

5、Executors 工具类

三、Thread类常用方法

四、线程安全问题

1、共享资源的冲突

2、问题演示

2.1、创建售票线程运行结果如下:

3、问题分析

4、问题解决-线程同步

4.1、同步代码块(synchronized)

4.2、同步方法(synchronized)

4.3、同步锁(ReentrantLock):

五、死锁

1、死锁演示

2、产生死锁的原因

3、如何避免死锁

六、线程通讯

1、为什么要线程通信

2、线程通讯方式

总结

线程创建四大方式

线程安全与同步

死锁与线程通信

核心要点


一、什么是多线程

1、线程与进程

程序 为了完成某个任务和功能,选择一种编程语言编写的一组指令的集合。
软件 一个或多个程序(可执行代码)​ + 相关的资源文件、配置文件、文档等,构成一个软件系统或软件产品。
进程 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
线程 线程是进程中的一个执行单元,是CPU调度的最小单位,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。

简而言之:一个软件中至少有一个应用程序,应用程序的一次运行就是一个进程,一个进程中至少有一个线程。进程是资源的容器,线程是执行的单元。

2、并发与并行

并发 指两个或多个事件在同一时刻发生(同时发生)。指在同一时刻,有多条指令在多个处理器上同时执行。
并行 指两个或多个事件在同一个时间段内发生。指在同一个时刻只能有一条指令执行,但多个进程的指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。

3、线程调度

分时调度 所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间
抢占式调度 优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度

二、如何创建&&启动线程

        当运行Java程序时,其实已经有一个线程了,那就是main线程。

        那么如何创建和启动main线程以外的线程呢?

1、继承Thread类

        Java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。

Java中通过继承Thread类来创建启动多线程的步骤如下:

  • 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
  • 创建Thread子类的实例,即创建了线程对象
  • 调用线程对象的start()方法来启动该线程

代码示例:

public class MyThread extends Thread{
    @Override
    public void run() {//线程的业务逻辑
        for (int i = 0; i<10; i++){
            System.out.println("mythread线程正在执行:"+new Date().getTime());
        }
    }
}
class ThreadTest {
    public static void main(String[] args) {
        //1.自定义线程打印
        new MyThread1().start();
        //2.主线程循环打印
        for (int i=0; i<10; i++){
            System.out.println("main主线程正在执行:"+new Date().getTime());
        }
    }
}

执行效果如下:

2、实现Runnable接口

        Java有单继承的限制,当我们无法继承Thread类时,那么该如何做呢?在核心类库中提供了Runnable接口,我们可以实现Runnable接口,重写run()方法,然后再通过Thread类的对象代理启动和执行我们的线程体run()方法

步骤如下:

  • 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  • 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  • 调用线程对象的start()方法来启动线程。代码如下:
public class MyThread2 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
           System.out.println("mythread2线程正在执行:"+ new Date().getTime());
        }
    }
}
class ThreadTest {
    public static void main(String[] args) {
        //1.自定义线程打印
        new Thread(new MyThread2()).start();
        //2.主线程循环打印
        for (int i=0; i<10; i++){
            System.out.println("main主线程正在执行:"+new Date().getTime());
        }
    }
}

执行效果如下:

        在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。

        实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的

tips:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。

3、实现Callable接口

        我们发现Runnable没有返回值,不支持泛型的返回值不可以抛出异常,我们可以通过 Callable 接口创建线程。在核心类库中提供了 java.util.concurrent.Callable接口,我们可以实现 Callable 接口,重写 call()方法,然后通过 FutureTask类包装,再由 Thread类的对象代理启动和执行我们的线程体 call()方法,并且能够获取线程执行的结果。

步骤如下:

  • 定义 Callable 接口的实现类,并重写该接口的 call()方法,该 call()方法的方法体同样是该线程的线程执行体,且该方法可以有返回值并可以抛出异常
  • 创建 Callable 实现类的实例,并使用 FutureTask类包装该实例(FutureTask实现了 RunnableFuture接口,而 RunnableFuture继承了 Runnable接口)。
  • FutureTask实例作为 Threadtarget来创建 Thread对象,该 Thread对象才是真正的线程对象。
  • 调用线程对象的start()方法来启动线程。代码如下:
public class MyThread3 implements Callable<String> {
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 100; i++) {
           System.out.println("mythread3线程正在执行:"+ new Date().getTime());
        }
        return "MyThread3的call()方法运行结束";
    }
}
class ThreadTest {
    public static void main(String[] args) {
        //1.自定义线程打印
        MyThread3 myThread3 = new MyThread3();
        //FutureTask是个帮助类,实现了Runable
        FutureTask<String> futureTask = new FutureTask<>(myThread3);
        new Thread(futureTask).start();
        //2.主线程循环打印
        for (int i=0; i<10; i++){
            System.out.println("main主线程正在执行:"+new Date().getTime());
        }
    }
}

代码效果如下:

4、线程池-ThreadPoolExecutor

        Java 有创建线程开销较大的问题,当我们需要频繁创建线程执行任务时,可以使用线程池来管理和复用线程。在核心类库中提供了 java.util.concurrent.ThreadPoolExecutor类,我们可以创建线程池实例,将任务(Runnable 或 Callable)提交给线程池,由线程池统一管理和调度线程的执行,提高资源利用率和系统响应速度。

步骤如下:

  • 定义线程池的参数,包括核心线程数、最大线程数、空闲线程存活时间、任务队列等,并使用这些参数创建 ThreadPoolExecutor 实例。
  • 创建任务(可以是 Runnable 或 Callable 接口的实现类),定义线程执行体。
  • 将任务提交给线程池执行,线程池会自动分配空闲线程或创建新线程来执行任务。
  • 对于 Callable 任务,可以通过返回的 Future 对象获取任务执行结果。
  • 任务执行完成后,根据实际情况关闭线程池,释放资源。
4.1、线程池的核心参数及作用
corePoolSize(核心线程数)
  • 通俗解释:线程池里长期 “在编” 的固定员工数量。
  • 只要线程池还在运行,这些核心线程即使闲着也不会被轻易辞退,随时准备接活。
  • 比如你开了一家外卖店,常年雇 2 个骑手,不管有没有订单,这 2 个人都一直在店里待命,这 2 就是 corePoolSize。
maximumPoolSize(最大线程数)
  • 通俗解释:店里最多能同时容纳的员工总数。
  • 当订单暴增,2 个核心骑手忙不过来时,你会临时雇一些兼职骑手,但总人数不能超过这个上限。
  • 比如最多只能雇 4 个人(2 个固定 + 2 个兼职),那 4 就是 maximumPoolSize。
 keepAliveTime + unit(空闲存活时间 + 时间单位)
  • 通俗解释:兼职骑手没活干时,能在店里待多久才被辞退。
  • 比如订单少了,兼职骑手闲下来了,你规定他们最多闲 30 秒,30 秒后还没新订单就让他们下班,这 30 秒就是 keepAliveTime,秒就是 unit。
  • 核心线程默认不会被辞退(除非设置了 allowCoreThreadTimeOut(true)),这个参数主要管非核心的兼职线程。
 workQueue(任务队列)
  • 通俗解释:顾客下单后,如果所有骑手都在忙,订单就先排队等着的 “订单队列”。
  • 比如 2 个核心骑手都在送单,新订单就先放进队列,等骑手回来再处理。
  • 如果队列也满了,才会去雇兼职骑手。
threadFactory(线程工厂)
  • 通俗解释:用来 “生产” 新员工(线程)的工具。
  • 你可以给每个新骑手起个统一格式的名字(比如 “外卖骑手 - 001”),或者设置优先级,这些都由工厂来定义。
handler(拒绝策略)
  • 通俗解释:当骑手全忙、订单队列也满了,新订单来了该怎么处理。
  • 比如:
  1. 直接拒单(AbortPolicy):告诉顾客 “忙不过来,你换别家吧”(抛异常)。
  2. 老板自己送(CallerRunsPolicy):老板亲自上阵送这单(提交任务的线程自己执行)。
  3. 扔掉最早的订单(DiscardOldestPolicy):把队列里最久的订单扔掉,接新的。
  4. 默默扔掉新订单(DiscardPolicy):啥也不说,直接把新订单丢了。

代码如下:

import java.util.concurrent.*;

public class ThreadPoolExecutorTest {
    public static void main(String[] args) {
        // 1. 定义线程池参数
        int corePoolSize = 2;          // 核心线程数(固定员工数)
        int maximumPoolSize = 4;       // 最大线程数(最多员工数)
        long keepAliveTime = 30;       // 非核心线程空闲30秒后销毁
        TimeUnit unit = TimeUnit.SECONDS;
        
        // 任务队列:容量为2的有界队列(最多排2个订单)
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);
        
        // 线程工厂:自定义线程名称(方便排查问题)
        ThreadFactory threadFactory = new ThreadFactory() {
            private int count = 1;
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r);
                thread.setName("外卖骑手-" + count++);
                return thread;
            }
        };
        
        // 拒绝策略:提交任务的线程自己执行(老板亲自送)
        RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();

        // 2. 创建线程池
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                threadFactory,
                handler
        );

        // 3. 提交7个任务(模拟7个订单)
        for (int i = 1; i <= 7; i++) {
            final int taskId = i;
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println(
Thread.currentThread().getName() + " 正在送订单" + taskId);
                        Thread.sleep(1000); // 模拟送单耗时1秒
                        System.out.println(
Thread.currentThread().getName() + " 完成订单" + taskId);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        // 4. 关闭线程池(等待所有任务完成)
        executor.shutdown();

效果如下:

5、Executors 工具类

        Executors是 JDK 提供的一个线程池工具类(位于java.util.concurrent包下),它封装了ThreadPoolExecutor的创建细节,提供了一系列静态工厂方法,让你可以快速创建各种类型的线程池,无需手动设置corePoolSize、workQueue等复杂参数。代码如下:

public class ThreadTest {
    public static void main(String[] args) {
        // 创建固定3个线程的线程池
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);

        // 提交5个任务
        for (int i = 1; i <= 5; i++) {
            int taskNum = i;
            fixedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(
                        Thread.currentThread().getName() + " 执行任务" + taskNum);
                    try {
                        Thread.sleep(1000); // 模拟任务耗时
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        fixedThreadPool.shutdown(); // 关闭线程池
    }
}

运行效果如下:

三、Thread类常用方法

方法

用途

start()

启动线程,并执行对象的run()方法

run()

线程在被调度时执行的操作

getName()

返回线程的名称

setName(String name)

设置该线程名称

static Thread

currentThread()

返回当前线程。在Thread子类中就是this,通常用于主线程和

Runnable实现类

static void yield()

释放当前cpu的执行权

join()

在线程a中调用线程bjoin(),此时线程a就进入阻塞状态,直到线程b完全执行完以后

stop

已过时。当执行此方法时,强制结束当前线程。

static void sleep(long millis)

令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队

isAlive()

判断当前线程是否存活

四、线程安全问题

1、共享资源的冲突

案例一:

        有几个人围着桌子吃蛋糕,当你举起叉子,准备叉起最后一块蛋糕,正当叉子碰到蛋糕的时候才发现它已经被另一个叉子叉起。这类现象就是共享资源(蛋糕)中出现的冲突,而两个人同时用叉子叉同一块蛋糕的过程我们就称为并发

案例二:

2、问题演示

2.1、创建售票线程运行结果如下:

程序出现了两个问题:

  1. 相同的票数,比如5这张票被卖了两回。
  2. 不存在的票,比如0票与-1票,是不存在的。   

3、问题分析

线程安全问题都是由全局变量及静态变量引起的。

若每个线程对全局变量、静态变量只读,不写,一般来说,这个变量是线程安全的;

若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

综上所述,线程安根本原因:  多个线程在操作共享的数据

4、问题解决-线程同步

        专业:即线程操作共享数据时其他线程不可以参与执行,直到该线程完成操作;民间:同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说;

        为了保证每个线程都能正常执行共享资源操作,Java引入了7种线程同步机制:

1)同步代码块(synchronized

2)同步方法(synchronized

3)同步锁(ReenreantLock

4)特殊域变量(volatile

   5)局部变量(ThreadLocal

   6)阻塞队列(LinkedBlockingQueue

   7)原子变量(Atomic

4.1、同步代码块(synchronized

同步代码块

synchronized  关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

语法:

synchronized(钥匙){

需要同步操作的代码

}

钥匙:

  1. 钥匙可以是任意类型。
  2. 多个线程要使用同一把钥匙。

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

使用同步代码块代码如下:

public class Ticket implements Runnable {
    private int ticktNum = 10;
    //定义锁对象
    Object obj = new Object();

    public void run() {
        while (true) {
            //同步代码块
            synchronized (obj) {
                if (ticktNum > 0) {
                    //1.模拟出票时间
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //2.打印进程号和票号,票数减1
                    String name = Thread.currentThread().getName();
                    System.out.println("线程" + name + "售票:" + ticktNum--);
                }
            }
        }
    }
}

执行结果如下:

4.2、同步方法(synchronized

同步方法:

使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

格式:

public synchronized void method(){

可能会产生线程安全问题的代码

}

同步锁是谁?

对于非static方法,同步锁就是this

对于static方法,同步锁是当前方法所在类的字节码对象(类名.class)

使用同步方法代码如下:

public class Ticket implements Runnable {
    private int ticktNum = 10;
    //定义锁对象
    Object obj = new Object();

    public void run() {
        while (true) {
            sellTicket();
        }
    }
    //同步方法
    private synchronized void sellTicket() {
        if (ticktNum > 0) {
            try {
                //1.模拟出票时间
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //2.打印进程号和票号,票数减1
            String name = Thread.currentThread().getName();
            System.out.println("线程" + name + "售票:" + ticktNum--);
        }
    }
}

执行结果如下:

4.3、同步锁(ReentrantLock

java.util.concurrent.locks.Lock 提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

同步锁方法:

public void lock() :加同步锁。

public void unlock() :释放同步锁。

使用同步锁代码如下:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;


public class Ticket implements Runnable {
    private int ticktNum = 10;

    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            lock.lock();
            try {
                if (ticktNum > 0) {
                    //1.模拟出票时间
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //2.打印进程号和票号,票数减1
                    String name = Thread.currentThread().getName();
                    System.out.println("线程" + name + "售票:" + ticktNum--);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                //放锁
                lock.unlock();
            }
        }
    }
}

执行效果如下:

五、死锁

多线程改善了系统资源的利用率并提高了系统的处理能力。然而,并发执行也带来了新的问题--死锁。

  1. 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
  2. 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续

1、死锁演示

public class DeadLockTest {
    private static Object r1 = new Object();//定义成静态变量,使线程可以共享实例
    private static Object r2 = new Object();//定义成静态变量,使线程可以共享实例
    public static void main(String[] args) {
        Thread p1 = new Thread() {
            @Override
            public void run() {
                synchronized (r1) {
                    System.out.println("Thread1 get r1");
                    try {
                        // 休眠1秒,确保线程2有时间获取LOCK_B,放大死锁概率
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                    }
                    synchronized (r2) {
                        System.out.println("Thread2 get t2");
                    }
                }
            }
        };

        Thread p2 = new Thread() {
            @Override
            public void run() {
                synchronized (r2) {
                    System.out.println("Thread2 get p2");
                    try {
                        // 休眠1秒,确保线程1有时间获取LOCK_A
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                    synchronized (r1) {
                        System.out.println("Thread2 get t1");
                    }
                }
            }
        };
        p1.start();
        p2.start();
    }
}

执行效果如下:

2、产生死锁的原因

线程 1 持有r1,等待获取r2;

 线程 2 持有r2,等待获取r1;

 两个线程都不会释放已持有的锁,且都在等待对方的锁,形成循环等待,最终导致死锁。

3、如何避免死锁

避免一个线程同时获取多个锁;

六、线程通讯

1、​​​​​​​为什么要线程通信

        多个线程并发执行时,在默认情况下CPU是随机切换线程的,有时我们希望CPU按我们的规律执行线程,此时就需要线程之间协调通信。

​​​​​​​2、线程通讯方式

        线程间通信常用常用方法:

wait() 一旦执行此方法,当前线程就进入阻塞状态,并释放cpu资源。
notify()

一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。

notifyAll() 一旦执行此方法,就会唤醒所有被wait的线程。

线程通信的例子:使用两个线程打印 1-100。线程1, 线程2 交替打印

class ThreadCommunication implements Runnable {
    private int i = 1;
    public void run() {
        while (true) {
            synchronized (this) {
                this.notify();//唤醒等待此钥匙的线程
                if (i <= 100) {
                    System.out.println(Thread.currentThread().getName() + ":" + i++);
                } else {
                    break;
                }
                try {
                    this.wait();//拿到此钥匙的线程进入阻塞状态,并释放cpu资源
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

总结

Java多线程编程是实现并发执行的核心技术。程序是完成特定任务的指令集合,而软件则是程序、资源文件和文档的综合体。进程是运行中的应用程序,拥有独立内存空间;线程是进程内的执行单元,是CPU调度的最小单位。

线程创建四大方式

  1. 继承Thread类:重写run()方法,直接创建子类实例调用start()

  2. 实现Runnable接口:解决单继承限制,将实现类实例作为Thread的target

  3. 实现Callable接口:支持返回值、泛型和异常抛出,需用FutureTask包装

  4. 线程池(ThreadPoolExecutor):最推荐方式,通过核心参数(corePoolSize、maximumPoolSize等)管理线程复用

线程安全与同步

多线程共享资源时会出现线程安全问题,如售票系统的重复售票。解决方案包括:

  • 同步代码块(synchronized块)

  • 同步方法(synchronized方法)

  • 同步锁(ReentrantLock)

  • volatile变量、ThreadLocal等

死锁与线程通信

死锁发生在多个线程互相等待对方释放资源时,可通过避免嵌套锁、使用定时锁等方式预防。线程通信通过wait()、notify()、notifyAll()实现协调,典型应用于生产者-消费者模式。

核心要点

  • Java采用抢占式线程调度

  • Thread类提供了线程控制的基本API

  • 线程池显著提升性能,避免频繁创建销毁开销

  • 合理使用同步机制确保数据一致性

  • 避免死锁需注意锁的获取顺序

掌握多线程技术能显著提升程序性能,但需谨慎处理同步和通信问题,确保程序稳定可靠。实际开发中推荐使用线程池配合Callable/Runnable,结合合适的同步策略。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐