并发编程入门---多线程【随笔】

多线程

基本概念
Process 与 Thread

说起进程,就不得不说下程序。程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。而进程则是执行程序的一次执行过程,它是一个动态的概念。是系统资源分配的单位,通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程是CPU调度执行的单位。

进程与线程的区别总结:

  • 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

  • 资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

  • 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

  • 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

  • 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

  • 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

线程核心概念
  • 线程就是独立的执行路径;
  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程、gc线程等;
  • main() 称之为主线程,为系统的入口,用于执行整个程序;
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的。
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制;
  • 线程会带来额外的开销,如 cpu 调度时间,并发控制开销。
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。

20201210193120955

线程的创建

常用的三种线程创建方式:

  • 继承 Thread
  • 实现 Runnable 接口
  • 实现 Callable 接口
继承 Thread

自定义线程类继承 Thread 类;重写 run() 方法,编写线程执行体;创建线程对象,调用 start() 方法启动线程。具体案例如下:

public class FileUtil {

    public static void downloader(String url, String name) throws IOException {
        try {
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            throw new IOException(e.getMessage());
        }
    }
}
public class ThreadTest extends Thread {

    private String url;

    private String name;

    public ThreadTest(String url, String name) {
        this.url = url;
        this.name = name;
    }

    @Override
    public void run() {
        try {
            FileUtil.downloader(url, name);
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 下载了文件名为: " + name);
    }

    public static void main(String[] args) {
        ThreadTest threadTest1 = new ThreadTest("http://akieqh.com/images/gallery/anime/akieay-anime-425638501314678784.webp", "test1.webp");
        ThreadTest threadTest2 = new ThreadTest("http://akieqh.com/images/gallery/anime/akieay-anime-425638422809890816.webp", "test2.webp");
        ThreadTest threadTest3 = new ThreadTest("http://akieqh.com/images/gallery/anime/akieay-anime-425638545140961280.webp", "test3.webp");

        threadTest1.start();
        threadTest2.start();
        threadTest3.start();
    }

}
实现 Runnable 接口

自定义类实现 Runnable 接口;实现 run() 方法,编写线程执行体;创建线程对象,调用 start() 方法启动。案例如下:

public class RunnableTest implements Runnable {

    private static String winner;

    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {

            if (Thread.currentThread().getName().equals("兔子") && i % 10 == 0) {
                try {
                    Thread.sleep(40);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                try {
                    Thread.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            boolean flag = gameOver(i);
            if (flag) {
                break;
            }
            System.out.println(Thread.currentThread().getName() + "-----> 跑了 " + i + "步");
        }
    }

    private boolean gameOver(int steps) {
        if (StrUtil.isNotBlank(winner)) {
            return true;
        } else {
            if (steps == 100) {
                winner = Thread.currentThread().getName();
                System.out.println("winner is " + winner);
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        RunnableTest rt = new RunnableTest();
        new Thread(rt, "兔子").start();
        new Thread(rt, "乌龟").start();
    }
}

相对来说更推荐通过实现 Runnable 接口的方式;避免单继承局限性,灵活方便,方便同一个对象被多个线程使用。

实现 Callable 接口

实现 Callable 接口,需要返回值类型,重写 call 方法,需要抛出异常,步骤为:创建目标对象、创建执行服务、提交执行、获取结果、关闭服务。案例如下:

public class CallableTest implements Callable<Boolean> {

    private String url;

    private String name;

    public CallableTest(String url, String name) {
        this.url = url;
        this.name = name;
    }

    @Override
    public Boolean call() throws Exception {
        try {
            FileUtil.downloader(url, name);
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        System.out.println(Thread.currentThread().getName() + " 下载了文件名为: " + name);
        return true;
    }

    public static void main(String[] args) throws Exception {
        CallableTest callableTest1 = new CallableTest("http://akieqh.com/images/gallery/anime/akieay-anime-425638501314678784.webp", "test1.webp");
        CallableTest callableTest2 = new CallableTest("http://akieqh.com/images/gallery/anime/akieay-anime-425638422809890816.webp", "test2.webp");
        CallableTest callableTest3 = new CallableTest("http://akieqh.com/images/gallery/anime/akieay-anime-425638545140961280.webp", "test3.webp");

        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
                .setNamePrefix("thread").build();
        ExecutorService es = new ThreadPoolExecutor(3, 5, 2000L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(64), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

        Future<Boolean> submit1 = es.submit(callableTest1);
        Future<Boolean> submit2 = es.submit(callableTest2);
        Future<Boolean> submit3 = es.submit(callableTest3);

        Boolean result1 = submit1.get();
        Boolean result2 = submit2.get();
        Boolean result3 = submit3.get();
        System.out.println("result1: " + result1);
        System.out.println("result2: " + result2);
        System.out.println("result3: " + result3);

        es.shutdownNow();
    }
}
线程的状态
状态转换图

20201210195233906

20201210195310910

Java 线程在运行的生命周期中的指定时刻只能处于以下状态之一:

  • NEW:创建状态,尚未启动的线程处于此状态。
  • RUNNABLE:运行状态,在 java 虚拟机中执行的线程处于此状态【就绪、运行】。
  • BLOCKED:阻塞状态,被阻塞等待监视器锁定的线程处于此状态。
  • WAITING:等待状态,正在等待另一个线程执行特定动作【通知、中断】的线程处于此状态。
  • TIMED_WAITING:超时状态,正在等待另一个线程执行动作达到指定等待时间的线程处于此状态【达到指定时间自动返回】。
  • TERMINATED:终止状态,已退出的线程处于此状态。

20201222143323016

测试查看各状态案例:

public class ThreadStateTest {

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

        Thread.State state = thread.getState();
        System.out.println(state); // NEW

        thread.start();
        state = thread.getState();
        System.out.println(state); // RUNNABLE

        while (state != Thread.State.TERMINATED) {
            Thread.sleep(100);
            state = thread.getState();
            System.out.println(state);
        }
    }
}
常用方法

20201210195416136

线程优先级

Java 提供了一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。但是当 CPU 比较闲的时候,设置线程优先级几乎不会有任何作用,而且很多操作系统压根不会理会你设置的线程优先级,所以不要让业务过度依赖于线程的优先级

线程的优先级用数字表示,范围从 1~10,数值越大的优先级越高,默认线程优先级为 5:

  • Thread.MIN_PRIORITY = 1;
  • Thread.NORM_PRIORITY = 5;
  • Thread.MAX_PRIORITY = 10;

线程优先级具有继承特性: 在当前线程内启动的新线程默认继承当前线程的优先级。

优先级低只是意味着获得调度的概率低,并不是优先级低就不会被调用了,这都是要看 CPU 的调度,优先级低的也有可能被先调用。

thread.setPriority(Thread.MAX_PRIORITY); // 设置优先级
thread.getPriority(); // 获取优先级
线程延时

sleep(time) 指定当前线程阻塞的毫秒数;sleep 存在异常 InterruptedException,sleep 时间达到后线程进入就绪状态,sleep 可以模拟网络的延时,倒计时等。每一个对象都有一个锁,sleep 不会释放锁。

thread.setPriority(Thread.MIN_PRIORITY);
合并线程

join 合并线程,在线程中插入执行另一个线程,该线程被阻塞,直到插入执行的线程完全执行完毕以后,该线程才继续执行下去,可以理解为插队。

thread.join();
线程礼让

礼让线程,让当前执行的线程暂停但不阻塞,将线程从运行状态转为就绪状态,让 CPU 重新调度。礼让不一定成功,结果取决于 CPU。需要注意的是,让出的CPU并不是代表当前线程不再运行了,如果在下一次竞争中,又获得了 CPU 时间片当前线程依然会继续运行。另外,让出的时间片只会分配给与当前线程 相同优先级 的线程

Thread.yield();
停止线程

停止线程:不推荐使用 JDK 提供的 stop()、destroy() 方法【已废弃】;推荐通过代码逻辑让线程自己停止下来,建议使用一个标志位作为终止变量,当 flag = false,则终止线程运行。

案例如下:

public class StopTest implements Runnable {
    /**
     * 设置一个标识位
     */
    private boolean flag = true;

    @Override
    public void run() {
        int i = 0;
        while (flag) {
            System.out.println("run.......Thread " + i++);
        }
    }

    /**
     * 设置一个公开的方法停止线程,转换标识位
     */
    public void stop() {
        this.flag = false;
    }

    public static void main(String[] args) {
        StopTest stopTest = new StopTest();
        new Thread(stopTest).start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("main-->" + i);
            if (i == 900) {
                stopTest.stop();
                System.out.println("线程被终止");
            }
        }
    }
}
守护线程

线程分为 用户线程 和 守护线程,虚拟机必须确保用户线程执行完毕,虚拟机不用等待守护线程执行完毕;只要当前 JVM 实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着 JVM 一同结束工作。守护线程的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。

  • 用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
  • 守护 (Daemon) 线程:运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作。
public class DeamonTest {
    public static void main(String[] args) {
        Thread thread = new Thread(new God());
        thread.setDaemon(true);
        thread.start();

        new Thread(new You()).start();
    }
}

class God implements Runnable {

    @Override
    public void run() {
        while (true) {
            System.out.println("人生不易,勿忘初心");
        }
    }
}

class You implements Runnable {

    @Override
    public void run() {
        int i = 365;
        while (i > 0) {
            System.out.println("你开心的活者...,还剩" + i-- +"天");
        }
        System.out.println("-=====你挂了====-");
    }
}

特别注意:

  • thread.setDaemon(true) 必须在 thread.start() 之前设置,否则会抛出一个 IllegalThreadStateException 异常,你不能把正在运行的常规线程设置为守护线程。
  • 在 Daemon 线程中产生的新线程也是 Daemon 的。
  • 不是所有的业务都可以分配给 Daemon 来执行,比如 DB操作、读写操作或者计算逻辑。因外在用户线程执行完成的时候,你不能保证 Daemon 已经完成了预期的任务;一旦用户线程完成退出,Daemon 就会被销毁,不管其中的任务是否已经全部完成,这对程序来说是毁灭性的。
  • 守护线程在退出的时候并不一定会执行 finnaly 块中的代码,所以守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑
线程同步

由于同一进程的多个线程共享一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了确保数据在方法中被访问时的正确性,在访问时加入 锁机制 synchronized,当一个线程获得对象的排它锁时,独占资源,其他线程必须等待,使用完成释放锁后其它线程才能去获取。

但同时也带来了以下问题:

  • 一个线程持有锁会导致其它所有需要此锁的线程挂起;
  • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换 和 调度延时,引起性能问题;
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题。
synchronized

我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需要针对方法提出一套机制,这套机制就是 synchronized 关键字,它包括两种用法:synchronized 方法synchronized 块

synchronized 方法 控制对 “对象” 的访问,每个对象对应一把锁,每个 synchronized 方法 都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。缺陷:若将一个大的方法声明为 synchronized 将会影响效率。

同步块:synchronized (Obj) {}

Obj 称之为 同步监视器

  • Obj 可以是任何对象,但是推荐使用共享资源作为同步监视器
  • 同步方法中无需指定同步监视器,因为同步方法的同步监视器是 this,就是这个对象本身,或者是 class

同步监视器的执行过程:

  • 第一个线程访问,锁定同步监视器,执行其中代码
  • 第二个线程访问,发现同步监视器被锁定,无法访问
  • 第一个线程访问完毕,解锁同步监视器
  • 第二个线程访问,发现同步监视器没有锁,然后锁定并访问

一、同步方法

public class UnsafeBuyTicket {
    
    public static void main(String[] args) {
        BuyTicket bt = new BuyTicket();

        new Thread(bt, "小王").start();
        new Thread(bt, "小张").start();
        new Thread(bt, "黄牛").start();
    }
}

class BuyTicket implements Runnable {

    /**
     * 票
     */
    private int ticketNumber = 10;
    /**
     * 停止标志
     */
    private boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            buy();
        }
    }

    /**
     * 同步方法 synchronized 默认锁的是 this
     */
    public synchronized void buy() {
        if (ticketNumber <= 0) {
            flag = false;
            return;
        }
        System.out.println(Thread.currentThread().getName() + " 拿到了第 " + ticketNumber-- + "张票");
    }
}

二、同步块

public class UnsafeBank {

    public static void main(String[] args) {
        Account account = new Account(1000, "中古测试卡");

        new Thread(new Drawing(account, 50), "小明").start();
        new Thread(new Drawing(account, 100), "小木").start();
    }
}

class Account {
    /**
     * 银行卡余额
     */
    int money;
    /**
     * 银行卡名称
     */
    String cardName;

    public Account(int money, String cardName) {
        this.money = money;
        this.cardName = cardName;
    }
}

class Drawing implements Runnable {
    /**
     * 账户
     */
    private Account account;
    /**
     * 取出的钱
     */
    private int drawingMoney;
    /**
     * 手上的钱
     */
    private int nowMoney;

    public Drawing(Account account, int drawingMoney) {
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {
        synchronized (account) {
            if (account.money < drawingMoney) {
                System.out.println("银行卡 " + account.cardName + " 余额不足,无法取款");
                return;
            }

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            account.money -= drawingMoney;
            nowMoney += drawingMoney;
            System.out.println(Thread.currentThread().getName() + " 从银行卡 " + account.cardName + " 取款 " + drawingMoney);
            System.out.println(Thread.currentThread().getName() + " 身上的 money 为: " + nowMoney);
            System.out.println("银行卡" + account.cardName + "余额为: " + account.money);
        }
    }
}
死锁

死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)。
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

cff16df19c2ffe9621333dd219990df69664a007

以下为一个死锁示例:下面有两个资源分别被两个线程占用,并且都想要获得对方的资源且不愿意释放自己的资源,造成互相等待的情况。

public class DeadLock {

    public static void main(String[] args) {
        new Thread(new Makeup(0, "雨墨")).start();
        new Thread(new Makeup(1, "雨轩")).start();
    }
}

/**
 * 口红
 */
class Lipstick {

}

/**
 * 镜子
 */
class Mirror {

}

/**
 * 化妆,模拟互相持有对方的所需要资源
 */
class Makeup implements Runnable {

    /**
     * 资源-口红【一份】
     */
    static Lipstick lipstick = new Lipstick();
    /**
     * 资源-镜子【一份】
     */
    static Mirror mirror = new Mirror();

    private int choice;

    private String girlName;

    public Makeup(int choice, String girlName) {
        this.choice = choice;
        this.girlName = girlName;
    }

    @Override
    public void run() {
        try {
            makcup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void makcup() throws InterruptedException {
        if (choice == 0) {
            synchronized (lipstick) {
                System.out.println(girlName + "获得口红资源");
                Thread.sleep(1000);
                synchronized (mirror) {
                    System.out.println(girlName + "获得镜子资源");
                }
            }
        } else {
            synchronized (mirror) {
                System.out.println(girlName + "获得镜子资源");
                Thread.sleep(1000);
                synchronized (lipstick) {
                    System.out.println(girlName + "获得口红资源");
                }
            }
        }
    }
}

20201222173044788

产生死锁的四个必要条件:

  • 互斥条件:线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放
  • 请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  • 循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞

上面列出来死锁的四个必要条件,我们只要想办法破环其中任意一个或多个条件就可以避免死锁发生。

  • 破坏互斥条件:这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
  • 破坏请求与保持条件: 一次性申请所有的资源。
  • 破坏不剥夺条件: 占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  • 破坏循环等待条件: 靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

如下面修改获取资源的顺序,将资源的获取顺序改为一致,这时就能正常运行,不会产生死锁【破坏循环等待条件】。

	private void makcup() throws InterruptedException {
        synchronized (lipstick) {
            System.out.println(girlName + "获得口红资源");
            Thread.sleep(1000);
            synchronized (mirror) {
                System.out.println(girlName + "获得镜子资源");
            }
        }
    }

20201222173201839

Lock

从 JDK 5.0 开始,Java 提供了更强大的线程同步机制----通过显示定义同步锁对象来实现同步;同步锁使用 Lock 对象充当,java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象。ReentrantLock 实现了 Lock 接口,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的 LockReentrantLock ,它可以显式的加锁、释放锁。

以下为使用案例:

public class LockTest {

    public static void main(String[] args) {
        SyncLock syncLock = new SyncLock();
        new Thread(syncLock, "小明").start();
        new Thread(syncLock, "小选").start();
        new Thread(syncLock, "小飞").start();
    }
}

class SyncLock implements Runnable {

    private static int ticketNums = 10;

    /**
     * 定义 lock 锁
     */
    private final ReentrantLock reentrantLock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
           try {
               // 加锁
               reentrantLock.lock();
               if (ticketNums <= 0) {
                   break;
               }
               try {
                   Thread.sleep(400);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println(Thread.currentThread().getName() + "抢到第" + ticketNums-- +"张票");
           }finally {
               // 释放锁
               reentrantLock.unlock();
           }
        }
    }
}
synchronized 与 Lock 对比
  • Lock 是显式锁(手动开启和关闭锁);synchronized 是隐式锁,出了作用域自动释放。

  • Lock 只有代码块锁;synchronized 有代码块与方法锁。

  • 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有良好的扩展性(提供更多子类)

  • 优先使用顺序:

    Lock > 同步代码块(已进入了方法体,分配了相应资源)> 同步方法(在方法体之外)

线程通信
基本介绍

线程通信的目的是为了更好的协作,线程无论是交替式执行,还是接力式执行,都需要进行通信告知。那么 java 线程是如何通信的呢,大致有以下几种方式:

  • volatile

  • 等待/通知机制

  • join方式

volatile 关键字方式

volatile 有两大特性,一是可见性,二是有序性,禁止指令重排序,其中可见性就是可以让线程之间进行通信。volatile 语义保证线程可见性有两个原则保证:

  • 所有 volatile 修饰的变量一旦被某个线程更改,必须立即刷新到主内存
  • 所有 volatile 修饰的变量在使用之前必须重新读取主内存的值

20201214161119649

工作内存2 能够感知到工作内存1 更新 a 值是靠的总线,工作内存1 在将值刷新的主内存时必须经过总线,总线就能告知其他线程有值被改变,那么其他线程就会主动读取主内存的值来更新。

public class Test {

    private static volatile boolean flag = true;

    public static void main(String[] args) {
        new Thread(() -> {
            while (true){
                if (flag){
                    System.out.println("trun on");
                    flag = false;
                }
            }
        }).start();

        new Thread(() -> {
            while (true){
                if (!flag){
                    System.out.println("trun off");
                    flag = true;
                }
            }
        }).start();
    }
}

如果将 volatile 关键字去掉,线程切换一定次数后将不能感知到 flag 的变化,最开始能感知是线程启动时间差的原因。

等待/通知机制

这是一个线程同步问题,生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。等待通知机制是基于 waitnotify 方法来实现的,在一个线程内调用该线程锁对象的 wait 方法,线程将进入等待队列进行等待直到被通知或者被唤醒。注意:在调用 wait 方法时必须要先释放锁,如果没有锁就会抛出异常,所以需要先获取锁。

  • 对于生产者,没有生产产品之前,要通知消费者等待,而生产了产品之后,又需要马上通知消费者消费
  • 对于消费者,在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费,并且在新的产品生产前需要等待。
  • 在生产者消费者问题中,仅有 synchronized 是不够的,synchronized 可以阻止并发更新同一个共享资源,实现了同步;但是 synchronized 不能用来实现不同线程之间的消息传递(通信)。

Java 提供了几个方法解决线程之间的通信问题:

20201211143948316

注意:以上均是 Object 类的方法,都只能在同步方法或者同步代码块中使用,否则会抛出异常 lllegalMonitorStateException

等待/通知机制—管程法

20201211144725767

并发协作模型 ”生产者/消费者模式" —> 管程法

  • 生产者:负责生产数据的模块(可能是方法、对象、线程、进程)
  • 消费者:负责处理数据的模块(可能是方法、对象、线程、进程)
  • 缓冲区:消费者不能直接使用生产者的数据,它们之间有个 “缓冲区”,生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据。

管程法使用案例如下:

public class TubePassTest {

    public static void main(String[] args) {
        SynContainer synContainer = new SynContainer();
        new Thread(new Provider(synContainer), "生产者").start();
        new Thread(new Consumer(synContainer), "消费者").start();
    }
}

/**
 * 生产者
 */
class Provider implements Runnable {

    SynContainer synContainer;

    public Provider(SynContainer synContainer) {
        this.synContainer = synContainer;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            Chicken chicken = new Chicken(i);
            synContainer.push(chicken);
            System.out.println(Thread.currentThread().getName() + " 生产了产品 " + chicken.id);
        }
    }
}

/**
 * 消费者
 */
class Consumer implements Runnable {

    SynContainer synContainer;

    public Consumer(SynContainer synContainer) {
        this.synContainer = synContainer;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 100; i++) {
            Chicken chicken = synContainer.pop();
            System.out.println(Thread.currentThread().getName() + " 消费了产品 " + chicken.id);
        }
    }
}

/**
 * 产品
 */
class Chicken {
    int id;

    public Chicken(int id) {
        this.id = id;
    }
}

/**
 * 缓冲区
 */
class SynContainer {

    private static Chicken[] chickens = new Chicken[10];

    private static int count = 0;

    public synchronized void push(Chicken chicken) {
        // 如果容器满了,就需要等待消费者消费
        while (count == chickens.length) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 如果没有满,我们就需要丢入产品
        chickens[count++] = chicken;
        // 通知消费者消费
        this.notifyAll();
    }

    public synchronized Chicken pop() {
        // 判断是否存在产品可以消费,不存在则等待
        while (count == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 消费产品
        Chicken chicken = chickens[--count];
        // 通知生产者生产
        this.notifyAll();
        return chicken;
    }
}
等待/通知机制—信号灯法

并发协作模型 ”生产者/消费者模式" —> 信号灯法

  • 生产者:负责生产数据的模块(可能是方法、对象、线程、进程)
  • 消费者:负责处理数据的模块(可能是方法、对象、线程、进程)
  • 标志位:用来判断是否存在可以消费的产品
public class FlagTest {

    public static void main(String[] args) {
        Tv tv = new Tv();
        new Player(tv).start();
        new Watcher(tv).start();
    }
}

/**
 * 生产者-->演员
 */
class Player extends Thread {
    Tv tv;

    public Player(Tv tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if (i % 2 == 0) {
                this.tv.play("测试节目" + i);
            } else {
                this.tv.play("广告时间" + i);
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 消费者-->观众
 */
class Watcher extends Thread {
    Tv tv;

    public Watcher(Tv tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            this.tv.watch();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 产品-->节目
 */
class Tv {
    /**
     * 表演的节目
     */
    String voice;
    /**
     * 标志位,是否需要表演节目
     */
    boolean flag = true;

    public synchronized void play(String voice) {
        while (!this.flag) {
            // 节目没有观看完成时,需要等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("演员表演了: " + voice);
        this.voice = voice;
        this.flag = !this.flag;
        //通知观众观看
        this.notifyAll();
    }

    public synchronized void watch() {
        while (this.flag) {
            // 没有节目时,需要等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观众观看了: " + this.voice);
        this.flag = !this.flag;
        // 通知演员表演
        this.notifyAll();
    }
}
join 方式

join 其实可以理解成是线程合并,当在一个线程调用另一个线程的 join() 方法时,当前线程阻塞等待被调用 join 方法的线程执行完毕才能继续执行,所以 join 的好处能够保证线程的执行顺序,但是若调用线程的 join 方法,就已经失去了并行的意义,虽然存在多个线程,但是本质上还是串行的。最后 join 的实现其实是基于等待通知机制的,如下图:

20201214162609733

测试案例:

public class JoinTest {

    public static void main(String[] args) throws InterruptedException {
        JoinThread jt = new JoinThread();
        Thread thread = new Thread(jt);
        thread.start();
        for (int i = 0; i < 200; i++) {
            if (i == 100) {
                thread.join();
            }
            System.out.println("main 线程执行" + i);
        }
    }
}

/**
 * join 合并线程,待此线程执行完成后,再执行其它线程,其它线程在它执行的期间阻塞,可以理解为插队。
 */
class JoinThread implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println("vip 线程执行 ->" + i);
        }
    }
}
线程池

经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。我们可以提前创建多个线程,放入线程池中,使用时直接获取,使用完放回线程池中。可以避免频繁创建销毁、实现重复利用

使用线程池的好处:

  • 提高响应速度(减少了创建线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 便于线程的管理,重要参数:
    • corePoolSize:核心池的大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止
 ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
                .setNamePrefix("thread-pool-test-").build();
        ExecutorService es = new ThreadPoolExecutor(3, 5, 2000L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(64), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

        es.execute(() -> {
            System.out.println(Thread.currentThread().getName() + " 线程池使用案例输出提示..........");
        });
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值