Skip to content

多线程问题


用户线程和守护线程

基本介绍

(1)用户线程:也叫工作线程,其结束方式为当线程所承载的任务执行完毕,或是通过特定通知机制触发结束

(2)守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束

通俗理解:当主线程结束时,子线程无论是否执行完毕,都必须结束

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

使用方法

注意使用逻辑: 首先设置为守护线程类型,之后再启动线程

java
setDaemon(true)

代码示例

java
public class main {
    public static void main(String[] args) {
        a a = new a();
        a.setDaemon(true);
        a.start();

        for (int i = 1; i <= 5; i++) {
            System.out.println("主线程执行中 " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class a extends Thread{
    @Override
    public void run() {
        int i = 0;
        while (true){
            System.out.println("子线程执行中:" + ++i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 输出结果
主线程执行中 1
子线程执行中:1
子线程执行中:2
主线程执行中 2
主线程执行中 3
子线程执行中:3
主线程执行中 4
子线程执行中:4
主线程执行中 5
子线程执行中:5
子线程执行中:6

代码分析

(1)首先设置子线程为守护线程,之后启动子线程

(2)在子线程中设置死循环输出,如果是正常情况下主线程结束了,子线程还未执行完成,整个进程还不会结束

(3)由于子线程被设置为守护线程,无论子线程是否执行完成,当主线程执行完成子线程也会退出

线程同步(synchronized)

问题引入

设置三个卖票窗口,启动线程,开始买票

代码示例

java
public class main {
    public static void main(String[] args) {
        a a1 = new a();
        a a2 = new a();
        a a3 = new a();

        a1.start();
        a2.start();
        a3.start();
    }
}

class a extends Thread {
    private static int i = 5;  // 共享票数

    @Override
    public void run() {
        while (true) {
            if (i <= 0) {
                System.out.println("售票结束...");
                break;
            }
            // 销售票不进行同步,可能会导致多个线程同时售出同一张票
            System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票,剩余票数:" + --i);
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 输出结果
窗口 Thread-1 售出一张票,剩余票数:4
窗口 Thread-2 售出一张票,剩余票数:2
窗口 Thread-0 售出一张票,剩余票数:3
窗口 Thread-1 售出一张票,剩余票数:1
窗口 Thread-0 售出一张票,剩余票数:0
窗口 Thread-2 售出一张票,剩余票数:-1
售票结束...
售票结束...
售票结束...

存在的问题

(1)使用静态变量实现共享,让三个窗口去卖这 100 张票,但是出现了超卖的现象

(2)三个线程(窗口)共享一个资源,多个线程可能在某一时刻同时执行 run()方法,进行售票,导致了超卖问题的出现

基本介绍

应用场景: 解决多个线程同时访问同一资源时产生的问题

引入关键字:synchronized

(1)在多线程编程中,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻只有一个线程访问,从而确保数据的完整性

(2)也可以通过线程管理来解决:线程同步,即当一个线程在对内存进行操作时,其他线程不能访问这个内存地址进行操作,直到该线程完成操作,其他线程才能访问该内存地址进行操作。

两种实现方法

(1)同步代码块

得到对象的锁才能操作同步代码

java
synchronized(对象){
    // 需要被同步的代码
}

(2)方法声明

表示整个方法为同步方法

java
public synchronized void m(){
    // 需要被同步的代码
}

使用细节

(1)同步方法如果没有使用 static 修饰:默认锁对象为 this

(2)如果方法使用 static 修饰,默认锁对象为当前类 . class

(3)要求多个线程的锁对象为同一个,即要求该对象是三个线程共享的一个对象,也就是说三个线程操作的是同一个对象

互斥锁

基本介绍

(1)每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任何时刻,只有一个线程访问该对象

(2)当某个对象用 synchronized 修饰时,表示该对象在任何时刻只能由一个线程访问(体现了互斥)

(3) 同步的局限性:导致程序的执行效率要降低

优先选择同步代码块,同步的范围小,代码执行的效率高

(4) 同步方法(非静态)的锁可以是 this(当前对象),也可以是其他对象(要求是同一个对象

(5) 同步方法(静态)的锁为当前类本身

⚠️ 注意:当该线程释放对象锁后,之前访问该对象的线程有可能再次拿到该对象锁,对该对象进行二次访问

互斥锁的理解

(1)当某个线程需要访问同一对象时,多个线程就形成了竞争状态

(2)哪个线程拿到了该对象的锁(打开厕所门),才可以访问该对象(进入厕所),其他线程需要等待(排队状态)该线程访问完成后(释放锁)才可以执行

理解同一对象

(1)案例一

不是同一个对象,创建了三个 a 类对象,分别启动自身线程

java
a a1 = new a();
a a2 = new a();
a a3 = new a();

a1.start();
a2.start();
a3.start();

(2)案例二

同一个对象,启动了三个线程,线程的操作对象都是 a

java
a a = new a();
new Thread(a).start();
new Thread(a).start();
new Thread(a).start();

静态锁

解决超卖问题:由于票数是三个对象共享的属性,此时应该使用静态锁,锁对象是当前类 . class

代码示例

java
public class main {
    public static void main(String[] args) {
        a a1 = new a();
        a a2 = new a();
        a a3 = new a();

        a1.start();
        a2.start();
        a3.start();
    }
}

class a extends Thread {
    private static int i = 5;  // 共享票数

    @Override
    public void run() {
        while (true) {
            synchronized (a.class) {
                if (i <= 0) {
                    System.out.println("售票结束...");
                    break;
                }
                // 销售票不进行同步,可能会导致多个线程同时售出同一张票
                System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票,剩余票数:" + --i);
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

// 输出结果
窗口 Thread-1 售出一张票,剩余票数:4
窗口 Thread-2 售出一张票,剩余票数:3
窗口 Thread-0 售出一张票,剩余票数:2
窗口 Thread-0 售出一张票,剩余票数:1
窗口 Thread-0 售出一张票,剩余票数:0
售票结束...
售票结束...
售票结束...

代码分析

从代码结果可以证明,当该线程释放对象锁后,之前访问该对象的线程有可能再次拿到该对象锁,对该对象进行二次访问

非静态锁

(1)锁是当前对象(this)

java
public class main {
    public static void main(String[] args) {
        a a = new a();
        new Thread(a).start();
        new Thread(a).start();
        new Thread(a).start();
    }
}

class a extends Thread {
    private int i = 5;  // 共享票数

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                if (i <= 0) {
                    System.out.println("售票结束...");
                    break;
                }
                // 销售票不进行同步,可能会导致多个线程同时售出同一张票
                System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票,剩余票数:" + --i);
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

// 输出结果
窗口 Thread-1 售出一张票,剩余票数:4
窗口 Thread-2 售出一张票,剩余票数:3
窗口 Thread-2 售出一张票,剩余票数:2
窗口 Thread-2 售出一张票,剩余票数:1
窗口 Thread-2 售出一张票,剩余票数:0
售票结束...
售票结束...
售票结束...

代码分析

创建了 a 类对象,启动三个线程,操作对象都是 a,此时用互斥锁解决线程同步问题,实现了同一时刻该对象只能被一个线程访问

(2)锁是同一个类中的对象(基本介绍中提到的其他对象)

java
class a extends Thread {
    Object obj = new Object();
    private int i = 5;  // 共享票数
    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                .....
            }
        }
    }
}

代码分析

由于 obj 是 a 类的实例变量,然而多线程要求操作的是同一个对象,这里操作的是同一个对象中的 obj 对象,即都是操作 obj 对象,即所有线程访问的对象也就是同一个类对象

死锁

基本介绍

多个线程占用了对方的锁资源,不肯相让,导致了死锁,这是很危险的,一定要避免

代码示例

java
public class main {
    public static void main(String[] args) {
        a A = new a(true);
        A.setName("线程A");
        a B = new a(false);
        B.setName("线程B");

        A.start();
        B.start();
    }
}

class a extends Thread {
    static Object o1 = new Object();
    static Object o2 = new Object();
    boolean flag;

    public a(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");
                }
            }
        }
    }
}

// 运行结果
线程B 进入3
线程A 进入1
// 直接卡住

死锁分析

(1)创建了两个对象,为了实现线程同步,类变量用 static 修饰(在不同类对象中共享),确保不同类对象操作的是同一个锁对象

(2)A 对象进入 if 分支,拿到了 o1 锁;B 对象进入 else 分支,拿到了 o2 锁

(3)两个对象进入同步代码块,对于 A 对象而言,需要跳出同步代码块需要拿到 o2 这个锁,但是 o2 锁在初始时,在 else 分支中被 B 对象拿走,此时 o2 锁并没有得到释放,这就造成了死锁(B 对象同理)

(4)当两个线程需要结束时,需要的对象锁被对方拿走了,这是线程无法结束,即程序也就无法退出

释放锁

会释放锁的场景

(1) 当前线程的同步方法,同步代码块执行结束

(2) 当前线程在同步代码块, 同步方法中遇到 break、return

(3) 当前线程在同步代码块,同步方法中出现了未处理的 Error 或 Exception,由于异常导致结束

(4) 当前线程在同步代码块,同步方法中执行了线程对象的 wait()方法,当前线程暂停,并释放锁

不会释放锁的场景

(1) 线程执行同步代码块或同步方法时,程序调用 Thread.sleep()、Thread.yield()方法暂时停止当前线程的执行,不会释放锁

(2) 线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放锁

注意:应尽量避免使用suspend()方法和 resume()方法来控制线程,两个方法已经过时,不推荐使用