美文网首页
java 笔记 - 多线程 (重点)

java 笔记 - 多线程 (重点)

作者: 阿巳交不起水电费 | 来源:发表于2025-04-18 19:20 被阅读0次

一、进程与线程的概念

进程:进程是程序的基本执行实体。【线程是进程里面的】
线程:线程是操作系统能够进行运算调度的最小单位。它被包含在进程中,是进程中的实际运作单位。(简单理解:应用软件中互相独立,可以同时运行的功能

程序的执行是需要时间的,多线程就是让cpu像牛马一样不间断的工作,在等待的时间也要去做其他事情,充分利用cpu来达到提高效率的目的,一句话:不许歇着,起来干活!

image.png

二、并发与并行的概念

并发:在同一时刻,有多个指令在单个cpu交替执行
并行:在同一时刻,有多个指令在多个cpu同时执行

image.png

三、多线程的三种实现方式

方式1.继承 Thread 类并重写run方法

myThread 类

package com.进程和多线程;

//多线程的第一种实现方式:继承 Thread 类并重写run方法
public class myThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName()+ " hello world");
        }
    }
}

测试类

package com.进程和多线程;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class demo1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 方式1
        method1();
    }

    // 方式一:多线程的第一种实现方式:继承 Thread 类并重写run方法
    private static void method1() {
        System.out.println("--------------------------------- 方式一:多线程的第一种实现方式:继承 Thread 类并重写run方法 ---------------------------------");
        myThread t1 = new myThread();
        myThread t2 = new myThread();
        // 给线程取名字
        t1.setName("线程1");
        t2.setName("线程2");
        // 开启线程 - 可以看到控制台是交替执行的两个线程 - “并发”
        t1.start();
        t2.start();
    }


}

方式2.实现 Runnable 接口重写run方法

myThread2 类

package com.进程和多线程;

// 多线程的第二种实现方式:实现 Runnable 接口重写run方法
public class myThread2 implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {

            // Thread.currentThread() 获取当前执行的线程
            System.out.println(Thread.currentThread().getName() + " hello world");
        }
    }
}

测试类

package com.进程和多线程;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class demo1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 方式2
        method2();
    }

    // 方式二:多线程的第二种实现方式:实现 Runnable 接口重写run方法
    private static void method2(){
        System.out.println("--------------------------------- 方式二:多线程的第二种实现方式:实现 Runnable 接口重写run方法 ---------------------------------");

        // 定义一个任务
        myThread2 m = new myThread2();

        // 将任务传递给线程,表示执行这个任务一次
        Thread t1 = new Thread(m);
        // 将任务传递给线程,表示执行这个任务两次
        Thread t2 = new Thread(m);

        // 给线程取名字
        t1.setName("线程1");
        t2.setName("线程2");

        // 开启线程 - 可以看到控制台是交替执行的两个线程 - “并发”
        t1.start();
        t2.start();
    }
}

方式3.利用Callable接口和Future接口实现 - 注意:这种方式可以获取多线程的 “返回值”

myThread3 类,注意你想要的线程的返回值也就是 Callable 的泛型

package com.进程和多线程;

import java.util.concurrent.Callable;

/**
 * 想要获取多线程的返回值,那么使用第三种方式 利用Callable接口和Future接口实现
 * 多线程的第三种实现方式:
 * 1.定义一个类实现 Callable 接口重写call方法(有返回值的,表示多线程的运行结果)
 * 2.创建 Callable 的对象(表示多线程要执行的任务)
 * 3.创建 FutureTask 对象来管理多线程的运行结果
 * 4.创建线程对象
 * 5.在 FutureTask 里面获取多线程的运行结果
 */
public class myThread3 implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        // 求1-100的和
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}

测试类

package com.进程和多线程;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class demo1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 方式3
        method3();
    }

    // 方式三:利用Callable接口和Future接口实现 - 这种方式可以获取多线程的返回值
    private static void method3() throws ExecutionException, InterruptedException {
        System.out.println("--------------------------------- 方式三:多线程的第三种实现方式 - 利用Callable接口和Future接口实现 ---------------------------------");
        // 1.定义一个类实现 Callable 接口重写call方法(有返回值的,表示多线程的运行结果)
        // 2.创建 Callable 的对象(表示多线程要执行的任务)
        myThread3 m3 = new myThread3();
        // 3.创建 FutureTask 对象来管理多线程的运行结果
        FutureTask<Integer> ft = new FutureTask<>(m3);

        // 4.创建线程对象
        Thread t1 = new Thread(ft);
        t1.start();

        // 5.在 FutureTask 里面获取多线程的运行结果
        Integer result = ft.get();
        System.out.println(result);

    }
}

四、多线程中常用的成员方法

image.png

下面列出的是几个难以理解的方法

(1) 当前的线程对象 Thread.currentThread()

Thread.currentThread() 代表执行到这行代码时当前的线程对象,如果是main方法里面直接调用会发现有个main线程

当JVM虚拟机启动之后,会自动启动多条线程,其中一条线程就是main线程,他的作用就是去调用main方法并执行里面的代码。


image.png

(2) 线程的优先级相关方法 setPriority() 和 getPriority()

Java 在主流系统 如Windows/Linux上,线程调度是抢占式的。特点:

线程优先级(Priority):Java 线程有优先级(1~10,默认5),但不保证严格按优先级执行。高优先级线程更可能被调度器选中,也就是带权重的随机
时间片(Time Slicing):大多数现代操作系统(如Windows、Linux)使用时间片轮转的抢占式调度。每个线程执行一段时间后会被强制暂停,让其他线程运行。
不可控性:开发者无法精确控制线程切换的时机,由JVM和操作系统共同决定。

    private static void memberMethods() {
        // 采用的上面第二种线程实现方式 
        myThread2 m = new myThread2();
        Thread t1 = new Thread(m, "飞机");
        Thread t2 = new Thread(m, "坦克");
        System.out.println("默认线程优先级:" + t1.getPriority() + "--" + t2.getPriority());
        // 线程优先级是 1~10 之间
        t1.setPriority(1);
        t2.setPriority(8);
        t1.start();
        t2.start();
    }

(3) 守护线程 setDaemon() (备胎线程)

当其他非守护线程执行完毕,守护线程就会陆续结束,即使它的代码未完全执行完毕。
注意这个结束不是立即结束,是陆续结束。就比如有个聊天窗口,里面的文件传输设置为守护线程,当聊天窗口关闭,那么文件传输也就不用传了,可以停了。

image.png

(4) 礼让线程 Thread.yield() --- 了解,平时很少用

礼让线程就是把cpu的执行权让出去,然后重新开始和其他线程争夺cpu执行权,让几个线程尽可能的均匀执行。
比如这个例子,如果没有下面的礼让线程代码,那么执行结果可能是某个线程执行了很久才轮到其他线程,而有了礼让线程那么打印结果就会比较均匀。

image.png

(5) 插入线程 join() --- 了解,平时很少用

表示的是把某个线程插入到正在执行的线程“前”执行,也就是插个队,先执行这个线程再执行当前线程。

比如下面的代码,如果没有t.join()这行代码,那么t线程和main线程会抢夺执行权,交替执行!如果我想让t线程执行完毕后再去执行main线程后面的代码,那么给t线程插个队就行了。

image.png

五、线程的生命周期

image.png

六、线程的安全问题

因为线程执行的时候有“随机性”,线程在执行代码的时候随时可能被其他线程抢走执行权,那么在“操作共享数据”的时候就有问题。
比如卖票的例子,总共100张票,有3个线程卖,那么可能出现卖的票重复或者超出范围的情况,因为线程的执行有随机性!

为了解决这个问题,那么出现了一个概念:同步代码块

(1)同步代码块

就像抢厕所一样,如果有人在里面那其他人就不能进去了,只能等里面的人处理完了出来了才能再抢这个厕所。


image.png

代码如下:
售票类 - SellTicket

package com.进程和多线程2_安全问题之卖票例子;

public class SellTicket extends Thread {

    // 表示这个类的所有对象都共享 ticket 数据
    static int ticket = 0;

    // synchronized 需要指定一个唯一对象,用 static 随便定义一个就行了,但是一般会使用这个类的字节码对象
    // static Object obj = new Object();

    @Override
    public void run() {
        while (true) {

            // 将共享代码用 synchronized 包裹,代表锁起来,当有线程在里面时其他线程不能执行里面的代码
            synchronized (SellTicket.class) {
                if (ticket >= 100) {
                    break;
                }
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket++;
                System.out.println(getName() + "正在卖第" + ticket + "张票!!");
            }

        }
    }
}

测试类

package com.进程和多线程2_安全问题之卖票例子;

public class demo1 {
    public static void main(String[] args) {
        SellTicket t1 = new SellTicket();
        SellTicket t2 = new SellTicket();
        SellTicket t3 = new SellTicket();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

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

特别注意这里 synchronized 里面的对象一定要是唯一的,这样才能锁起来。

(2)同步方法

image.png

将上面的 售票类 - SellTicket 改写成同步方法的方式,测试类不变

package com.进程和多线程2_安全问题之卖票例子;

public class SellTicket extends Thread {

    // 表示这个类的所有对象都共享 ticket 数据
    static int ticket = 0;


    // 方式二:同步方法的方式
    @Override
    public void run() {
        while (true) {
            // 执行同步方法
            if(extracted()) break;
        }
    }

    //synchronized 包裹的是同步方法
    static synchronized boolean extracted() {
        if (ticket >= 100) {
            return true;
        }
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        ticket++;
        System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票!!");
        return false;
    }
}

特别注意:这里是用的static synchronized修饰的,因为这个卖票类是直接继承的 Thread 类,我们在使用的时候根据这个类创建了三个不同的对象,这三个对象之间需要共享锁,因此需要用 static 修饰。
如果是实现 Runnable 接口这种方式来创建线程的,那就可以 不用static 修饰,只需要创建一次对象,如对象a,然后再创建三个Thread对象来共享这个对象a就行了。这里 synchronized 锁的对象就是this,也就是对象a。

(3)同步方法的扩展知识:StringBuilder 和 StringBuffer 的区别
StringBuilder 和 StringBuffer 在使用上基本是一模一样的,但 StringBuilder 对于多线程是不安全的,这个时候需要改为使用 StringBuffer。如果是单线程的就直接使用 StringBuilder 就行了

image.png

七、Lock 锁

相比同步代码块synchronized,Lock 可以自己来手动上锁,手动释放锁。注意:Lock 锁是接口不能直接实例化,需要使用他的实现类ReentrantLock来实例化。

image.png

将上面的 售票类 - SellTicket 改写成Lock 锁的方式,测试类不变

package com.进程和多线程2_安全问题之卖票例子;

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

public class SellTicket extends Thread {

    // 表示这个类的所有对象都共享 ticket 数据
    static int ticket = 0;

    // 方式三:lock 方法的方式 start ---------

    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            // 加锁
            lock.lock();

            try {
                if (ticket >= 100) {
                    break;
                }
                Thread.sleep(10);
                ticket++;
                System.out.println(getName() + "正在卖第" + ticket + "张票!!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 释放锁 - 注意:break 后会执行 finally里面的代码后才终止循序
                // finally 的优先级高于 break/return(除非调用 System.exit() 或 JVM 崩溃)。
                lock.unlock();
            }
        }
    }
    // 方式三:lock 方法的方式 end ---------
}

这里有两个注意点:

1.Lock 这里是 static 修饰的,同样是因为这个类是继承的 Thread ,我们在使用的时候实例化了三次这个类,为了保证三个实例化对象都共享一把锁,所以需要使用 static
2.lock.unlock(); 释放锁是放到的 try catch 的 finally 里面,这里有个特性是 finally 的优先级高于 break/return(除非调用 System.exit() 或 JVM 崩溃)。所以当执行 break 的时候,会先执行 finally 里面的代码才会去终止 while 循环。

八、死锁的错误

死锁,不需要学习其他代码,这是一种程序的错误,我们需要理解并且避免使用它。死锁是指两个或多个线程在执行过程中,因争夺资源而造成的一种互相等待的现象,导致这些线程都无法继续执行下去,程序无法正常运行。

典型死锁示例:

image.png
要想不出现死锁,我们平时写代码的时候需要注意不要让锁出现嵌套的情况

九、生产者和消费者(等待唤醒机制)

参考 :黑马教程

相关文章

  • Java编程思想读书笔记

    此篇Java编程思想读书笔记,没有涉及到多线程,多线程部分会有专门专题讲解。 读书笔记记录了重点章节和概要,更详细...

  • Java多线程编程核心技术【笔记】

    Java多线程编程核心技术【笔记】 第一章 Java多线程技能 使用多线程的场景? 阻塞 多线程提高运行效率 依赖...

  • 2021校招 复习总结

    笔记导航: JAVA: 泛型 反射和动态代理 注解 JAVA多线程 ReentrantLock,Volatile,...

  • Java的CountDownLatch笔记,让你加深对多线程的理

    Java的CountDownLatch笔记,让你加深对多线程的理解

  • 线程池

    Java多线程 线程的同步是Java多线程编程的重点和难点,往往让人搞不清楚什么是竞争资源、什么时候需要考虑同步,...

  • 史上最全Java多线程面试题及答案

    多线程并发编程是Java编程中重要的一块内容,也是面试重点覆盖区域。所以,学好多线程并发编程对Java程序员来来说...

  • Java高并发程序设计1

    Java多线程一直是Java学习的重点与难点,本人今天重点学习Java高并发一书,对其中的知识点做一个梳理。 本文...

  • Java后端知识体系

    基础重点(必须扎实) Java语言 语言基础 《Java核心技术》基础语法面向对象常用API异常处理集合IO多线程...

  • Java多线程学习笔记1

    多线程学习笔记 在之前的java开发中一般都是java web方向的工作,对多线程使用的非常少。了解仅限于Runn...

  • java高效并发学习笔记(一)java内存模型

    java高效并发学习笔记(一)java内存模型 学习JVM+JAVA多线程中,学习的书籍是《深入理解java虚拟机...

网友评论

      本文标题:java 笔记 - 多线程 (重点)

      本文链接:https://www.haomeiwen.com/subject/zearbjtx.html