JavaSE——多线程1:创建线程的三种方式、Thread常见成员方法、锁、等待唤醒机制

一、线程相关概念

程序(program):        

        是为完成特定任务、用某种语言编写的一组指令的集合。简单地说:就是我们写的代码。

进程:

        1.进程是指运行中的程序,比如我们使用QQ,就启动了一个进程,操作系统就会为该进程分配内存空间;当我们使用微信时,又启动了一个进程,操作系统将为微信分配新的内存空间。

         2.进程是程序的一次执行过程,或是正在运行的一个程序。是动态过程:有它自身的产生、存在和消亡的过程。

线程:

        线程是由进程创建的,是进程的一个实体。一个进程可以有多个线程。比如百度网盘在同时下载多个文件。

单线程:

        同一时刻,只允许执行一个线程。

多线程:

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

并发:

        同一时刻,有多个指令在单个CPU上交替执行。造成一种“貌似同时”的错觉,简单的说,单核cpu实现的多任务就是并发。

并行:

        同一时刻,有多个指令在多个CPU上同时执行。多核cpu可以实现并行。

        并发和并行也可以同时进行,比如打开的进程超过cpu的个数,导致单个cpu控制多个进程,在多个进程之间来回切换。

线程安全隐患:

        如果有多个线程在同时运行,而这些线程可能会同时运行某段代码。程序每次运行的结果如果和单线程的运行结果保持一致,而且其他变量的值也和预期的一样,就是线程安全。

如果不一样,就会有线程安全隐患。

出现线程安全隐患的条件:

  • 多条线程

  • 共享资源

  • 有修改(更新 删除 添加)操作

代码实现查看cpu核数:

public class CpuNum {
    public static void main(String[] args) {
        Runtime runtime = Runtime.getRuntime();
        int cpuNums = runtime.availableProcessors();
        System.out.println(cpuNums); // 6
    }
}

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

(一)继承Thread类

  1. 定义一个类继承

  2. 重写run方法

  3. 创建子类对象,就是线程对象

  4. 调用start方法,开启线程让线程执行

public class ThreadDemo1 {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        MyThread1 t2 = new MyThread1();

        t1.setName("线程1");
        t2.setName("线程2");

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

class MyThread1 extends Thread {
    @Override
    public void run() {
        // 书写线程要执行的方法
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + " HelloWorld");
        }
    }
}

运行结果:

(二)实现Runnable接口(推荐)

  1. 定义类实现Runnable接口

  2. 重写接口中的run方法

  3. 创建Thread类对象

  4. 将Runnable接口的子类对象作为参数传递给Thread类的构造方法

  5. 调用Thread类的start方法开启线程

public class ThreadDemo2 {
    public static void main(String[] args) {
        MyThread2 mr = new MyThread2();

        Thread t1 = new Thread(mr);
        Thread t2 = new Thread(mr);

        t1.setName("线程1");
        t2.setName("线程2");

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

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

运行结果:

也可以直接传参

lambda表达式与方法引用:

public class TestDemo {
    public static void main(String[] args) {
        new Thread(TicketDemo::saleTicket, "窗口1").start();
        new Thread(TicketDemo::saleTicket, "窗口2").start();
        new Thread(TicketDemo::saleTicket, "窗口3").start();

        // 相当于
        new Thread(new Runnable() {
            @Override
            public void run() {
                TicketDemo.saleTicket();
            }
        }, "窗口4").start();

        new Thread(() -> TicketDemo.saleTicket(), "窗口4").start();
    }
}

class TicketDemo {
    static int ticket = 100;

    public static void saleTicket() {
        while (true) {
            if (ticket <= 0) {
                break;
            }
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket-- + "张票");
        }
    }
}

(三)实现Callable接口

  1. 创建一个类,实现Callable接口
  2. 重写call方法(call方法有返回值,表示多线程运行的结果)
  3. 创建自定义类的对象(表示多线程要执行的任务)
  4. 创建FutureTask的对象(管理多线程运行的结果)
  5. 创建Thread类的对象,调用start方法,启动线程
  6. FutureTask.get()方法可以获取多线程运行的结果

假设:张三 有2000元;李四对张三转1000;王五对张三转1500;张三妻子取钱1200。

实现方式一:

public class AccountDemo implements Callable<BigDecimal> {
    // 张三的账户余额
    private static BigDecimal balance = new BigDecimal("2000");

    // 转账和取钱的余额
    private BigDecimal money;

    // false 转账     true 取钱
    private boolean flag = false;

    public AccountDemo(BigDecimal money, boolean flag) {
        this.money = money;
        this.flag = flag;
    }

    @Override
    public BigDecimal call() throws Exception {
        if (!flag) {
            // 转账
            System.out.println(Thread.currentThread().getName() + "转账:" + money);
            balance = balance.add(money);
        } else {
            // 取钱
            if (money.compareTo(balance) > 0) {
                throw new RuntimeException("余额不足");
            } else {
                System.out.println(Thread.currentThread().getName() + "取钱:" + money);
                balance = balance.subtract(money);
            }
        }
        return balance;
    }
}

测试类:

public class AccountDemoTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        BigDecimal money1 = new BigDecimal("1000");
        BigDecimal money2 = new BigDecimal("1500");
        BigDecimal money3 = new BigDecimal("1200");

        AccountDemo lisiCun = new AccountDemo(money1, false);
        AccountDemo wangwuCun = new AccountDemo(money2, false);
        AccountDemo wifeQu = new AccountDemo(money3, true);

        FutureTask<BigDecimal> task1 = new FutureTask<>(lisiCun);
        FutureTask<BigDecimal> task2 = new FutureTask<>(wangwuCun);
        FutureTask<BigDecimal> task3 = new FutureTask<>(wifeQu);

        Thread t1 = new Thread(task1, "李四");
        Thread t2 = new Thread(task2, "王五");
        Thread t3 = new Thread(task3, "wife");

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

        System.out.println(task1.get());
        System.out.println(task2.get());
        System.out.println(task3.get());
    }
}

运行结果:

实现方式二:lambda表达式

public class AccountDemoTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        BigDecimal money1 = new BigDecimal("1000");
        BigDecimal money2 = new BigDecimal("1500");
        BigDecimal money3 = new BigDecimal("1200");

        AccountDemo lisiCun = new AccountDemo(money1, false);
        AccountDemo wangwuCun = new AccountDemo(money2, false);
        AccountDemo wifeQu = new AccountDemo(money3, true);

        FutureTask<BigDecimal> task1 = new FutureTask<>(lisiCun);
        FutureTask<BigDecimal> task2 = new FutureTask<>(wangwuCun);
        FutureTask<BigDecimal> task3 = new FutureTask<>(wifeQu);

        Thread t1 = new Thread(task1, "李四");
        Thread t2 = new Thread(task2, "王五");
        Thread t3 = new Thread(task3, "wife");

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

        System.out.println(task1.get());
        System.out.println(task2.get());
        System.out.println(task3.get());
    }
}

测试类:

public class AccountDemo2Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        BigDecimal money1 = new BigDecimal("1000");
        BigDecimal money2 = new BigDecimal("1500");
        BigDecimal money3 = new BigDecimal("1200");

        FutureTask<BigDecimal> task1 = new FutureTask<>(() -> AccountDemo2.saveMoney(money1));
        FutureTask<BigDecimal> task2 = new FutureTask<>(() -> AccountDemo2.saveMoney(money2));
        FutureTask<BigDecimal> task3 = new FutureTask<>(() -> AccountDemo2.quMoney(money3));

        Thread t1 = new Thread(task1, "李四");
        Thread t2 = new Thread(task2, "王五");
        Thread t3 = new Thread(task3, "wife");

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

        System.out.println(task1.get());
        System.out.println(task2.get());
        System.out.println(task3.get());
    }
}

运行结果:

(四)多线程三种实现方式对比

        当需要线程返回结果时,可以使用Callable接口。如果不需要返回结果,那么Runnable接口就足够了。

三、Thread常见的成员方法

1.getName():返回此线程的名称

如果我们没有给线程设置名字,线程也有默认的名字。

格式:Thread-X (X序号,从0开始)

public class ThreadDemo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

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

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + "@" + i);
        }
    }
}

2.setName(String name):设置线程的名字(构造方法也可以)

如果需要给线程设置名字,可以用set方法进行设置,也可以用构造方法设置。

public class ThreadDemo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread("线程1");
        MyThread t2 = new MyThread("线程2");

        // t1.setName("线程");
        
        t1.start();
        t2.start();
    }
}

class MyThread extends Thread {
    public MyThread() {
    }

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + "@" + i);
        }
    }
}

3.currentThread:获取当前线程对象

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

        哪条线程执行到这个方法,此时获取的就是哪条线程的对象。

        在以前,我们写的所有的代码,其实都是运行在main线程当中。

public class ThreadDemo {
    public static void main(String[] args) {
        // 哪条线程执行到这个方法,此时获取的就是哪条线程的对象
        System.out.println(Thread.currentThread().getName()); // main
    }
}

4.sleep(long time):线程休眠

  1. 哪条线程执行到这个方法,那么哪条线程就会在这里停留对应的时间
  2. 方法的参数:表示睡眠的时间,单位为毫秒
  3. 当时间到了之后,线程会自动地醒来,继续执行下面的其他代码。
4.1在main方法中使用sleep方法
public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("白日依山尽");
        Thread.sleep(5000);
        System.out.println("黄河入海流");
    }
}

运行结果:间隔5秒后打印下一句话

4.2在继承了Thread类中使用sleep方法
public class ThreadDemo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread("线程1");
        MyThread t2 = new MyThread("线程2");

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

class MyThread extends Thread {
    public MyThread() {
    }

    public MyThread(String name) {
        super(name);
    }

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

运行结果:

5.线程的优先级:setPriority与getPriority

java中采用抢占式调度:优先级相同的情况下,随机选择一个线程执行。如果线程的优先级大,抢到CPU资源的概率更高。

线程的优先级从低到高,从1开始,10结束。

如果没有手动设置,则默认值为5。

线程的优先级只表示:该线程有更大的机会抢到资源,并不一定先执行完毕。

public class TestDemo5 {
    public static void main(String[] args) {
        /*// 最大优先级
        System.out.println(Thread.MAX_PRIORITY);
        // 最小优先级
        System.out.println(Thread.MIN_PRIORITY);
        // 默认优先级
        System.out.println(Thread.NORM_PRIORITY);*/

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName());
            }
        }, "线程1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName());
            }
        }, "线程2");

        t1.setPriority(4);
        t2.setPriority(6);

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

运行结果:线程2优先级虽然比线程1的优先级高,但是并不一定先执行,抢到的概率更高而已。 

 

6.setDaemon(boolean):设置守护线程

        当其他所有的非守护线程执行完毕之后,守护线程会陆陆续续结束。

        注意:守护线程不是立即结束,它也需要反应时间。

        守护线程应用场景:OO聊天框在打开的同时也在下载文件,当聊天框关闭,文件下载也会被关闭。

public class ThreadDemo {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        MyThread2 t2 = new MyThread2();

        t1.setName("非守护线程");
        t2.setName("守护线程");

        // 将第二个线程设置为守护线程
        t2.setDaemon(true);

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

class MyThread1 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(getName() + "---" + i);
        }
    }
}

class MyThread2 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            System.out.println(getName() + "---" + i);
        }
    }
}

运行结果:

6.yield():出让线程/礼让线程

能够让结果尽可能均匀一些,但是不是绝对的。

public class ThreadDemo {
    public static void main(String[] args) {
        MyThread1 t1 = new MyThread1();
        MyThread1 t2 = new MyThread1();

        t1.setName("线程1");
        t2.setName("线程2");

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

class MyThread1 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(getName() + "---" + i);
            // 表示出让当前CPU的执行权
            Thread.yield();
        }
    }
}

7.join():插入线程/插队线程

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        MyThread t = new MyThread();
        t.setName("线程1");
        t.start();

        /**
         * 表示把t这个线程,插入到当前线程之前
         * t:线程1
         * 当前线程:main线程
         */
        t.join();

        // 执行在main线程当中的
        for (int i = 1; i <= 5; i++) {
            System.out.println("main线程" + i);
        }
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(getName() + "---" + i);
            Thread.yield();
        }
    }
}

运行结果:

四、线程的生命周期

        sleep方法会让线程睡眠,睡眠时间到了之后,不会立刻执行后面的代码。

        因为sleep方法执行完毕之后,线程又恢复到了就绪状态,此时,该线程会去重新抢夺CPU的执行权,抢到了才会执行后面的代码。

五、线程的安全问题

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

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

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

class MyThread extends Thread {
    static int ticket = 0;

    @Override
    public void run() {
        while (true) {
            if (ticket < 100) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket++;
                System.out.println(getName() + "正在卖第" + ticket + "张票");
            } else {
                break;
            }
        }
    }
}

运行结果:相同的票出现了多次;出现了超出范围的票。

原因分析:当某个线程执行到了tocket++,还没有开始打印,打印这行代码就被其他线程抢夺了。

根本原因:线程执行时,有随机性。CPU的执行权随时有可能被其他线程抢走。

六、同步代码块——synchronized

格式:

synchronized(锁对象){
    可能会产生线程安全隐患的代码
}
  1. 特点1:锁默认打开,有一个线程进去了,锁自动关闭
  2. 特点2:里面的代码全部执行完毕,线程出来,锁自动打开

修改代码,实现想要的结果: 

class TicketDemo {
    // 票是多个线程共享的数据
    static int ticket = 100;

    // 锁对象一定要保证多个锁对象是共享的
//    static Object obj = new Object();

    public static void saleTicket() {
        // 卖票
        while (true) {
            try {
                // 让当前线程休眠1毫秒
                Thread.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            synchronized ("a") { // 自动加锁
                if (ticket <= 0) {
                    break;
                }
                System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket-- + "张票");
            } // 自动释放锁
        }
    }
}

运行结果:

同步代码块的细节:

  1. 同步代码块要写在死循环的里面。
  2. 括号里的锁一定要是唯一的,同步代码块中的锁对象可以是任意的对象。但是有多个线程时,要使用同一个锁对象才能保证线程安全。
  3. 方法区中的所有内容都被线程共享的。所以方法区的内容都可以作为锁对象,所以可以写synchronized ("a") 或者可以用当前类.class替代,或者任意一个类.class作为锁对象,因为类和String都在方法区中。
  4. 注意,锁对象中不能使用(new 类名),此时对象在堆中,无法共享。

七、同步方法与静态同步方法

(一)同步与异步介绍

同步:一段代码在同一时刻只允许一个线程执行。

异步:一段代码在同一时刻允许多个线程执行。

  • 同步一定是线程安全
  • 线程安全不一定同步
  • 异步不一定线程不安全
  • 线程不安全一定是异步
  1. ArrayList和LinkedList、HashMap是异步线程不安全的
  2. Hashtable、Vector是同步线程安全的
  3. ConcurrentHashMap是异步线程安全的,采用了分桶锁的机制

        以ConcurrentHashMap为例,它的每一个位置称为“桶(bucket)”,每个链表上都加了锁,线程1不执行完,线程4无法对第一条链表进行修改,此时看起来是同步的;但是整体上看,可以有多个线程同时访问多个链表,此时就是异步的。

(二)同步方法 

格式:

权限修饰符 sychronized 返回值类型 方法名(方法参数){...}

特点1:同步方法是锁住方法里面所有的代码。

特点2:锁对象不能自己指定。

            同步方法的锁对象:this,就是当前方法的调用者,this只有一个,多个方法共享同一个this。

            静态同步方法的锁对象:类名.class

编写技巧:先编写同步代码块,再将代码块中的代码抽取成一个方法。

抽取前:

public class TestDemo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        Thread t1 = new Thread(ticket, "窗口1");
        Thread t2 = new Thread(ticket, "窗口2");
        Thread t3 = new Thread(ticket, "窗口3");

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

class Ticket implements Runnable {
    int count = 100;

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1L);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized ("a") {
                if (count <= 0) {
                    break;
                } else {
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + count-- + "张票");
                }
            }
        }
    }
}

抽取后:

public class TestDemo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        Thread t1 = new Thread(ticket, "窗口1");
        Thread t2 = new Thread(ticket, "窗口2");
        Thread t3 = new Thread(ticket, "窗口3");

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

class Ticket implements Runnable {
    int count = 100;

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1L);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            if (method()) break;
        }
    }

    public synchronized boolean method() {
        if (count <= 0) {
            return true;
        } else {
            System.out.println(Thread.currentThread().getName() + "正在卖第" + count-- + "张票");
            return false;
        }
    }
}

运行结果:

关于StringBuilder与StringBuffer:

单线程情况下,不需要考虑数据安全,用StringBuilder;如果是多线程情况下,需要考虑数据安全,用StringBuffer。

(三)静态同步方法 

// 静态同步方法  静态同步方法的锁对象  类名.class 
private synchronized static boolean method() {
    if (ticket <= 0) {
       return true;
    }
     System.out.println(Thread.currentThread().getName() + ",您的座位是" + ticket--);
     return false;
}

八、Lock锁

        sychronized关键字是让线程进入代码块后自动开锁和关锁,我们也可以手动指定开锁和关锁。

        这样子就可以直接看到在哪里加上了锁,在哪里释放了锁。Lock是JDK5之后出现的一个新的锁对象。

        相比于比使用sychronized方法和语句,Lock可以获得更广泛的锁定操作:

        Lock中提供了获得锁和释放锁的方法:

  • void lock():获得锁
  • void unlock():释放锁,无论怎样都要释放锁,所以需要将这段代码放在finally中

        Lock是接口不能直接实例化,需要采用它的实现类ReentrantLock来实例化

代码实现卖票手动加锁和释放锁:

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();

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

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

class MyThread extends Thread {
    static int ticket = 0;
    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            lock.lock();
            try {
                if (ticket == 100) {
                    break;
                } else {
                    Thread.sleep(100);
                    ticket++;
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

运行结果:

九、死锁

        多线程死锁是在多线程环境中常见的一个问题,它指的是多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些线程都将无法向前推进。以下是对多线程死锁的详细解析:

(一)死锁产生的原因

  1. 系统资源竞争:系统中拥有的多个不可剥夺资源数量不足以满足多个线程运行的需要。当线程在运行过程中因为争夺这些资源而无法继续时,就可能陷入僵局。例如,两个线程分别持有一个资源并等待对方持有的另一个资源,从而形成死锁。
  2. 进程推进顺序非法:线程在运行过程中请求和释放资源的顺序不当,也可能导致死锁。

(二)死锁产生的必要条件

同时满足以下四个条件时,才会发生死锁:

  1. 互斥条件:线程要求对所分配的资源进行排他型控制,即在一段时间内,某个资源仅为一个线程所占有。
  2. 不剥夺条件:线程所获得的资源在未使用完毕之前,不能被其他线程强行夺走,即只能由获得该资源的线程自己来主动释放。
  3. 请求和保持条件:线程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他线程占有。此时,请求线程被阻塞,但对自己已获得的资源保持不放。
  4. 循环等待条件:存在一种线程资源的循环等待链。链中的每一个线程已获得的资源同时被链中的下一个线程所请求。

(三)死锁的解决方案

为了避免和解决多线程死锁问题,可以采取以下措施:

  1. 避免使用多个锁:仔细设计代码,尽量避免使用多个锁,从而减少死锁的概率。
  2. 加锁顺序一致:如果必须使用多个锁,确保在不同的线程中以相同的顺序获取锁。这样可以减少死锁的可能性。
  3. 使用定时锁:利用如Java中的java.util.concurrent包提供的定时锁(如TryLock方法)来替代传统的锁。这样可以在尝试获取锁时设置一个超时时间,如果超时则放弃请求,从而避免死锁。
  4. 使用Lock对象代替synchronized:使用Lock接口提供的可重入锁(如ReentrantLock)代替synchronized关键字。这样可以更好地控制锁的获取和释放过程,从而避免死锁。
  5. 使用线程池:通过线程池管理线程的创建和销毁,可以降低死锁的可能性。因为线程池中的线程是复用的,减少了线程创建和销毁的频率,从而降低了死锁的风险。
  6. 检测和恢复死锁:使用工具或者编写代码来检测死锁的发生,并采取相应的措施进行恢复。例如,中断某个线程或者释放某个锁,以打破死锁状态。

        综上所述,多线程死锁是一个需要重视和解决的问题。通过合理设计代码、使用适当的锁机制以及检测和恢复死锁等措施,可以有效地预防和解决多线程死锁问题,提高程序的稳定性和性能。

十、等待唤醒机制

生产者消费者模式是一个十分经典的多线程协作的模式,一方生产数据,另一方消费数据,交替执行,而不是随机执行。

生产者和消费者(常见方法):

void wait():当前线程等待,直到被其他线程唤醒。

void notify():随机唤醒单个线程。

void notifyAll():唤醒所有线程。

(一)吃货厨师案例

代码实现:

定义一个桌子,上面有面条时,吃货开吃,没有面条时,厨师做面条。

public class Desk {
    // 桌子上是否有面条:0:没有面条    1:有面条
    public static int foodFlag = 0;

    // 总个数
    public static int count=10;

    // 锁对象
    public static Object lock = new Object();
}

定义吃货:

public class Foodie extends Thread {
    @Override
    public void run() {
        while (true){
            synchronized (Desk.lock){
                if (Desk.count==0){
                    break;
                }else {
                    if (Desk.foodFlag==1){
                        // 桌子上有食物,厨师就等待
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }else {
                        // 桌子上没有食物,厨师开始做饭
                        System.out.println("厨师做了一晚面条");
                        // 修改桌子上面条的状态
                        Desk.foodFlag=1;
                        // 唤醒吃货吃面条
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}

定义厨师:

public class Cook extends Thread {
    @Override
    public void run() {
        while (true){
            synchronized (Desk.lock){
                if (Desk.count==0){
                    break;
                }else {
                    if (Desk.foodFlag==0){
                        // 没有面条就等待
                        try {
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }else {
                        // 有面条就开吃
                        Desk.count--;
                        System.out.println("吃货吃了一碗面条,还能再吃"+Desk.count+"碗");
                        // 吃完后修改面条状态
                        Desk.foodFlag=0;
                        // 唤醒厨师
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}

测试代码:

public class ThreadDemo {
    public static void main(String[] args) {
        Cook cook = new Cook();
        Foodie foodie = new Foodie();

        cook.setName("吃货");
        foodie.setName("厨师");

        cook.start();
        foodie.start();
    }
}

运行结果:

(二)学生问问题案例

public class TestDemo9 {
    public static void main(String[] args) {
        Student student = new Student();
        student.setName("小红");
        student.setGender("女生");
        new Thread(new Ask(student)).start();
        new Thread(new Change(student)).start();
    }
}

// 问问题类
class Ask implements Runnable{

    private Student student;

    public Ask(Student student) {
        this.student = student;
    }

    @Override
    public void run() {
        while (true){
            synchronized (student){
                if (!student.flag){
                    try {
                        // 让当前线程陷入等待
                        student.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                System.out.println("老师,我是" + student.getName() + ",我是" + student.getGender() + "我要问问题");
                student.flag = false;
                // 唤醒线程
                student.notify();
            }
        }
    }
}


// 切换学生类
class Change implements Runnable{

    private Student student;

    public Change(Student student) {
        this.student = student;
    }

    @Override
    public void run() {
        while (true){
            synchronized (student){
                if (student.flag){
                    try {
                        student.wait();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
                if ("小刚".equals(student.getName())){
                    student.setName("小红");
                    student.setGender("女生");
                }else {
                    student.setName("小刚");
                    student.setGender("男生");
                }
                student.flag = true;
                // 唤醒一个正在等待的线程
                student.notify();
            }
        }
    }
}

十一、阻塞队列的唤醒等待机制

        阻塞队列是一种特殊的队列,它支持两个附加操作:在尝试添加元素到已满队列时会阻塞,在尝试从空队列中移除元素时也会阻塞。这种队列通常用于生产者-消费者问题,其中生产者线程向队列中添加元素,而消费者线程从队列中移除元素。

        阻塞队列的继承结构:

        Iterable—>Collection—>Queue—>BlockingQueue

        BlockingQueue的实现类:

        (1)ArrayBlockingQueue:底层是数组,有界。

        ArrayBlockingQueue 提供了阻塞的 put 和 take 方法。这意味着如果队列已满,put 方法会阻塞直到有空间可用;如果队列为空,take 方法会阻塞直到有元素可取。

        这种特性非常适合生产者—消费者模型,其中生产者(如Cook)生成数据并放入队列,而消费者(如Foodie)从队列中取出数据并处理。 

        (2)LinkedBlockingQueue:底层是链表,无界。但不是真正的无界,最大值为int的最大值。

下面,我们来利用阻塞队列完成生产者和消费者(等待唤醒机制)的代码:

注意:生产者和消费者必须使用同一个阻塞队列。

Cook类:

public class Cook extends Thread {
    ArrayBlockingQueue<String> queue;

    public Cook(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                queue.put("面条");
                System.out.println("厨师做了一碗面条");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

Foodie类:

public class Foodie extends Thread {
    ArrayBlockingQueue<String> queue;

    public Foodie(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true) {
            try {
                String take = queue.take();
                System.out.println(take);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

测试类:

public class TestDemo {
    public static void main(String[] args) {
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);

        Cook cook = new Cook(queue);
        Foodie foodie = new Foodie(queue);

        cook.setName("厨师");
        foodie.setName("吃货");

        cook.start();
        foodie.start();
    }
}

运行结果:有重复是因为打印语句在锁的外面,底层确实是一个在生产数据,一个在消费数据

十二、线程的六大状态

十三、wait和sleep的区别

        sleep需要指定休眠时间,到点自然醒。如果线程没有锁,那么会释放执行权。如果线程有锁,不释放执行权。这个方法是设计在Thread类上的方法,是一个静态方法。

        wait可以指定也可以不指定时间。如果不指定时间需要唤醒。释放执行权也释放锁。这个方法是设计在Object上的一个普通方法。

        注意:使用wait和notify必须结合锁来使用,而且要使用同一个锁对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值