Java中的多线程入门解读

Java中的多线程

一. 线程的创建

在Java中使用线程有两种方法:

  1. 继承Thread
  2. 实现Runnable接口

1. 继承Thread 类 使用 线程

例:

public class Main {
    public static void main(String[] args) {
        Cat cat = new Cat();
        cat.start();
    }
}

class Cat extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("喵");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

解读

  1. 当一个类继承了Thread 类时, 该类就可以当作线程使用
  2. 我们会重写其中的run() 方法, 写入自己需要的东西
  3. Thread 类 之中的 run() 方法也是实现了 Runnable 中的 run() 方法

2. 实现Runnable 接口 来 使用线程

因为Java是单继承的, 在某些情况下一个类已经继承了一个另外一个类, 这个时候再继承Thread
显然不太可能, 这个时候就可以实现Runnable 接口来使用多线程

例:

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
        new Thread(dog).start();
    }
}

class Dog implements Runnable {
    @Override
    public void run() {
        while (true) {
            System.out.println("汪 " + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }
}

解读

  1. 没有start() 方法, 所以可以将其放入Thread 中, 再调用start() 方法
  2. 这里其实使用了一个设计模式: 代理模式
// 线程代理类, 模拟了一个最简单的Thread
class ThreadProxy implements Runnable {
    private Runnable target = null;
    @Override
    public void run() {
        if (target != null) {
            target.run(); // 动态绑定机制
        }
    }

    public ThreadProxy(Runnable target) {
        this.target = target;
    }
    
    public void start() {
        start0();
    }
    
    public void start0() {
        run();
    }
}

通过代码可以看出, 其实是在start0()帮助 Dog 类 运行 run() 方法
这就是代理模式(静态代理模式)

二. 多线程机制说明

  1. 我们的main 函数 其实是一个线程, 可以看作由java进程创建的一个线程, 而Cat 又是 main 进程
    创建的一个子线程
  2. 子线程 Thread-0main 线程可以同时执行, 不会阻塞
  3. 可以利用Jconsole 来监控进程执行情况
  4. 主线程main 结束, 其子线程可能还在继续执行, 但是如果一个进程下所有的线程全部结束, 那么这个进程也会跟着结束
  5. 主线程和子线程还可以继续创建子线程

Catstart()方法与run() 方法的区别

只有执行start 方法 才算是创建一个新的线程, 如果直接调用Catrun() 方法, 并不算创建线程. 而只是在main线程 中 调用了一个方法, 所以此时还是只有main 线程一个

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

start0() 是一个native本地方法, 是由JVM调用, 底层采用c/c++ 实现

private native void start0();

调用start0() 方法 之后, 线程变为可运行状态, 具体什么时候执行,
取决于CPU的调度与资源

总结:

  1. 从Java的设计上来看, 通过继承Thread 和 实现 Runnable 接口来创建线程其实本质上没有区别, 因为Thread 也实现了 Runnable 接口
  2. 实现Runnable 接口的方式更加适合多个线程共享一个资源的情况, 并且避免了单继承的局限性

三. 线程的使用

1. main线程中控制子线程

如果希望主线程中去控制子线程, 可以设置一个Boolean变量来控制run() 方法的终止, 就可以
通知 子线程结束

public class Main {
    public static void main(String[] args) throws InterruptedException {
        T1 t1 = new T1();
        t1.start();

        Thread.sleep(1000);

        t1.setLoop(false);
    }
}

class T1 extends Thread {
    private boolean loop = true;
    @Override
    public void run() {
        while (loop) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("线程" + Thread.currentThread().getName() + " 运行中..");
        }
    }

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

2. 常用方法

第一组方法

主要用于线程的基本信息和操作

  1. setName() 设置线程名称
  2. getName() 返回该线程的名称
  3. start() 使得该线程开始执行, Java虚拟机底层调用该线程的start0() 方法
  4. run() 调用该线程对象的run() 方法, 注意这里并没有创建新的线程
  5. setPriority() 更改线程的优先级
  6. getPriority() 获取线程的优先级
  7. sleep() 在指定的毫秒数内让当前执行的线程休眠
  8. interrupt() 结束休眠
第二组方法

线程的礼让和插队

  1. yield() 线程的礼让, 让出占用的cpu, 让其他线程执行, 但是礼让的时间不确定, 而且也不一定礼让成功

  2. join() 线程的插队, 插队的线程一旦插入成功, 则肯定先执行完插入的线程的所有任务, 再执行本身的任务 (在t1中调用t2.join(), 则t2插入到t1中, 先执行完t2)

案例: main 线程中创建子线程, 子线程每隔一秒输出一次hello, 输出20次, 主线程每隔一秒输出一次hi, 输出20次
当主线程运行5次之后, 就让子线程先输出完

public class Main {
    public static void main(String[] args) throws InterruptedException {
        T1 t1 = new T1();
        t1.start();

        for (int i = 0; i < 20; i++) {
            Thread.sleep(100);
            System.out.println("hello");
            if (i == 5) {
                t1.join();
            }
        }
    }
}

class T1 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("hi");
        }
    }
}

四. 用户线程与守护线程

用户线程: 也叫工作线程, 当线程的任务执行完或者线程以被通知的方式结束
守护线程: 一般是为工作线程服务的, 当所有的用户线程结束, 守护线程自动结束
常见的守护线程: 垃圾回收机制

例:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        T1 t1 = new T1();
        t1.setDaemon(true); // 设置为守护线程, 注意先设置再start()
        t1.start();

        for (int i = 0; i < 20; i++) {
            Thread.sleep(100);
            System.out.println("hello");
        }
    }
}

class T1 extends Thread {
    @Override
    public void run() {
        for (; ;) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("hi");
        }
    }

}

五. 线程的生命周期与七大状态

JDK中Thread 定义了枚举类型代表线程的状态

官方将进程状态分成六种, 但是如果如图, 将Runnable 状态 细化为 ReadyRunning, 就可以分为七种状态, 这也就是我们常说的线程的七大状态

六. 线程同步机制

synchronized
在多线程编程中, 一些敏感的数据不允许被多个线程同时访问, 此时就可以使用同步访问技术,
保证数据在任何同一时刻, 最多有一个线程访问, 以此保证数据的完整性

实现同步的方法

  1. 同步代码块
synchronized (对象) { // 得到对象的锁, 才能操作同步代码
    // 需要被同步的代码
}
  1. 声明方法为同步方法
public synchronized void m(String name) {
    // 需要被同步的代码
}

案例: 多窗口售票问题

将售卖方法设置为同步方法, 这样的话同一时刻只会有一个窗口售卖

public class Main {
    public static void main(String[] args) throws InterruptedException {
        SellTicket sellTicket = new SellTicket();
        Thread thread1 = new Thread(sellTicket);
        Thread thread2 = new Thread(sellTicket);
        Thread thread3 = new Thread(sellTicket);

        thread1.setName("窗口1");
        thread2.setName("窗口2");
        thread3.setName("窗口3");

        thread1.start();
        thread2.start();
        thread3.start();

    }
}

class SellTicket implements Runnable {
    private int ticketNum = 100;

    @Override
    public void run() {
        while (true) {
            boolean ok = sell();
            if (!ok) {
                break;
            }
        }
    }
    public synchronized boolean sell() {
        if (ticketNum <= 0) {
            System.out.println("票已经售卖完");
            return false;
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("售票处" + Thread.currentThread().getName() + "售出一张票, 剩余票数为 " + (--ticketNum));
        return true;
    }
}

七. 线程的互斥锁与死锁

互斥锁

Java语言中, 引入了对象互斥锁的概念, 来保证共享数据操作的完整性

  1. 每个对象都对应于一个可称为"互斥锁" 的标记, 这个标记用来保证在任意时刻, 只能有一个线程访问
    该对象
    2.关键字synchronized 与对象的互斥锁联系, 当某个对象用synchronized 修饰时, 表明该对象在任意时刻只能由一个线程访问
  2. 同步的局限性: 导致程序的执行效率变低
  3. 同步方法(非静态)的锁可以是this, 也可以是其他对象; 同步方法(静态)的锁为当前对象本身


    利用售票代码改写
public boolean sell() {
    synchronized (this) {
        if (ticketNum <= 0) {
            System.out.println("票已经售卖完");
            return false;
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("售票处" + Thread.currentThread().getName() + "售出一张票, 剩余票数为 " + (--ticketNum));
        return true;
    }
}

这就是同步代码块
注意synchronized 后面的括号中可以填其他的对象, 表明关联的是这个对象的互斥锁
但是如果是静态的方法和代码块, 就只能关联到这个类本身

public static void m1() {
    synchronized (SellTicket.class) {
        System.out.println("m1");
    }
}

注意:
xxx.classthis 的区别, 前者关联的是类, 锁的是这个类, 后者锁的是这个类new 出的 对象!
所以静态同步只能锁该类,
换言之, 当每次都new 一个对象的情况下, 下面的代码是没有意义的

public void m1() {
    synchronized (this) {
        System.out.println("m1");
    }
}

因为每一次创建线程都会new一个对象, 这样的话就代表每个对象都锁自己, 是没意义的

死锁

多个线程占用了对方的锁资源, 不肯相让, 就会导致卡在这里, 造成死锁
案例

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() {
        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. 如果线程1flag = true, 就会持有o1的对象锁, 但是如果o2的对象锁拿不到, 就会堵塞
  2. 如果线程2flag = false, 就会持有o2 的对象锁, 但是如果o1 的对象锁拿不到, 就会堵塞
    这个时候两个线程就有可能死锁, 分别持有对方想要的资源, 但是不肯相让

锁的释放

以下情况下会释放锁
  1. 当前线程的同步方法/同步代码块执行结束
  2. 当前线程在同步代码块/同步方法中遇到break/return
  3. 当前线程在同步代码块/同步方法中发现了未处理的Error/Exception, 导致异常结束
  4. 当前线程在同步代码块/同步方法中执行了线程对象的wait() 方法, 当前线程暂停
以下情况下不会释放
  1. 线程执行同步代码块/同步方法时, 调用yield/join 方法
  2. 线程执行同步代码块/同步方法时, 其他线程调用了该线程的suspend() 方法(被挂起)
    (这里已经不推荐使用了)
posted @   Xingon2356  阅读(5)  评论(0编辑  收藏  举报
编辑推荐:
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
点击右上角即可分享
微信分享提示