Java多线程(含生产者消费者模式详解)

2023-05-16

多线程

导航

  • 多线程
    • 1 线程、进程、多线程概述
    • 2 创建线程 (重点)
      • 2.1 继承Thread类(Thread类也实现了Runnable接口)
      • 2.2 实现Runnable接口(无消息返回)
      • 2.3 实现callable接口(有消息返回)
      • 2.4 线程如何停止?
    • 3 线程的一些方法
      • 3.1 线程休眠__sleep
        • 3.1.1 利用线程休眠来模拟网络延时,放大问题
        • 3.1.2 利用sleep方法来模拟倒计时
      • 3.2 线程礼让__yield
      • 3.3 线程强制执行__join
      • 3.4 观测线程状态
        • 3.4.1 线程的几种状态
      • 3.5 线程优先级(priority)
        • 改变、获取优先级
      • 3.6 线程守护
    • 4 线程同步 (重点)
      • 4.1 概述
      • 4.2 并发
      • 4.3 同步方法和同步代码块
        • 4.3.1 同步方法
        • 4.3.2 同步方法解决买票问题
        • 4.3.3 同步代码块
        • 4.3.4 同步代码块解决取款问题
      • 4.4 死锁
        • 4.4.1 死锁代码
        • 4.4.2 解决死锁
      • 4.5 Lock(锁)
        • 4.5.1 并发案例
        • 4.5.2 Lock解决问题
      • 4.6 synchronized 和 Lock 比较
    • 5 线程通信
      • 5.1 生产者消费者模式
      • 5.2 管程法
      • 5.3 信号灯法
    • 6 线程池
      • 6.1 线程池创建
      • 6.2 ThreadPoolExecutor
        • 6.2.1 ThreadPoolExecutor七个参数
        • 6.2.2 临时线程什么时候创建?什么时候会开始拒绝任务?
        • 6.2.3 创建线程池对象实例
      • 6.3 Executors
      • 6.4 阿里巴巴Java开发手册建议

1 线程、进程、多线程概述

  1. 线程:是操作系统中能够进行运算调度的最小单位,包含在进程中,是进程中的实际运作单位。
  2. 进程:是程序执行一次的过程。
  3. 多线程:一个进程可以并发出多个线程,这就是多线程。

2 创建线程 (重点)

2.1 继承Thread类(Thread类也实现了Runnable接口)

public class MyThread extends Thread {
	@Override
    public void run() {
        System.out.println("开启线程:" + Thread.currentThread().getName());
    }
    
    public static void main(String[] args) {
       	new MyThread().start();
        new MyThread().start();
        new MyThread().start();
    }
}

第一次输出:

开启线程:Thread-1
开启线程:Thread-2
开启线程:Thread-0

第二次输出:

开启线程:Thread-1
开启线程:Thread-0
开启线程:Thread-2

可以发现 每次输出,每次输出,顺序不同,说明他们是同时在执行(如果是单核单cpu,实际上不是同时)。

2.2 实现Runnable接口(无消息返回)

public class MyThread implements Runnable {
	@Override
    public void run() {
        System.out.println("开启线程:" + Thread.currentThread().getName());
    }
    
    public static void main(String[] args) {
       	new Thread(new MyThread()).start();
        new Thread(new MyThread()).start();
        new Thread(new MyThread()).start();
    }
}

第一次输出:

开启线程:Thread-0
开启线程:Thread-1
开启线程:Thread-2

第二次输出:

开启线程:Thread-1
开启线程:Thread-2
开启线程:Thread-0

结论同上,开启线程基本上是同时进行的(如果是单核单cpu,实际上不是同时)。

2.3 实现callable接口(有消息返回)

实现callable接口创建线程要用到FutureTask类。

public class MyThread implements Callable<String> {
    @Override
    public String call() {
        System.out.println("开启线程:" + Thread.currentThread().getName());
        return Thread.currentThread().getName();
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> f1 = new FutureTask<>(new MyThread());
        FutureTask<String> f2 = new FutureTask<>(new MyThread());
        FutureTask<String> f3 = new FutureTask<>(new MyThread());
        new Thread(f1).start();
        new Thread(f2).start();
        new Thread(f3).start();
        System.out.println("f1的返回值是:" + f1.get());
        System.out.println("f2的返回值是:" + f2.get());
        System.out.println("f3的返回值是:" + f3.get());
    }
}

第一次输出:

开启线程:Thread-1
开启线程:Thread-0
开启线程:Thread-2
f1的返回值是:Thread-0
f2的返回值是:Thread-1
f3的返回值是:Thread-2

第二次输出:

开启线程:Thread-0
开启线程:Thread-2
f1的返回值是:Thread-0
开启线程:Thread-1
f2的返回值是:Thread-1
f3的返回值是:Thread-2

结论同上,但实现callable接口可以返回消息。

2.4 线程如何停止?

虽然jdk提供了stop方法和destroy方法,但是更推荐的是,用外部标志位来告诉程序是否继续运行。

public class Test{
    public static void main(String[] args) throws InterruptedException {
        NeedStop ns = new NeedStop();
        new Thread(ns).start();
        Thread.sleep(10);
        ns.flag = false;
    }
}

class NeedStop implements Runnable {
    boolean flag = true;
    @Override
    public void run() {
        int i = 0;
        while (flag) {
            System.out.println("运行了 " + i++ + " 次");
        }
    }
}

当 flag 变为 false时,线程便终止了,这是很安全的做法。

3 线程的一些方法

3.1 线程休眠__sleep

例:如买票的系统,假如没有处理并发问题,就可能会存在多个人买到同一张票,或者余票为负等情况。

3.1.1 利用线程休眠来模拟网络延时,放大问题

public class Account {
    String cardId;
    int RMB;

    public Account(String cardId, int RMB) {
        this.cardId = cardId;
        this.RMB = RMB;
    }

    public static void main(String[] args) throws InterruptedException {
        Account ac = new Account("123456", 100000);
        new Thread(new DrawMoney(ac, 100000), "小明").start();
        new Thread(new DrawMoney(ac, 100000),"小红").start();
        Thread.sleep(1000);
        System.out.println("剩余 " + ac.RMB + " 元");
    }
}

class DrawMoney implements Runnable {
    Account ac;
    int money;

    public DrawMoney(Account ac, int money) {
        this.ac = ac;
        this.money = money;
    }

    public void drawMoney(Account ac) throws InterruptedException {
        if (money <= ac.RMB) {
            System.out.println(Thread.currentThread().getName() + "取走了 " + money + " 元");
            Thread.sleep(10);
            ac.RMB -= money;
        }
        else {
            System.out.println("余额不足!");
        }
    }

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

多次输出发现:

可能会出现小明小红同时取到钱,余额为负数的情况。

当小红进入if判断时,ac.RMB还没来得及扣除。

3.1.2 利用sleep方法来模拟倒计时

package com.stop;

public class SleepTest {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("倒计时:");
        for (int i = 10; i > 0; i--) {
            Thread.sleep(1000);
            System.out.println(i);
        }
    }
}

一秒输出一个数。

3.2 线程礼让__yield

  1. 让当前线程暂停,但不阻塞
  2. 将线程从运行=>就绪
  3. 让cpu重新调度,礼让不一定成功

相当于大家重回同一起跑线,重新争夺资源

public class YieldTest implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "正在执行");
        Thread.yield();
        System.out.println(Thread.currentThread().getName() + "=>结束");
    }

    public static void main(String[] args) {
        new Thread(new YieldTest(), "a").start();
        new Thread(new YieldTest(), "b").start();
    }
}

如果礼让成功,则a(或b)开始和结束不会连续出现。

3.3 线程强制执行__join

执行join会让其他线程阻塞,待当前线程结束后,其他线程才能执行,如同霸道的插队。

public class JoinTest implements Runnable{
    @Override
    public void run() {
        for (int i = 10; i > 0; i--) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("vip来了" + i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread test = new Thread(new JoinTest(), "vip");
        test.start();
        for (int i = 0; i < 500; i++) {
            if (i == 250) {
                test.join();
            }

            System.out.println("main" + i);
        }
    }
}

当main中i == 250时,就会被阻塞然后让咱们的vip线程执行完毕,再继续执行main线程。

3.4 观测线程状态

3.4.1 线程的几种状态

  1. NEW(新建)

    线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。

  2. RUNNABLE(就绪)

    线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。

  • 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
  1. BLOCKED(阻塞于锁)

    同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。

  2. WAITING(等待)

    进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

    等待阻塞 – 通过调用线程的wait()方法,让线程等待某工作的完成。

  3. TIMED WAITING(超时等待)

    其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

  4. TERMINATED(终止)

    终止线程的线程状态。 线程已完成执行。

public class StatusTest implements Runnable{
    @Override
    public void run() {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("//");
    }

    public static void main(String[] args) throws InterruptedException {
        Thread test = new Thread(new StatusTest());
        System.out.println(test.getState());
        test.start();
        System.out.println(test.getState());
        Thread.State state = test.getState();
        while (state != Thread.State.TERMINATED) {
            Thread.sleep(1000);
            state = test.getState();
            System.out.println(state);
        }

        test.start();	// 线程死亡后不可以再次启动
    }
}

线程从 NEW= >RUNNABLE=> TIME_WAITTING阻塞了10秒=>TERMINNATED

最后再次启动线程时 将报错。

3.5 线程优先级(priority)

线程调度按照优先级决定应该先调谁,所有就绪状态的线程都会被监控。具体调谁具体还得看CPU心情。

改变、获取优先级

改变:setPriority(int xxx)

获取:getPriority()

优先级最高为10

最低为1

默认优先级是5

public class PriorityTest implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "  " + Thread.currentThread().getPriority());
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new PriorityTest());
        Thread t2 = new Thread(new PriorityTest());
        Thread t3 = new Thread(new PriorityTest());
        Thread t4 = new Thread(new PriorityTest());
        Thread t5 = new Thread(new PriorityTest());
        t1.setPriority(10);
        t2.setPriority(8);
        t4.setPriority(3);
        t5.setPriority(1);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
    }

}

3.6 线程守护

线程分为用户线程和守护(daemon)线程,JVM只确保用户线程执行完毕而不用等待守护线程执行完毕

守护线程如:监控内存、垃圾回收等

Thread种的**setDaemon(boolean on)**方法可以设置线程是否为守护线程,默认为false,用户线程。

public class DaemonTest extends Thread{
    @Override
    public void run() {
        while (true) {
            System.out.println("====守护====");
        }
    }

    public static void main(String[] args) {
        Thread daemon = new DaemonTest();
        daemon.setDaemon(true);
        daemon.start();
        new NormalThread().start();
    }
}

class NormalThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("====用户====");
        }
    }
}

这段代码将会在用户主线程结束时,守护线程结束,不会死循环。

4 线程同步 (重点)

4.1 概述

在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。但是多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。

同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。

“同”字从字面上容易理解为一起动作

其实不是,“同”字应是指协同、协助、互相配合。

如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B依言执行,再将结果给A;A再继续操作。

在多线程编程里面,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。

4.2 并发

在同一时刻,有多个线程同时访问 某一个(一些)资源,带来数据的不安全性 、不稳定性、不确定性。

同步就是为了解决并发问题。

下面这段代码就存在着并发问题:

public class Account {
    String cardId;
    int RMB;

    public Account(String cardId, int RMB) {
        this.cardId = cardId;
        this.RMB = RMB;
    }

    public static void main(String[] args) throws InterruptedException {
        Account ac = new Account("123456", 100000);
        new Thread(new DrawMoney(ac, 100000), "小明").start();
        new Thread(new DrawMoney(ac, 100000),"小红").start();
        Thread.sleep(1000);
        System.out.println("剩余 " + ac.RMB + " 元");
    }
}

class DrawMoney implements Runnable {
    Account ac;
    int money;

    public DrawMoney(Account ac, int money) {
        this.ac = ac;
        this.money = money;
    }

    public void drawMoney(Account ac) throws InterruptedException {
        if (money <= ac.RMB) {
            System.out.println(Thread.currentThread().getName() + "取走了 " + money + " 元");
            Thread.sleep(10);
            ac.RMB -= money;
        }
        else {
            System.out.println("余额不足!");
        }
    }

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

多次输出发现:
可能会出现小明小红同时取到钱,余额为负数的情况。

当小红进入if判断时,ac.RMB还没来得及扣除。

4.3 同步方法和同步代码块

解决并发问题的 synchronized

synchronized 可以给代码加锁,同一时刻只有一个线程可以执行被锁定的代码

synchronized又分为同步方法和同步代码块

4.3.1 同步方法

若给方法加上锁

public synchronized void methodName() {
    
}

每次只能有一个线程执行这个方法

4.3.2 同步方法解决买票问题

现有以下代码:

public class Tickets implements Runnable {
    private  int ticketNum = 10;
    boolean flag =true;
    public void buy() throws InterruptedException {
        if (ticketNum <= 0) {
            flag = false;
            return;
        }
        System.out.println(Thread.currentThread().getName() + " 拿到第 " + ticketNum + "张 票");
        Thread.sleep(100);
        ticketNum--;
    }

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

    public static void main(String[] args) {
        Tickets station = new Tickets();
        new Thread(station, "小明").start();
        new Thread(station, "小红").start();
        new Thread(station, "小张").start();

    }
}

会出现多人拿到同一张票或者拿到不存在的票的安全问题,此时,同步方法可以解决这个问题。

只需要给buy()方法加上锁,那么同一时间,只能有一个线程执行buy()。因为小明、小红、小张,都是同一个Tickets对象,所以this是相同的,同一时间只能有一个线程进入这个方法。

4.3.3 同步代码块

synchronized(obj) {

​ code……

}

obj填锁对象。谁先拿到obj谁就先执行,其余线程只能排队。

4.3.4 同步代码块解决取款问题

之前取款的例子

public class Account {
    String cardId;
    int RMB;

    public Account(String cardId, int RMB) {
        this.cardId = cardId;
        this.RMB = RMB;
    }

    public static void main(String[] args) throws InterruptedException {
        Account ac = new Account("123456", 100000);
        new Thread(new DrawMoney(ac, 100000), "小明").start();
        new Thread(new DrawMoney(ac, 100000),"小红").start();
        Thread.sleep(1000);
        System.out.println("剩余 " + ac.RMB + " 元");
    }
}

class DrawMoney implements Runnable {
    Account ac;
    int money;

    public DrawMoney(Account ac, int money) {
        this.ac = ac;
        this.money = money;
    }

    public void drawMoney(Account ac) throws InterruptedException {
        if (money <= ac.RMB) {
            System.out.println(Thread.currentThread().getName() + "取走了 " + money + " 元");
            Thread.sleep(10);
            ac.RMB -= money;
        }
        else {
            System.out.println("余额不足!");
        }
    }

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

此时再用同步方法并不能解决并发问题,因为小明和小红不是同一个this(不同的DrawMoney实例),所以可以同时执行

同步代码块 可以解决这个问题,用synchronized把关键的代码加上锁(很显然是29~36行)

public void drawMoney(Account ac) throws InterruptedException {
    synchronized (ac) {    
    	if (money <= ac.RMB) {
            System.out.println(Thread.currentThread().getName() + "取走了 " + money + " 元");
            Thread.sleep(10);
            ac.RMB -= money;
        }
        else {
            System.out.println("余额不足!");
        }
    }
}

那么在同一时间内,只能有一个线程能够对ac账户进行操作。

4.4 死锁

当两个线程拿着对方需要的锁而不释放时,因为双方都拿不到锁,所以就成了死锁,线程就阻塞在那里。

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。产生死锁的原因,主要包括:

  • 系统资源不足;
  • 程序执行的顺序有问题;
  • 资源分配不当等。

如果系统资源充足,进程的资源请求都能够得到满足,那么死锁出现的可能性就很低;否则,

就会因争夺有限的资源而陷入死锁。其次,程序执行的顺序与速度不同,也可能产生死锁。产生死锁的四个必要条件:

  • 互斥条件:一个资源每次只能被一个进程使用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

4.4.1 死锁代码

public class DeadLock implements Runnable {
    public static final String lock1 = "Lock_1";
    public static final String lock2 = "Lock_2";
    boolean flag;

    public DeadLock(boolean flag) {
        this.flag = flag;
    }

    public void dead() throws InterruptedException {
        if (flag) {
            synchronized (lock1) {
                System.out.println(Thread.currentThread().getName() + " 拿到了lock1");
                Thread.sleep(1000);
                synchronized (lock2) {
                    System.out.println(Thread.currentThread().getName() + " 拿到了lock2");
                }
            }
        } else {
            synchronized (lock2) {
                System.out.println(Thread.currentThread().getName() + " 拿到了lock2");
                Thread.sleep(1000);
                synchronized (lock1) {
                    System.out.println(Thread.currentThread().getName() + " 拿到了lock1");
                }
            }
        }
    }

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

    public static void main(String[] args) {
        new Thread(new DeadLock(true)).start();
        new Thread(new DeadLock(false)).start();
    }
}

一个线程在 第 12 行拿到了lock1,在他休眠时另一个线程在 20 行拿到了lock2,拿到lock1的线程休眠结束后,需要拿lock2的锁,可是lock2在另一个线程手里,所以lock1就等待lock2释放,而lock2那边也是同理,在等待lock1释放,互相阻塞在那里,这就是死锁。

解决方法也很简单,lock1 用完 就释放掉,lock2同理

4.4.2 解决死锁

于是我们不再继续嵌套书写synchronized

public class Solute implements Runnable{
        public static final String lock1 = "Lock_1";
        public static final String lock2 = "Lock_2";
        boolean flag;

        public Solute(boolean flag) {
            this.flag = flag;
        }

        public void dead() throws InterruptedException {
            if (flag) {
                synchronized (lock1) {
                    System.out.println(Thread.currentThread().getName() + " 拿到了lock1");
                    Thread.sleep(1000);
                }
                synchronized (lock2) {
                    System.out.println(Thread.currentThread().getName() + " 拿到了lock2");
                }
            } else {
            synchronized (lock2) {
                System.out.println(Thread.currentThread().getName() + " 拿到了lock2");
                Thread.sleep(1000);
            }
            synchronized (lock1) {
                System.out.println(Thread.currentThread().getName() + " 拿到了lock1");
            }
        }
    }


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

    public static void main(String[] args) {
        new Thread(new Solute(true)).start();
        new Thread(new Solute(false)).start();
    }
}

lock1唤醒后,释放lock1,lock2同理。

4.5 Lock(锁)

作用和synchronized类似,都是用来解决并发问题。Lock是一个接口,而synchronized是一个关键字。Lock是接口意味着它有许多方法,在复杂的情况下比synchronized要方便。

4.5.1 并发案例

继续用经典的买票案例:

package com.locklock;

public class TestLock implements Runnable {
    private int ticketsNum;
    private boolean flag;

    public TestLock(int ticketsNum, boolean flag) {
        this.ticketsNum = ticketsNum;
        this.flag = flag;
    }

    public void buyTickets() throws InterruptedException {
        if (ticketsNum > 0) {
            Thread.sleep(100);
            ticketsNum--;
            System.out.println(Thread.currentThread().getName() + " 来买了第 " + ticketsNum + " 张票");
        } else {
            flag = false;
            System.out.println("已售罄!");
        }
    }

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

    public static void main(String[] args) {
        TestLock testLock = new TestLock(10,true);
        new Thread(testLock, "xm").start();
        new Thread(testLock, "xh").start();
        new Thread(testLock, "xz").start();
    }
}

4.5.2 Lock解决问题

已经知道,刚刚这段代码有问题,会出现多个人拿到同一张票,或者拿到无效票(<=0)的情况。

按照之前的方法,只需把buyTichets()方法变成同步方法即可。

现在我们不用synchronized关键字,用Lock里面的方法。

package com.locklock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TestLock implements Runnable {
    private int ticketsNum;
    private boolean flag;
    Lock lock = new ReentrantLock();

    public TestLock(int ticketsNum, boolean flag) {
        this.ticketsNum = ticketsNum;
        this.flag = flag;
    }

    public void buyTickets() throws InterruptedException {
        try {
            lock.lock();
            if (ticketsNum > 0) {
                Thread.sleep(100);
                ticketsNum--;
                System.out.println(Thread.currentThread().getName() + " 来买了第 " + ticketsNum + " 张票");
            } else {
                flag = false;
                System.out.println("已售罄!");
            }
        } finally {
            lock.unlock();
        }
    }

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

    public static void main(String[] args) {
        TestLock testLock = new TestLock(10,true);
        new Thread(testLock, "xm").start();
        new Thread(testLock, "xh").start();
        new Thread(testLock, "xz").start();
    }
}

Lock接口创建ReentrantLock实例(多态),把关键的代码块用lock()方法和unlock()方法包裹起来,和synchronized (this) {}类似。

最好用try环绕代码、finally环绕unlock(),这样即使上面代码有异常,也会释放锁。

4.6 synchronized 和 Lock 比较

synchronizedLock
类型关键字接口
范围锁方法和代码块只能锁代码块
形式隐式锁,作用于外自动释放显示锁,手动释放
性能比较底层指令来控制锁,少量同步性能更好,大量同步

使用优先级:

  • Lock锁 > 同步代码块 > 同步方法

5 线程通信

线程通信就是当多个线程共同操作共享的资源时,互相告知自己的状态以避免资源争夺

5.1 生产者消费者模式

生产者消费者模式并不是 GOF 提出的 23 种设计模式之一,23 种设计模式都是建立在面向对象的基础之上的,但其实面向过程的编程中也有很多高效的编程模式,生产者消费者模式便是其中之一。

在实际的软件开发过程中,经常会碰到如下场景:某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等)。产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。单单抽象出生产者和消费者,还够不上是生产者-消费者模式。该模式还需要有一个缓冲区处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据。

为了不至于太抽象,我们举一个寄信的例子(虽说这年头寄信已经不时兴,但这个例子还是比较贴切的)。假设你要寄一封平信,大致过程如下:

1、你把信写好——相当于生产者制造数据

2、你把信放入邮筒——相当于生产者把数据放入缓冲区

3、邮递员把信从邮筒取出——相当于消费者把数据取出缓冲区

4、邮递员把信拿去邮局做相应的处理——相当于消费者处理数据

生产者:负责生产数据的模块。

消费者:负责处理数据的模块。

缓冲区:消费者要通过缓冲区才能使用生产者生产的数据。

5.2 管程法

通过变量的值控制

public class  ProducerCustorm {

    public static void main(String[] args) {
        SynContainer container = new SynContainer();
        new Producer(container).start();
        new Consumer(container).start();
    }
}
    // 生产者
class Producer extends Thread {
    final SynContainer container;
    public Producer(SynContainer container) {
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            container.push(new Chicken(container.id));
        }
    }
}
    // 消费者
class Consumer extends Thread {
    SynContainer container;
    public Consumer(SynContainer container) {
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 0; i <50; i++) {
                container.pop();
        }
    }
}
    // 产品
class Chicken {
    int id; //产品编号
    public Chicken(int id) {
        this.id = id;
    }
}

// 缓冲区
class  SynContainer {
    int id = 1; // 产品编号
    // 需要一个容器
    Chicken[] chickens = new Chicken[10];
    // 计数器
    int count = 0;

    // 生产者放入产品
    public synchronized void push(Chicken chicken) {
        // 如果容器 满了 则消费者消费
        if (count == chickens.length) {
            // 生产者等待消费者消费
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 被消费者通知生产
        if (count < chickens.length) {
            chickens[count++] = chicken;
            id++;
            System.out.println(Thread.currentThread().getName() +  ": 生产了" + chicken.id + "只鸡");
            // 通知消费者消费
            this.notifyAll();
        }

    }

    // 消费者消费产品
    public synchronized void pop() {
        if (count == 0) {
            // 等待生产者生产
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 如果可以消费
        if ( count != 0 ) {
            Chicken chicken = chickens[--count];
            System.out.println("消费了第" + chicken.id + "只鸡");
        // 通知生产者生产
            this.notifyAll();
        }
    }
}

用一个数值和别的线程传递信息,告诉他们是否可以继续就绪。

5.3 信号灯法

通过标志位控制

public class ProducerAndConsumer {
    public static void main(String[] args) {
        Buffered buffered = new Buffered();
        new Producers(buffered, "厨师").start();
        new Consumer(buffered, "顾客").start();
    }

}

// 生产者
class Producers extends Thread {
    Buffered buffered;

    public Producers(Buffered buffered, String name) {
        super(name);
        this.buffered = buffered;
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            buffered.push(new Bread(i));
        }
        System.out.println(Thread.currentThread().getName() +  "工作了一天,该休息了!");
    }
}

// 消费者
class Consumer extends Thread {
    Buffered buffered;

    public Consumer(Buffered buffered, String name) {
        super(name);
        this.buffered = buffered;
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            buffered.pop();
        }
    }
}

// 缓冲区
class Buffered {
    // 产品
    Bread bread;
    // 信号灯
    boolean flag = false;

    //生产者生产
    public synchronized void push(Bread bread) {
        if (flag) {
            System.out.println(Thread.currentThread().getName() + ": 已经有面包了,休息会儿!");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        if (!flag) {
            System.out.println(Thread.currentThread().getName() + ": 没东西吃了,赶快做……");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.bread = bread;
            System.out.println("厨师做了:" + (bread.getId()+1) + " 个面包");
            System.out.println("现在有东西吃了!");
            this.flag = !this.flag;
            this.notifyAll();
        }
    }


    //消费者消费
    public synchronized void pop() {
        if (!flag) {
            System.out.println(Thread.currentThread().getName() + ": 面包呢?");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        if (flag) {
            System.out.println("销售了:" + (bread.getId()+1) + " 个面包");
            this.flag = !this.flag;
            System.out.println(Thread.currentThread().getName() + ": 面包吃完了!");
            this.notifyAll();
        }
    }

}

// 产品
class Bread {
    private int id;

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

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
}

就是设置一个标志位,然后告诉其他线程是否可以工作,协同处理一个数据。

6 线程池

如果有请求就新建一个线程,那么会创建很多线程,严重影响性能,线程池里面的线程可以复用,提高了性能。

6.1 线程池创建

ExecutorService 接口代表线程池

创建方式一般有两种:

  1. 使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象
  2. 使用Executors调用方法返回不同特点的线程池对象

6.2 ThreadPoolExecutor

6.2.1 ThreadPoolExecutor七个参数

ThreadPoolExecutor(

int corePoolSize,

int maximumPoolSize,

long keepAliveTime,

TimeUnit unit,

BlockingQueue workQueue,

ThreadFactory threadFactory,

RejectedExecutionHandler handler)

创建一个新 ThreadPoolExecutor给定的初始参数。

一共七个参数

  1. corePoolSize:指定线程池的数量(核心线程)。不能小于0。
  2. maximumPoolSize:指定线程池可支持的最大线程数。最大数量>=核心线程数量。
  3. keepAliveTime指定临时线程的最大存活时间。不能小于0。
  4. unit:指定存活时间单位(秒、分、时、天)。
  5. workQueue:指定任务队列。不能为null。
  6. threadFactor:指定用哪线程工厂创建线程。不能为nul。
  7. handler:指定线程忙、任务满的时候,新任务来了怎么办。不能为null。

6.2.2 临时线程什么时候创建?什么时候会开始拒绝任务?

  • 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。
  • 核心线程和临时线程都在忙,任务队列也满了,新的任务来的时候才会开始拒绝。

6.2.3 创建线程池对象实例

知道了一些原理过后,我们开始尝试创建对象

ExecutorService pool = new ThreadPoolExecutor(
    3,
    10,
    5,
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(5),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.AbortPolicy());

拒绝策略:

策略
ThreadPoolExecutor.AbortPolicy丢弃任务并抛出异常(默认)
ThreadPoolExecutor.DiscardPolicy丢弃任务,不抛出异常
ThreadPoolExecutor.DiscardOldestPolicy抛弃队列中等待最久的任务,然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy不进入线程池、由主线程调用任务的run方法

使用线程池:

//对于实现了Runnable接口的类
pool.execute(Runnable target);
//与下面这个效果相同
new Thread(Runnable target).start();
//对于实现了Callable接口的类
Future<T> r1 = pool.submit(Callable target);
//获取Call方法的值
r1.get();

6.3 Executors

一个工具类,提供了简单创建线程池的方法。

查看帮助文档,有很多静态方法可以调用。

最常用的是

public static ExecutorService newFixedThreadPool(int nThreads)

6.4 阿里巴巴Java开发手册建议

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

  • 说明:Executors 返回的线程池对象的弊端如下: 1) FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2) CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
    ,
    new ArrayBlockingQueue<>(5),
    Executors.defaultThreadFactory(),
    new ThreadPoolExecutor.AbortPolicy());
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java多线程(含生产者消费者模式详解) 的相关文章

  • Ubuntu下的CuteCom串口详细调试教程

    I MX6ULL嵌入式开发学习 串口调试 一 Ubuntu下的串口调试助手安装 嵌入式开发学习过程中学习到串口调试这一章 xff0c 以前在Win10操作时都有相对应的串口调试界面 xff0c 安装个串口驱动在电脑设备端口里面看到COM3时
  • STM32 串口

    文章目录 USART 通信协议RS 232与TTL电平 串口通信数据包组成USART功能框图讲解引脚寄存器状态寄存器 USART SR 数据寄存器 USART DR 控制寄存器 USART CR 波特比率寄存器 USART BRR 发送过程
  • printf二进制数据

    基于之前这篇文章的代码改进了下 xff1a http blog csdn net xzongyuan article details 28889063 之前打印的数字没有补0 我打印了内存信息 xff0c 结果是这样的 xff0c 不能对齐
  • 分析MySQL数据类型的长度

    分析MySQL数据类型的长度 MySQL有几种数据类型可以限制类型的 34 长度 34 xff0c 有CHAR Length VARCHAR Length TINYINT Length SMALLINT Length MEDIUMINT L
  • 为vscode配置clangd

    目录 安装clangd 后端安装clangd 前端修改基础配置生成compile commands json文件基本效果补全warning提醒自动修改存在问题 注意事项 clangd能提供更好的补全和提示 xff0c 自带检查一些warni
  • 论文笔记(十九)RGB-D Object Tracking: A Particle Filter Approach on GPU

    RGB D Object Tracking A Particle Filter Approach on GPU 文章概括摘要1 介绍2 贡献3 粒子滤波器4 可能性评估5 实施细节6 实验A 物体模型B 合成序列C 真实序列 7 结论8 鸣
  • Ubuntu 命令行 访问网页

    安装w3m 1 进入 root apt get install w3m 2 测试是否成功 xff1a w3m https blog csdn net x xx xxx xxxx article details 92574331
  • 代码管理中Trunk、Branches、Tags的区别和联系

    我们可以将这三者想象成一棵树的组成部分 trunk为树干branches为树枝tags为整棵树 trunk用于主线开发 branches用于定制版本 修复bugs 并行开发等使用 tags用于存放release版本 xff0c 阶段性代码
  • linux使用curl请求(带参数)

    1 2 3 curl G d 34 c 61 amp a 61 34 http www net index php
  • 惯导系列(二):滤波相关的算法

    前言 我又消失了一段时间 xff0c 这段时间研究了惯性导航有关的算法 xff0c 整理了不少博客 xff0c 字数比较多 xff0c 图片比较多 学到了很多知识 目录 前言 本节介绍 一 Mahony算法 1 1 PID控制算法 1 2
  • STM32 CAN 设置多个过滤器接收多ID方法

    1 标识符列表模式 xff0c 32位模式下 void MX CAN Init void 这里是实现了两个地址的接收 一个是用来接收广播信息 一个用来接收私有地址 如果想实现多个地址可以添加多个过滤器组 stm32103 有0 13 共14
  • linux下运行动态库问题 cannot open shared object file: No such file or directory

    如果动态库不在同一级目录下 xff0c 则需要将以上文件的目录加载到动态库搜索路径中 xff0c 设置的方式有以下几种 一 将动态库路径加入到LD LIBRARY PATH环境变量 1 在终端输入 xff1a export LD LIBRA
  • 几个串口通信协议的整理

    一 UART UART是一个大家族 xff0c 其包括了RS232 RS499 RS423 RS422和RS485等接口标准规范和总线标准规范 它们的主要区别在于其各自的电平范围不相同 嵌入式设备中常常使用到的是TTL TTL转RS232的
  • 单片机中断的过程

    1 根据响应的中断源的中断优先级 使相应的优先级状态触发器置1 xff1b 2 把当前程序计数器PC的内容压入堆栈 xff0c 保护断点 xff0c 寻找中断源 xff1b 3 执行硬件中断服务子程序调用 xff1b 4 清除相应的中断请求
  • Ruby学习札记(3)- Ruby中gem的安装与卸载

    Ruby 学习札记 3 Ruby 中 gem 的安装与卸载 在 Ruby 中有 gem 包这种概念 xff0c 类似 PHP 中的 pear xff0c 相当于一种插件 具体可以 Google 一下 xff08 1 xff09 查看已经安装
  • 【linux】ubuntu20.04 运行软件 提示找不到过时的库 libQtCore.so.4、libQtGui.so.4、libpng12.so.0

    先上结果 1 nxView运行起来 环境 硬件 xff1a Jetson Xavier NX 套件 系统 xff1a Ubuntu 20 04 软件 xff1a nxView 43 libQtCore so 4 解决 0 现象 运行软件提示
  • rtt相关问题总结

    1 总结RT Thread的启动流程 xff08 启动文件部分跳过 xff09 关中断 rt hw interrupt disable 板级初始化 xff1a 需在该函数内部进行系统堆的初始化 rt hw board init 打印 RT
  • FTP 客户端C实现

    使用 Socket 通信实现 FTP 客户端程序 FTP 概述 文件传输协议 xff08 FTP xff09 作为网络共享文件的传输协议 xff0c 在网络应用软件中具有广泛的应用 FTP的目标是提高文件的共享性和可靠高效地传送数据 在传输
  • Qt编写串口通信程序全程图文讲解

    说明 我们的编程环境是windows xp下 xff0c 在Qt Creator中进行 xff0c 如果在Linux下或直接用源码编写 xff0c 程序稍有不同 xff0c 请自己改动 在Qt中并没有特定的串口控制类 xff0c 现在大部分
  • VLC播放器调试经验总结

    一 前言 在使用VS学习VLC源码时 xff0c 可以打断点分析变量数据 xff0c 跟踪代码流程 xff0c 方便我们理解源码 但是在定位音视频卡顿 延时等疑难问题时 xff0c 这一招就不管用了 xff0c 因为打上断点就会导致实时计算

随机推荐