【JAVA】多线程详解

❤️ Author: 老九
☕️ 个人博客:老九的CSDN博客
🙏 个人名言:不可控之事 乐观面对
😍 系列专栏:

进程和线程

线程

  • 因为计算机的发展,系统支持多任务了。所以就需要并发编程。通过多进程,是完全可以实现并发编程的,但是也有个问题:如果要频繁的创建/销毁进程,就需要分配内存,打开文件,就需要释放资源,释放内存。执行任务的成本较高,主要是因为资源的 创建 和 释放 不是高效的,所以成本较高。

实现并发编程中,解决 创建 和 销毁 消耗资源大的问题,有两个方法:

1.进程池:进程池可以解决问题,提高效率。但是也有问题,池子里的闲置进程,不使用的时候也在消耗系统资源,消耗的系统资源太多了。
2.使用线程来实现并发编程:线程比进程更轻量,每个进程可以执行一个任务,每个进程也能执行一个任务(执行一段代码),也能够并发编程,创建线程的成本比创建进程要低很多,销毁线程的成本也比销毁进程低很多,调度线程的成本也比调度进程低很多。
3.但是线程不是越多越好,如果线程多了,这些线程可能要竞争同一个资源,这个时候,整体的速度就受到了限制,因为整体硬件资源是有限的。

线程和进程的区别和联系

1.进程包含线程:一个进程里可以有一个线程,也可以有多个线程。
2.进程和线程都是为了处理并发编程这样的场景。但是进程有问题,频繁的创建和释放的时候效率很低,相比之下,线程更轻量,创建和释放的效率更高。因为线程少了申请和释放的过程
3.操作系统创建进程,要给进程分配资源,进程是操作系统分配资源的基本单位。操作系统创建的线程,是要在 CPU 上调度执行,线程是操作系统调度执行的基本单位。
4.进程具有独立性,每个进程有各自的虚拟地址空间,一个进程挂了,不会影响到其它进程。同一个进程中的多个线程,公用同一个内存空间,一个线程挂了,可能影响到其他线程,甚至导致程序崩溃。
5.线程比进程轻量的原因:进程重量重在资源的申请释放。线程是包含在进程当中的,一个进程中的多个线程共用同一份资源。只是创建第一个进程的时候(由于要分配资源),成本是相对高的,后续在这个进程中再创建其他线程,这个时候成本就要更低一些,因为不必再分配资源了。
6.可以把进程比作一个工厂,线程就是生产线,线程多了之后,生产效率就高了,如果再建一个工厂来生产,效率也可以变高,但是资源花费大,所以通过增加一条生产线(线程)来提高效率的话,资源花费就很小。

并发编程

Java 中并发编程主要用多线程,不同于其他语言。go 语言是通过多协程来实现,erlang 是通过 actor 模型实现并发, js 是通过定时器 + 实际回调的方式实现并发。

最基本的多线程代码

  • 通过 Thread 来创建,不过这里是创建一个自己的 MyTread 并重写 run 方法来看线程的情况。run 方法就描述了线程内部要执行什么代码代码如下:
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("hello thread");
    }
}
public class TestDemo {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();
    }
}
  • run 方法里面描述了线程内部要执行哪些代码,每个线程都是并发执行的(各自执行各自的代码,就是告诉线程,要执行的代码是什么)。不是一定义这个类,一写 run 方法,线程就创建出来,相当于有活了,但是还没干。调用 new 的对象的 start 方法,才是真正的创建了线程。这里可以创建很多个线程,这些线程都是同一个进程内部创建的。运行结果如下:
    在这里插入图片描述

最简单的并发编程代码

  • 一个进程中,至少会有一个线程,在一个 Java 进程中,至少会有一个调用 main 方法的线程**,自己创建的 t 线程,和自动创建的 main 线程,就是并发执行的关系**(宏观上看起来是同时执行)。这里的代码就是 MyThread 和 main 一起并发执行。代码如下:
class MyThread2 extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("Thread!");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class TestDemo2 {
    public static void main(String[] args) {
        MyThread2 t = new MyThread2();
        t.start();
        while (true) {
            System.out.println("Main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

这里的结果就是 Thread!和 Main 交替输出,每次输出和上次输出差不多相隔一秒:
在这里插入图片描述
在阻塞一秒之后,先唤醒 Thread 还是 Main,是不确定的。对于操作系统来说,内部对于线程之间的调度顺序,在宏观上可以认为是随机的**(抢占式执行)**。

Runnable接口

Runnable 就是在描述一个任务,然后重写 run 方法,就是要执行的任务内容。然后通过 Runnable 把描述好的任务交给 Thread 实例:

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("hello");
    }
}
public class TestDemo3 {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start();
    }
}

在这里插入图片描述

通过匿名内部类

匿名继承Thread类

public static void main(String[] args) {
    Thread t = new Thread() {
        @Override
        public void run() {
            System.out.println("Thread");
        }
    };
    t.start();
}

这里的匿名内部类是继承自 Thread 类,同时重写了 run 方法,同时再 new 出这个匿名内部类的实例。运行结果如下:

匿名Runnable

public static void main(String[] args) {
    Thread t = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("thread");
        }
    });
    t.start();
}

这里 new 的 Runnable 针对这个创建的匿名内部类,同时 new 出的 Runnable 实例传给 Thread 的构造方法。

Thread和Runnable的选择

  • 通常认为选择 Runnable 来写更好一些,能够做到让线程和线程执行的任务,更好的解耦。写代码注重 高内聚,低耦合。Runnable 单纯的只是描述了一个任务,至于这个任务是要通过一个进程来执行,还是线程池来执行,还是协程来执行,Runnable 并不关心,Runnable 里面的代码也不关心。

lambda表达式

在使用多线程的时候,也可以写成 lambda 表达式,这种表达式方法更简单:

public static void main(String[] args) {
    Thread t = new Thread(()-> {
        System.out.println("Thread");
    });
    t.start();
}

多线程对时间的优化

我们来计算两个变量的自增,从 0 自增到 10亿,然后对比时间。一种是串行执行,一种是并发执行。不过要注意的是,多线程当中的时间戳代码是在 main 线程中,所以要等到 t1 和 t2 都执行玩然后再计时。所以通过 join(); 方法来等待计时结束,代码如下:

public static void serial() {
    long beg = System.currentTimeMillis();
    long a = 0;
    for (int i = 0; i < 10_0000_0000; i++) {
        a++;
    }
    long b = 0;
    for (int i = 0; i < 10_0000_0000; i++) {
        b++;
    }
    long end = System.currentTimeMillis();
    System.out.print((end-beg)+"ms");
}
public static void concurrency() throws InterruptedException {
    long beg = System.currentTimeMillis();
    Thread t1 = new Thread(()->{
        long a = 0;
        for (int i = 0; i < 10_0000_0000; i++) {
            a++;
        }
    });
    t1.start();
    Thread t2 = new Thread(()->{
        long b = 0;
        for (int i = 0; i < 10_0000_0000; i++) {
            b++;
        }
    });
    t2.start();
    t1.join();
    t2.join();
    long end = System.currentTimeMillis();
    System.out.print((end-beg)+"ms");
}
public static void main(String[] args) throws InterruptedException {
    serial();
    System.out.println();
    concurrency();
}

在这里插入图片描述

  • t1.join()在t2.start()之前会导致t1线程先执行完后再执行t2线程,而t2.start()在t1.join()之前会让t1和t2可以并行执行。
  • 可以看出,多线程的运行效率确实比串行要高,不过如果数很小的时候,就不适合用多线程了,因为创建线程也需要时间,如果很小的话,用串行就够了。多线程适用于 CPU 密集型的程序,程序要进行大量的计算,使用多线程就可以更充分的利用 CPU 的多核资源。

Thread 类的属性和方法

Thread创建线程对象并命名

在创建完 Thread 对象之后,可以对其进行命名。命名之后在调试的时候很方便:

public static void main(String[] args) {
    Thread t1 = new Thread(()-> {
        while (true) {
            System.out.println("Thread t1");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    },"Thread t1");
    t1.start();

    Thread t2 = new Thread(()-> {
        while (true) {
            System.out.println("Thread t2");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    },"Thread t2");
    t2.start();
}

isAlive

  • 创建出对象之后,在调用 start 之前,系统当中是没有对应线程的,在 run 方法执行完了之后,系统当中的线程就销毁了。但是 t 这个对象可能还存在。通过 isAlive 就能判断当前系统的线程的运行情况。
public class TestDemo{
    public static void main(String[] args) throws InterruptedException {
       Thread t1 = new Thread(()->{
           int count = 0;
           while (count < 2){
               System.out.println("thread t1");
               count++;
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       },"Thread t1");
        t1.start();
        t1.join();
        System.out.println(t1.isAlive());
    }
}

start

**start 决定了系统中是不是真的创建出线程,run 只是一个普通的方法,描述了任务的内容。**代码如下:

public static void main(String[] args) {
    Thread t = new Thread(()-> {
        while (true) {
            System.out.println("Thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t.start();
}

在这里插入图片描述
那么把 start 换成 run :

public static void main(String[] args) {
    Thread t = new Thread(()-> {
        while (true) {
            System.out.println("Thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t.run();
}

在这里插入图片描述
发现 run 也能输出 Thread 。**但关键是 run 并没有创建线程,这里的 run 是输出了任务的内容,而不是创建线程。Thread 则是在操作系统当中创建线程。**下面用一个更简单理解的代码来演示:

public static void main(String[] args) {
    Thread t = new Thread(()-> {
        while (true) {
            System.out.println("Thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t.start();
    while (true) {
        System.out.println("Main");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述
因为 start 是创建线程,所以会和 main 线程并发执行。如果换成 run 的话:

public static void main(String[] args) {
    Thread t = new Thread(()-> {
        while (true) {
            System.out.println("Thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t.run();
    while (true) {
        System.out.println("Main");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述
只输出了任务内容,没有创建线程,只是从上往下执行。

中断线程

就是让线程停下来,线程停下来的关键,是要让线程对应的 run 方法执行完。(还有一个特殊情况:是 main 这个线程,对于 main 来说,得是 main 方法执行完,线程就完了)

设置自定义标志位

通过手动设置一个标志位,来控制线程是否要执行结束。代码如下:

private static boolean isQuit = false;
public static void main(String[] args) {
    Thread t = new Thread(()-> {
        while (!isQuit) {
            System.out.println("Thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    t.start();
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    isQuit = true;
    System.out.println("终止线程");
}

在这里插入图片描述
因为多个线程共同用一个虚拟地址空间,因此 main 线程修改的 isQuit 和 t 线程判断的 isQuit 是同一个值。

使用 Thread 内置的标志位

通过Thread.interrupted()方法终止线程
通过Thread.currentThread().isInterrupted(),其中 currentThread 能够获取到当前线程的实例。

public static void main(String[] args) {
    Thread t = new Thread(()-> {
        while (!Thread.currentThread().isInterrupted()) {
            System.out.println("Thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println("收尾工作");
                break;
            }
        }
    });
    t.start();
    try {
        Thread.sleep(5000);
    } catch (InterruptedException e) {

        e.printStackTrace();
    }
    t.interrupt();
}

在这里插入图片描述
这里调用interrupt()方法,可能会出现两种情况;
1.如果线程处于就绪状态,就是设置线程的标志位为true
2.如果t线程处在阻塞状态(sleep 休眠了),就会触发一个interruptException异常,使其从阻塞状态被唤醒,允许线程在捕获异常后继续执行或进行清理操作。

线程等待join

多个线程之间,调度顺序不确定。线程之间的执行是按照调度器来安排的,这个过程可能是无序,随机的。线程等待,就是其中一种,控制线程执行顺序的手段,主要是控制线程结束的先后顺序。
哪个线程调用join,就等这个线程执行完毕之后(对应的线程的run方法执行完),再执行别的线程
但是 join 默认情况下,是死等。所以 join 提供了另外一个版本,可以执行等待时间,最长等多久,等不到就撤了。就是在 join(时间)

public class JoinExample {
    public static void main(String[] args) {
        // 创建一个子线程
        Thread workerThread = new Thread(new Worker());
        workerThread.start();

        // 等待子线程执行,最多等待2秒
        try {
            workerThread.join(2000);
            if (workerThread.isAlive()) {
                System.out.println("等待超时,主线程继续执行");
            } else {
                System.out.println("子线程已经完成,主线程继续执行");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class Worker implements Runnable {
        @Override
        public void run() {
            System.out.println("子线程开始执行");
            try {
                Thread.sleep(3000); // 模拟子线程执行3秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("子线程执行结束");
        }
    }
}

获取当前线程的引用

Thread 自带的 getName实现

通过 currentThread() 的 getName() 方法 ,不过要注意的是,哪个线程调用这个方法,获取到的就是哪个线程的引用。

public static void main(String[] args) {
    Thread t = new Thread() {
        @Override
        public void run() {
            System.out.println(this.getName());
        }
    };
    t.start();
}

这里是通过 Thread 的方式来创建线程。此时在 run 方法当中,直接通过 this 拿到的就是 Thread 的实例。运行结果如下:
在这里插入图片描述
没指定名字,默认是 0。

Runnable 实现

如果是 Runnable 的话,就不能用 this.getName 了,因为 Runnable 是一个单纯的任务,没有 name 属性。会直接抛出受查异常。所以只能用 Thread.currentThread().getName() ,如果是 lambda 表达式,也是这样。代码如下:

public static void main(String[] args) {
    Thread t = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
        }
    });
    t.start();
}

在这里插入图片描述

线程休眠 sleep

进程和线程都是通过PCB描述的,PCB通过 双向链表 组织的,通过双向链表,就可以可以轻松地在PCB之间建立前后关系,如果一个进程包含了多个线程,所对应的就是一组PCB,PCB上有个字段tgroupid,这个id就相当于进程的id,同一个进程当中若干个线程的tgroupId是相同的。
>

线程安全

线程安全是线程当中最重要,最复杂的问题。多进程是最基本的处理并发编程的任务。操作系统调用线程的时候,是随机的(抢占式执行),因为是抢占式的,所以可能出现 bug 如果调度随机性,引入了 bug,那么就认为代码线程是不安全的。

class Counter {
    public static int count;
    public void increase() {
        count++;
    }
}
public class Test2 {
    private static Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

在代码中,用 count 作为两个线程自增的变量。运行多次,结果如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
运行多次之后发现结果总是不能达到 100000,就说明程序有 bug,两个程序是并发执行的,如果两个线程同时自增,就只加了 1。
count++ 其实是 三个 CPU指令:
1.把内存当中的 count 值,加载到 CPU 寄存器当中。
2.把寄存器当中的值 + 1。
3.把寄存器的值写回到 内存的 count 当中。

  • 这里加的结果是在 50000 - 100000 之间。因为有一部分是串行的,有一部分是交错的。所以,如果能让 t1 先执行完,然后再让 t2 执行,就可以解决这样的问题了。

通过加锁来保证线程安全

像上面这种情况,就可以通过加锁来解决,我们这里使用 synchronized 来对 count++ 加锁,因为问题是出在 count++ 这里,所以我们对 count++ 加锁就好了。也就是在自增之前先加锁,自增之后解锁。解锁之后再执行另外一个线程。加锁之后,并发程度降低,数据更安全了,但是速度慢了。并发性越高,数据越不安全,但速度越快,但是可能会出现一些问题,就像这里的 count++ 。实际开发当中,一个线程要做的事很多。可能只有某一个步骤有线程安全,所以只对有线程安全的加锁就好了。代码如下:

class Counter {
    public static int count;
    synchronized public void increase() {
        count++;
    }
}
public class Test2 {
    private static Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(()-> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(counter.count);
    }
}

在这里插入图片描述

线程不安全的原因

1.线程是抢占式执行,线程间的调度充满随机性,是线程不安全的万恶之源。
2.多个线程对同一个变量进行操作。
3.针对变量的操作不是原子性的(要么全部执行完,要不就不执行)
4.内存可见性:是指当某个线程正在使用对象状态而另一个线程在同时修改该状态,需要确保当一个线程修改了对象状态后,其他线程能够立即看到发生的状态变化。例如:针对同一个变量,一个线程进行读操作(循环进行很多次),一个线程进行修改操作(合适的时候执行一次)。读内存比读寄存器慢很多,循环一直去读的话,消耗就会很多,因此,频繁的读内存的值,就会非常低效,而且修改的线程迟迟不修改,读到的值一直是一样的值。所以,读的时候,就可能不从内存读数据了,而是直接从寄存器里面读。如果此时 把值修改了,那么就读不到这个值了。
5.指令重排序:也是编译器优化的一种操作。就是调整代码的执行顺序,执行效果不变,但是效率就提高了。调整的前提也是逻辑不变。代码是单线程的话,一般不会出问题,如果是多线程的话,就可能出现问题,避免问题还是通过 synchronized 加锁来操作。

内存可见性导致线程不安全

private static int isQuit = 0;
public static void main(String[] args) {
    Thread t = new Thread(()->{
        while (isQuit == 0) {

        }
        System.out.println("循环结束,t 线程退出");
    });
    t.start();
    Scanner scanner = new Scanner(System.in);
    System.out.println("请输入一个 isQuit 的值:");
    isQuit = scanner.nextInt();
    System.out.println("main 线程执行完毕");
}

在这里插入图片描述
通过图片可以看到,当输入值之后,已经不满足线程执行的条件了,但是线程并没有停止,就是因为内存可见性的原因,导致线程还在运行。如果在主线程中加入sleep的话,引入一个短暂的睡眠来确保写入的值已经在主内存中可见,然后在新线程中进行检查。

private static volatile int isQuit = 0;

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        while (isQuit == 0) {

        }
        System.out.println("循环结束,t 线程退出");
    });
    t.start();
    Scanner scanner = new Scanner(System.in);
    System.out.println("请输入一个 isQuit 的值:");
    isQuit = scanner.nextInt();

    // 确保isQuit的值在新线程中可见
    try {
        Thread.sleep(100); // 休眠一段时间,确保主线程写入的值可见
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("main 线程执行完毕");
}

解决内存可见性的办法(volatile)

1.使用 synchronized 关键字加锁。不光保证原子性,还能保证内存可见性。被 synchronized 包裹起来的代码,编译器就不敢轻易的做出上面优化的那种操作。
2.使用 volatile 关键字,volatile 和原子性无关,但是能保证内存可见性。就会禁止编译器做出优化,使得编译器每次判断的时候,都会重新从内存当中读取 isQuit 的值。

private static volatile int isQuit = 0;
public static void main(String[] args) {
    Thread t = new Thread(()->{
        while (isQuit == 0) {

        }
        System.out.println("循环结束,t 线程退出");
    });
    t.start();
    Scanner scanner = new Scanner(System.in);
    System.out.println("请输入一个 isQuit 的值:");
    isQuit = scanner.nextInt();
    System.out.println("main 线程执行完毕");
}

在这里插入图片描述

指令重排序

指令重排序也会影响到线程安全问题,也是编译器优化的一种操作。举例:去超市买东西:
在这里插入图片描述
如果按照菜单顺序买菜的话,就会绕很多路,浪费很多时间。如果重新排序之后再买的话,就是下面这种情况:
在这里插入图片描述
就会节省很多时间,这就是指令重排序带来的优化。不过在有些时候,写的功能很多的情况下,指令重排序也会导致程序,出现 bug。所以通过 synchronized 来解决这种问题。

synchronized使用

synchronized 是同步的意思。多线程中,线程安全中,同步 指的是“互斥”,一个进行的时候,另外一个就不能进行了。

直接修饰使用方法

使用 synchronized 直接修饰普通方法。本质是对某个对象进行加锁。在 Java 当中,每个类都是继承自 Object 。每个 new 出来的实例,里面一方面包含了自己安排的属性,一方面包含了“对象头”,对象的一些元数据。此时的锁对象就是 this,如下图所示:
在这里插入图片描述

当多个线程试图获得锁时,只有当它们竞争相同的锁对象时才会发生竞争。如果多个线程分别尝试获取不同的锁对象,它们之间不会互相竞争。
举个简单的例子来理解:假设有两个线程A和B,它们都希望访问两个不同的资源,资源X和资源Y。如果线程A尝试获取资源X的锁,而线程B尝试获取资源Y的锁,它们之间没有竞争,因为它们操作的是不同的资源,互不干扰。
但是,如果线程A和线程B都试图获取相同的资源X的锁,那么它们将会发生竞争,因为它们都想要独占相同的资源。在这种情况下,只有一个线程能够成功获取锁,而另一个线程必须等待或采取其他操作。

修饰代码块

使用 synchronized 修饰一个代码块。需要显示指定针对哪个对象加锁(Java 中的任意对象都可以作为锁的对象)。代码如下:

public void increase() {
    synchronized (this) {
        count++;
    }
}

修饰一个静态方法

使用 synchronized 修饰一个静态方法。相当于针对当前的类对象加锁,也就是反射。把 synchronized 修饰到 static 方法上:
在这里插入图片描述
就相当于是下面这种情况:
在这里插入图片描述

可重入锁

就是外层先加了一次锁,然后里层再对对象加一次锁。代码示例:

synchronized public void increase() {
    synchronized (this) {
        count++;
    }
}

外层锁: 进入方法,则开始加锁,这次能够加锁成功,因为当前锁没有人占用。
里层锁: 进入代码块,开始加锁,这次加锁不能加锁成功,因为这个锁被外层占用了,要等到外层锁释放,里层锁才能加锁。
外层锁要执行完整个方法,才能释放。但是要想执行完整个方法,就得让里层锁加锁成功继续往下走。所以就变成死锁了。
为了防止出现这种情况,JVM 就实现了可重入锁,就是发生这种操作的的时候,不会死锁。就是可重入锁内部,会记录当前的锁被哪个线程占用,同时也会记录一个加锁次数。线程 a 针对锁第一次加锁的时候,是可以加锁成功的。锁内部就记录了当前的占用着的是 a,加锁次数是 1。后续再 a 对锁进行加锁,此时就不是真加锁,而是单纯的把计数器自增,加锁次数为 2。然后在解锁的时候,先把计数进行 -1,当锁的计数减到 0 的时候,就真的解锁。可重入锁的意义就是:降低了程序员的负担(降低了使用成本,提高了开发效率),但也带来了代价,程序中需要又更高的开销(维护锁属于哪个线程,并且加减计数,降低了运行效率)。

哲学家就餐问题

在这里插入图片描述
每个哲学家都很固执,在想要吃饭的时候,如果筷子被别人占用,就会死等下去。所以,如果五个人同时拿起左手边的筷子,就陷入死锁了。每个人都能拿起左手的筷子,每个人都拿不起右手的筷子。

死锁的四个必要条件

1.互斥使用:一个锁被一个进程或线程占用之后,其他线程占用不了(锁的本质,保证原子性)。
2.不可抢占:一个锁被一个线程占用之后,其他线程不能把这个锁给抢走(挖墙脚不行)。
3.请求和保持:进程或线程至少有一个资源,并且在请求其他资源时保持对己有资源的占有(在等待其他资源时不释放自己资源)
4.环路等待:等待关系,成环了:A 等 B,B 等 C,C 又等 A。避免环路等待:约定好,针对多把锁加锁的时候,有固定的顺序就好。所有的线程都遵守同样的规则顺序,就不会出现环路等待。

Java标准库当中的类

线程安全的部分:
1.ConcurrentHashMap
2.StringBuffer
3.String
线程不安全部分:
1.ArrayList
2.LinkedList
3.HashMap
4.TreeMap
5.HashSet
6.TreeSet
7.StringBuilder

volatile 与 synchronized

volatile 只保证内存可见性,不保证原子性。禁止编译器优化,保证内存可见性。
如果无脑用 synchronized 的话,其他线程会被阻塞,直到获得锁的线程执行完毕。这会导致线程在争夺锁时发生阻塞,浪费了CPU的时间;一旦线程被阻塞,下次的执行时间是不可控的,线程可能在阻塞队列中等待,导致程序的性能和响应时间都不可预测;从而影响了高性能。
volatile 就不会引起线程阻塞。

wait和notify

wait 和 notify 。就是等待和通知。是处理线程调度随机性的问题的,不喜欢随机性,需要让彼此之间有个固定的顺序。join 也是一种控制顺序的方式,更倾向于控制线程结束。
调用 wait 方法就会陷入阻塞。阻塞到有其他线程通过 notify 来通知。 wait 内部会做三件事:1、先释放锁 2、等待其他线程的通知 3、收到通知之后,重新获取锁,并继续往下执行。因此,想用 wait/notify 就得搭配 synchronized。代码如下:

public static void main1(String[] args) throws InterruptedException {
    Object object = new Object();
    //wait 哪个对象,就得针对哪个对象加锁。
    synchronized (object) {
        System.out.println("wait 前");
        object.wait();
        System.out.println("wait 后");
    }
}

在这里插入图片描述

搭配举例

在第一个线程 wait 之后,就可以通过第二个线程的 notify 来唤醒第一个线程。代码如下:

import java.util.Scanner;

public class JoinExample {
    public static Object locker = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("wait 之前");
                try {
                    locker.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("wait 之后");
            }
        },"wait");
        t1.start();
        System.out.println(Thread.currentThread().getName());
        Thread.sleep(3000);
        Thread t2 = new Thread(() -> {
            synchronized (locker) {
                System.out.println("notify 之前");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                locker.notify();
                System.out.println("notify 之后");
            }
        },"notify");
        t2.start();
    }
}

然后代码当中就是 wait 三秒之后,进入线程二,然后打印出 notify 之后,再等待 3秒,然后使用 notify 唤醒。
注意:只有将Thread.start方法触发之后,才开始创建执行线程
运行结果如下:
在这里插入图片描述
然后唤醒:
在这里插入图片描述

notifyAll

假如有一个对象 lock 有 10 个线程,都调用了 o.wait 此时 10 个线程都是阻塞状态。如果调用了 o.notify 就会把 10 个当中的一个给唤醒(唤醒哪个不确定),使用 notifyAll 就会把所有的 10 个线程都给唤醒。wait 唤醒之后,就会重新尝试获取到锁(这个过程就会发生竞争),直到把10个线程全部唤醒后执行完再结束。

    class Example1 {
        private final Object lock = new Object();

        public void threadA() throws InterruptedException {
            synchronized (lock) {
                System.out.println("Thread A is doing some work.");
                lock.wait(); // 线程A等待
                System.out.println("Thread A has been notified and is continuing.");
            }
        }

        public void threadB() throws InterruptedException {
            synchronized (lock) {
                System.out.println("Thread B is doing some work.");
                lock.wait(); // 线程B等待
                System.out.println("Thread B has been notified and is continuing.");
            }
        }

        public void notifyThreads() {
            synchronized (lock) {
                System.out.println("Notifying all waiting threads.");
                lock.notifyAll(); // 唤醒所有等待的线程
            }
        }

    }

public class Example {
    public static void main(String[] args) throws InterruptedException {
        Example1 e = new Example1();

        Thread threadA = new Thread(() -> {
            try {
                e.threadA();
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            }
        });

        Thread threadB = new Thread(() -> {
            try {
                e.threadB();
            } catch (InterruptedException e1) {
                e1.printStackTrace();
            }
        });

        Thread threadC = new Thread(() -> {
            e.notifyThreads();
        });

        threadA.start();
        threadB.start();

        Thread.sleep(1000);
        threadC.start();
    }
}





♥♥♥码字不易,大家的支持就是我坚持下去的动力♥♥♥
版权声明:本文为CSDN博主「亚太地区百大最帅面孔第101名」的原创文章

评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李小浦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值