JavaEE:多线程(初阶)

文章目录

一、认识线程(Thread)

1、概念

① 什么是线程

一个线程就是一个 “执行流”. 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 “同时” 执行着多份代码。

② 为什么要有线程

首先, “并发编程” 成为 “刚需”.

  • 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU 资源.
  • 有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程.

其次, 虽然多进程也能实现 并发编程, 但是线程比进程更轻量.

  • 创建线程比创建进程更快.
  • 销毁线程比销毁进程更快.
  • 调度线程比调度进程更快.

最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 “线程池”(ThreadPool) 和 “协程”(Coroutine)

关于线程池我们后面再介绍. 关于协程的话题我们此处暂时不做过多讨论.

③ 进程和线程的区别

  • 进程是包含线程的. 每个进程至少有一个线程存在,即主线程;
  • 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间;
  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位;

④ Java 的线程 和 操作系统线程 的关系

线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).

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

2、第一个多线程程序

例1:

package thread;

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("hello thread!");
    }
}

public class Demo1 {
    public static void main(String[] args) {
        // 创建一个线程
        // Java 中创建线程,离不开一个关键的类,thread
        // 一种比较朴素的创建线程的方式,是写一个子类,基础thread,重写其中的 run 方法
        Thread t = new MyThread();
        t.start();

        System.out.println("hello main!");
    }
}

在这里插入图片描述

3、创建线程

① 继承 Thread 类

  1. 继承 Thread 来创建一个线程类.
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("hello thread!");
    }
}
  1. 创建 MyThread 类的实例
Thread t = new MyThread();
  1. 调用 start 方法启动线程
t.start(); // 线程开始运行

② 实现 Runnable 接口

  1. 实现 Runnable 接口
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("hello thread!");
    }
}
  1. 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数
Runnable runnable = new MyRunnable();
        Thread t = new Thread(runnable);
  1. 调用 start 方法
t.start(); // 线程开始运行

③ 对比

  • 继承 Thread 类, 直接使用 this 就表示当前线程对象的引用.
  • 实现 Runnable 接口, this 表示的是 MyRunnable 的引用. 需要使用 Thread.currentThread()

④ 其他变形

匿名内部类创建 Thread 子类对象

// 使用匿名类创建 Thread 子类对象
Thread t1 = new Thread() {
    @Override
    public void run() {
        System.out.println("使用匿名类创建 Thread 子类对象");
   }
};
  • 匿名内部类创建 Runnable 子类对象
// 使用匿名类创建 Runnable 子类对象
Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("使用匿名类创建 Runnable 子类对象");
   }
});
  • lambda 表达式创建 Runnable 子类对象
// 使用 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));
Thread t4 = new Thread(() -> {
    System.out.println("使用匿名类创建 Thread 子类对象");
});

4、多线程的优势-增加运行速度

可以观察多线程在一些场合下是可以提高程序的整体运行效率的。
例2:

package thread;

public class Demo6 {
    // 1. 单个线程,串行的,完成 20 亿次自增.
    // 2. 两个线程,并行的,完成 20 亿次自增.
    private static final long COUNT = 20_0000_0000;

    private static void serial() {
        // 需要把方法执行的时间给记录下来
        // 记录当前的毫秒级时间戳
        long beg = System.currentTimeMillis();

        int a = 0;
        for (long i = 0; i < COUNT; i++) {
            a++;
        }
        a = 0;
        for (long i = 0; i < COUNT; i++) {
            a++;
        }

        long end = System.currentTimeMillis();
        System.out.println("单个线程消耗的时间:" + (end - beg) + "ms");
    }

    private static void concurrency(){
        long beg = System.currentTimeMillis();

        Thread t1 = new Thread(() -> {
            int a = 0;
            for (long i = 0; i < COUNT; i++) {
                a++;
            }
        });

        Thread t2 = new Thread(() -> {
            int a = 0;
            for (long i = 0; i < COUNT; i++) {
                a++;
            }
        });

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

        try {
            t1.join();
            t2.join();
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        long end = System.currentTimeMillis();
        System.out.println("两个线程消耗的时间:" + (end - beg) + "ms");
    }

    public static void main(String[] args) {
        concurrency();
        serial();
    }
}

在这里插入图片描述

二、Thread 类及常见方法

1、Thread 的常见构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名
【了解】Thread(ThreadGroup group,Runnable target)线程可以被用来分组管理,分好的组即为线程组,这个目前我们了解即可

2、Thread 的几个常见属性

方法说明
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()
  • ID 是线程的唯一标识,不同线程不会重复
  • 名称是各种调试工具用到
  • 状态表示线程当前所处的一个情况,下面我们会进一步说明
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
  • 是否存活,即简单的理解,为 run 方法是否运行结束了
  • 线程的中断问题,下面我们进一步说明

3、启动一个线程-start()

创建 Thread 实例,并没有真的在操作系统内核里创建出线程;

调用 start() 方法,线程才真正独立去执行了。

注意:start() 和 run() 的区别!

  • 调用start 方法方可启动线程并使线程进入就绪状态,而run 方法只是thread 的一个普通方法调用,还是在主线程里执行。

4、中断一个线程

① 使用 自己 的标志位

例3:

package thread;

public class Demo9 {
    // 用一个布尔变量表示线程是否结束
    // 这个变量是一个成员变量,而不是局部变量
    private static boolean isQuit = false;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!isQuit) {
                System.out.println("线程运行中....");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("新线程执行结束!");
        });

        t.start();

        Thread.sleep(5000);
        System.out.println("控制新线程退出");
        isQuit = true;
    }
}

在这里插入图片描述

② 使用 Thread 自带的标志位

例4:

package thread;

public class Demo10 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("线程运行中....");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // e.printStackTrace();

                    // [1] 立即退出
                    // break;

                    System.out.println("新线程即将退出!");
                    // [2] 稍后退出,此处的 sleep 可以换成任意的用来收尾工作的代码
                    // try {
                    //     Thread.sleep(3000);
                    // } catch (InterruptedException interruptedException) {
                    //     interruptedException.printStackTrace();
                    // }

                    // [3] 不退出!啥都不做,就相当于忽略了异常
                }
            }
            System.out.println("新线程已退出!");
        });

        t.start();

        Thread.sleep(5000);
        System.out.println("控制新线程退出!");
        t.interrupt();
    }
}

5、等待一个线程-join()

有时候我们需要控制一个线程结束,再执行其他工作,就需要一个等待线程结束的方法:join().
例5:

package thread;

public class Demo11 {
    private static Thread t1 = null;
    private static Thread t2 = null;

    public static void main(String[] args) throws InterruptedException {
        System.out.println("main begin");
        t1=new Thread(()->{
            System.out.println("t1 begin");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1 end");
        });
        t1.start();

        t2 =new Thread(()->{
            System.out.println("t2 begin");
            try {
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t2 end");
        });
        t2.start();

        t2.join();
        System.out.println("main end");
    }
}

在这里插入图片描述

方法说明
public void join()等待线程结束
public void join(long millis)等待线程结束,最多等 millis 毫秒
public void join(long millis, int nanos)同理,但可以更高精度

join 的作用

  1. 如果被等待的线程还没执行完,就阻塞等待
  2. 如果被等待的线程已经执行完,就直接返回

6、获取当前线程引用

如果是继承 Thread 然后重新 run 方法,可以直接在 run 中使用 this 得到线程的实例;
但如果是 Runnable 或者 lambda,this 就不行了(指向 Thread 实例);
需要使用 Thread currentThread().

方法说明
public static Thread currentThread();返回当前线程对象的引用

7、休眠当前线程

之前我们也用到过休眠线程的方法 sleep();

注意:sleep(1000):休眠时间不小于 1000 ms,实际时间大于 1000 ms,误差在 10 ms 以下。

方法说明
public static void sleep(long millis) throws InterruptedException休眠当前线程 millis毫秒
public static void sleep(long millis, int nanos) throws InterruptedException可以更高精度的休眠

三、线程的状态

1、观察线程的所有状态

枚举遍历线程的所有状态:

public class ThreadState {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
       }
   }
}
  • NEW: 创建了 Thread 对象,但还没有调用 start 方法
  • RUNNABLE: 就绪状态——1.正在 CPU 上运行. 2.还没在 CPU 运行,但已经准备好了
  • BLOCKED: 阻塞状态(等待锁)
  • WAITING: 阻塞状态(线程中调用 wait)
  • TIMED_WAITING: 阻塞状态(线程中通过 sleep 进入阻塞)
  • TERMINATED: 系统里的线程已经执行完毕,销毁了(run 执行完了),但 Thread 对象还在

2、线程状态和状态转移的意义

在这里插入图片描述

3、观察线程的状态和转移

例6:sleep

package thread;

public class Demo13 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 在 start 之前获取,获取到的是线程还未创建的状态
        System.out.println(t.getState());

        t.start();

        // 在 start 之后 sleep 之前,线程正在 CPU 上运行
        System.out.println(t.getState());

        Thread.sleep(500);

        // 在 sleep 之后获取,线程通过 sleep 进入阻塞
        System.out.println(t.getState());

        t.join();

        // 在 join 之后获取,获取到的是线程已经结束后的状态
        System.out.println(t.getState());
    }
}

在这里插入图片描述
例7:yield
yield 大公无私,让出 CPU;不改变线程的状态, 但是会重新去排队;相当于 sleep(0).

package thread;

public class Demo14 {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("张三");
                    Thread.yield();
                }
            }
        }, "t1");
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("李四");
                }
            }
        }, "t2");
        t2.start();
    }
}

可以明显的看到:使用 yield 时, 张三的数量远远少于李四。

四、多线程带来的的风险-线程安全 (重点)

1、观察线程不安全

例8:

package thread;

// 创建两个线程,让这两个线程同时并发的对一个变量,自增 5w 次,最终预期能够一共自增 10w次
class Counter {
    // 用来保存计数的变量
    public int count;

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

public class Demo14 {
    // 这个实例用来进行累加
    public static Counter counter = new Counter();

    public static void main(String[] args) {
        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();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count: " + counter.count);
    }
}

在这里插入图片描述
运行后发现,每次运行得到的结果都不同,是一个 5w 到 10 w之间的随机数,那么这是为什么呢?

count++ 对于操作系统来讲,CPU 会先从内存中读取 数据,然后在 CPU 中完成 运算,最后再把寄存器中的 数据 写到内存中.

一共三步,如果多个线程同时执行上述三步操作,执行顺序就会出现多种排列组合,只有当 二者互不干扰,连续完整执行上述三步操作时,才会达到我们想要的结果.

如果所有指令全部达到要求,则结果为 10w;如果所有指令均没有达到要求,则结果为 5w;
但实际上由于线程执行的不确定性,不确定有多少次指令达到要求,所以结果为 5w~10w.

2、线程安全的概念

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

3、线程不安全的原因

① 操作系统的随机调度/抢占式执行

这个也是造成线程不安全的万恶之源,对于我们程序员来说,也无能为力!

② 多个线程修改同一个变量

注意,一定是 多个线程 修改 同一个 变量,才会造成线程不安全!

这个比如刚才举的例子,t1 和 t2 两个线程都对 count 进行修改,就会造成线程不安全。这类问题,我们在写代码的时候就可以进行针对控制,可以通过调整程序的设计,进行规避。但不是所有的场景都可以规避!!!

③ 原子性

Ⅰ概念

我们把不可拆分的最小单位称为原子

那么其他操作可访问该操作的,就称为不具备原子性;如果给该操作加上一把锁,使其变为不可访问,那就是原子的;有时也把这个现象叫做同步互斥,表示操作是互相排斥的.

Ⅱ带来的问题

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

例如
通过 = 来修改,= 只对应一道指令,是原子的;
通过 ++ 来修改,++ 对应三道指令,不是原子的.

④ 内存可见性

Ⅰ概念

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

Ⅱ带来的问题

一个线程修改,一个线程读取,就容易因为可见性,引发问题.

例如
线程1 进行反复的读 和判断,
如果在正常情况下,线程1 在读和判断,线程2 突然写了一下,写完以后,线程1 就能立即读到内存的变换,从而让判断出现变换;
但是在程序运行过程中,可能会涉及一个操作 “优化”.比如:线程1 优化后,只判断,不读了,此时线程2 发生改变时,线程1 感受不到了.

例9:

package thread;

import java.util.Scanner;
import java.util.concurrent.Callable;

public class Demo16 {
    // 写一个内部类,此时这个内部类处在 Demo16 的内部,和刚才那个 Counter 不是一个作用域了
    static class Counter {
        public int flag = 0;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            while (counter.flag == 0) {
                // 执行循环,但是此处循环啥都不做
            }
            System.out.println("t1 finish");
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            // 让用户输入一个数字,赋值给 flag
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            counter.flag = scanner.nextInt();
        });
        t2.start();
    }
}

在这里插入图片描述
此处输入 1 后,线程 t1 并没有结束,这是为什么呢?

t1 的工作(反复执行):

  1. LOAD:读内存的数据到 CPU 寄存器
  2. TEST:检查 CPU 寄存器的值是否和预期的一样

读内存比读 CPU 寄存器慢几千倍,上万倍
即 t1 的操作注意慢在 LOAD 上.
对于 LOAD,每次进行 LOAD 的结果又没有变化,就进行了 “优化”.
“优化” 后,相当于只从进行了一次 LOAD,然后反复进行 TEST 操作
此时,t2 线程对 flag 进行修改,但是 t1 不在进行 LOAD 操作,就看不见内存中的修改.

这就是内存可见性问题。

“优化”:系统在执行正确的前提下,做出变换使性能更优.
在单线程环境下:没有问题
在多线程环境下:由于多线程环境过于复杂,编译器 / JVM / 操作系统进行 “优化” 时,可能产生误判.

针对这个问题,java 引入了 volatile 关键字,让程序员手动的禁止编译器对某个变量进行 “优化”.

⑤ 指令重排序

指令重排序,也是 操作系统 / 编译器 / JVM 优化操作!!!

指令重排序就是 调整顺序 达到了 加快速度的效果!

编译器对于指令重排序的前提是 “保持逻辑不发生变化”. 这一点在单线程环境下比较容易判断, 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.

例如:Test t = new Test();
1.创造内存空间
2.在这个内存空间上构造一个对象
3.把这个内存的引用赋给 t

其中 2 和 3 的顺序是可以调换的:
在单线程下,调换顺序,无影响;
在多线程下,另一个线程尝试读取 t 的引用,
如果按照 2,3,第二个线程读到 t 为 非空 的时候,t 一定是一个有效的对象;
如果按照 3,2,第二个线程读到 t 为 非空 的时候,t 可能是一个无效的对象;

4、解决例中的问题

例10:

package thread;

class Counter1 {
    // 用来保存计数的变量
    public int count;

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

public class Demo15 {
    public static Counter1 counter1 = new Counter1();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter1.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter1.increase();
            }
        });
        t1.start();
        t2.start();

        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count: " + counter1.count);
    }
}

在这里插入图片描述

五、synchronized 关键字-监视器锁monitor lock

1、synchronized 的特性

① 互斥

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

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

② 刷新内存

synchronized 的工作过程:

  1. 获得互斥锁
  2. 从主内存拷贝变量的最新副本到工作的内存
  3. 执行代码
  4. 将更改后的共享变量的值刷新到主内存
  5. 释放互斥锁

所以 synchronized 也能保证内存可见性. 具体代码参见后面 volatile 部分.

③ 可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;

理解 “把自己锁死”
一个线程没有释放锁, 然后又尝试再次加锁!然后无法进行锁的操作,就会造成 死锁。这样的锁称为 不可重入锁.

Java 中的 synchronized 是 可重入锁, 因此没有上面的问题.

在可重入锁的内部, 包含了 “线程持有者” 和 “计数器” 两个信息.

  • 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取 到锁, 并让计数器自增.
  • 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)

2、synchronized 使用示例

① 直接修饰普通方法

public class SynchronizedDemo {
    public synchronized void methond() {
   }
}

② 修饰静态方法

public class SynchronizedDemo {
    public synchronized static void method() {
   }
}

③ 修饰代码块

  1. 锁当前对象
public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
       }
   }
}
  1. 锁类对象
public class SynchronizedDemo {
    public void method() {
        synchronized (SynchronizedDemo.class) {
       }
   }
}

注意: 两个线程竞争同一把锁, 才会产生阻塞等待.

  • 例11:两个线程分别用两把锁
package thread;

public class Demo15 {
    public static Object Locker1 = new Object();
    public static Object Locker2 = new Object();

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

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

在这里插入图片描述

  • 例12:两个线程竞争同一把锁
package thread;

public class Demo15 {
    public static Object Locker1 = new Object();
    public static Object Locker2 = new Object();

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

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

在这里插入图片描述

六、volatile 关键字

1、volatile 能保证内存可见性

volatile 修饰的变量, 能够保证 “内存可见性”.

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

  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存

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

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

对于 例9 中提到的内存可见性引发的 线程不安全问题,通过 volatile 关键字可以进行解决.
例13:

package thread;

import java.util.Scanner;
import java.util.concurrent.Callable;

public class Demo16 {
    // 写一个内部类,此时这个内部类处在 Demo16 的内部,和刚才那个 Counter 不是一个作用域了
    static class Counter {
        volatile public int flag = 0;
    }

    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            while (counter.flag == 0) {
                // 执行循环,但是此处循环啥都不做

				// 加了sleep 以后,循环速度慢了,读内存操作就不频繁了,就不会触发 “优化”
				/*try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }*/
            }
            System.out.println("t1 finish");
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            // 让用户输入一个数字,赋值给 flag
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入一个整数:");
            counter.flag = scanner.nextInt();
        });
        t2.start();
    }
}

在这里插入图片描述

2、volatile 不保证原子性

volatile 和 synchronized 有着本质的区别.

  • synchronized 是两个线程写,能够保证原子性,
  • volatile 是一个线程读,一个线程写,保证的是内存可见性.

3、volatile 解决指令重排序

因为 volatile 可以防止操作系统进行 “优化”,而指令重排序的本质也就是 “优化”,所以 volatile 也可以解决指令重排序问题,这里就不展开讲了。

七、wait 和 notify

  • wait() / wait(long timeout): 让当前线程进入等待状态.
  • notify() / notifyAll(): 唤醒在当前对象上等待的线程.

1、wait()方法

wait 做的事情:

  • 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  • 释放当前的锁
  • 满足一定条件时被唤醒, 重新尝试获取这个锁.

wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.
调用 wait 的对象和 synchronized 里使用的锁对象是一个对象,还得和调用 notify 的对象是一个对象.

wait 结束等待的条件:

  • 其他线程调用该对象的 notify 方法.
  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.

例14:wait

package thread;

public class Demo17 {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized(object){
            System.out.println("wait 之前");
            object.wait();
            System.out.println("wait 之后");
        }
    }
}

在这里插入图片描述
这样会出现一种情况,程序一直在等待,不能继续执行了,所以我们需要唤醒等待,就用到了 notify().

2、notify()方法

notify 方法是唤醒等待的线程.:

  • 方法 notify() 也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知 notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

例15:

package thread;

import java.util.Scanner;

// 创建两个线程,一个线程调用 wait,一个线程调用 notify
public class Demo18 {
    // 这个对象用来作为锁对象
    public static Object Locker = new Object();

    public static void main(String[] args) {
        // 用来等待
        Thread waitTask = new Thread(() -> {
            synchronized (Locker) {
                try {
                    System.out.println("wait 开始");
                    Locker.wait();
                    System.out.println("wait 结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        waitTask.start();

        // 创建一个用来通知、唤醒的线程
        Thread notifyTask = new Thread(() -> {
            // 让用户来控制,用户输入个内容后,再执行通知
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入任意内容,开始通知:");
            // next 会阻塞,直到用户真正输入内容以后
            scanner.next();

            synchronized (Locker) {
                System.out.println("notify 开始");
                Locker.notify();
                System.out.println("notify 结束");
            }
        });
        notifyTask.start();
    }
}

在这里插入图片描述

3、notifyAll()方法

notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.

注意: 虽然是同时唤醒 多 个线程, 但是这 多 个线程需要竞争锁. 所以并不是同时执行, 而仍然是有先有后的执行.

4、wait 和 sleep 的对比(面试题)

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

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

当然为了面试的目的,我们还是总结下:

  1. wait 需要搭配 synchronized 使用. sleep 不需要.
  2. wait 是 Object 的方法 sleep 是 Thread 的静态方法.

八、多线程案例

1、单例模式

单例模式是校招中最常考的设计模式之一.

单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.
单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种.

① 饿汉模式

程序启动,则立即创建实例

例16:饿汉模式

// 单列,饿汉的方式
class Singleton {
    // static:静态,实际效果,和 字面含义 没有任何关联关系
    //         实际的含义:类属性 / 类方法
    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }

    // 构造方法设为私有!其他的类就不能 new Singleton()
    private Singleton() {
    }
}

② 懒汉模式

程序启动,等到真正使用的时候,再创建实例.

例17:懒汉模式

// 懒汉模式的实现
class SingletonLazy{
    private static SingletonLazy instance = null;

    public static SingletonLazy getInstance(){
        // 首次调用 getinstance 才会创建实例
        if (instance == null) {
        	instance = new SingletonLazy();
        }
        return instance;
    }
}

上面的懒汉模式的实现是线程不安全的.

  • 线程安全问题发生在首次创建实例时. 如果在多个线程中同时调用 getInstance 方法, 就可能导致创建出多个实例.
  • 一旦实例已经创建好了, 后面再多线程环境调用 getInstance 就不再有线程安全问题了(不再修改instance了),当线程已经安全以后,再尝试加锁,就非常影响效率了.
  • 每个线程都有自己的寄存器内容,按理来说不会出现优化,但是实际上,谁也不能保证不会出现“优化”,所以为了防止 操作系统“优化” 造成 “内存可见性” 问题,加上 volatile 是更稳妥的做法.

例18:懒汉模式(多线程)

// 懒汉模式(多线程)的实现
class SingletonLazy {
	// 为了避免 "内存可见性" 导致读取的 instance 出现偏差, 于是补充上 volatile
    volatile private static SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
    	// 当线程已经安全以后,再不再加锁了
        if (instance == null) {
        	// 加锁, 双重if,降低锁竞争的频率
            synchronized (SingletonLazy.class) {
            	// 拿到锁以后,再进一步确认,是否要初始化
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
}

2、阻塞式队列

① 概念

阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.

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

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

阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型.

无锁队列:也是一种线程安全的队列,实现内部没有使用锁,更加高校,但是同时消耗更多的 CPU 资源.
消息队列:在队列中涵盖多种不同 “类型” 的元素.取元素的时候可以按照某个类型来去,做到针对该类型的 “先进先出”.

② 生产者消费者模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。

生产者和消费者彼此之间没有交流,而是通过阻塞队列来进行,生产者生产完毕直接给阻塞队列,消费者直接通过阻塞队列获取,两者不见面.

  1. 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力.
  2. 阻塞队列也能使生产者和消费者之间 解耦.

③ 标准库中的阻塞队列

在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.

  • BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
  • put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
  • BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.

例19:生产者消费者模型

package thread;

import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

public class Demo20 {
    public static void main(String[] args) {
        BlockingDeque<Integer> queue = new LinkedBlockingDeque<>();

        Thread customer = new Thread(() -> {
            while (true) {
                try {
                    int value = queue.take();
                    System.out.println("消费元素:" + value);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        customer.start();

        Thread producer = new Thread(() -> {
            int n = 0;
            while (true) {

                try {
                    System.out.println("生产元素:" + n);
                    queue.put(n);
                    n++;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();
    }
}

在这里插入图片描述

④阻塞队列实现

  • 使用 while 而不使用 if,是因为线程从 阻塞 到被 notify 唤醒后,但是并没有立即抢到锁,然后导致条件再次成立了,此时需要继续 wait,但是 if 就会执行代码,从而出现 bug.
  • 使用 synchronized 进行加锁控制.
  • 使用 volatile 防止内存可见性问题.
  • put 插入元素的时候, 判定如果队列满了, 就进行 wait;等到 take 取出元素的时候,队列不满了,就进行 notify.
  • take 取出元素的时候, 判定如果队列为空, 就进行 wait;等到 put 插入元素的时候,队列不空了,就进行 notify.

例20:阻塞队列实现

package thread;

// 自己模拟实现一个阻塞队列.
// 基于数组的方式来实现队列.
// 提供两个核心方法:
// 1. put 入队列
// 2. take 出队列
class MylockingQueue {
    // 假定最大是 1000 个元素,当然也可以设定成可配置的
    private int[] items = new int[1000];
    // 队首的位置
    private int head = 0;
    // 队尾的位置
    private int tail = 0;
    // 队列的元素个数
    volatile private int size = 0;

    // 入队列
    public void put(int value) throws InterruptedException {
        synchronized (this) {
            while (size == items.length) {
                // 队列已满,就等待
                this.wait();
            }
            items[tail] = value;
            tail++;
            if (tail == items.length) {
                // 注意如果 tail 达到数组末尾,就需要从头开始
                tail = 0;
            }
            size++;
            // 即使没有线程在等待,多调用几次 notify 也没有影响
            this.notify();
        }
    }

    // 出队列
    public Integer take() throws InterruptedException {
        int ret = 0;
        synchronized (this) {
            while (size == 0) {
                // 队列为空,就等待
                this.wait();
            }
            ret = items[head];
            head++;
            if (head == items.length) {
                head = 0;
            }
            size--;
            this.notify();
        }
        return ret;
    }
}

public class Demo21 {
    public static void main(String[] args) throws InterruptedException {
        MylockingQueue queue = new MylockingQueue();
        Thread customer = new Thread(() -> {
            while (true) {
                int value = 0;
                try {
                    value = queue.take();
                    System.out.println("消费:" + value);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        customer.start();

        Thread producer = new Thread(() -> {
            int value = 0;
            while (true) {
                try {
                    queue.put(value);
                    System.out.println("生产:" + value);
                    value++;

                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();
    }
}

小知识:if 判断 和 取余% 的区别

// if判断
if (tail == items.length) {
	// 注意如果 tail 达到数组末尾,就需要从头开始
	tail = 0;
}
// 取余%
tail = tail % items.length;

两者的效果是一样的,只是效率上有所差距。

% 带来的问题:

  1. 可读性不好:if判断,简单明了;%取余,不一定能看懂.
  2. 对于 %取余,只有 操作数 为 2n 时,被优化为 与运算,此时比较高效.

3、定时器

类似于 “闹钟”.

① 标准库中的定时器

例21:标准库中的定时器

package thread;

import java.util.Timer;
import java.util.TimerTask;

public class Demo22 {
    public static void main(String[] args) {
        // java.util 里的一个组件
        Timer timer= new Timer();
        // schedule 这个方法的效果就是 “安排一个任务”
        // 不是立即执行,而是 3000 ms 之后再执行
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("这是要安排一个任务");
            }
        },3000);
    }
}

② 实现定时器

想要实现定时器,首先得有一个定时器类,然后还要有要执行的任务类。

  1. 创建一个 MyTask 类,里面包含一个 Runnable 对象和一个 time,同时需要按照时间排序放入优先队列中,所以需要实现 Comparable 接口.
// 通过这个类来描述一个任务
class MyTask implements Comparable<MyTask> {
    // 任务要干什么
    private Runnable command;
    // 任务什么时候干
    private long time;

    public MyTask(Runnable command, long after) {
        this.command = command;
        // 此处记录的时间是一个绝对的时间戳. 不是 “多长时间之后执行”
        this.time = System.currentTimeMillis() + after;
    }

    // 执行任务的方法,直接在内部调用 Runnable 的 run 即可.
    public void run() {
        command.run();
    }

    public long getTime() {
        return time;
    }

    @Override
    public int compareTo(MyTask o) {
        // 希望时间小的在前面,时间大的在后面
        // 这里谁减谁 才能达到时间小的在前
        // 写代码验证一下
        return (int) (this.time - o.time);
    }
}
  1. 创建一个 MyTimer 类即定时器类,通过 PriorityBlockingQueue 来组织若干个 MyTask 对象;定义一个 schedule 向队列中插入 MyTask 对象.
// 实现定时器类
class MyTimer {
    // 使用一个优先级队列来保存若干个任务
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    // command:要执行的任务是什么
    // after:多长时间之后执行这个任务
    public void schedule(Runnable command, long after) {
        MyTask myTask = new MyTask(command, after);
        queue.put(myTask);
    }
}
  1. 创建一个线程 t,用来判断是否能执行任务,具体方法为不停扫描队首元素,即距离执行时间最短的任务.
// 实现定时器类
class MyTimer {
    // 省略前面代码
    // ...

    public MyTimer() {
        // 在这里启动一个线程.
        Thread t = new Thread(() -> {
            while (true) {
                // 循环过程中,就不断的尝试从队列中获取到队首元素.
                // 判定队首元素当前的时间是否就绪。如果就绪了就执行,不就绪,就不执行.
                try {
                    synchronized (locker) {
                        MyTask myTask = queue.take();
                        long curTime = System.currentTimeMillis();
                        if (myTask.getTime() > curTime) {
                            // 时间还没到,塞回到队列中
                            queue.put(myTask);
                        } else {
                            // 时间到了,直接执行任务
                            myTask.run();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}
  1. 这样就完成了一个基本能执行的定时器,但是这样会存在一个问题:盲等问题!
    例如:此时距离下一个任务还有半小时,我从现在开始每过一会就要看一下,这样会造成 CPU 的浪费,所以不如我们使用 wait,然线程等半小时即可!
// 实现定时器类
class MyTimer {
	// 省略前面代码
    // ...

    // 用来阻塞等待的锁对象
    private Object locker = new Object();

    public MyTimer() {
        // 在这里启动一个线程.
        Thread t = new Thread(() -> {
            while (true) {
                // 循环过程中,就不断的尝试从队列中获取到队首元素.
                // 判定队首元素当前的时间是否就绪。如果就绪了就执行,不就绪,就不执行.
                try {
                    synchronized (locker) {
                        MyTask myTask = queue.take();
                        long curTime = System.currentTimeMillis();
                        if (myTask.getTime() > curTime) {
                            // 时间还没到,塞回到队列中
                            queue.put(myTask);
                            locker.wait(myTask.getTime() - curTime);
                        } else {
                            // 时间到了,直接执行任务
                            myTask.run();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}
  1. 但是这样也会有一个问题,就是如果当前线程还有 30 分钟工作,但是我插入了一个还有 20
    分钟工作的线程,现在再等30分钟,那就不行了,所以我们要在 schedule 里加上 notify
    进行唤醒,这样在每次插入的时候,我们就可以进行一次更新队首.
public void schedule(Runnable command, long after) {
	MyTask myTask = new MyTask(command, after);
	queue.put(myTask);
	synchronized (locker) {
		locker.notify();
	}
}
  1. 修改以后还有一个问题,在 MyTimer 类的构造器中,我们进行队首的扫描,那么如果在 take
    的时候出现了问题,即队列为空,线程就阻塞了,同时因为此时 take 并不会释放锁,所以锁仍然被占用,这就会导致在 schedule 中无法获取 锁,无法执行 notify,最终导致形成死锁.
// 实现定时器类
class MyTimer {
	// 省略前面代码
    // ...
    
    public MyTimer() {
        // 在这里启动一个线程.
        Thread t = new Thread(() -> {
            while (true) {
                // 循环过程中,就不断的尝试从队列中获取到队首元素.
                // 判定队首元素当前的时间是否就绪。如果就绪了就执行,不就绪,就不执行.
                try {
                    synchronized (locker) {
                        while (queue.isEmpty()) {
                            locker.wait();
                        }
                        MyTask myTask = queue.take();
                        long curTime = System.currentTimeMillis();
                        if (myTask.getTime() > curTime) {
                            // 时间还没到,塞回到队列中
                            queue.put(myTask);
                            locker.wait(myTask.getTime() - curTime);
                        } else {
                            // 时间到了,直接执行任务
                            myTask.run();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

完整代码:
例22:实现定时器

package thread;

import java.util.PriorityQueue;
import java.util.Timer;
import java.util.concurrent.PriorityBlockingQueue;

// 通过这个类来描述一个任务
class MyTask implements Comparable<MyTask> {
    // 任务要干什么
    private Runnable command;
    // 任务什么时候干
    private long time;

    public MyTask(Runnable command, long after) {
        this.command = command;
        // 此处记录的时间是一个绝对的时间戳. 不是 “多长时间之后执行”
        this.time = System.currentTimeMillis() + after;
    }

    // 执行任务的方法,直接在内部调用 Runnable 的 run 即可.
    public void run() {
        command.run();
    }

    public long getTime() {
        return time;
    }

    @Override
    public int compareTo(MyTask o) {
        // 希望时间小的在前面,时间大的在后面
        // 这里谁减谁 才能达到时间小的在前
        // 写代码验证一下
        return (int) (this.time - o.time);
    }
}

// 实现定时器类
class MyTimer {
    // 用来阻塞等待的锁对象
    private Object locker = new Object();

    // 使用一个优先级队列来保存若干个任务
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();

    // command:要执行的任务是什么
    // after:多长时间之后执行这个任务
    public void schedule(Runnable command, long after) {
        MyTask myTask = new MyTask(command, after);
        queue.put(myTask);
        synchronized (locker) {
            locker.notify();
        }
    }

    public MyTimer() {
        // 在这里启动一个线程.
        Thread t = new Thread(() -> {
            while (true) {
                // 循环过程中,就不断的尝试从队列中获取到队首元素.
                // 判定队首元素当前的时间是否就绪。如果就绪了就执行,不就绪,就不执行.
                try {
                    synchronized (locker) {
                    	while (queue.isEmpty()) {
                            locker.wait();
                        }
                        MyTask myTask = queue.take();
                        long curTime = System.currentTimeMillis();
                        if (myTask.getTime() > curTime) {
                            // 时间还没到,塞回到队列中
                            queue.put(myTask);
                            locker.wait(myTask.getTime() - curTime);
                        } else {
                            // 时间到了,直接执行任务
                            myTask.run();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

public class Demo23 {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("1111");
            }
        }, 2000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("2222");
            }
        }, 4000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("3333");
            }
        }, 6000);
    }
}

4、线程池

①概念

线程池,解决问题的思路:

  • 将线程创建完毕后,放入线程池.
  • 当需要使用线程的时候,直接从线程池中获取,而不是通过系统来创建.
  • 当线程使用完毕时,还回线程池中,而不是通过系统来销毁.

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

②标准库中的线程池

Executors 创建线程池的几种方式:

  • newFixedThreadPool: 创建固定线程数的线程池.
  • newCachedThreadPool: 创建线程数目动态增长的线程池.
  • newSingleThreadExecutor: 创建只包含单个线程的线程池.
  • newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.

例23:标准库中的线程池

package thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo24 {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        // Executors.newCachedThreadPool();

        threadPool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });
    }
}

③实现线程池

例24:实现线程池

package thread;

import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

// 实现线程池类
// 简单实现:固定 10 个线程的线程池
class MyThreadPool {
    // 这个队列就是 “任务队列” 把当前线程池要完成的任务都放到这个队列中.
    // 再由线程池内部的工作线程负责完成它们
    private BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();

    // 核心方法,往线程池中插入任务
    public void submit(Runnable runnable) {
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 设定线程池中有几个线程
    public MyThreadPool(int n) {
        // 构造方法中,就需要创建一些线程,让这些线程负责完成上述执行任务的工作!!
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                while (!Thread.currentThread().isInterrupted()) {
                    try {
                        Runnable runnable = queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }
            });
            t.start();
        }
    }
}

public class Demo25 {
    public static void main(String[] args) {
        MyThreadPool myThreadPool =new MyThreadPool(10);
        for (int i = 0; i < 100; i++) {
            myThreadPool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello");
                }
            });
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

WE-ubytt

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

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

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

打赏作者

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

抵扣说明:

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

余额充值