多线程及其特性

1. 认识线程(Thread)

1.1 概念

1) 线程是什么

⼀个线程就是⼀个 "执⾏流". 每个线程之间都可以按照顺序执⾏⾃⼰的代码. 多个线程之间 "同时" 执⾏着多份代码.线程,就是程序里一条独立的 "执行流水线"。一个程序可以同时跑 多条流水线,这就是多线程

2) 为啥要有线程

⾸先, "并发编程" 成为 "刚需"

• 单核 CPU 的发展遇到了瓶颈. 要想提⾼算⼒, 就需要多核 CPU. ⽽并发编程能更充分利⽤多核 CPU

资源.

• 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做⼀些其他的⼯作, 也需要⽤到并发编程.
其次, 虽然多进程也能实现 并发编程, 但是线程⽐进程更轻量

• 创建线程⽐创建进程更快.

• 销毁线程⽐销毁进程更快.

• 调度线程⽐调度进程更快.
最后, 线程虽然⽐进程轻量, 但是⼈们还不满⾜, 于是⼜有了 "线程池和 "协程"

3) 进程和线程的区别

• 进程是包含线程的. 每个进程⾄少有⼀个线程存在,即主线程。但是不能没有线程

• 进程和进程之间不共享内存空间. 同⼀个进程的线程之间共享同⼀个内存空间(内存,硬盘,网络带宽.......)

• 进程是系统分配资源 的基本单位,线程是系统调度执行 的基本单位。

• 多个线程之间,可能会相互影响。如果⼀个线程挂了, 就会把同进程内的其他线程⼀起带⾛(整个进程崩溃)

• 多个进程之间,一般不会相互影响。一个进程崩了,不会影响到其他进程。(进程的隔离性)

•线程是当下是实现并发编程的主流方式。通过多线程,就可以充分利用 多核CPU 。但是,也不是线程数目越多越好。线程数目达到一定量后,把多个核心都利用拆分之后,在增加线程,就会无法提高效率,也有可以会影响效率。(线程调度也是有开销的)

4) Java 的线程 和 操作系统线程 的关系

• 线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对⽤⼾层提供了⼀些 API 供⽤⼾使⽤。

• java 标准库中 Thread 类可以视为是对操作系统提供的 API 进⾏了进⼀步的抽象和封装.

1.2 第⼀个多线程程序

• 每个线程都是⼀个独⽴的执⾏流

• 多个线程之间是 "并发" 执⾏的.

复制代码
import java.util.Random;  
public class test {  
    private static class MyThread extends Thread {  
        @Override  
        public void run() {  
            Random random = new Random();  
            while (true) {  
                // 打印线程名称  
                System.out.println(Thread.currentThread().getName());  
                try {  
                    // 随机停⽌运⾏ 0-9 秒  
                    Thread.sleep(random.nextInt(10));  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
        }  
    }  
    public static void main(String[] args) {  
        MyThread t1 = new MyThread();  
        t1.start();  
        Random random = new Random();  
        while (true) {  
            // 打印线程名称  
            System.out.println(Thread.currentThread().getName());  
            try {  
                Thread.sleep(random.nextInt(10));  
            } catch (InterruptedException e) {  
                // 随机停⽌运⾏ 0-9 秒  
                e.printStackTrace();  
            }  
        }  
    }  
}

1.3使⽤ jconsole 命令观察线程

1.4 创建线程

⽅法1 继承 Thread 类

继承 Thread 来创建⼀个线程类.

复制代码
class MyThread extends Thread {  
    @Override  
    public void run() {  
        System.out.println("这⾥是线程运⾏的代码");  
    }  
}  
  
public class demo1 {  
    public static void main(String[] args) {  
        MyThread myThread = new MyThread();  
        myThread.start();  
    }  
}

⽅法2 实现 Runnable 接⼝

复制代码
class MyRunnable implements Runnable {  
    @Override  
    public void run() {  
        System.out.println("这⾥是线程运⾏的代码");  
    }  
}  
public class demo1 {  
    public static void main(String[] args) {  
        new Thread(new MyRunnable()).start();  
    }  
}

其他变形

• 匿名内部类创建 Thread ⼦类对象

复制代码
public class demo1 {  
    public static void main(String[] args) {  
        // 使用匿名类创建 Thread 子类对象  
        Thread thread = new Thread() {  
            @Override  
            public void run() {  
                System.out.println("使用匿名类创建 Thread 子类对象");  
            }  
        };  
        // 启动线程  
        thread.start();  
    }  
}

• 匿名内部类创建 Runnable ⼦类对象

复制代码
public class demo1 {  
    public static void main(String[] args) {  
        // 使⽤匿名类创建 Runnable ⼦类对象  
        Thread t2 = new Thread(new Runnable() {  
            @Override  
            public void run() {  
                System.out.println("使⽤匿名类创建 Runnable ⼦类对象");  
            }  
        });  
  
        // 启动线程  
        t2.start();  
    }  
}

• lambda 表达式创建 Runnable ⼦类对象

复制代码
public class demo1 {  
    public static void main(String[] args) {  
        // 使⽤ lambda 表达式创建 Runnable ⼦类对象  
        Thread t3 = new Thread(() -> System.out.println("使⽤匿名类创建 Thread ⼦类对象"));  
        Thread t4 = new Thread(() -> {System.out.println("使⽤匿名类创建 Thread ⼦类对象");  
              
        });  
        t3.start();  
        t4.start();  
  
  
    }  
}

1.5 多线程的优势-增加运⾏速度

可以观察多线程在⼀些场合下是可以提⾼程序的整体运⾏效率的。

2. Thread 类及常⻅⽅法

2.1 Thread 的常⻅构造⽅法

复制代码
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

2.2 Thread 的⼏个常⻅属性

ID 是线程的唯⼀标识,不同线程不会重复

名称是身份的标识

状态表⽰线程当前所处的⼀个情况

优先级⾼的线程理论上来说更容易被调度到

关于后台线程,需要记住⼀点:JVM会在⼀个进程的所有⾮后台线程结束后,才会结束运⾏。

是否存活,即简单的理解,为 run ⽅法是否运⾏结束了

前台线程:如果某个线程在执行的过程中,能够阻止进程结束,这个线程就是"前台线程"

后台线程:如果某个线程在执行的过程中,不能够阻止进程结束,(虽然线程在执行着,但是进程要结束了,此时这个线程也要随之结束)这个线程就是"后台线程"

2.3 启动⼀个线程 - start()

覆写 run ⽅法是提供给线程要做的事情的指令清单

线程对象可以认为是把 李四、王五叫过来了

调⽤ start() ⽅法,就是喊⼀声:"⾏动起来!",线程才真正独⽴去执⾏了。

调⽤ start ⽅法, 才真的在操作系统的底层创建出⼀个线程.

💡经典面试题:start () 和 run () 方法的区别

start () 是启动线程,run () 只是普通方法调用;start () 会真正开启新线程,run () 不会。

  • Thread.start()

    • 属于 Thread 类的方法
    • 真正启动一个新线程 :会让线程进入就绪状态 ,等待 CPU 调度,自动 调用线程的 run() 方法
    • 多线程的正确启动方式
  • Thread.run()

    • 属于 Runnable 接口的方法
    • 只是一个普通的成员方法
    • 直接调用 run()不会创建新线程 ,代码在当前调用线程(比如 main 线程) 中同步执行
对比维度 start () 方法 run () 方法
是否创建新线程 ✅ 会创建新线程 ❌ 不会创建新线程
执行线程 新的独立线程 当前调用线程(如 main 线程)
执行方式 异步执行(和主线程同时跑) 同步执行(必须等 run 执行完才往下走)
调用次数 只能调用1 次(线程生命周期限制) 可以调用多次(普通方法)
本质 启动线程的入口 业务逻辑的载体

2.4 中断⼀个线程

2.4-1: 使⽤⾃定义的变量来作为标志位

复制代码
public class demo2 {  
    private static class MyRunnable implements Runnable {  
        public volatile boolean isQuit = false;  
        @Override  
        public void run() {  
            while (!isQuit) {  
                System.out.println(Thread.currentThread().getName()  
                        + ": 别管我,我忙着转账呢!");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
            System.out.println(Thread.currentThread().getName()  
                    + ": 啊!险些误了⼤事");  
        }  
    }  
    public static void main(String[] args) throws InterruptedException {  
        MyRunnable target = new MyRunnable();  
        Thread thread = new Thread(target, "李四");  
        System.out.println(Thread.currentThread().getName()  
                + ": 让李四开始转账。");  
        thread.start();  
       Thread.sleep(10 * 1000);  
        System.out.println(Thread.currentThread().getName()  
                + ": ⽼板来电话了,得赶紧通知李四对⽅是个骗⼦!");  
        target.isQuit = true;  
    }  
  
}

2.4-2: 使⽤ Thread.interrupted()

复制代码
public class demo2 {  
    private static class MyRunnable implements Runnable {  
        @Override  
        public void run() {  
            // 两种⽅法均可以  
            while (!Thread.interrupted()) {  
                //while (!Thread.currentThread().isInterrupted()) {  
                System.out.println(Thread.currentThread().getName()  
                        + ": 别管我,我忙着转账呢!");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                    System.out.println(Thread.currentThread().getName()  
                            + ": 有内⻤,终⽌交易!");  
                    // 注意此处的 break
                    break;  
                }  
            }  
            System.out.println(Thread.currentThread().getName()  
                    + ": 啊!险些误了⼤事");  
        }  
    }  
  
    public static void main(String[] args) throws InterruptedException {  
        MyRunnable target = new MyRunnable();  
        Thread thread = new Thread(target, "李四");  
        System.out.println(Thread.currentThread().getName()  
                + ": 让李四开始转账。");  
        thread.start();  
        Thread.sleep(10 * 1000);  
        System.out.println(Thread.currentThread().getName()  
                + ": ⽼板来电话了,得赶紧通知李四对⽅是个骗⼦!");  
        thread.interrupt();  
    }  
  
}

thread 收到通知的⽅式有两种:

  1. 如果线程因为调⽤ wait/join/sleep 等⽅法⽽阻塞挂起,则以 InterruptedException 异常的形式通知,清除中断标志
    ◦ 当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽
    略这个异常, 也可以跳出循环结束线程.
  2. 否则,只是内部的⼀个中断标志被设置,thread 可以通过
    ◦ Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
    这种⽅式通知收到的更及时,即使线程正在 sleep 也可以⻢上收到。

2.5 等待⼀个线程 - join()

操作系统,针对多个线程的执行,是一个"随机调度,抢占式执行"的过程。

线程等待就是确定两个线程的"结束顺序"

有时,我们需要等待⼀个线程完成它的⼯作后,才能进⾏⾃⼰的下⼀步⼯作例如,王五只有等李四工作完成工作之后才能工作,这时我们需要⼀个⽅法明确等待线程的结束。

复制代码
public class demo3 {  
        public static void main(String[] args) throws InterruptedException {  
            Runnable target = () -> {  
                for (int i = 0; i < 10; i++) {  
                    try {  
                        System.out.println(Thread.currentThread().getName()  
                                + ": 我还在⼯作!");  
                        Thread.sleep(1000);  
                    } catch (InterruptedException e) {  
                        e.printStackTrace();  
                    }  
                     }  
                System.out.println(Thread.currentThread().getName() + ": 我结束了!");  
            };  
            Thread thread1 = new Thread(target, "李四");  
            Thread thread2 = new Thread(target, "王五");  
            System.out.println("先让李四开始⼯作");  
            thread1.start();  
            thread1.join();  
            System.out.println("李四⼯作结束了,让王五开始⼯作");  
            thread2.start();  
            thread2.join();  
            System.out.println("王五⼯作结束了");  
        }  
    }

2.6 获取当前线程引⽤

方法 说明
public static Thread currentThread() 返回当前线程对象的引用
复制代码
public class ThreadDemo {
	 public static void main(String[] args) {
	 Thread thread = Thread.currentThread();
	 System.out.println(thread.getName());
	 }
}

2.7 休眠当前线程

线程的调度是不可控的,所以,这个⽅法只能保证实际休眠时间是⼤于等于参数设置的休眠时间的。

3. 线程的状态

3.1 观察线程的所有状态

线程的状态是⼀个枚举类型 Thread.State

复制代码
public class ThreadState {
	 public static void main(String[] args) {
		 for (Thread.State state : Thread.State.values()) {
		 System.out.println(state);
		 }
	 }
}

• NEW: 安排了⼯作, 还未开始⾏动(new)

• RUNNABLE: 可⼯作的. ⼜可以分成正在⼯作中和即将开始⼯作.(runnable)

• BLOCKED: 这⼏个都表⽰排队等着其他事情(blocked)

• WAITING: 这⼏个都表⽰排队等着其他事情(waiting)

• TIMED_WAITING: 这⼏个都表⽰排队等着其他事情(timed-waiting)

• TERMINATED: ⼯作完成了.(terminated)

4. 多线程带来的的⻛险-线程安全 (重点)

4.1 线程安全的概念

如果多线程环境下代码运⾏的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

4.2 观察线程不安全

复制代码
public class demo4 {  
    // 此处定义⼀个 int 类型的变量  
    private static int count = 0;  
  
    public static void main(String[] args) throws InterruptedException {  
        Thread t1 = new Thread(() -> {  
            // 对 count 变量进⾏⾃增 5w 次  
            for (int i = 0; i < 50000; i++) {  
                count++;  
            }  
        });  
        Thread t2 = new Thread(() -> {  
            // 对 count 变量进⾏⾃增 5w 次  
            for (int i = 0; i < 50000; i++) {  
                count++;  
            }  
        });  
        t1.start();  
        t2.start();  
        // 如果没有这俩 join, 肯定不⾏的. 线程还没⾃增完, 就开始打印了. 很可能打印出来的 count 就是个 0        t1.join();  
        t2.join();  
        // 预期结果应该是 10w        System.out.println("count: " + count);  
    }  
}

4.3 线程不安全的原因

线程调度是随机的 这是线程安全问题的 罪魁祸⾸

随机调度使⼀个程序在多线程环境下, 执⾏顺序存在很多的变数

原因1-->修改共享数据

多个线程修改同⼀个变量

原因2-->原子性

如果⼀个线程正在对⼀个变量操作,中途其他线程插⼊进来了,如果这个操作被打断了,结果就可能是错误的。

原因3-->可⻅性

可⻅性指, ⼀个线程对共享变量值的修改,能够及时地被其他线程看到

原因4-->指令重排序

CPU / 编译器为了提高执行效率,会在不影响 单线程执行结果的前提下,打乱代码的实际执行顺序 但在多线程下,这种重排会导致逻辑错误、数据不一致。

4.4 解决之前的线程不安全问题

复制代码
public class demo5 {  
    // 此处定义⼀个 int 类型的变量  
    private static int count = 0;  
    public static void main(String[] args) throws InterruptedException {  
        Object locker = new Object();  
        Thread t1 = new Thread(() -> {  
            // 对 count 变量进⾏⾃增 5w 次  
            for (int i = 0; i < 50000; i++) {  
                synchronized (locker) {  
                    count++;  
                }  
            }  
        });  
        Thread t2 = new Thread(() -> {  
            // 对 count 变量进⾏⾃增 5w 次  
            for (int i = 0; i < 50000; i++) {  
                synchronized (locker) {  
                    count++;  
                }  
            }  
        });  
        t1.start();  
        t2.start();  
        // 如果没有这俩 join, 肯定不⾏的. 线程还没⾃增完, 就开始打印了. 很可能打印出来count 就是个 0        t1.join();  
        t2.join();  
        // 预期结果应该是 10w        System.out.println("count: " + count);  
    }  
}

5. synchronized 关键字 - 监视器锁 monitor lock

5.1 synchronized 的特性

1) 互斥

synchronized 会起到互斥效果 , 某个线程执⾏到某个对象的 synchronized 中时, 其他线程如果也执⾏到同⼀个对象 synchronized 就会阻塞等待.

• 进⼊ synchronized 修饰的代码块, 相当于 加锁

• 退出 synchronized 修饰的代码块, 相当于 解锁

2) 可重⼊

synchronized 同步块对同⼀条线程来说是可重⼊的,不会出现⾃⼰把⾃⼰锁死的问题;

⼀个线程没有释放锁, 然后⼜尝试再次加锁.

// 第⼀次加锁, 加锁成功

lock();

// 第⼆次加锁, 锁已经被占⽤, 阻塞等待.

lock();

这样的锁称为**不可重⼊锁. Java 中的 synchronized 是 可重⼊锁, 因此没有上⾯的问题.

5.2 synchronized的使⽤

synchronized 本质上要修改指定对象的 "对象头". 从使⽤⻆度来看, synchronized 也势必要搭配⼀个具体的对象来使⽤.

1) 修饰代码块: 明确指定锁哪个对象.

锁任意对象

复制代码
public class SynchronizedDemo {
	 private Object locker = new Object();
	 
	 public void method() {
		 synchronized (locker) {
		 
		 }
	 }
}

锁当前对象

复制代码
public class SynchronizedDemo {
	 public void method() {
		 synchronized (this) {
		 }
	 }
}

2) 直接修饰普通⽅法: 锁的 SynchronizedDemo 对象

复制代码
public class SynchronizedDemo {
	 public synchronized void methond() {
	 
	 }
}

3) 修饰静态⽅法: 锁的 SynchronizedDemo 类的对象

复制代码
public class SynchronizedDemo {
	 public synchronized static void method() {
	 
	 }
}

synchronized 锁的是什么. 两个线程竞争同⼀把锁, 才会产⽣阻塞等待.

两个线程分别尝试获取两把不同的锁, 不会产⽣竞争.

💡Java 死锁

一、死锁一句话定义

两个或多个线程,互相持有对方需要的锁,谁都不释放,导致程序永久卡住。

就像:

  • 你拿着我要的钥匙
  • 我拿着你要的钥匙
  • 我们都不肯松手
    卡死,谁都动不了

二、死锁的 4 个必要条件(必须同时满足)

只要破坏任意一条,死锁就不会发生。

  1. 互斥:资源只能被一个线程占用

  2. 请求与保持:线程拿着自己的锁,又去申请别人的锁

  3. 不可剥夺:锁只能自己释放,不能被抢走

  4. 循环等待 :线程形成一个环形等待链

    • A 等 B
    • B 等 A

    public class demo6 {
    public static void main(String[] args) {
    Object lockA = new Object();
    Object lockB = new Object();

    复制代码
             // 线程1:先拿A,再拿B  
             new Thread(() -> {  
                 synchronized (lockA) {  
                     System.out.println("线程1持有 lockA");  
                     try { Thread.sleep(100); } catch (Exception e) {}  
    
                     synchronized (lockB) {  
                         System.out.println("线程1持有 lockB");  
                     }  
                 }  
             }).start();  
    
             // 线程2:先拿B,再拿A  
             new Thread(() -> {  
                 synchronized (lockB) {  
                     System.out.println("线程2持有 lockB");  
                     try { Thread.sleep(100); } catch (Exception e) {}  
    
                     synchronized (lockA) {  
                         System.out.println("线程2持有 lockA");  
                     }  
                 }  
             }).start();  
         }  

    }

运行结果:程序直接卡住,不动了 这就是死锁

三、死锁的特点

  • 程序不报错
  • 不退出
  • CPU 占用不高
  • 线程全部阻塞
  • 日志不动,接口不返回,服务假死

四、怎么避免死锁?(最实用)

破坏 4 个条件中的任意一个即可!

最常用、最简单的 3 种方案:

1. 统一锁的获取顺序(最有效)

所有线程都按照 lockA → lockB 的顺序拿锁

不要一个 A→B,一个 B→A

2. 使用定时锁(tryLock)

拿不到锁就超时放弃,不一直死等

3. 避免嵌套锁

不要在一个锁里面再拿另一个锁

5.3 Java 标准库中的线程安全类

Java 标准库中很多都是线程不安全的 . 这些类可能会涉及到多线程修改共享数据, ⼜没有任何加锁措

施.

• ArrayList • LinkedList • HashMap

• TreeMap • HashSet • TreeSet

• StringBuilder

但是还有⼀些是线程安全的. 使⽤了⼀些锁机制来控制.

• String • StringBuffer

6. volatile 关键字

volatile 能保证内存可⻅性

代码在写⼊ volatile 修饰的变量的时候,

• 改变线程⼯作内存中volatile变量副本的值

• 将改变后的副本的值从⼯作内存刷新到主内存

代码在读取 volatile 修饰的变量的时候,

• 从主内存中读取volatile变量的最新值到线程的⼯作内存中

• 从⼯作内存中读取volatile变量的副本

volatile 不保证原⼦性

volatile 和 synchronized 有着本质的区别. synchronized 能够保证原⼦性, volatile 保证的是内存可⻅性.

  • 单线程下,编译器 / JVM 的优化是安全的

    单线程中,编译器和 JVM 会基于as-if-serial规则做优化(比如指令重排、缓存优化),保证程序执行结果和代码顺序执行的结果完全一致,不会出问题。

  • 多线程下,优化会出现 "误判",导致内存可见性问题

    多线程场景下,JVM / 编译器的优化是基于 "单线程视角" 做的,无法感知其他线程的存在。比如:

    • 把变量的值缓存到寄存器 / 本地内存,不及时写回主内存

    • 对指令进行重排,导致其他线程看到的执行顺序错乱

      这些优化在单线程下没问题,但多线程下就会导致一个线程的修改,另一个线程看不到,也就是内存可见性问题。

  • 为什么要做这些优化?

    核心目的是提升程序执行效率

    • 降低程序员写代码的门槛,即使代码写得不够高效,编译器也能自动优化
    • 减少内存访问、提升 CPU 流水线利用率,让程序跑得更快

7. wait 和 notify

完成这个协调⼯作, 主要涉及到三个⽅法

• wait() / wait(long timeout): 让当前线程进⼊等待状态.

• notify() / notifyAll(): 唤醒在当前对象上等待的线程.

注意: wait, notify, notifyAll 都是 Object 类的⽅法

7.1 wait()⽅法

wait 做的事情:

• 使当前执⾏代码的线程进⾏等待. (把线程放到等待队列中)

• 释放当前的锁

• 满⾜⼀定条件时被唤醒, 重新尝试获取这个锁.

wait 要搭配 synchronized 来使⽤. 脱离 synchronized 使⽤ wait 会直接抛出异常.

wait 结束等待的条件:

• 其他线程调⽤该对象的 notify ⽅法.

• wait 等待时间超时 (wait ⽅法提供⼀个带有 timeout 参数的版本, 来指定等待时间).

• 其他线程调⽤该等待线程的 interrupted ⽅法, 导致 wait 抛出 InterruptedException 异常.

复制代码
public class demo7 {  
    public static void main(String[] args) throws InterruptedException {  
        Object object = new Object();  
        synchronized (object) {  
            System.out.println("等待中");  
            object.wait(1000);  
            System.out.println("等待结束");  
        }  
    }  
  
}

7.2 notify()⽅法

notify ⽅法是唤醒等待的线程.

• ⽅法notify()也要在同步⽅法或同步块中调⽤,该⽅法是⽤来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。

• 如果有多个线程等待,则有线程调度器随机挑选出⼀个呈 wait 状态的线程。(并没有 "先来后到")

• 在notify()⽅法后,当前线程不会⻢上释放该对象锁,要等到执⾏notify()⽅法的线程将程序执⾏

完,也就是退出同步代码块之后才会释放对象锁

7.3 notifyAll()⽅法

notify⽅法只是唤醒某⼀个等待线程. 使⽤notifyAll⽅法可以⼀次唤醒所有的等待线程

💡理解 notify 和 notifyAll

notify 只唤醒等待队列中的⼀个线程. 其他线程还是乖乖等着

notifyAll ⼀下全都唤醒, 需要这些线程重新竞争锁

7.4 wait 和 sleep 的对⽐(⾯试题)

其实理论上 wait 和 sleep 完全是没有可⽐性的,因为⼀个是⽤于线程之间的通信的 ,⼀个是让线程阻塞⼀段时间,

唯⼀的相同点就是都可以让线程放弃执⾏⼀段时间.

总结一下:

  1. wait 需要搭配 synchronized 使⽤. sleep 不需要
  2. wait 是 Object 的⽅法 sleep 是 Thread 的静态⽅法.
  3. 使用wait目的是 为了提前唤醒,sleep就是固定时段的阻塞,不涉及唤醒
  4. wait默认的是 死等。sleep和锁无关。

8. 多线程案例

8.1 单例模式

单例模式能保证某个类在程序中只存在唯⼀⼀份实例, ⽽不会创建出多个实例

单例模式具体的实现⽅式有很多. 最常⻅的是 "饿汉" 和 "懒汉" 两种

饿汉模式

类加载的同时, 创建实例

复制代码
class Singleton {
 private static Singleton instance = new Singleton();
 private Singleton() {}
 public static Singleton getInstance() {
	 return instance;
	 }
}

懒汉模式-单线程版

类加载的时候不创建实例. 第⼀次使⽤的时候才创建实例.懒汉模式的实现是线程不安全的.

复制代码
class Singleton {
	private static Singleton instance = null;
	 private Singleton() {}
	 public static Singleton getInstance() {
		 if (instance == null) {
		 instance = new Singleton();
		 }
		 return instance;
	 }
}

懒汉模式-多线程版

加上 synchronized 可以改善这⾥的线程安全问题

复制代码
class Singleton {
	 private static Singleton instance = null;
	 private Singleton() {}
	 public synchronized static Singleton getInstance() {
		 if (instance == null) {
			 instance = new Singleton();
		 }
		 return instance;
	 }
}

8.2 阻塞队列

1.阻塞队列是什么

阻塞队列是⼀种特殊的队列. 也遵守 "先进先出" 的原则

阻塞队列能是⼀种线程安全的数据结构, 并且具有以下特性:

• 当队列满的时候, 继续⼊队列就会阻塞, 直到有其他线程从队列中取⾛元素.

• 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插⼊元素.

阻塞队列的⼀个典型应⽤场景就是 "⽣产者消费者模型". 这是⼀种⾮常典型的开发模型.

2.⽣产者消费者模型

  1. 阻塞队列就相当于⼀个缓冲区,平衡了⽣产者和消费者的处理能⼒. (削峰填⾕)
  2. 阻塞队列也能使⽣产者和消费者之间 解耦.

3.阻塞队列实现

• 通过 "循环队列" 的⽅式来实现.

• 使⽤ synchronized 进⾏加锁控制.

• put 插⼊元素的时候, 判定如果队列满了, 就进⾏ wait. (注意, 要在循环中进⾏ wait. 被唤醒时不⼀定队列就不满了, 因为同时可能是唤醒了多个线程).

• take 取出元素的时候, 判定如果队列为空, 就进⾏ wait. (也是循环 wait)

8.3 定时器

1.定时器是什么

定时器也是软件开发中的⼀个重要组件. 类似于⼀个 "闹钟". 达到⼀个设定的时间之后, 就执⾏某个指定好的代码

2.标准库中的定时器

• 标准库中提供了⼀个 Timer 类. Timer 类的核⼼⽅法为 schedule .

• schedule 包含两个参数. 第⼀个参数指定即将要执⾏的任务代码, 第⼆个参数指定多⻓时间之后

执⾏ (单位为毫秒).

3.实现定时器

• ⼀个带优先级队列(不要使⽤ PriorityBlockingQueue, 容易死锁!)

• 队列中的每个元素是⼀个 Task 对象.

• Task 中带有⼀个时间属性, 队⾸元素就是即将要执⾏的任务

• 同时有⼀个 worker 线程⼀直扫描队⾸元素, 看队⾸元素是否需要执⾏

8.4 线程池

线程池最⼤的好处就是减少每次启动、销毁线程的损耗。

标准库中的线程池

• 使⽤ Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.

• 返回值类型为 ExecutorService

• 通过 ExecutorService.submit 可以注册⼀个任务到线程池中.

实现线程池

• 核⼼操作为 submit, 将任务加⼊线程池中

• 使⽤ Worker 类描述⼀个⼯作线程. 使⽤ Runnable 描述⼀个任务.

• 使⽤⼀个 BlockingQueue 组织所有的任务

• 每个 worker 线程要做的事情: 不停的从 BlockingQueue 中取任务并执⾏.

• 指定⼀下线程池中的最⼤线程数 maxWorkerCount; 当当前线程数超过这个最⼤值时, 就不再新增

线程了.

9. 对⽐线程和进程

优点

  1. 创建⼀个新线程的代价要⽐创建⼀个新进程⼩得多
  2. 与进程之间的切换相⽐,线程之间的切换需要操作系统做的⼯作要少很多
  3. 线程占⽤的资源要⽐进程少很多
  4. 能充分利⽤多处理器的可并⾏数量
  5. 在等待慢速I/O操作结束的同时,程序可执⾏其他的计算任务
  6. 计算密集型应⽤,为了能在多处理器系统上运⾏,将计算分解到多个线程中实现
  7. I/O密集型应⽤,为了提⾼性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

区别

  1. 进程是系统进⾏资源分配和调度的⼀个独⽴单位,线程是程序执⾏的最⼩单位。
  2. 进程有⾃⼰的内存地址空间,线程只独享指令流执⾏的必要资源,如寄存器和栈。
  3. 由于同⼀进程的各线程间共享内存和⽂件资源,可以不通过内核进⾏直接通信。
  4. 线程的创建、切换及终⽌效率更⾼。
相关推荐
良木生香2 小时前
【C++初阶】C++编程基石:编码表&&STL的入门指南
c语言·开发语言·数据结构·c++·算法
达帮主2 小时前
19.1 C语言链表 -- 简单
c语言·开发语言·链表
怎么没有名字注册了啊2 小时前
解决qt制作软件.app迁移问题(发布)Mac
开发语言·qt
大大杰哥2 小时前
Docker笔记
java·docker
aq55356002 小时前
三大Linux系统终极对决
linux·运维·服务器
ch.ju2 小时前
Java程序设计(第3版)第二章——选择结构
java
llm大模型算法工程师weng2 小时前
Java高并发架构设计:从理论到实战的全链路解决方案
java·开发语言
gihigo19982 小时前
MATLAB地震面波数值模拟方案
开发语言·matlab
CeshirenTester2 小时前
Claude Code 不只是会写代码:这 10 个 Skills,才是效率分水岭
android·开发语言·kotlin