Java多线程(含生产者消费者模式详解)_java多线程生产者消费者模式实例-程序员宅基地

技术标签: java  安全  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 比较

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

使用优先级:

  • 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());
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/yangsf_/article/details/123760952

智能推荐

全网首发,Python解决某象滑动还原验证码100%还原_python还原验证图片-程序员宅基地

文章浏览阅读1.1k次。与一般的滑动验证码不同,某象的滑动还原验证码是将图像上下两块分割,然后在随机一块往右移动,将两块拼图移动成完整的图像才算成功,事实上,解决这类验证码比普通的验证码还要简单。_python还原验证图片

Win7下多线程中OpenFileDialog和SaveFileDialog失效的解决办法(转载)-程序员宅基地

文章浏览阅读185次。在程序中,通常会使用独立线程来操作OpenFileDialog或者SaveFileDialog控件,但是在某些情况下(Win7系统下)调用 ShowDialog方法并不显示选择路径对话框。此时需要对启动线程的地方进行处理一下即可,方法如下: 方法一 Threadapp=newThread(newParameteriz在程序中,通常会使用独立线程来操作OpenFileDialog或者SaveFile..._win7 openfiledialog

vim常用命令以及配置文件_vim dw命令-程序员宅基地

文章浏览阅读1.7k次,点赞9次,收藏9次。vim常用命令以及配置文件_vim dw命令

python3一键安装脚本_Linux编译安装python3(附一键安装脚本)-程序员宅基地

文章浏览阅读444次。Linux下大部分系统默认自带python2.x的版本,最常见的是python2.6或python2.7版本,默认的python被系统很多程序所依赖,比如centos下的yum就是python2写的,所以默认版本不要轻易删除,否则会有一些问题,如果需要使用最新的Python3那么我们可以编译安装源码包到独立目录,这和系统默认环境之间是没有任何影响的,python3和python2两个环境并存即可首..._python3一键安装脚本

python串口发送大量数据卡顿问题_py通信串口发送数据太快卡掉应用程序-程序员宅基地

文章浏览阅读1.2k次。我用python写了一个FPGA的自动化验证平台,使用串口自动往fpga输入数据,然后我用的python numpy将数组通过串口发送数据。问题就出现在这,我的数组数据量非常庞大,于是我想出了以下解决方案:1,将大数组切分为小数组,多次调用serial.write,中间用定时器隔开(实际我用的时候用的sleep,停止运行,因为懒得查怎么搞定时器,好像并没什么用,还是会很卡顿,有时间再搞清楚定时器吧)。2,将大数组切分为多个小数组,多次调用serial.wtite,不同的是,中间使用serial.fl_py通信串口发送数据太快卡掉应用程序

Flutter 入门与实战(九),retrofit设计模式_flutter retrofit-程序员宅基地

文章浏览阅读647次。Clip.hardEdge:从名字就知道,这种方式很粗糙,但是裁剪的效率最快;Clip.antiAliasSaveLayer:最为精细的裁剪,但是非常慢,不建议使用;Clip.none:默认值,如果内容区没有超出容器边界的话,不会做任何裁剪。内容超出边界的话需要使用别的裁剪方式防止内容溢出。圆形扁平按钮这里需要提一下, Flutter 2.0以前的扁平按钮是FlatButton,使用起来很简单,但是很多场合不太满足,因此2.0以后引入了 TextButton 替代。TextButton 多了一个._flutter retrofit

随便推点

常见电机工作原理-程序员宅基地

文章浏览阅读2k次,点赞3次,收藏16次。参考链接:https://space.bilibili.com/1855672581?spm_id_from=333.788.b_765f7570696e666f.1一、直流有刷电机定子:一边一个定子为极性相反的电磁铁转子:由很多线圈绕成,通电后就是电磁铁换向器:换向器上连接着线圈,电机旋转时可以给不同线圈供电保持旋转电刷:后面有个弹簧顶着,使它保持与换向器紧贴着供电二、无刷直流电机1.介绍无刷电机没有电刷,内部不会产生火花或摩擦噪音,也不需要更换、维护电刷。定子具有绕组和线圈激_电机工作原理

python实现局域网攻击_python实现ARP欺骗(宿舍停网警告)-程序员宅基地

文章浏览阅读523次。arp全称为“地址解析协议”,是根据IP获取对应mac地址的一种协议。主机上也是根据arp缓存表进行内网主机的信息交互的,arp缓存表存放着ip对应mac地址的关系。如果一台主机需要上网,就需要先找到自己的网关ip。由于在以太网中都是用mac地址进行主机的交互的,所以要在arp缓存表中根据ip地址查询对应的mac地址。。找到mac地址后就可以将自己的请求发给网关。网关再代之进行转发。这就是一个主机..._py下线局域网

CSS引入方式_css import url-程序员宅基地

文章浏览阅读770次。CSS(层叠样式表)定义了HTML元素在页面中的样式、布局以及整个页面的布局。CSS遵循W3C规范,实现了跨浏览器的标准化。导入的CSS会被直接导入到HTML或CSS文件中,成为文件的一部分。属性设置为目标链接的CSS文件路径,rel属性设置为。表示链接样式表,type属性设置为。3.1 链接样式(最常用)标签链接外部的CSS文件。直接使用HTML元素的。在CSS文件中直接使用。在HTML文件中需要在。在HTML初始化时,_css import url

Java学习笔记_06_main函数必须用public修饰确保调用权限是最大在任何情况下都可以访问;-程序员宅基地

文章浏览阅读179次。一、继承1、通过extends关键字可以实现类与类的继承格式:class 子类名 extends 父类名{}父类:基类、超类子类:派生类以人类、老师类、学生类举例先创建一个人类public class Person { //私有化成员变量 private String name; private int age; //set/get方法 public vo_main函数必须用public修饰确保调用权限是最大在任何情况下都可以访问;

前端js操作符: ?/?? /!!用法_?//-程序员宅基地

文章浏览阅读1.9k次。最近大概浏览别人源码的过程中发现有些操作符我竟然从来没用过。经过查询做个笔记。_?//

Python字符串转换为十六进制数组的方法_python 将字符串转化成16进制array-程序员宅基地

文章浏览阅读532次。通过使用这种方法,我们可以方便地将任意字符串转换为十六进制数组,以便于手动粘贴和处理。你可以将这个方法应用于不同的场景,如将字符串转换为十六进制表示的字节序列,或者在数据处理和通信中使用十六进制数据。在Python编程中,有时候我们需要将字符串转换为十六进制数组,以便于手动粘贴和处理。函数在字符串的左侧填充零,以确保每个十六进制值都有两位。最后,我们将转换后的十六进制值添加到。的函数,它接受一个字符串作为输入,并返回一个包含每个字符对应十六进制表示的数组。函数返回的字符串以"0x"开头,我们使用切片操作。_python 将字符串转化成16进制array

推荐文章

热门文章

相关标签