likes
comments
collection
share

多线程

作者站长头像
站长
· 阅读数 67

1. 认识线程

1.1. 概念

  1. 什么是线程?

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

举个例子就是造一台电脑,一个人造的话就是单线程,如果将电脑的每个部位分给多个人,每个人干自己的活,这就是多线程。

  1. 为什么要有多线程?

并发变成成为现代所需

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

其实多进程也能实现并发变成,但是线程要比进程更轻量级。

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

在拥有了线程之后,为了方便管理,又有了“线程池”、“协程”。

  1. 进程和线程的区别
  • 进程是包含线程的,每个进程至少有一个线程的存在,即主线程(管理其他线程)。
  • 进程和进程之间不共享内存,在同一进程下的线程共享同一内存空间。
  • 进程是分配资源的最小单位,线程是系统调度的最小单位

多线程

1.2. 体验多线程编程

编写一个简单的并发编程程序,我们发现

  • 每个线程都是一个独立的执行流
  • 多个线程之间是“并发”执行的
class ThreadDemo extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("ThreadDemo");
        }
    }
}

public class Demo1 {
    public static void main(String[] args) {//主线程
        ThreadDemo threadDemo = new ThreadDemo();
        threadDemo.start();
        while (true) {
            System.out.println("main");
        }
    }
}

多线程

1.3. 创建线程

1.3.1. 继承Thread

  1. 继承Thread来创建一个线程类
class ThreadDemo extends Thread {
    @Override
    public void run() {
        //编写线程运行代码
    }
}
  1. 创建线程类实例
ThreadDemo threadDemo = new ThreadDemo();
  1. 启动调用start()方法启动线程
threadDemo.start();

优点

  • 实现简单
  • 直接使用线程方法:可以直接调用Thread类中的方法,如start(),sleep(),getId()

缺点

  • 不适合多继承:Java中类不能多继承。
  • 因为直接继承自Thread类,创建的对象会包含Thread类的一些额外特性和数据,可能会比实现Runnable接口更重。

1.3.2. 实现Runnable接口

  1. 创建线程类ThreadDemo2并实现Runnable接口
class ThreadDemo2 implements Runnable {
    @Override
    public void run() {
        //编写线程运行代码
    }
}
  1. 创建Thread实例,调用Thread的构造返方法并传递线程类实例(ThreadDemo2)
Thread thread = new Thread(new ThreadDemo2());
  1. 调用start()方法
thread.start();

优点

  • 适合多继承
  • 资源分离:通过将线程的任务与线程控制分离(任务在Runnable实现类中,控制在Thread类中),可以更清晰地管理线程的行为和业务逻辑。
  • 更轻的类: 实现Runnable接口的类不需要继承Thread,所以不会有额外的线程控制数据,类的重量较轻。

缺点

  • 需要额外的Thread实例: 必须创建一个Thread对象来运行实现Runnable接口的任务,略显冗余。

1.4. 多线程的优势

多线程是可以提高程序整体运行效率的,例如下面我们使用计时来观察单线程和多线程的效果。

public class Demo3 {
    // 多线程并不一定就能提高速度,可以观察,count 不同,实际的运行效果也是不同的
    private static final long count = 10_0000_0000;

    public static void main(String[] args) throws InterruptedException {
        // 使用并发方式
        concurrency();
        // 使用串行方式
        serial();
    }

    private static void concurrency() throws InterruptedException {
        long begin = System.nanoTime();
        // 利用一个线程计算 a 的值
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 0;
                for (long i = 0; i < count; i++) {
                    a--;
                }
            }
        });
        thread.start();
        // 主线程内计算 b 的值
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        // 等待 thread 线程运行结束
        thread.join();
        // 统计耗时
        long end = System.nanoTime();
        double ms = end - begin;
        System.out.printf("并发: %f 毫秒%n", ms);
    }

    private static void serial() {
        // 全部在主线程内计算 a、b 的值
        long begin = System.nanoTime();
        int a = 0;
        for (long i = 0; i < count; i++) {
            a--;
        }
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        long end = System.nanoTime();
        double ms = end - begin;
        System.out.printf("串行: %f 毫秒%n", ms);
    }
}

多线程

2. Thread类

2.1. Thread的常见构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名
Thread(ThreadGroup group, Runnable target)线程可以被用来分组管理,分好的组即为线程组
Thread thread1 = new Thread();
Thread thread2 = new Thread(new ThreadDemo2());
Thread thread3 = new Thread("线程3");
Thread thread4 = new Thread(new ThreadDemo2(),"线程4");

2.2. Thread的常见属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()
  • ID是线程的唯一标识(不会重复)
  • 名称在调试时候常用
  • 状态标识线程当前所处的一个情况
  • 优先级高的线程理论上来说更容易被调度到
  • 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
  • run方法运行结束后线程结束

2.3. start()

run方法并不是启动线程,之前我们重写run方法是书写线程执行内容。调用start()方法才是正真执行此线程

2.4. 中断一个线程

线程进入到执行状态之后如何进行中断?

目前常见的有两种方法:

  • 通过共享的标记来进行沟通
  • 调用interrupt()方法中断

示例一:使用自定义的变量来作为标志位

  • 需要给标志位加上volatile关键字
public class Demo5 {
    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, "thread线程");
        System.out.println(Thread.currentThread().getName()
                + ": 开始执行。");
        thread.start();
        Thread.sleep(10 * 1000);
        System.out.println(Thread.currentThread().getName()
                + ": 终止执行!");
        target.isQuit = true;
    }
}

多线程

示例二:使用Thread.Interrupted() 或者Thread.currentThread().isInterrupted()代替自定义标志位

方法说明
public void interrupt()中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位
public static boolean interrupted()判断当前线程的中断标志位是否设置,调用后清除标志位
public boolean isInterrupted()判断对象关联的线程的标志位是否设置,调用后不清除标志位
  • 使用thread对象的interrupted()方法通知线程结束
public class Demo6 {
    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            while (!Thread.interrupted()) {
                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, "thread线程");
        System.out.println(Thread.currentThread().getName()
                + ":thread开始执行。");
        thread.start();
        Thread.sleep(10 * 1000);
        System.out.println(Thread.currentThread().getName()
                + ":thread终止执行!");
        thread.interrupt();
    }
}

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

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

示例三 :观察标志位是否清除

标志位就类似于一个开关,Thread.isInterrupted()相当于按下开关,然后开关自动复位

Thread.currentThread().isInterrupted()按下开关,但是不复位

  1. 使用Thread.isInterrupted(),线程中断会清除标志位
public class Demo7 {
    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.interrupted());
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "Demo7");
        thread.start();
        thread.interrupt();
    }
}

多线程

  1. 使用 Thread.currentThread().isInterrupted() , 线程中断标记位不会清除
public class Demo7 {
    private static class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().isInterrupted());
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable target = new MyRunnable();
        Thread thread = new Thread(target, "Demo7");
        thread.start();
        thread.interrupt();
    }
}

多线程

2.5. 等待线程

有时候代码需要一个程序执行完毕之后再继续往下执行。就需要用到join()方法

public class Demo8 {
    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("王五工作结束了");
    }
}

上面这代码就是李四先工作完,然后王五在开始工作。

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

2.6. 获取当前线程引用

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

2.7. 休眠线程

这个方法我们上面已经用过了,就是让线程停止运行一段时间。

方法说明
public static void sleep(long millis) throws InterruptedException休眠当前线程 millis毫秒
public static void sleep(long millis, int nanos) throws InterruptedException可以更高精度的休眠
public class Demo10 {
    public static void main(String[] args) throws InterruptedException {
        System.out.println(System.currentTimeMillis());
        Thread.sleep(3 * 1000);
        System.out.println(System.currentTimeMillis());
    }
}

3. 线程的状态

3.1. 观察线程的所有状态

public class Demo1 {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values())
            System.out.println(state);
    }
}
NEW		//安排了工作, 还未开始行动
RUNNABLE		 //可工作的. 又可以分成正在工作中和即将开始工作.
BLOCKED		//表示排队等着其他事情
WAITING		//表示排队等着其他事情
TIMED_WAITING		//表示排队等着其他事情
TERMINATED		//工作完成了

3.2. 线程状态和状态转移的意义

网图(侵权删)

多线程

多线程

3.3. 线程状态转换

使用isAlive方法判定线程的存活状态

  1. 观察new、runnable、terminated
public class Demo2 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            for (int i = 0; i < 1_0000; i++) {
            }
        }, "李四");
        System.out.println(t.getName() + ": " + t.getState());
        t.start();
        while (t.isAlive()) {
            System.out.println(t.getName() + ": " + t.getState());
        }
        System.out.println(t.getName() + ": " + t.getState());
    }
}
李四: NEW
李四: RUNNABLE
李四: RUNNABLE
李四: RUNNABLE
李四: RUNNABLE
李四: TERMINATED
  1. 观察WAITING、BLOCKED、TIMED_WAITING状态的转换
public class Demo3 {
    public static void main(String[] args) {
        final Object object = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    while (true) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }, "t1");
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    System.out.println("hehe");
                }
            }
        }, "t2");
        t2.start();
    }
}

我们直接使用jconsole看到t1的状态是TIMED_WAITING 、t2的状态是BLOCKED,阻塞者是t1 多线程多线程

接着我们将t1中的sleep换成wait

public class Demo4 {
    public static void main(String[] args) {
        final Object object = new Object();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    try {
                        object.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "t1");
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (object) {
                    System.out.println("hehe");
                }
            }
        }, "t2");
        t2.start();
    }
}

多线程

hehe //输出打印

总结一下

  • BLOCKED 表示等待获取锁, WAITING 和 TIMED_WAITING 表示等待其他线程发来通知
  • TIMED_WAITING 线程在等待唤醒,但设置了时限; WAITING 线程在无限等待唤醒
  1. yield()让出cpu
public class Demo5 {
    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("t1");
                    // 先注释掉, 再放开
                    // Thread.yield();
                }
            }
        }, "t1");
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("t2");
                }
            }
        }, "t2");
        t2.start();
    }
}
  • 不使用yield时t1打印数量与t2相差无几
  • 使用yield时t1数量大于t2

yield不会改变线程的状态,但是会重新去排队

4. 线程安全

4.1. 举例线程不安全

public class Demo6 {
    static class Counter {
        public int count = 0;
        void increase() {
            count++;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        final Counter counter = new Counter();
        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);
    }
}
54111 //运行结果

为什么会出现这种情况?

假设当i = 10000的时候,t1、t2并行同时判断i都不小于50000,所以会同时调用increase方法,假设此时内存中的count = 20000,但是t1 执行完count++操作之后,得20001,此时内存的count还未及时更新,但是t2线程获取到的count也为20000,执行完count++操作也是20001,2次执行只加了一次值。所以所以如果程序运行越快,最终结果与预期结果相差越多。

4.2. 线程安全的概念

具体来说,线程安全意味着:

  1. 数据完整性:多个线程同时操作共享数据时,不会导致数据的不一致或破坏。
  2. 原子性:线程执行的关键操作是不可分割的,即使有多个线程并发执行,也不会出现中间状态。
  3. 可见性:一个线程对共享数据的修改,对于其他线程是可见的,即其他线程能够看到最新的修改结果。
  4. 有序性:操作执行的顺序是可预期的,不会因为线程调度的不确定性而导致错误的执行顺序。

简单来说就是如果多线程环境下的代码运行的结果符合我们的预期,也就是多线程运行的结果与单线程运行的结果一致,那么就是线程安全的。

4.3. 线程不安全的原因

4.3.1. 修改共享数据

对于上面的线程不安全的示例中,counter.count变量是一个共享变量,它在堆上,所以可以被多个线程访问。

多线程

4.3.2. 原子性

  1. 什么是原子性?

如果我们把一段代码比喻为一间房,假设房间不上锁,那么在A进入房间之后,B也可以进入房间。如果加锁且这个锁只有A有钥匙,那么在A出来之前其他人是进不去的,这就是原子性。

  1. 一条Java语句不一定是原子性的,也不一定是一条指令

比如我们代码中的n++,其实是由三步组成的:

    1. 从内存中把数据读到CPU
    2. 进行数据更新
    3. 把数据写回到CPU
  1. 不保证原子性会出现什么问题?

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

4.3.3. 可见性

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

  1. Java 内存模型(JMM):java虚拟机规范中定义了Java内存模型.

目的是屏蔽掉各种硬件和操作系统的内容访问差异,以实现让Java程序在各种平台下都能达到一致并发效果。

多线程

  • 线程之间的共享变量存在 主内存 (Main Memory).
  • 每一个线程都有自己的 "工作内存" (Working Memory) .
  • 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
  • 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存

由于每个线程都有自己的工作内存,这些内存中的内容相当于同一个共享变量的“副本”。此时修改线程1的工作内容中的值,线程2的工作内存不一定会即使发生变化。

  1. 初始化情况下,两个线程的工作内容一致。

多线程

  1. 一旦线程1修改了a的值,此时主内存中不一定能及时更新。对应的线程2的工作内容的a的值也不一定能及时同步

多线程

此时引入了两个问题: 为啥要整这么多内存?为啥要这么麻烦的拷来拷去?

  1. 为啥整这么多内存?实际并没有这么多 "内存". 这只是 Java 规范中的一个术语, 是属于 "抽象" 的叫法.所谓的 "主内存" 才是真正硬件角度的 "内存". 而所谓的 "工作内存", 则是指 CPU 的寄存器和高速缓存.
  2. 为啥要这么麻烦的拷来拷去?因为 CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级, 也就是几千倍, 上万倍)

比如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的. 但是如果只是第一次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9 次读数据就不必直接访问内存了. 效率就大大提高了

4.3.4. 代码顺顺序性

例如程序是这样的:

  1. 去小卖部买一瓶饮料
  2. 去教室拿本书
  3. 去小卖部买支笔

如果是在单线程的情况下,JVM、CPU指令集会对器进行优化,比如按照1->3->2执行,也是可以的,可以少跑一趟小卖部,这样叫做指令重排序。

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

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

public class Demo6 {
    static class Counter {
        public int count = 0;
        synchronized void increase() {
            count++;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        final Counter counter = new Counter();
        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);
    }
}

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

5.1. synchronized的特征

  1. 互斥

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

  • 进入synchronized修饰的代码块,相当于加锁。
  • 退出synchronized修饰的代码块,箱单故意解锁。
  • synchronized用的锁时存在Java对象里的。

例如t1先进入count++程序,那么t1就对此程序加锁了,只有它执行完毕,才会解锁。t2在此过程只能等待。

  1. 刷新内存

synchronized的工作过程:

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

所以synchronized也能保证内存可见性

  1. 可重入

什么是可重入的?

本来上过锁的程序线程想再次获取的话应该是阻塞等待释放。可重入指可重复获得锁,例如线程t1已经获取一个加锁对象,他想再次获取此加锁对象是可以的。

内部机制

当一个线程第一次进入同步方法时,它获取对象的锁并持有它。每当它再次进入这个或其他同步方法时,Java 允许线程再次获取这个锁,而不会阻塞自己。这是通过一个计数器来实现的:

  • 第一次获取锁时,计数器设为 1。
  • 每次重入(即再次获取锁)时,计数器递增。
  • 每次退出同步方法时,计数器递减。
  • 当计数器为 0 时,锁被释放。

5.2. synchronized使用示例

synchronized本质上要修改指定对象的“对象头”。从使用角度来看,synchronized也势必要搭配一个具体的对象来使用。

  1. 修饰普通方法:锁Demo1中的静态方法fun
public class Demo1 {
    public synchronized void fun() {
        //方法内容
    }
}
  1. 修饰静态方法:
public class Demo1 {
    public synchronized static void fun() {
        //方法内容
    }
}
  1. 修饰代码块:指定锁哪个对象

锁当前对象

class Demo2 {
    public void fun() {//此时fun方法不可为静态方法
        synchronized (this) {
    
        }
    }
}

锁类对象

class Demo2 {
    public void fun() {
        synchronized (Demo2.class) {

        }
    }
}

5.3. 线程安全类举例

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

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder

线程安全类

  • Vector
  • HashTable
  • ConcurrentHashMap
  • StringBuffer

还有String虽然没有加锁,但是String是不能修改的,所以仍然是线程安全的

6. volatile关键字

volatile能保证内存可见性

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

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

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

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

但是volatile不保证原子性

这个很好验证,我们只要将synchronized拿掉,并给变量加上volatile。

public class Demo6 {
    static class Counter {
        public volatile int count = 0;
        void increase() {
            count++;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        final Counter counter = new Counter();
        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);
    }
}
55566 //输出结果

看到结果不等于10_0000就知道了

synchronized 也能保证内存可见性

synchronized 既能保证原子性, 也能保证内存可见性.

对上面的代码进行调整:

  • 去掉 flag 的 volatile
  • 给 t1 的循环内部加上 synchronized, 并借助 counter 对象加锁.
public class Demo6 {
    static class Counter {
        public int flag = 0;
    }
    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            while (true) {
                synchronized (counter) {
                    if (counter.flag != 0) {
                        break;
                    }
                }
            }
            System.out.println("循环结束!");
        });
        Thread t2 = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入一个整数:");
            counter.flag = scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

7. wait和notify

由于线程之间是抢占式的,因此线程之间执行的先后顺序难以预知。

想合理协调多个线程之间的执行顺序怎么办呢?

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

wait,notify,notifyAll都是Object类的方法

7.1. wait()

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

wait结束等待的条件:

  • 其他线程调用该对象的notify方法
  • wait等待时间超时(wait方法提供一个带有timeout参数的版本,来指定等待时间)
  • 其他线程调用该等待线程的interrupted方法,导致wait抛出InterruptedException
public static void main(String[] args) throws InterruptedException {
    Object object = new Object();
    synchronized (object) {
        System.out.println("等待中");
        object.wait();
        System.out.println("等待结束");
    }
}

这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了另外一个方法唤醒的方法notify() 。

7.2. notify()

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

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

        public WaitTask(Object locker) {
            this.locker = locker;
        }

        @Override
        public void run() {
            synchronized (locker) {
                while (true) {
                    try {
                        System.out.println("wait 开始");
                        locker.wait();
                        System.out.println("wait 结束");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    static class NotifyTask implements Runnable {
        private Object locker;

        public NotifyTask(Object locker) {
            this.locker = locker;
        }

        @Override
        public void run() {
            synchronized (locker) {
                System.out.println("notify 开始");
                locker.notify();
                System.out.println("notify 结束");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(new WaitTask(locker));
        Thread t2 = new Thread(new NotifyTask(locker));
        t1.start();
        Thread.sleep(1000);
        t2.start();
    }
}

7.3. notifyAll()

notify方法唤醒一个等待线程,使用notifyAll方法可以一次性唤醒所有的等待线程,在这里就不细讲了。

转载自:https://juejin.cn/post/7376276140595068937
评论
请登录