Java-线程基础

文章目录

  • 一、基础知识
  • 二、实现方式
  • 三、线程分析
    • 1. start 方法分析
    • 2. 执行流程分析
  • 四、线程方法
  • 五、线程的生命周期
  • 六、生产消费模型
  • 七、线程的安全
    • 1. 线程安全问题
    • 2. 编程模型
    • 3. 线程同步机制
      • 3.1 synchronized
      • 3.2 volatile
      • 3.3 ReentrantLock
  • 八、线程的死锁
  • 九、锁的释放时机


一、基础知识

进程:是指运行中的程序,是动态过程(有它自身的生产、存在和消亡的过程),是操作系统进行任务分配和调度的基本单位,我们可以在任务管理器中看到的程序都是进程。比如使用 QQ,就启动了一个进程,操作系统就会为该进程分配内存空间;或者是玩坦克大战,它也是一个进程,操作系统也会为它分配内存空间。

线程:由进程创建的,是进程的一个实体,一个进程可以拥有多个线程,线程不能单独存在,线程的资源是由进程进行管理的,是操作系统运算调度的最小单位。比如我们使用 QQ 跟多个人进行聊天,就相当于开启了多个线程,每一个线程负责你跟某个人的信息传输;或者说坦克大战游戏,一个线程负责画出飞机,一个线程负责计算位置、碰撞。

单线程:同一时刻,只允许执行一个线程。

多线程:同一时刻,可以执行多个线程。

并发:同一个时刻,多个任务交替执行,造成一种 “貌似同时” 的错觉,简单的说,单核 CPU 实现的多任务就是并发。

并行:同一个时刻,多个任务同时执行,多核 CPU 可以实现并行。


二、实现方式

实现线程通常有三种方式,分别是:

  • 继承 Thread 类
  • 实现 Runnable 接口
  • 实现 Callable 接口

方式一:继承 Thread 类

实现步骤:① 继承 Thread 类;② 重写 run 方法

public class ThreadDemo {
    public static void main(String[] args) {
        // 创建线程
        MyThread myThread = new MyThread();
        // 启动线程
        myThread.start();
    }
}

class MyThread extends Thread {

    @Override
    public void run() {
        // 编写业务逻辑
        System.out.println("业务逻辑...");
    }
}

方式二:实现 Runnable 接口

我们可以看到 Thread 是实现了 Runnable 接口,所以也可以通过实现 Runnable 接口的方式来实现线程。

Java-线程基础_第1张图片

其次,Java 是单继承的,一个类只能有一个父类,如果继承了 Thread 类就无法再继承其它类了,所以很显然通过 实现 Runnable 接口 的方式实现线程更加灵活。

实现步骤:① 实现 Runnable 接口;② 重写 run 方法

public class RunnableDemo {
    public static void main(String[] args) {
        
        // 方式一
        Runnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
        
        // 方式二
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 编写业务逻辑
                System.out.println("业务逻辑...");
            }
        }).start();
        
    }
}

class MyRunnable implements Runnable {

    @Override
    public void run() {
        // 编写业务逻辑
        System.out.println("业务逻辑...");
    }
}

这种方式实现线程,底层是用到了一个静态代理的设计模式,关于代理模式可以参见:设计模式-代理模式 这篇博客。

方式三:实现 Callable 接口

这种实现线程的方式需要重写 call() 方法,该方法会在线程结束时产生一个返回值,通常我们做异步回调的时候会用到。

实现步骤:

  • ① 创建 Callable 接口的实现类,并重写 call()方法;
  • ② 创建 Callable 实现类的实例,使用 FutureTask 类来包装Callable 对象(该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值);
  • ③ 使用 FutureTask 对象作为Thread 对象的 target 创建并启动新线程;
  • ④ 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值
public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        // 创建一个线程类
        MyCallable myCallable = new MyCallable();
        // 创建一个装载线程的对象 FutureTask
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        // 启动线程
        Thread thread = new Thread(futureTask, "线程A");
        thread.start();
        // 获取最后的返回值
        Integer result = futureTask.get();
        System.out.println("result = " + result);

    }
}

class MyCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        // 编写业务逻辑
        System.out.println("业务逻辑...");
        return null;
    }
}

三、线程分析


1. start 方法分析

为什么开启线程使用的是 start() 方法 而不是 run() 方法?

如果直接调用 run() 方法,那么就是一个普通的方法调用 ,并没有真正的启动一个线程,程序会执行完 run() 方法中的逻辑才会继续向下进行;而 start() 方法才会开启线程。

start() 方法的源码:

    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

追进去可以看到里面调用了一个 start0() 方法。

start0() 方法源码:

    private native void start0();

start0() 方法是本地方法,底层是 C++ 实现的,由 JVM 调用,它会将线程变成 可运行状态(就绪状态),具体什么时候执行,取决于 CPU,由 CPU 统一调度。


2. 执行流程分析

我们再来看下这段代码:

public class JConsoleDemo {
    public static void main(String[] args) {

        // 开启第一个线程
        startThread();
        // 开启第二个线程
        startThread();

        for (int i = 0; i < 30; i++) {
            try {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() + ":执行了 " + (i+1) + " 次");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void startThread() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    try {
                        Thread.sleep(1000);
                        System.out.println(Thread.currentThread().getName() + ":执行了 " + (i+1) + " 次");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}

这段代码就是一个程序,我们运行这个代码就相当于开启了一个进程,代码运行到 main() 方法的时候相当于在这个进程中开启了一个主线程,代码每运行完 startThread() 方法时在主线程中都会开启了一个线程,也就是说这段代码除了主线程 (main) 之后又会开启两个子线程 (Thread-0Thread-1),主线程和子线程是交替执行的,各个线程的结束不会影响到其它的线程。

Java-线程基础_第2张图片

Java-线程基础_第3张图片

也可以通过 JConsole 工具(可以在 JDK 的 bin 目录下找到)监控线程的执行情况。JConsole 的使用可参见博客:JConsole-的使用

Java-线程基础_第4张图片

可以看到刚运行的时候 JConsole 中是存在三个线程的,当主线程中的循环 30.fori 结束之后,另外两个线程并没有受到影响,仍在执行,直到线程执行结束。

Java-线程基础_第5张图片


四、线程方法

常用方法:

常用方法 功能说明
public void setName​(String name) 将此线程的名称更改为等于参数 name
public String getName​() 返回此线程的名称
public void start() 用于启动线程,使其进入就绪状态,等待被调度执行
public void run() 线程的执行逻辑通常在 run() 方法中实现,当线程启动后会自动调用 run() 方法
public int getPriority​(int newPriority) 获取线程的优先级
public void setPriority​(int newPriority) 更改此线程的优先级
public static void sleep​(long millis) 使当前正在执行的线程停留(暂停执行)指定的毫秒数,这取决于系统定时器和调度程序的精度和准确性
public void interrupt​() 中断线程的执行,向线程发送中断信号,线程可以根据中断信号作出相应处理
public static void yield​() 用于暂停当前线程的执行,让出CPU资源给其他线程
public void join() 线程插队,插队的线程一旦插队成功,则肯定先执行完插入的线程所有的任务
public void setDaemon​(boolean on) 将此线程标记为 daemon 线程或用户线程
public final native boolean isAlive() 判断线程是否处于活动状态

setName() 和 getName() 方法的使用

public class getSetNameExampe {
    public static void main(String[] args) {
        String beforeName = Thread.currentThread().getName();
        Thread.currentThread().setName("主线程");
        String afterName = Thread.currentThread().getName();

        System.out.println("beforeName = " + beforeName);
        System.out.println("afterName = " + afterName);
    }
}

sleep()方法的使用:

public class SleepExample {
    public static void main(String[] args) {
        System.out.println("Thread is running");
        try {
            Thread.sleep(2000); // 线程暂停2秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Thread is awake");
    }
}

join()方法的使用:

public class JoinExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            System.out.println("Thread 1 is running");
        });
        Thread thread2 = new Thread(() -> {
            try {
                thread1.join(); // 等待thread1执行完毕
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Thread 2 is running");
        });

        thread1.start();
        thread2.start();
    }
}

interrupt()方法的使用:

public class InterruptExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("Thread is running");
                try {
                    Thread.sleep(1000); // 线程暂停1秒
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); // 恢复中断状态
                }
            }
            System.out.println("Thread is interrupted");
        });

        thread.start();
        try {
            Thread.sleep(5000); // 主线程暂停5秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread.interrupt(); // 中断线程
    }
}

yield()方法的使用:

public class YieldExample implements Runnable {
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " is running");
            Thread.yield(); // 让出CPU资源给其他线程
        }
    }

    public static void main(String[] args) {
        YieldExample yieldExample = new YieldExample();

        Thread thread1 = new Thread(yieldExample, "Thread 1");
        Thread thread2 = new Thread(yieldExample, "Thread 2");

        thread1.start();
        thread2.start();
    }
}

在上面的例子中,我们创建了两个线程(Thread 1和 Thread 2),它们共享一个 Runnable 实例(YieldExample)。在 run() 方法中,每个线程会循环打印自己的名称,并调用 yield() 方法让出 CPU 资源。

当我们运行这个程序时,你会看到输出中 Thread 1 和 Thread 2 交替打印,这是因为每个线程在打印自己的名称后,调用 yield() 方法,暂停自己的执行,让其他线程有机会获得 CPU 资源并执行。

请注意,yield() 方法并不能保证让其他线程一定能获得 CPU 资源,它仅是一个暗示,告诉调度器可以切换到其他线程,但具体的调度还是由操作系统的线程调度器决定。

总之,yield() 方法可以在多线程编程中用于合理地让出 CPU 资源,提高应用程序的并发性能。然而,它的使用需要慎重考虑,并在实际应用中根据具体情况进行合理的调整和测试。

setPriority()方法的使用:

该方法用于设置线程的优先级。优先级是一个整数值,范围从 1~10,默认优先级为 5。较高优先级的线程更有可能在竞争 CPU 资源时被调度执行。

public class PriorityExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            System.out.println("Thread 1 is running");
        });

        Thread thread2 = new Thread(() -> {
            System.out.println("Thread 2 is running");
        });

        // 设置线程优先级
        thread1.setPriority(Thread.MIN_PRIORITY); // 最低优先级
        thread2.setPriority(Thread.MAX_PRIORITY); // 最高优先级

        thread1.start();
        thread2.start();
    }
}

在上面的例子中,我们创建了两个线程(Thread 1和Thread 2)。通过调用 setPriority() 方法,我们将 Thread 1 的优先级设置为最低优先级(MIN_PRIORITY),将 Thread 2 的优先级设置为最高优先级(MAX_PRIORITY)。当我们运行这个程序时,尽管优先级设置并不一定会影响线程的执行顺序,但较高优先级的线程(Thread 2)更有可能优先被调度执行。因此,你可能会看到线程 2 的输出先于线程 1 的输出。

需要注意的是,线程优先级的设置可能因操作系统和 CPU 种类而有所差异,并不是所有平台都能完全支持线程优先级的设置,而且过度依赖线程优先级可能导致可移植性和可维护性的问题。因此,在实际应用中,应谨慎使用线程优先级,并避免过度依赖它来进行程序设计。总之,setPriority() 方法可以用于设置线程的优先级,但在实际应用中应慎重考虑,并结合实际场景和需求进行合理的设置。

通知线程退出:

当线程执行完任务后,会自动退出,除此之外还可以通过使用变量来控制 run 方法退出的方式停止线程,即通知方式。

例如:

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        // 创建线程
        MyThread myThread = new MyThread();
        // 启动线程
        myThread.start();

        // 过 15s 后终止线程 myThread
        Thread.sleep(15000);
        myThread.setLoop(false);
        System.out.println("主线程结束~~");
    }
}

class MyThread extends Thread {

    private int count = 0;
    private boolean loop = true;

    public void setLoop(boolean loop) {
        this.loop = loop;
    }

    @Override
    public void run() {

        while (loop) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println((++count) + ": to do something ...");
        }
        System.out.println("子线程结束~~");
    }
}

用户线程和守护线程:

  • 用户线程:也叫工作线程,当线程的任务执行完或通知方式结束
  • 守护线程:一般是为了工作线程服务的,当所有的用户线程结束,守护线程自动结束

常见的守护线程:垃圾回收机制

例如:如果主线程的子线程是死循环的,我们想要主线程执行完毕之后,子线程也随之结束,便可以将子线程设置为主线程的收回线程。

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        // 创建线程
        MyDaemonThread myDaemonThread = new MyDaemonThread();
        // 设置为守护线程
        myDaemonThread.setDaemon(true);
        // 启动线程
        myDaemonThread.start();

        // 过 10s 后终止
        Thread.sleep(10000);
        System.out.println("main 线程结束~~");
    }
}

class MyDaemonThread extends Thread {

    private int count = 0;

    @Override
    public void run() {

        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println((++count) + ": to do something ...");
        }
    }
}

五、线程的生命周期

Java-线程基础_第6张图片

JDK 中用 Thread.State 枚举表示了线程的几种状态:

public class Thread implements Runnable {

	...

    public enum State {

		// 尚未启动的线程处于此状态
        NEW,

		// 在 Java 虚拟机中执行的线程处于此状态
        RUNNABLE,

		// 被阻塞等待监视器锁定的线程处于此状态
        BLOCKED,

		// 正在等待另一个线程执行特定动作的线程处于此状态
        WAITING,

		// 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
        TIMED_WAITING,

		// 已退出的线程处于此状态
        TERMINATED;
    }

	...
}
  • NEW(初始状态):实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。
  • RUNNABLE(可运行状态)
    • READY(就绪状态):就绪状态只是说线程资格运行,调度程序没有挑选到该线程,该线程就永远是就绪状态。时机:① 调用线程的 start() 方法,此线程进入就绪状态;② 当前线程sleep()方法结束,其他线程 join() 结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入就绪状态;③ 当前线程时间片用完了,调用当前线程的 yield() 方法,当前线程进入就绪状态;④ 锁池里的线程拿到对象锁后,进入就绪状态。
    • RUNNING(运行状态):线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态。这也是线程进入运行状态的唯一的一种方式。
  • BLOCKED(阻塞状态):阻塞状态是线程阻塞在进入 synchronized 关键字修饰的方法或代码块(获取锁)时的状态。
  • WAITING(等待状态):处于这种状态的线程不会被分配 CPU 执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态。
  • TIMED_WAITING(计时等待状态):处于这种状态的线程不会被分配 CPU 执行时间,不过无须无限期等待被其他线程显示地唤醒,在达到一定时间后它们会自动唤醒。
  • TERMINATED(终止状态):当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。在一个终止的线程上调用 start() 方法,会抛出 java.lang.IllegalThreadStateException 异常。

六、生产消费模型

对于线程的学习,我们可以从最为经典的 生产 --> 队列 --> 消费 模型开始:一些线程向队列中存入数据,一些线程从队列中取出数据。

以奶茶店制作奶茶,外卖小哥拿走奶茶为例:

存入奶茶
取走奶茶
奶茶小妹
柜台
外卖小哥

如图所示,奶茶小妹就相当于一个生产线程,外卖小哥就相当于一个消费线程,他们都共享一个柜台,那么这个柜台就相当于一个共享队列。当用户点奶茶外卖时,奶茶小妹(生产者)就需要制作奶茶放到柜台上,外卖小哥(消费者)便会从柜台上取走奶茶。

我们可以用代码来描写这一场景:

import java.util.LinkedList;
import java.util.Random;

public class SellMilkTea {
    public static void main(String[] args) {

        // 柜台(共享队列)
        final LinkedList<MilkTea> counter = new LinkedList<>();

        // 奶茶小妹(生产者线程)
        MilkTeaGirl milkTeaGirl = new MilkTeaGirl(counter);
        milkTeaGirl.setName("奶茶小妹");
        milkTeaGirl.start();
        // 外卖小哥(消费者线程)
        TakeawayBoy takeawayBoy = new TakeawayBoy(counter);
        takeawayBoy.setName("外卖小哥");
        takeawayBoy.start();
    }
}

// 奶茶
class MilkTea {
    // 订单号
    private Integer id;

    // 有参构造
    public MilkTea(Integer id) {
        this.id = id;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
}

// 奶茶小妹(生产者线程)
class MilkTeaGirl extends Thread {

    // 柜台(共享队列)
    private volatile LinkedList<MilkTea> counter;
    // 序列 id -> 订单号
    private int serialId = 0;

    public MilkTeaGirl(LinkedList<MilkTea> counter) {
        this.counter = counter;
    }

    @Override
    public void run() {

        System.out.println(Thread.currentThread().getName() + "上班了...");
        while (true) {
            System.out.println(Thread.currentThread().getName() + "正在制做奶茶中...");
            // 制作一杯奶茶
            try {
                Random random = new Random();
                int time = random.nextInt(1000) + 500;
                // 每杯奶茶的制作时间可能不一样
                Thread.sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            MilkTea milkTea = new MilkTea(++serialId);
            // 放到柜台上
            counter.add(milkTea);
            System.out.println(Thread.currentThread().getName() + "制做完奶茶并放在柜台上...");
            System.err.println("此时柜台上存放 " + counter.size() + " 杯奶茶 ~~ ");
        }
    }

}

// 外卖小哥(消费者线程)
class TakeawayBoy extends Thread {

    // 柜台(共享队列)
    private volatile LinkedList<MilkTea> counter;

    public TakeawayBoy(LinkedList<MilkTea> counter) {
        this.counter = counter;
    }

    @Override
    public void run() {

        System.out.println(Thread.currentThread().getName() + "上班了...");
        while (true) {
            if (counter.size() > 0) {
                // 柜台上有奶茶外卖小哥就会赶来奶茶店
                try {
                    Random random = new Random();
                    int time = random.nextInt(1100) + 500;
                    // 外卖小哥来奶茶店取奶茶的间隔时间可能不一样
                    Thread.sleep(time);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "进入奶茶店...");
                // 取走奶茶
                counter.remove();
                System.out.println(Thread.currentThread().getName() + "拿走了一杯奶茶...");
                System.err.println("此时柜台上还剩 " + counter.size() + " 杯奶茶 ~~ ");
            }
        }
    }
}

Java-线程基础_第7张图片


七、线程的安全


1. 线程安全问题

在现实生活中,一家奶茶店,制作奶茶的奶茶小妹可能有多个,取走奶茶的外卖小哥也会有多个,那么就相当于这种情况:

存入奶茶
存入奶茶
取走奶茶
取走奶茶
取走奶茶
奶茶小妹
柜台
奶茶小妹
外卖小哥
外卖小哥
外卖小哥

在上述案例中我们也创建 2位奶茶小妹制作奶茶,3 位外卖小哥取奶茶,代码如下:

    public static void main(String[] args) {

        // 柜台(共享队列)
        final LinkedList<MilkTea> counter = new LinkedList<>();

        // 奶茶小妹(生产者线程)
        MilkTeaGirl milkTeaGirl1 = new MilkTeaGirl(counter);
        milkTeaGirl1.setName("奶茶小妹1号");
        MilkTeaGirl milkTeaGirl2 = new MilkTeaGirl(counter);
        milkTeaGirl2.setName("奶茶小妹2号");
        milkTeaGirl1.start();
        milkTeaGirl2.start();
        // 外卖小哥(消费者线程)
        TakeawayBoy takeawayBoy1 = new TakeawayBoy(counter);
        takeawayBoy1.setName("外卖小哥1号");
        TakeawayBoy takeawayBoy2 = new TakeawayBoy(counter);
        takeawayBoy2.setName("外卖小哥2号");
        TakeawayBoy takeawayBoy3 = new TakeawayBoy(counter);
        takeawayBoy3.setName("外卖小哥3号");
        takeawayBoy1.start();
        takeawayBoy2.start();
        takeawayBoy3.start();
    }

Java-线程基础_第8张图片

运行代码之后就可能会出现如上所示的错误,当柜台上只有 1 杯奶茶的时候(counter.size() > 0),外卖小哥 3 号和外卖小哥 1 号都知道这一情况并向奶茶店赶了过来,外卖小哥 3 号先达到奶茶店拿走了这杯奶茶,导致柜台上已经没有奶茶了,这时外卖小哥 1 号赶到奶茶店,仍然执行了 counter.remove() 做出了取奶茶的逻辑,但是没有从柜台上拿到奶茶,就出错了。

这就是线程安全的问题,当柜台上还有 1 杯 奶茶的时候,外卖小哥 3 号和 1 号都以为可以过来取奶茶,但是当一个小哥取走这杯奶茶之后另外一个小哥是取不到奶茶的。

引发线程安全问题的条件:

  • 是否具备多线程的环境 — 多个奶茶小妹和外卖小哥
  • 是否有共享数据 — 柜台上的奶茶
  • 是否有多条语句操作共享数据 — 放奶茶到柜台、从柜台取奶茶

变量对线程安全的影响:

  • 实例变量:在堆中
  • 静态变量:在方法区中
  • 局部变量:在栈中

以上三大变量中,局部变量永远不会存在线程安全的问题,因为一个线程一个栈,局部变量永远不共享;实例变量和静态变量在堆或者方法区中都只有一个,堆和方法区都是多线程共享的,所以可能存在线程安全问题。

要想解决线程安全的问题,就需要在共享变量的操作,多个线程不能并发执行。


2. 编程模型

  • 同步编程模型: 同步编程模型是一种按照代码的顺序同步执行的模型。在同步模型中,任务按照顺序逐行执行,每个任务的执行必须等待前一个任务的完成。当一个任务执行时,后面的任务被阻塞,直到当前任务完成。在同步模型中,程序的执行顺序是可预测的,逻辑相对直观简单。
  • 异步编程模型: 异步编程模型是一种非阻塞的模型,允许在任务执行期间继续执行其他任务。在异步模型中,任务的执行不会阻塞后续任务的执行,可以并发地执行多个任务。当一个任务启动后,它会在后台进行处理,同时允许程序继续执行其他任务。异步编程模型通常使用回调函数、事件驱动等机制来处理任务的结果和事件。

区别:

同步编程模型 异步编程模型
执行方式 按照代码的顺序同步执行,即代码逐行执行,每行代码执行完毕后才能执行下一行 是非阻塞的,可以在任务执行期间继续执行其他任务
阻塞与非阻塞 是阻塞的,即当一个任务执行时,会阻塞后续任务的执行,直到当前任务执行完毕 是非阻塞的,可以在任务执行期间继续执行其他任务,不需要等待当前任务的完成
响应性 在 IO 等待期间会一直阻塞,无法充分利用系统资源 可以提高系统的响应性,特别是在处理IO密集型操作时。通过异步方式,可以在等待IO操作完成的同时,继续执行其他任务,提高系统的并发能力和吞吐量
复杂性 相对简单,代码逻辑按照顺序执行,更容易理解和调试 相对复杂一些,因为需要处理回调函数、事件驱动等异步操作。需要更多的代码和处理机制来处理异步任务的执行和结果处理
并发性 在面对大量并发任务时,容易造成线程阻塞和性能瓶颈 更适用于处理并发任务,可以充分利用多核处理器和并行计算的优势

3. 线程同步机制

在多线程编程中,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性。也可以这么理解:线程同步,即当有一个线程在对内存进行操作时,其它线程都不可以对这个内存进行操作,直到该线程完成操作,其它线程才能对该内存地址进行操作。

Java 中提供了几种线程同步机制来确保多个线程之间的协调和数据安全:

  • synchronized 关键字
  • volatile 关键字
  • Lock 接口及其实现类

通过使用这些线程同步机制,可以确保多线程之间的协调和数据安全,避免出现竞态条件等问题。在应用程序中根据具体的需求选择合适的同步机制来保证线程安全是非常重要的。


3.1 synchronized

synchronized 关键字可以用于方法或代码块,确保同一时间只有一个线程可以访问被 synchronized 修饰的代码段。它提供了互斥锁的功能,保证了线程安全。

示例:

	public synchronized void synchronizedMethod() {
	    // 同步方法
	    // 线程在进入该方法之前会获取对象的锁
	    // 其他线程无法进入该方法,直到当前线程执行完毕并释放锁
	    // ...
	}

	public void synchronizedBlock() {
	    // 普通代码块同步
	    synchronized (this) { // 得到对象的锁,才能操作同步代码
	        // 这部分代码会被一个线程独占执行
	        // 其他线程在当前线程执行完毕之前会被阻塞
	        // ...
	    }
	}

例如上述多线程案例中,将外卖小哥取奶茶的动作用 synchronized 包裹起来,就能解决其线程安全的问题了:

    @Override
    public void run() {

        System.out.println(Thread.currentThread().getName() + "上班了...");
        while (true) {
            synchronized (TakeawayBoy.class) {
                if (counter.size() > 0) {
                    // 柜台上有奶茶外卖小哥就会赶来奶茶店
                    try {
                        Random random = new Random();
                        int time = random.nextInt(1100) + 500;
                        // 外卖小哥来奶茶店取奶茶的间隔时间可能不一样
                        Thread.sleep(time);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "进入奶茶店...");
                    // 取走奶茶
                    counter.remove();
                    System.out.println(Thread.currentThread().getName() + "拿走了一杯奶茶...");
                    System.err.println("此时柜台上还剩 " + counter.size() + " 杯奶茶 ~~ ");
                }
            }
        }
    }

虽然 synchronized 可以解决线程安全相关的问题,但是由于 synchronized 是重量级锁,会降低程序执行的效率和系统的吞吐量,在不得已的情况下才会现在该方式。


3.2 volatile

volatieJava 提供的一种轻量级的同步机制,相比 synchronized 更轻量级,它不会引起线程上下文的切换和调度,但是 volatie 变量的同步性较差,而且使用也是更容易出错。

volatie 的两大特点分别是:① 可见性;② 有序性。

在上述生产消费模型中为了保证共享变量的可见性是有用到这个关键字的。

回到上面例子,竟然共享变量柜台 counter 已经被该关键字修饰的话,可以在外卖小哥取奶茶时再进行判断,避免外卖小哥取空。

修改代码如下:

    @Override
    public void run() {

        System.out.println(Thread.currentThread().getName() + "上班了...");
        while (true) {
            if (counter.size() > 0) {
                // 柜台上有奶茶外卖小哥就会赶来奶茶店
                try {
                    Random random = new Random();
                    int time = random.nextInt(1100) + 500;
                    // 外卖小哥来奶茶店取奶茶的间隔时间可能不一样
                    Thread.sleep(time);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "进入奶茶店...");
                if (counter.size() > 0) {
                    // 取走奶茶
                    counter.remove();
                    System.out.println(Thread.currentThread().getName() + "拿走了一杯奶茶...");
                    System.err.println("此时柜台上还剩 " + counter.size() + " 杯奶茶 ~~ ");
                } else {
                    System.out.println(Thread.currentThread().getName() + "没有拿到奶茶!");
                }
            }
        }
    }

3.3 ReentrantLock

Java 5 之后引入的一个新的互斥体实现 ReentrantLock,它是一种基于AQS(Abstract Queued Synchronizer)框架的应用实现,是 JDK 中一种线程并发访问的同步手段,它的功能类似于 synchronized 是一种互斥锁,可以保证线程安全。相对于 synchronizedReentrantLock具备如下特点:

  • 可中断:可以设置超时时间,或者在获取锁的过程中响应中断
  • 可以设置为公平锁:按照线程请求锁的顺序来分配锁
  • 支持多个条件变量:与 synchronized 一样,都支持可重入

使用步骤:

  1. 创建 ReentrantLock 对象
    ReentrantLock lock = new ReentrantLock();
    
  2. 获取锁
    lock.lock();
    
  3. 释放锁
    lock.unlock();
    

Luck 锁来保证线性安全问题,外卖小哥的消费线程可修改为

// 外卖小哥(消费者线程)
class TakeawayBoy extends Thread {

    // 柜台(共享队列)
    private volatile LinkedList<MilkTea> counter;
    // 创建 ReentrantLock 对象
    private static final ReentrantLock lock = new ReentrantLock();

    public TakeawayBoy(LinkedList<MilkTea> counter) {
        this.counter = counter;
    }

    @Override
    public void run() {

        System.out.println(Thread.currentThread().getName() + "上班了...");
        while (true) {
            // 获取锁
            lock.lock();
            if (counter.size() > 0) {
                // 柜台上有奶茶外卖小哥就会赶来奶茶店
                try {
                    Random random = new Random();
                    int time = random.nextInt(1100) + 500;
                    // 外卖小哥来奶茶店取奶茶的间隔时间可能不一样
                    Thread.sleep(time);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "进入奶茶店...");
                // 取走奶茶
                counter.remove();
                System.out.println(Thread.currentThread().getName() + "拿走了一杯奶茶...");
                System.err.println("此时柜台上还剩 " + counter.size() + " 杯奶茶 ~~ ");
            }
            // 释放锁
            lock.unlock();
        }
    }
}

八、线程的死锁

死锁 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种相互等待的现象,若无外力作用,他们都将无法推进下去,陷入永久等待状态,这种现象称为死锁。

举一个例子:小明和小丽分别被关在两个不同的房间,小明身上有小丽房间开门的钥匙,小丽身上有小明房间的钥匙,如果小明想要出去救小丽,就得打开自己的房门,但是自己房门的钥匙又在小丽身上,同理小丽也无法救出小明 ,这就是死锁。

死锁的特点有:

  • 占有一定的资源,等待对方释放资源
  • 获得对方资源前不释放自己占有的资源

死锁的原因主要有:

  • 系统资源不足
  • 进程运行推进顺序不合适
  • 资源分配不当等
public class DeadLock {
    public static void main(String[] args) {
        
        final Object lock1 = new Object();
        final Object lock2 = new Object();

        Thread thread1 = new Thread(() -> {
            synchronized (lock1) {
                System.out.println("线程1已获取锁1,等待获取锁2...");
                try { Thread.sleep(100);} catch (InterruptedException e) {}
                System.out.println("线程1获取锁2...");
                synchronized (lock2) {
                    System.out.println("线程1已完全获取锁...");
                }
            }
        });

        Thread thread2 = new Thread(() -> {
            synchronized (lock2) {
                System.out.println("线程2已获取锁2,等待获取锁1...");
                try { Thread.sleep(100);} catch (InterruptedException e) {}
                System.out.println("线程2获取锁1...");
                synchronized (lock1) {
                    System.out.println("线程2已完全获取锁...");
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

这个程序创建了两个线程,每个线程都尝试获取两个对象的锁。每个线程都首先获取一个锁,然后尝试获取另一个锁。如果两个线程同时执行,并且都尝试获取 lock1 再获取 lock2 ,那么它们将互相等待对方释放锁,从而形成死锁。


九、锁的释放时机

锁的释放时机取决于使用锁的具体上下文和同步机制。以 ReentrantLock 为例,当以下情况发生时,锁会被释放:

  • 当前线程的同步方法或代码块执行结束
  • 当前线程在同步方法或代码块中遇到了 return、break,终止了同步代码块的执行
  • 当前线程在同步方法或代码块中出现了未处理的 Error 或 Exception,导致当前线程异常结束
  • 当前线程在同步方法或代码块中执行了锁对象的 wait() 方法,当前线程被挂起,并释放锁
  • 另外,其他线程执行了同步监听器对象的 wait() 方法或者当前线程的 stop() 方法时,也会释放锁

如果持有锁的线程还没有执行完同步代码块,那么就不会释放锁。例如,在线程的 sleep() 或 yield() 方法中不会释放锁,因为这些方法会让当前线程放弃CPU使用权但不会结束执行。同样,如果其他线程调用了当前线程的 suspend() 方法,当前线程被暂停但不会释放锁。注意,Thread 类的 suspend() 方法已经被废弃。


参考博客:

Java 实现线程的三种方式:https://blog.csdn.net/Crush258_/article/details/126273119
Java 线程 基础知识总结:https://blog.csdn.net/Zhangguohao666/article/details/103438581
Java多线程(一篇从0讲透):https://www.cnblogs.com/buchizicai/archive/2023/04/01/17277623.html
Java多线程(超详细):https://blog.csdn.net/zdl66/article/details/126297036
JConsole-的使用:https://blog.csdn.net/xhmico/article/details/130720808

你可能感兴趣的:(入门教程,java)