多线程编程

一、什么是多线程

多线程是指在一个程序中同时执行多个独立的任务或操作。每个任务或操作都是由一个单独的线程来执行,而这些线程共享程序的资源和内存空间。与单线程相比,多线程可以提高程序的运行效率和响应速度,因为它可以充分利用 CPU 的多核处理能力,同时也可以避免某些操作阻塞其他操作的问题。

image-20240714101818588

1.1、多线程的概念和基本原理

多线程是一种并发编程的技术,它允许程序在同一个进程中同时执行多个独立的任务或操作。每个任务都由一个单独的线程来执行,而这些线程共享程序的资源和内存空间。image-20240714101845063

多线程的基本原理是通过将程序分成多个子任务,并创建对应数量的线程来同时执行这些子任务。每个线程都有自己的堆栈、寄存器和指令计数器等状态信息,可以独立地运行代码。不同线程之间可以进行通信和协调,通过锁、信号量、条件变量等机制来实现数据同步和互斥访问。

多线程在操作系统级别实现,通过操作系统提供的API(如POSIX标准中提供的pthread库)进行创建、管理和控制。在高级编程语言中也提供了相应的库或框架来支持多线程编程,如Java中的Thread类、C#中的Task类等。

1.2、多线程与单线程的区别

执行方式不同:单线程只能执行一个任务,而多线程可以同时执行多个任务。
程序性能不同:多线程可以充分利用CPU资源,提高程序运行效率,而单线程则无法充分利用CPU资源,导致程序运行速度变慢。
内存占用不同:多线程需要占用更多的内存空间和系统资源(如CPU时间片),因此对于内存有限或资源受限的应用场景,单线程更为适合。
编写难度不同:在编写过程中,多线程需要考虑到并发、数据安全等问题,需要对程序设计有一定了解和经验。而单线程相对来说比较简单易于编写。
错误处理方式不同:在单线程中如果出现异常错误会直接导致程序崩溃,在多线程中则需要使用特殊手段处理错误以保证程序稳定性。
image-20240714101934964

二、Java多线程

2.1、Java多线程的生命周期

线程是一个动态执行的过程,它也有一个从产生到死亡的过程。

下图显示了一个线程完整的生命周期。

image-20240714101241353

  • 新建状态:

    使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

  • 就绪状态:

    当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

  • 运行状态:

    如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

  • 阻塞状态:

    如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

    • 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
    • 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
    • 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
  • 死亡状态:

    一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。

2.2、Java多线程的优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。

Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。

默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

2.3、Java创建线程

线程创建方式

Java 提供了三种创建线程的方法:

  • 通过实现 Runnable 接口;
  • 通过继承 Thread 类本身;
  • 通过 Callable 和 Future 创建线程。

通过继承 Thread 类本身来创建线程(不推荐)

单继承局限性

public class ThreadDemo extends Thread{
    //重写Thread的run方法
    @Override
    public void run() {
        int x=300;
        while (x>0){
            x--;
            System.out.println("普线程:通过继承 Thread 类本身来创建线程");
        }
    }
    //主线程
    public static void main(String[] args) {
        //开启线程 调用run方法
        new ThreadDemo().start();
        int x=300;
        while (x>0){
            x--;
            System.out.println("主线程:我在学习线程!");
        }
    }
}

结果(穿插进行)

image-20240714103614348

通过实现 Runnable 接口来创建线程(推荐)

一个对象可被多个线程使用

public class ThreadDemo1 implements Runnable{
    //重写Thread的run方法
    @Override
    public void run() {
        int x=300;
        while (x>0){
            x--;
            System.out.println("普线程:通过实现 Runnable 接口来创建线程");
        }
    }
    //主线程
    public static void main(String[] args) {
        //开启线程 调用run方法
        new Thread(new ThreadDemo1()).start();
        int x=300;
        while (x>0){
            x--;
            System.out.println("主线程:我在学习线程!");
        }
    }
}

结果

image-20240714104228462

初识并发问题

public class ThreadDemo2 implements Runnable{
    //公共资源
    private int num = 300;
    //重写Thread的run方法
    @Override
    public void run() {
        while (num>0){
            //线程延时
//            try {
//                Thread.sleep(100);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
            num--;
            System.out.println(Thread.currentThread().getName()+"拿到一张票,还剩:"+num+"张");
        }
    }
    //主线程
    public static void main(String[] args) {
        //创建对象
        ThreadDemo2 threadDemo2 = new ThreadDemo2();
        //开启线程 调用run方法 可以取别名
        new Thread(threadDemo2,"小红").start();
        new Thread(threadDemo2,"小敏").start();
        new Thread(threadDemo2,"小百").start();
        new Thread(threadDemo2,"小希").start();
    }
}

结果

image-20240714105321584

发现线程并发问题

龟兔赛跑

public class Race implements Runnable{
    //胜利者
    private String winner=null;
    //跑步
    @Override
    public void run() {
        for (int i = 0;i <= 1000;i++){
            if (winner==null){
                //如果是兔子则每跑100步睡0.01毫秒
                if (Thread.currentThread().getName().equals("兔子")&&i%500==0){
                    try {
                        Thread.sleep((long) 0.01);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //判断跑到一百的胜利者
                if (ifWinner(i)){
                    System.out.println("胜利者是:"+winner);
                    break;
                }
                System.out.println(Thread.currentThread().getName()+"跑到了"+i+"米");
            }
        }
    }
    private Boolean ifWinner(int i){
        if (i==1000){
            winner=Thread.currentThread().getName();
            return true;
        }
        return false;
    }
}

结果:

image-20240714111432148

通过 Callable 和 Future 创建线程

Callable接口可以看作是Runnable接口的补充,call方法带有返回值,并且可以抛出异常。

newFixedThreadPool线程池

//用Callable创建的线程在线程池中运行
public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable c1=new MyCallable();
        //MyCallable c2=new MyCallable();

        ExecutorService service= Executors.newFixedThreadPool(2);
        //线程池的方式可以两个线程可以采用同一个Callable对像,也可以一个线程采用一个Callable对像
        Future<Integer> f1=service.submit(c1);//不需要FutureTask对象也不需要Thread对象
        Future<Integer> f2=service.submit(c1);

        Integer rs1=f1.get();
        System.out.println(rs1);
        Integer rs2=f2.get();
        System.out.println(rs2);

        //service.shutdown();//关闭线程池
    }
}

class MyCallable  implements Callable<Integer> {
    public Integer call() throws Exception {
        int i=0;
        for(;i<100;i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
        return i;
    }
}

静态代理

接口-》真实角色-》中介-》操作

三、Lambda表达式

3.1概念介绍

Java Lambda表达式是Java 8中最重要的新特性之一。

它们是一种可传递的匿名函数,可以作为参数传递给方法或存储在变量中,因此可以在需要的时候调用它们。

Lambda表达式的主要目的是简化Java代码,使其更易于阅读和编写。

Lambda表达式的语法非常简洁和清晰。它们由参数列表、箭头符号和方法体组成。参数列表指定传递给Lambda表达式的参数,箭头符号 “->” 分隔参数列表和方法体,方法体则包含Lambda表达式要执行的代码。

3.2 简单示例

下面是一个简单的Lambda表达式示例:

(int x, int y) -> x + y

这个Lambda表达式接受两个整数参数 x 和 y,并返回它们的和。可以将这个Lambda表达式存储在一个变量中,例如:

IntBinaryOperator add = (int x, int y) -> x + y;

这个代码创建了一个名为add的变量,它的类型是IntBinaryOperator,它接受两个整数参数并返回一个整数结果。

该变量被初始化为一个Lambda表达式,该表达式实现了相同的功能,即将两个整数相加。

3.3 Lambda优点

Lambda表达式的主要优点包括:

简化代码:Lambda表达式可以将冗长复杂的代码简化为几行简洁的代码。
可读性:Lambda表达式可以使代码更易于阅读和理解,因为它们更接近自然语言。
可传递性:Lambda表达式可以作为参数传递给方法或存储在变量中,使代码更具可重用性和灵活性。
并行处理:Lambda表达式可以与Stream API一起使用,使Java程序更容易地进行并行处理。
Lambda表达式是Java 8中最重要的新特性之一,它们为我们提供了一种更简单、更灵活、更易于使用的编程方式。

3.4集合操作

Lambda表达式可以与Java 8的新集合操作方法(如stream()和forEach())一起使用,使集合的处理更加简单、灵活和易于读写。

例如,假设有一个字符串列表,想要对该列表中的所有元素进行大写转换并输出到控制台上,可以使用以下代码:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream().map(String::toUpperCase).forEach(System.out::println);

这里,使用了stream()方法将列表转换为一个流,然后使用map()方法将每个字符串转换为大写形式,最后使用forEach()方法将结果输出到控制台。

3.5多线程编程

new Thread(() -> {
        // 执行后台任务
        // ...
        // 通知主线程任务已完成
        }).start();

3.6 事件处理

Lambda表达式可以作为事件监听器传递给GUI组件等,使事件处理更加简单和可读。

例如,假设我们有一个按钮,需要在用户单击它时执行某些操作。可以使用以下代码将Lambda表达式作为事件监听器传递给该按钮:

button.addActionListener(event -> {
    // 处理按钮单击事件
    // ...
});

这里,使用了Java中的ActionListener接口,并将一个Lambda表达式作为参数传递给它,该表达式将在用户单击按钮时执行。

3.7 排序

Lambda表达式可以用于Java中的排序算法中,使排序更加灵活和可读。

例如,假设我们有一个Person对象的列表,需要按照年龄进行排序。可以使用以下代码将Lambda表达式作为排序算法的参数传递给Collections.sort()方法:

List<Person> people = Arrays.asList(new Person("Alice", 25), new Person("Bob", 30), new Person("Charlie", 20));
Collections.sort(people, (p1, p2) -> p1.getAge() - p2.getAge());

这里,我们使用了Java中的Collections类的sort()方法,并将一个Lambda表达式作为参数传递给它,该表达式将比较两个Person对象的年龄并返回一个整数值,以指示它们的排序顺序。

3.8 过滤

Lambda表达式可以用于过滤集合中的元素,使代码更加简单和可读。

例如,假设有一个整数列表,需要过滤掉其中的偶数。可以使用以下代码将Lambda表达式作为过滤器传递给Java中的stream()方法:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> oddNumbers = numbers.stream().filter(n -> n % 2 != 0).collect(Collectors.toList());

这里,使用了Java中的stream()方法将列表转换为一个流,然后使用filter()方法过滤掉其中的偶数,最后使用collect()方法将过滤后的结果转换为一个新的列表。

3.9 映射

Lambda表达式可以用于将一个集合中的元素映射到另一个集合中,使代码更加简单和可读。

例如,假设我们有一个字符串列表,需要将其中的每个字符串转换为大写形式并存储到另一个列表中。可以使用以下代码将Lambda表达式作为映射器传递给Java中的stream()方法:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> upperCaseNames = names.stream().map(String::toUpperCase).collect(Collectors.toList());

这里,我们使用了Java中的stream()方法将列表转换为一个流,然后使用map()方法将每个字符串转换为大写形式,最后使用collect()方法将转换后的结果存储到一个新的列表中。

3.10 聚合

Lambda表达式可以用于聚合集合中的元素,例如,计算集合中的元素之和、平均值、最大值、最小值等。

以下是一个计算列表中元素之和的示例:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
System.out.println("Sum of numbers: " + sum);

这里,使用了Java中的stream()方法将列表转换为一个流,并使用reduce()方法计算流中元素的总和。reduce()方法接受两个参数:起始值和一个BinaryOperator类型的Lambda表达式。Lambda表达式将两个元素相加并返回它们的和。在这个例子中,将起始值设置为0,表示计算从0开始的累加和。

除了计算元素之和之外,还可以使用Lambda表达式计算元素的平均值、最大值、最小值等。以下是一个计算列表中元素平均值的示例:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
double average = numbers.stream().mapToInt(Integer::intValue).average().orElse(0.0);
System.out.println("Average of numbers: " + average);

首先使用mapToInt()方法将流中的元素转换为int类型,然后使用average()方法计算这些元素的平均值。如果列表为空,则orElse()方法返回默认值0.0。

3.11 函数式编程

Lambda表达式可以使Java更加接近函数式编程,使代码更加简洁和易于理解。

例如,假设有一个接口,其中包含一个抽象方法,需要在程序中实现该接口并调用该方法。可以使用以下代码将Lambda表达式作为接口实现传递给该方法:

interface MyInterface {
    int doSomething(int x, int y);
}

MyInterface myLambda = (x, y) -> x + y;
int result = myLambda.doSomething(3, 4);

这里,定义了一个名为myLambda的变量,它的类型是MyInterface,它接受两个整数参数并返回它们的和。然后,我们调用myLambda的doSomething()方法,并传递两个整数参数,得到它们的和并将结果存储在result变量中。

3.12 数据库操作

Lambda表达式可以与Java中的数据库操作API(如JDBC和Hibernate)一起使用,使数据库操作更加简单和可读。

例如,假设有一个数据库表,需要查询其中的数据并输出到控制台上。可以使用以下代码将Lambda表达式作为查询操作的参数传递给数据库API:

try (Connection conn = DriverManager.getConnection(url, username, password);
     Statement stmt = conn.createStatement();
     ResultSet rs = stmt.executeQuery("SELECT * FROM my_table")) {
    while (rs.next()) {
        int id = rs.getInt("id");
        String name = rs.getString("name");
        System.out.println("id: " + id + ", name: " + name);
    }
}

这里,使用了Java中的JDBC API,并将一个Lambda表达式作为查询操作的参数传递给executeQuery()方法,该表达式将处理查询结果集并输出到控制台上。

3.13 并行计算

Lambda表达式可以与Java中的并行计算API(如Java 8中的Parallel Streams和Fork/Join框架)一起使用,使计算更加高效和快速。

例如,假设有一个大型的整数列表,需要计算其中所有元素的平方和。可以使用以下代码将Lambda表达式作为计算器传递给Java 8中的Parallel Streams API:

List<Integer> numbers = new ArrayList<>();
for (int i = 1; i <= 1000000; i++) {
    numbers.add(i);
}
long sum = numbers.parallelStream().mapToLong(i -> i * i).sum();
System.out.println("Sum of squares: " + sum);

这里,使用了Java 8中的Parallel Streams API,它将列表转换为一个并行流,并使用mapToLong()方法计算每个元素的平方值,最后使用sum()方法将它们加起来得到总和。在此过程中,计算将在多个线程上并行执行,从而提高了计算效率。

3.14 GUI编程

Lambda表达式可以与Java中的GUI编程框架(如JavaFX和Swing)一起使用,使GUI编程更加简单和可读。

例如,假设有一个JavaFX应用程序,需要在用户单击按钮时执行一些操作。可以使用以下代码将Lambda表达式作为事件处理器传递给JavaFX框架:

Button button = new Button("Click me!");
button.setOnAction(event -> System.out.println("Button clicked!"));

这里,使用了JavaFX框架,并将一个Lambda表达式作为按钮的事件处理器传递给setOnAction()方法。该表达式将在用户单击按钮时执行,并输出一条消息到控制台上。

3.15 测试

Lambda表达式可以用于编写更加简洁和可读的单元测试代码,使测试更加容易理解和维护。

例如,假设有一个函数,需要进行单元测试以确保其正确性。可以使用以下代码将Lambda表达式作为测试断言传递给JUnit测试框架:

@Test
public void testMyFunction() {
    int result = myFunction(2, 3);
    assertEquals(6, result, () -> "myFunction(2, 3) should return 6");
}

这里,使用了JUnit测试框架,并将一个Lambda表达式作为测试断言传递给assertEquals()方法。该表达式将在测试失败时计算并返回错误消息,以帮助定位和修复问题。

3.16 总结

Lambda表达式是Java 8中最强大和灵活的新特性之一,它可以用于各种不同的编程任务,使代码更加简单、灵活和易于读写。

Lambda表达式的语法非常简洁,通常由一个参数列表、一个箭头符号和一个表达式主体组成。

例如,以下是一个简单的Lambda表达式:

x -> x * x

该表达式接受一个参数x,将其平方并返回结果。

Lambda表达式可以用于各种不同的编程任务,包括函数式编程、集合处理、数据库操作、Web开发、并行计算、GUI编程、测试等。使用Lambda表达式可以使代码更加简单、灵活和易于读写,并帮助开发人员减少代码的冗余和重复。

除了Lambda表达式之外,Java 8还引入了其他重要的新特性,例如Stream API、接口默认方法、方法引用、Optional类型等。这些新特性一起使Java变得更加现代化、强大和易于使用。

总之,Lambda表达式是Java编程中不可或缺的一部分,它使Java变得更加现代化、灵活和强大,并且是Java 8中最重要的新特性之一。

四、线程状态

4.1、start启动线程

public class ThreadTest02 {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        // 启动线程
        //t.run(); // 不会启动线程,不会分配新的分支栈。(这种方式就是单线程。)
        t.start();
        // 这里的代码还是运行在主线程中。
        for(int i = 0; i < 1000; i++){
            System.out.println("主线程--->" + i);
        }
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        // 编写程序,这段程序运行在分支线程中(分支栈)。
        for(int i = 0; i < 1000; i++){
            System.out.println("分支线程--->" + i);
        }
    }
}

注意:

t.run() 不会启动线程,只是普通的调用方法而已。不会分配新的分支栈。(这种方式就是单线程。)

t.start() 方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。
这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。
启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。
run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。

image-20240714141117330

image-20240714141125966

4.2、线程的sleep方法

方法名作用
static void sleep(long millis)让当前线程休眠millis秒

静态方法:Thread.sleep(1000);
参数是毫秒
作用: 让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其它线程使用。
这行代码出现在A线程中,A线程就会进入休眠。
这行代码出现在B线程中,B线程就会进入休眠。
Thread.sleep()方法,可以做到这种效果:
间隔特定的时间,去执行一段特定的代码,每隔多久执行一次。

public class ThreadTest06 {
    public static void main(String[] args) {
    	//每打印一个数字睡1s
        for(int i = 0; i < 10; i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);

            // 睡眠1秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

线程中断sleep()的方法

方法名作用
void interrupt()终止线程的睡眠
public class ThreadTest08 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable2());
        t.setName("t");
        t.start();

        // 希望5秒之后,t线程醒来(5秒之后主线程手里的活儿干完了。)
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 终断t线程的睡眠(这种终断睡眠的方式依靠了java的异常处理机制。)
        t.interrupt();
    }

}

class MyRunnable2 implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "---> begin");
        try {
            // 睡眠1年
            Thread.sleep(1000 * 60 * 60 * 24 * 365);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //1年之后才会执行这里
        System.out.println(Thread.currentThread().getName() + "---> end");
}

注意:

sleep方法并不释放资源

4.3 、终止线程的执行

强行终止(不推荐使用)

stop()方法

public class ThreadTest09 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable3());
        t.setName("t");
        t.start();

        // 模拟5秒
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 5秒之后强行终止t线程
        t.stop(); // 已过时(不建议使用。)
    }

}

class MyRunnable3 implements Runnable {

    @Override
    public void run() {
        for(int i = 0; i < 10; i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

}

注意:

这种方式存在很大的缺点:容易丢失数据

因为这种方式是直接将线程杀死了,线程没有保存的数据将会丢失。不建议使用。

合理结束

定义一个终止标识符

public class ThreadTest10 {
    public static void main(String[] args) {
        MyRunable4 r = new MyRunable4();
        Thread t = new Thread(r);
        t.setName("t");
        t.start();

        // 模拟5秒
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 终止线程
        // 你想要什么时候终止t的执行,那么你把标记修改为false,就结束了。
        r.run = false;
    }

}

class MyRunable4 implements Runnable {

    // 打一个布尔标记
    boolean run = true;
    
    @Override
    public void run() {
        for (int i = 0; i < 10; i++){
            if(run){
                System.out.println(Thread.currentThread().getName() + "--->" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else{
                // return就结束了,你在结束之前还有什么没保存的。
                // 在这里可以保存呀。
                //save....
    
                //终止当前线程
                return;
            }
        }
    }

}

为什么if()语句要在循环里面?
由于一个线程一直运行此程序,要是if判断在外面只会在启动线程时判断并不会结束,因此需要每次循环判断一下标记。

补充小知识:线程调度(了解)
1.常见的线程调度模型有哪些?
(1)抢占式调度模型:
那个线程的优先级比较高,抢到的CPU时间片的概率就高一些/多一些。
java采用的就是抢占式调度模型。

(2)均分式调度模型:
平均分配CPU时间片。每个线程占有的CPU时间片时间长度一样。
平均分配,一切平等。
有一些编程语言,线程调度模型采用的是这种方式。

4.4、线程调度

优先级:

实例方法:

常量:

常量名备注
static int MAX_PRIORITY最高优先级(10)
static int MIN_PRIORITY最低优先级(1)
static int NORM_PRIORITY默认优先级(5)

方法:

方法名作用
int getPriority()获得线程优先级
void setPriority(int newPriority)设置线程优先级
  • 最低优先级1
  • 默认优先级是5,main是5
  • 最高优先级10

优先级比较高的获取CPU时间片可能会多一些。(但也不完全是,大概率是多的。)

public class ThreadTest11 {
    public static void main(String[] args) {
        System.out.println("最高优先级:" + Thread.MAX_PRIORITY);//最高优先级:10
        System.out.println("最低优先级:" + Thread.MIN_PRIORITY);//最低优先级:1
        System.out.println("默认优先级:" + Thread.NORM_PRIORITY);//默认优先级:5
        

        // main线程的默认优先级是:5
        System.out.println(hread.currentThread().getName() + "线程的默认优先级是:" + currentThread.getPriority());
    
        Thread t = new Thread(new MyRunnable5());
        t.setPriority(10);
        t.setName("t");
        t.start();
    
        // 优先级较高的,只是抢到的CPU时间片相对多一些。
        // 大概率方向更偏向于优先级比较高的。
        for(int i = 0; i < 10000; i++){
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }

}

class MyRunnable5 implements Runnable {
    @Override
    public void run() {
        for(int i = 0; i < 10000; i++){
            System.out.println(Thread.currentThread().getName() + "-->" + i);
        }
    }
}

礼让线程:

静态方法:

方法名作用
static void yield()让位方法,当前线程暂停,回到就绪状态,让给其它线程。
public class ThreadTest12 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable6());
        t.setName("t");
        t.start();

        for(int i = 1; i <= 10000; i++) {
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }

}

class MyRunnable6 implements Runnable {

    @Override
    public void run() {
        for(int i = 1; i <= 10000; i++) {
            //每100个让位一次。
            if(i % 100 == 0){
                Thread.yield(); // 当前线程暂停一下,让给主线程。
            }
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }

}

yield()方法不是阻塞方法。让当前线程让位,让给其它线程使用。

yield()方法的执行会让当前线程从“运行状态”回到“就绪状态”。

注意:在回到就绪之后,有可能还会再次抢到

插队线程:

实例方法:

方法名作用
void join()将一个线程合并到当前线程中,当前线程受阻塞,加入的线程执行直到结束
void join(long millis)接上条,等待该线程终止的时间最长为 millis 毫秒
void join(long millis, int nanos)接第一条,等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒
public class ThreadTest13 {
    public static void main(String[] args) {
        System.out.println("main begin");

        Thread t = new Thread(new MyRunnable7());
        t.setName("t");
        t.start();
    
        //合并线程
        try {
            t.join(); // t合并到当前线程中,当前线程受阻塞,t线程执行直到结束。
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
        System.out.println("main over");
    }

}

class MyRunnable7 implements Runnable {

    @Override
    public void run() {
        for(int i = 0; i < 10000; i++){
            System.out.println(Thread.currentThread().getName() + "--->" + i);
        }
    }

}

注意: 一个线程.join(),当前线程会进入”阻塞状态“。等待加入线程执行完!

4.5、守护线程与用户线程

thread.setDaemon(true);开启守护线程,默认为flase,守护线程只有在所有用户线程结束才会结束

image-20240715112150768

public class Daemon {
    static class You implements Runnable{
        @Override
        public void run() {
            for (int i=0;i<300;i++){
                System.out.println("又活了一天");
            }
            System.out.println("GoodBye Word!");
        }
    }
    static class God implements Runnable{
        @Override
        public void run() {
            while (true){
                System.out.println("God 永远与你同在 ");
            }
        }
    }
    public static void main(String[] args) {
        You you = new You();
        God god = new God();
        Thread thread = new Thread(god);
        //设置为守护线程 默认为flase为用户线程
        thread.setDaemon(true);
        thread.start();
        new Thread(you).start();

    }
}

image-20240715111920445

五、线程安全

5.1、数据的安全问题

什么时候数据在多线程并发的环境下会存在安全问题呢?★★★★★

满足三个条件:

  1. 条件1:多线程并发
  2. 条件2:有共享数据
  3. 条件3:共享数据有修改的行为

满足以上3个条件之后,就会存在线程安全问题

class TestDemo{
    //账户
    static class Account{
        int meany;
        public int getMeany() {
            return meany;
        }
        public void setMeany(int meany) {
            this.meany = meany;
        }
    }
    //银行
    static class Bank implements Runnable{
        Account account = null;
        int xMeany = 0;
        public Bank(Account account) {
            this.account = account;
        }
        public int getxMeany() {
            return xMeany;
        }
        public void setxMeany(int xMeany) {
            this.xMeany = xMeany;
        }
        @Override
        public void  run() {
            try {
                //取款操作
                withdrawMoney(xMeany);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //取钱操作
        public void withdrawMoney(int xMeany) throws InterruptedException {
            if (account.meany-xMeany>=0){
                //放大问题的发生性
                Thread.sleep(100);
                account.meany-=xMeany;
                System.out.println(Thread.currentThread().getName()+"取出了"+xMeany+"元");
            }
            System.out.println("余额剩余:"+account.getMeany());
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //一个银行账户
        Account account = new Account();
        account.setMeany(100);
        //一个银行对象
        Bank bank = new Bank(account);
        bank.setxMeany(50);
        //三个线程共同取钱
        new Thread(bank,"boy").start();
        new Thread(bank,"YOU").start();
        new Thread(bank,"girl").start();
    }
}

结果

image-20240715150944569

怎么解决线程安全问题呢?
当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在线程安全问题,怎么解决这个问题?

线程排队执行。(不能并发)。用排队执行解决线程安全问题。

这种机制被称为:线程同步机制。

专业术语叫做:线程同步,实际上就是线程不能并发了,线程必须排队执行。

线程同步就是线程排队了,线程排队了就会 牺牲一部分效率 ,没办法,数据安全第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。

4.两个专业术语:
异步编程模型:
线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不需要等谁,这种编程模型叫做:异步编程模型。

其实就是:多线程并发(效率较高。)

异步就是并发。

同步编程模型
线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,两个线程之间发生了等待关系,这就是同步编程模型。

效率较低。线程排队执行。

同步就是排队。

5.2、synchronized-线程同步

线程同步机制的语法是:

synchronized(){
	// 线程同步代码块。
}

重点:
synchronized后面小括号() 中传的这个“数据”是相当关键的。这个数据必须是 多线程共享 的数据。才能达到多线程排队

class TestDemo{
    //账户
    static class Account{
        int meany;
        public int getMeany() {
            return meany;
        }
        public void setMeany(int meany) {
            this.meany = meany;
        }
    }
    //银行
    static class Bank implements Runnable{
        Account account = null;
        int xMeany = 0;
        public Bank(Account account) {
            this.account = account;
        }
        public int getxMeany() {
            return xMeany;
        }
        public void setxMeany(int xMeany) {
            this.xMeany = xMeany;
        }
        @Override
        public synchronized void run() {
            try {
                //取款操作
                withdrawMoney(xMeany);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //取钱操作
        public void withdrawMoney(int xMeany) throws InterruptedException {
            if (account.meany-xMeany>=0){
                //放大问题的发生性
                Thread.sleep(100);
                account.meany-=xMeany;
                System.out.println(Thread.currentThread().getName()+"取出了"+xMeany+"元");
            }
            System.out.println("余额剩余:"+account.getMeany());
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //一个银行账户
        Account account = new Account();
        account.setMeany(100);
        //一个银行对象
        Bank bank = new Bank(account);
        bank.setxMeany(50);
        //三个线程共同取钱
        new Thread(bank,"boy").start();
        new Thread(bank,"YOU").start();
        new Thread(bank,"girl").start();
    }
}

结果

image-20240715151406585

()中写什么?

那要看你想让哪些线程同步。

假设t1、t2、t3、t4、t5,有5个线程,你只希望t1 t2 t3排队,t4 t5不需要排队。怎么办?

你一定要在()中写一个t1 t2 t3共享的对象。而这个对象对于t4 t5来说不是共享的。

这里的共享对象是:账户对象。
账户对象是共享的,那么this就是账户对象!!!
()不一定是this,这里只要是多线程共享的那个对象就行。

注意:
在java语言中,任何一个对象都有“一把锁”,其实这把锁就是标记。(只是把它叫做锁。)
100个对象,100把锁。1个对象1把锁。

class TestDemo{
    //账户
    static class Account{
        int meany;
        public int getMeany() {
            return meany;
        }
        public void setMeany(int meany) {
            this.meany = meany;
        }
    }
    //银行
    static class Bank implements Runnable{
        Account account = null;
        int xMeany = 0;
        public Bank(Account account) {
            this.account = account;
        }
        public int getxMeany() {
            return xMeany;
        }
        public void setxMeany(int xMeany) {
            this.xMeany = xMeany;
        }
        @Override
        public void run() {
            try {
                //取款操作
                withdrawMoney(xMeany);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //取钱操作
        public void withdrawMoney(int xMeany) throws InterruptedException {
            //锁的是资源的删改
            synchronized (account){
                if (account.meany-xMeany>=0){
                    //放大问题的发生性
                    Thread.sleep(100);
                    account.meany-=xMeany;
                    System.out.println(Thread.currentThread().getName()+"取出了"+xMeany+"元");
                }
                System.out.println("余额剩余:"+account.getMeany());
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //一个银行账户
        Account account = new Account();
        account.setMeany(100);
        //一个银行对象
        Bank bank = new Bank(account);
        bank.setxMeany(50);
        //三个线程共同取钱
        new Thread(bank,"boy").start();
        new Thread(bank,"YOU").start();
        new Thread(bank,"girl").start();
    }
}

结果

image-20240715151406585

以下代码的执行原理?(★★★★★)

1、假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。

2、假设t1先执行了,遇到了synchronized,这个时候自动找“后面共享对象”的对象锁,
找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是
占有这把锁的。直到同步代码块代码结束,这把锁才会释放。

3、假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面
共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,
直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后
t2占有这把锁之后,进入同步代码块执行程序。

4、这样就达到了线程排队执行。

重中之重:
这个共享对象一定要选好了。这个共享对象一定是你需要排队
执行的这些线程对象所共享的。

class Account {
    private String actno;
    private double balance; //实例变量。

    //对象
    Object o= new Object(); // 实例变量。(Account对象是多线程共享的,Account对象中的实例变量obj也是共享的。)
    
    public Account() {
    }
    
    public Account(String actno, double balance) {
        this.actno = actno;
        this.balance = balance;
    }
    
    public String getActno() {
        return actno;
    }
    
    public void setActno(String actno) {
        this.actno = actno;
    }
    
    public double getBalance() {
        return balance;
    }
    
    public void setBalance(double balance) {
        this.balance = balance;
    }
    
    //取款的方法
    public void withdraw(double money){
        /**
         * 以下可以共享,金额不会出错
         * 以下这几行代码必须是线程排队的,不能并发。
         * 一个线程把这里的代码全部执行结束之后,另一个线程才能进来。
         */
        synchronized(this) {
        //synchronized(actno) {
        //synchronized(o) {
        
        /**
         * 以下不共享,金额会出错
         */
    	  /*Object obj = new Object();
            synchronized(obj) { // 这样编写就不安全了。因为obj2不是共享对象。
            synchronized(null) {//编译不通过
            String s = null;
            synchronized(s) {//java.lang.NullPointerException*/
            double before = this.getBalance();
            double after = before - money;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.setBalance(after);
        //}
    }

}

class AccountThread extends Thread {
    // 两个线程必须共享同一个账户对象。
    private Account act;

    // 通过构造方法传递过来账户对象
    public AccountThread(Account act) {
        this.act = act;
    }
    
    public void run(){
        double money = 5000;
        act.withdraw(money);
        System.out.println(Thread.currentThread().getName() + "对"+act.getActno()+"取款"+money+"成功,余额" + act.getBalance());
    }

}

public class Test {
    public static void main(String[] args) {
        // 创建账户对象(只创建1个)
        Account act = new Account("act-001", 10000);
        // 创建两个线程,共享同一个对象
        Thread t1 = new AccountThread(act);
        Thread t2 = new AccountThread(act);

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

}

以上代码锁this、实例变量都可以!因为这三个是线程共享!

5.3、补充:安全与非安全集合

List 不安全集合与CopyOnWriteArrayList安全集合

public class ListDemo {
    public static void main(String[] args) throws InterruptedException {
        //不安全的集合
        List<String> arrayList = new ArrayList<>();
        //10万线程对一个集合同时操作
        for (int i = 0;i<100000;i++){
            new Thread(()->{
                arrayList.add(Thread.currentThread().getName());
            }).start();
        }
        Thread.sleep(20000);
        System.out.println(arrayList.size());
        //安全的集合
        CopyOnWriteArrayList<String> arrayList1 = new CopyOnWriteArrayList<>();
        //10万线程对一个集合同时操作
        for (int i = 0;i<100000;i++){
            new Thread(()->{
                arrayList1.add(Thread.currentThread().getName());
            }).start();
        }
        Thread.sleep(20000);
        System.out.println(arrayList1.size());
    }
}

结果:

image-20240715153300890

5.4、发生死锁

都占有资源不放

public class DeadLockDemo {
    //口红
    static class LipStick{}
    //镜子
    static class MirRor{}
    //化妆
    static class MakeUP implements Runnable{
        //静态资源 是共享的
        static LipStick lipStick = new LipStick();
        static MirRor MirRor = new MirRor();
        //选择
        int chose = 0;
        String name = null;
        MakeUP(int chose,String name){
           this.chose=chose;
           this.name=name;
        }
        @Override
        public void run() {
            if (chose==0){
                synchronized (this.lipStick){
                    System.out.println(this.name+"拿到口红了");
                    synchronized (this.MirRor){
                        System.out.println(this.name+"拿到镜子了");
                    }
                }
            }else {
                synchronized (this.MirRor){
                    System.out.println(this.name+"拿到镜子了");
                    synchronized (this.lipStick){
                        System.out.println(this.name+"拿到口红了");
                    }
                }
            }
        }
    }
    public static void main(String[] args) {
        MakeUP makeUP = new MakeUP(0,"白雪公主");
        MakeUP makeUP1 = new MakeUP(1,"恶毒皇后");
        new Thread(makeUP).start();
        new Thread(makeUP1).start();
    }
}

结果

image-20240715160219523

占有资源释放

public class DeadLockDemo {
    //口红
    static class LipStick{}
    //镜子
    static class MirRor{}
    //化妆
    static class MakeUP implements Runnable{
        //静态资源 是共享的
        static LipStick lipStick = new LipStick();
        static MirRor MirRor = new MirRor();
        //选择
        int chose = 0;
        String name = null;
        MakeUP(int chose,String name){
           this.chose=chose;
           this.name=name;
        }
        @Override
        public void run() {
            if (chose==0){
                synchronized (this.lipStick){
                    System.out.println(this.name+"拿到口红了");
                }
                synchronized (this.MirRor){
                    System.out.println(this.name+"拿到镜子了");
                }
            }else {
                synchronized (this.MirRor){
                    System.out.println(this.name+"拿到镜子了");
                }
                synchronized (this.lipStick){
                    System.out.println(this.name+"拿到口红了");
                }
            }
        }
    }
    public static void main(String[] args) {
        MakeUP makeUP = new MakeUP(0,"白雪公主");
        MakeUP makeUP1 = new MakeUP(1,"恶毒皇后");
        new Thread(makeUP).start();
        new Thread(makeUP1).start();
    }
}

结果

image-20240715160301116

5.5、Lock(可重入锁)

原:

public class LockDemo implements Runnable{
    int num = 10;
    @Override
    public void run() {
        while (num>=0){
            try {
                Thread.sleep(10); //放大问题发生性
                System.out.println(num--);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {
        LockDemo lockDemo = new LockDemo();
        new Thread(lockDemo).start();
        new Thread(lockDemo).start();
        new Thread(lockDemo).start();
    }
}

结果:

image-20240715162003816

使用lock:

代码实现

public class LockDemo implements Runnable{
    int num = 10;
    private final ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true){
            try {
                //锁住
                lock.lock();
                if (num>=0){
                    Thread.sleep(100); //放大问题发生性
                    System.out.println(Thread.currentThread().getName()+":"+num--);
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                //开锁
                lock.unlock();
            }
        }
    }
    public static void main(String[] args) {
        LockDemo lockDemo = new LockDemo();
        new Thread(lockDemo,"1").start();
        new Thread(lockDemo,"2").start();
        new Thread(lockDemo,"3").start();
    }
}

结果

image-20240715163559879

5.6、Lock与synchronized的对比image-20240715161315632

六、生产者消费者问题

6.1、信号法

wait():为等待,释放资源

notify(XX):唤醒指定线程

notifyAll():唤醒其他所有线程

public class TubeDemo {

    public static void main(String[] args) {
        TV tv = new TV();
        Spectator spectator = new Spectator(tv);
        Actors actors = new Actors(tv);
        spectator.start();
        actors.start();

    }
    //观众
    static class Spectator extends Thread{
        TV tv = null;
        Spectator(TV tv){
            this.tv=tv;
        }
        @Override
        public void run() {
            try {
                this.watch();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //观看
        public void watch() throws InterruptedException {
            for (int i = 0; i < 20; i++) {
                this.tv.doWatch();
            }
        }

    }
    //演员
    static class Actors extends Thread{
        TV tv = null;
        Actors(TV tv){
            this.tv=tv;
        }

        @Override
        public void run() {
            try {
                this.play();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        public void play() throws InterruptedException {
            for (int i = 0; i < 20; i++) {
                if (i%2==0){
                    this.tv.doPlay("抖音记录美好生活!");
                }else {
                    this.tv.doPlay("我和你有个约会");
                }
            }
        }
    }
    //节目
    static class TV{
        //标志符 真为表演 假为观看
        Boolean flag = true;
        //表演的节目
        String vice = null;
        //计数器
        int num = 0;
        int num1 = 0;
        //表演
        public synchronized void doPlay(String vice) throws InterruptedException {
            if (this.flag==false){
                this.wait();//wait是等待,会释放资源,不像sleep,这里次序很重要,线程唤醒会从wait处开始执行
            }
            //通知演员表演
            this.vice = vice;
            num++;
            System.out.println("演员表演了:"+vice+"第"+num+"次");
            this.flag = !this.flag;
            this.notify();
        }

        //观看
        public synchronized void doWatch() throws InterruptedException {
            if (this.flag==true){
                this.wait();
            }
            //通知观众观看
            num1++;
            System.out.println("观众观看了:"+vice+"第"+num1+"次");
            this.flag = !this.flag;
            this.notify();

        }
    }
}

结果

image-20240716174847529

七、线程池

7.1、线程池

image-20240716175124068

7.2、使用线程

image-20240716175405181

public class ThreadPool {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建线程服务 newFixedThreadPool表示创建多少条线程 ExecutorService是接口
        ExecutorService service = Executors.newFixedThreadPool(10);
        //连接执行
        service.execute(new Thread1());
        service.execute(new Thread1());
        service.execute(new Thread1());
        //连接执行Callable接口线程
        Future<String> submit = service.submit(new Thread2());
        //获取返回值
        System.out.println(submit.get());
        //连接关闭
        service.shutdown();
    }

    static class Thread1 implements Runnable{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"创建了");
        }
    }

    static class Thread2 implements Callable<String>{

        @Override
        public String call() throws Exception {
            System.out.println(Thread.currentThread().getName()+"创建了");
            return "我是实现Callable接口的线程,有返回值哦";
        }
    }
}

结果:

image-20240716180215216

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐