一、基本概念

进程

  • 进程是指运行中的程序。启动一个进程,操作系统就会为该进程分配内存空间
  • 进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程:有它自身的产生、存在和消亡的过程
  • 在Windows系统中,可以使用 Esc + Shift + Ctrl 打开任务管理器查看进程

线程

  • 线程由进程创建,是进程的一个实体
  • 一个进程可以拥有多个线程,如同时进行多个下载任务

单线程与多线程

  • 单线程:同一个时刻,只允许执行一个线程
  • 多线程:同一个时刻,可以执行多个线程,比如:
    • 一个QQ进程,可以同时打开多个聊天窗口
    • 一个迅雷进程,可以同时下载多个文件

并发与并行

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

获取CPU核心数

public class CpuInfo {
    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();
        // 获取当前电脑的CPU数量
        int cpuNums = runtime.availableProcessors();
        System.out.println("CPU核心数: " + cpuNums);
    }
}

二、创建线程的两种方式

1. 继承Thread类

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

// 继承Thread类,可以作为线程使用
class Cat extends Thread {
    @Override
    public void run() { // 重写run方法以实现业务逻辑
        while (true) {
            System.out.println("喵");
            try {
                // 休眠一秒
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

2. 实现Runnable接口

Runnable接口
  • java是单继承的,在某些情况下一个类可能已经继承了某个父类。
    这时在用继承Thread类方法来创建线程显然不可能了。
  • java设计者们提供了另外一个方式创建线程,就是通过实现Runnable接口来创建线程
public class RunnableDemo {
    public static void main(String[] args) {
        Dog dog = new Dog();
        // 不能直接调用start()
        // 创建Thread对象,把dog对象(实现Runnable)放入Thread
        Thread thread = new Thread(dog);
        thread.start(); // 底层使用代理模式
    }
}

// 通过实现Runnable接口创建线程
class Dog implements Runnable {
    int i = 0;
    
    @Override
    public void run() {
        while (true) {
            System.out.println("汪" + ++i + " " + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            if (i == 10) {
                break;
            }
        }
    }
}

Thread类的代理模式实现原理

// 线程代理类,模拟了一个最简单的Thread类
class ThreadProxy implements Runnable {
    private Runnable target = null; // 属性,类型是Runnable
    
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
    
    public ThreadProxy(Runnable target) {
        this.target = target;
    }
    
    public void start() {
        start0(); // 这个方法是真正实现多线程的方法
    }
    
    private void start0() {
        run();
    }
}

三、线程机制

线程工作机制

添加图片描述

可以使用jconsole工具监视管理控制台 local终端输入jconsloe回车即可

  • 当main线程启动一个子线程Thread-0,主线程不会阻塞,会继续执行
  • 这时主线程和子线程交替执行
  • 主线程结束若子线程仍在进行,不会造成进程结束

start()与run()的区别

  • start():启动线程,会创建新的线程,调用run()方法
  • run():直接调用run方法,将会用主线程执行,没有开子线程,造成阻塞,串行化

请添加图片描述

start()源码分析

public synchronized void start() {
    start0();
}

// start0()是本地方法,由JVM调用,底层是C/C++实现
// 真正实现多线程效果的是start0(),而不是run()
private native void start0();

四、线程终止与同步

请添加图片描述

多线程存在问题请添加图片描述

线程终止

  • 当线程完成任务后,会自动退出。
  • 还可以通过使用变量来控制run方法退出的方式停止线程,即通知方式
// 通过通知方式终止线程
public class ThreadStop {
    public static void main(String[] args) throws InterruptedException {
        MyThread t1 = new MyThread();
        t1.start();
        
        System.out.println("main线程休眠10s...");
        Thread.sleep(10 * 1000);
        t1.setLoop(false); // 通知t1线程退出
    }
}

class MyThread extends Thread {
    private boolean loop = true;
    
    public void setLoop(boolean loop) {
        this.loop = loop;
    }
    
    @Override
    public void run() {
        while (loop) {
            // 业务逻辑代码
        }
    }
}

五、线程同步机制

synchronized关键字

  • 问题:多线程编程时,一些敏感数据不允许被多个线程同时访问
  • 解决方案:使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性
  • 原理:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作

同步代码块

synchronized(对象) {
    // 得到对象的锁,才能操作同步代码
    // 需要被同步的代码
}

同步方法

synchronized还可以放在方法声明中,表示整个方法为同步方法

public synchronized void method(String name) {
    // 需要被同步的代码
}

多窗口售票问题示例

public class SellTicket {
    public static void main(String[] args) {
        SellTicketTask task = new SellTicketTask();
        
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        Thread t3 = new Thread(task);
        
        t1.start();
        t2.start();
        t3.start();
    }
}

// 使用synchronized实现线程同步
class SellTicketTask implements Runnable {
    private static int ticketNum = 100;
    private boolean loop = true;
    
    // 同步方法
    public synchronized void sell() {
        if (ticketNum <= 0) {
            System.out.println(Thread.currentThread().getName() + "窗口售尽");
            loop = false;
            return;
        }
        
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        
        System.out.println("窗口" + Thread.currentThread().getName() + 
                         "卖出一张票,剩余票数" + --ticketNum);
    }
    
    // 使用同步代码块的方式
    public void sell2() {
        synchronized (this) { // 互斥锁在this对象上
            if (ticketNum <= 0) {
                System.out.println(Thread.currentThread().getName() + "窗口售尽");
                loop = false;
                return;
            }
            
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            
            System.out.println("窗口" + Thread.currentThread().getName() + 
                             "卖出一张票,剩余票数" + --ticketNum);
        }
    }
    
    @Override
    public void run() {
        while (loop) {
            sell(); // 或使用sell2()
        }
    }
}

请添加图片描述

六、互斥锁原理

互斥锁特性

  1. Java语言中引入了对象互斥锁的概念,来保证共享数据操作的完整性
  2. 每个对象都对应于一个可称为"互斥锁"的标记,保证在任一时刻,只能有一个线程访问该对象
  3. 关键字synchronized与对象的互斥锁联系。当某个对象被synchronized修饰时,表明该对象在任一时刻只能由一个线程访问
  4. 同步的局限性:可能导致程序的执行效率降低

锁对象的选择

  1. 同步方法(非静态):锁可以是this,也可以是其他对象(要求是同一个对象)
  2. 同步方法(静态):锁为当前类本身
    • 同步方法如果没有使用static修饰:默认锁对象为this
    • 如果方法使用static修饰:默认锁对象为当前类.class

实现的落地步骤:

  • 先分析上锁的代码
  • 选择同步代码块或同步方法
  • 要求多个线程的锁对象为同一个即可!

静态同步方法示例

class SellTicket03 implements Runnable {
    // 锁加在SellTicket03.class上
    public synchronized static void m1() {
        // 同步代码
    }
    
    // 静态方法中的同步代码块
    public static void m2() {
        synchronized (SellTicket03.class) {
            System.out.println("m2");
        }
    }
    
    @Override
    public void run() {
        // 线程执行逻辑
    }
}

七、死锁与释放锁

死锁示例

public class DeadLock {
    public static void main(String[] args) {
        DeadLockDemo deadLock1 = new DeadLockDemo(true);
        DeadLockDemo deadLock2 = new DeadLockDemo(false);
        
        deadLock1.start();
        deadLock2.start();
    }
}

class DeadLockDemo extends Thread {
    static Object o1 = new Object();
    static Object o2 = new Object();
    boolean flag;
    
    public DeadLockDemo(boolean flag) {
        this.flag = flag;
    }
    
    @Override
    public void run() {
        // 下面业务逻辑的分析:
        // 1. 如果flag为true,线程A就会先得到/持有o1对象锁,然后尝试去获取o2对象锁
        // 2. 如果线程A得不到o2对象锁,就会Blocked
        // 3. 如果flag为false,线程B就会先得到/持有o2对象锁,然后尝试去获取o1对象锁
        // 4. 如果线程B得不到o1对象锁,就会Blocked
        if (flag) {
            synchronized (o1) { // 对象互斥锁
                System.out.println(Thread.currentThread().getName() + "进入1");
                synchronized (o2) {
                    System.out.println(Thread.currentThread().getName() + "进入2");
                }
            }
        } else {
            synchronized (o2) {
                System.out.println(Thread.currentThread().getName() + "进入3");
                synchronized (o1) {
                    System.out.println(Thread.currentThread().getName() + "进入4");
                }
            }
        }
    }
}

释放锁的情况

会释放锁的操作:

  1. 当前线程的同步方法、同步代码块执行结束
    • 案例:上厕所,完事出来
  2. 当前线程在同步代码块、同步方法中遇到break、return
    • 案例:没有正常的完事,经理叫他修改bug,不得已出来
  3. 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
    • 案例:没有正常的完事,发现忘带纸,不得已出来
  4. 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停并释放锁
    • 案例:没有正常完事,觉得需要酝酿下,所以出来等会再进去

不会释放锁的操作:

  1. 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行不会释放锁
    • 案例:上厕所,太困了,在坑位上眯了一会
  2. 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁
    • 提示:应尽量避免使用suspend()和resume()来控制线程,不推荐使用

八、线程常用方法

核心方法

  1. setName():设置线程名称,使之与参数name相同
  2. getName():返回该线程的名称
  3. start():使该线程开始执行(底层会创建新的线程,调用run())
  4. run():线程的执行体(直接调用run()不会启动新线程)
  5. setPriority():更改线程的优先级(范围1-10)
  6. getPriority():获取线程的优先级
  7. sleep():线程的静态方法,使当前线程休眠指定的毫秒数
  8. interrupt():中断线程,但并没有真正的结束线程。一般用于中断正在休眠的线程
  9. yield():线程的让步,让出CPU,让其他线程执行。礼让的时间不确定,不一定礼让成功
  10. join():线程的插队。插队的线程一旦插队成功,则肯定先执行完插入的线程所有的任务
1.setName//设置线程名称,使之与参数name相同
2.getName//返回该线程的名称
3.start//使该线程开始执行:Java虚拟机底层调用该线程的start0方法
4.run //调用线程对象 run方法;
5.setPriority//更改线程的优先级
6.getPriority//获取线程的优先级
7.sleep//在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
8.interrupt//中断线程

守护线程

  1. 用户线程:也叫工作线程,当线程的任务执行完或通过通知方式结束
  2. 守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束
  3. 常见的守护线程:垃圾回收机制
// 设置守护线程
thread.setDaemon(true);

九、线程的七种状态

线程状态详解

  1. NEW:尚未启动的线程处于此状态
  2. RUNNABLE:在Java虚拟机中执行的线程处于此状态(包含READY与RUNNING两种子状态)
  3. BLOCKED:被阻塞等待监视器锁定的线程处于此状态
  4. WAITING:正在等待另一个线程执行特定动作的线程处于此状态
  5. TIMED_WAITING:正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
  6. TERMINATED:已退出的线程处于此状态

线程状态转换图

    NEW
     ↓
start()
     ↓
RUNNABLE ←→ BLOCKED(等待锁)
     ↓        ↓
  TERMINATED WAITING/TIMED_WAITING

十、线程监控工具

JConsole使用

  • 可以监视Java应用程序的线程状态
  • 在终端输入 jconsole 回车即可启动
  • 可以查看线程状态、死锁检测、内存使用等情况

线程同步的最佳实践

  1. 分析上锁的代码:确定哪些代码需要同步
  2. 选择同步方式:同步代码块或同步方法
  3. 确保锁对象一致:要求多个线程的锁对象为同一个
  4. 尽量减小同步范围:只同步必要的代码,提高效率
  5. 避免死锁:注意锁的获取顺序

通过合理使用多线程技术,可以充分利用CPU资源,提高程序性能,但需要注意线程安全、死锁等问题,确保程序的稳定性和正确性。请添加图片描述

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐