一文读懂JAVA多线程基础 包含售票问题详解 内容参考b站韩顺平老师

写在前面

本文个人JAVA学习笔记,内容参考b站韩顺平老师,源码可在个人github找到点击跳转

1.程序 进程 线程

  • 程序:是为完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象。
  • 进程:是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程,有它自身的产生,存在和消亡的过程。
  • 线程
    • 线程由进程创建的,是进程的一个实体
    • 一个进程可以拥有多个线程

2.并行 并发

  • 并行:多个CPU同一时刻执行多个任务
  • 并发:一个CPU同一时间段(采用时间片)同时执行多个任务
  • 注意:对于一个多核的电脑,并发和并行是可以同时存在的

3.创建线程的两种方法

3.1继承Thread类,重写run方法

案例引入
提出需求:
1.编写程序,开启一个线程,该线程每隔1秒,在控制台输出"猫咪学习java中"
2.当输出80次"猫咪学习java中"时结束该进程

package com.zjh;

public class Thread01 {
    public static void main(String[] args) throws InterruptedException {
        new Cat().start();
        System.out.println("主线程继续执行 "+Thread.currentThread().getName());
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程 i="+i);
            //让主线程休眠1秒
            Thread.sleep(1000);
        }
        
    }
}
class Cat extends Thread{
    @Override
    public void run() {
        int times = 0;
        while(true){
            System.out.println("小猫咪" + (++times) + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(times == 7) break;
        }
    }
}
主线程继续执行 main
主线程 i=0
小猫咪1Thread-0
主线程 i=1
小猫咪2Thread-0
主线程 i=2
小猫咪3Thread-0
主线程 i=3
小猫咪4Thread-0
主线程 i=4
小猫咪5Thread-0
小猫咪6Thread-0
小猫咪7Thread-0

Process finished with exit code 0

3.1.1多线程的机制

以cat案例为例

img

当我们运行程序时,就相当于启动了一个进程,然后,程序马上会进入我们的main方法中,进入main方法后,这时进程就开启了一个主线程叫做main线程。在这个主线程中,我们创建了一个Cat对象,由于这个Cat对象继承了Thread类,所以我们可以将其当作线程使用,因此当我们调用cat.start();时,我们的主线程就也创建了一个子线程叫做Thread-0线程,并且不会导致主线程阻塞(即主线程不会等到cat.start();方法执行完毕后,才继续执行后面代码)
如果我们运行程序,就会看到主线程和Thread-0线程交替执行,直到某一个线程先消亡,系统才会只执行那个未消亡的线程(但此时应用程序(进程)并未结束,只有所有线程都消亡了,我们的应用程序(进程)才会结束)

为什么启动线程用的是cat.start();而不是直接用cat.run();

start才能启动线程
run只是main主线程调用了Cat对象中的run方法,并没有开启一个新的线程,此时会引发阻塞现象

小猫咪1main
小猫咪2main
小猫咪3main
小猫咪4main
小猫咪5main
小猫咪6main
小猫咪7main
主线程继续执行 main
主线程 i=0
主线程 i=1
主线程 i=2
主线程 i=3
主线程 i=4

Process finished with exit code 0

start

  • 作用:1.启动当前线程 2.调用当前线程的重写的run方法
  • 调用start方法以后,一条路径代表一个线程,同时执行两线程时,因为时间片的轮换,所以执行过程随机分配,且一个线程对象只能调用一次start方法。

run

  • 在主线程中调用以后,直接在主线程一条线程中执行了该线程中run的方法。
  • 调用线程中的run方法,只调用run方法,并不新开线程
源码简单分析

1.当我们调用cat.start();时,系统会进入public synchronized void start() {} 这个方法

public synchronized void start() {
        group.add(this);
        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

核心代码:start0();
2.接着public synchronized void start() {} 这个方法会调用其中的核心方法start0();
start0();是native方法,即本地方法,由JVM调用,底层由c/c++实现,因此真正实现多线程效果的是start0()方法,而不是run()方法

private native void start0();
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

3.2实现Runnable接口,重写run方法

由于Java是单继承的,在某些情况下一个类可能已经继承了某个父类,这时在用继承Thread类来创建线程是不可能的了,因此Java设计者们提供了实现Runnable接口的方法来创建线程
案例演示
提出需求:
请编写程序,该程序可以每隔1秒,在控制台输出"hi",当输出10次后,自动退出

package com.zjh;

public class Thread02 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Thread thread = new Thread(dog);
        thread.start();
    }
}
class Dog implements Runnable{
    int count = 0;

    @Override
    public void run() {
        while (true) {
            System.out.println("狗叫" + (++count) + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (count == 10) {
                break;
            }
        }
    }
}

注意:

  1. 开启Runnable实现的线程不可以用对象名.start(),Runnable接口中只有run方法
  2. 不可以对象名.run(),这样就是等于和前面讲的一样,用main主线程直接调用方法,而不是开启线程
  3. 解决方法:创建Thread对象,把我们需要当作线程的对象(实现Runnable接口)放入Thread中然后再通过调用Thread对象中的start()方法完成线程的创建【代理模式】

image.png
代理简析:把Dog对象注入到Thread中的target
调用thread.start()实则本地方法调用start0()再调用target.run()方法执行注入进来的dog对象借口里的run方法代码体

package com.hspedu.threaduse;
 
/**
 * @author 韩顺平
 * @version 1.0
 * 通过实现接口Runnable 来开发线程
 */
public class Thread02 {
    public static void main(String[] args) {
       Tiger tiger = new Tiger();//实现了 Runnable
       ThreadProxy threadProxy = new ThreadProxy(tiger);
        threadProxy.start();
    }
}
 
class Animal {
}
 
class Tiger extends Animal implements Runnable {
 
    @Override
    public void run() {
        System.out.println("老虎嗷嗷叫....");
    }
}
 
//线程代理类 , 模拟了一个极简的Thread类
class ThreadProxy implements Runnable {//你可以把Proxy类当做 ThreadProxy
 
    private Runnable target = null;//属性,类型是 Runnable
 
    @Override
    public void run() {
        if (target != null) {
            target.run();//动态绑定(运行类型Tiger)
        }
    }
 
    public ThreadProxy(Runnable target) {
        this.target = target;
    }
 
    public void start() {
        start0();//这个方法时真正实现多线程方法
    }
 
    public void start0() {
        run();
    }
}
 

3.3两种方法的区别

  • 从java设计角度看是没区别,Thread类实现了Runnable接口
  • Runnable接口更适合多线程共享一个资源的情况,避免单继承的限制

3.3callable接口方式【少用】

特点

  1. call方法可以有返回值
  2. 方法可以抛出异常
  3. 支持泛型的返回值
  4. 需要借助FutureTask类,比如获取返回结果

实现方法

  1. 创建一个实现callable的实现类
  2. 实现call方法,将此线程需要执行的操作声明在call()中
  3. 创建callable实现类的对象
  4. 将callable接口实现类的对象作为传递到FutureTask的构造器中,创建FutureTask的对象
  5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start方法启动(通过FutureTask的对象调用方法get获取线程中的call的返回值)

代码实现

//实现callable接口的call方法
class NumThread implements Callable{

    private int sum=0;//

    //可以抛出异常
    @Override
    public Object call() throws Exception {
        for(int i = 0;i<=100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName()+":"+i);
                sum += i;
            }
        }
        return sum;
    }
}

public class ThreadNew {

    public static void main(String[] args){
        //new一个实现callable接口的对象
        NumThread numThread = new NumThread();

        //通过futureTask对象的get方法来接收futureTask的值
        FutureTask futureTask = new FutureTask(numThread);

        Thread t1 = new Thread(futureTask);
        t1.setName("线程1");
        t1.start();

        try {
            //get返回值即为FutureTask构造器参数callable实现类重写的call的返回值
           Object sum = futureTask.get();
           System.out.println(Thread.currentThread().getName()+":"+sum);
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

4.线程常用方法

  1. setName //设置线程名称,使之与参数name相同
  2. getName //返回该线程的名称
  3. start /使该线程开始执行; Java虚拟机底层调用该线程的start0方法
  4. run //直接调用线程对象run方法;
  5. setPriority //更改线程的优先级
10MAX_PRIORITY
线程可以拥有的最大优先级。
1MIN_PRIORITY
线程可以拥有的最小优先级。
5NORM_PRIORITY
分配给线程的默认优先级。
  • 先设置优先级,再启动
  1. getPriority //获取线程的优先级
  2. sleep
    1. 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
    2. 运行态–>阻塞(就绪)态
    3. 单位是ms 1s=1000ms
  3. interrupt //中断线程
    1. 相当于唤醒操作
    2. 休眠态–>就绪态
  4. stop停止

线程进行一般有一个标志量flag while(flag==true)进行
stop的原理就是 flag等于false终止循环

  1. yiled礼让
  • 会让运行中的线程切换到就绪状态,重新争抢cpu的时间片,争抢时是否获取到时间片看cpu的分配。
  • 如果此时cpu资源多的话,yield可能没效果
  1. join
  • 待指定线程执行完成之后,再执行其他线程,(其他线程被阻塞,等待这个线程先执行)
  • 强行插队,一定会插队成功
package com.zjh;

public class Thread03 {
    public static void main(String[] args) throws InterruptedException {
        T t = new T();
        t.start();
        for (int i = 0; i < 20; i++) {
            System.out.println("主" + "吃了第" + i + "个包子");
            Thread.sleep(1000);
            if(i == 5) {
                System.out.println("子线程先执行");
//                t.join();
                Thread.yield();
            }
        }
    }
}
class T extends Thread{
    @Override
    public void run() {
        for (int j = 0; j < 100; j++) {
            System.out.println("子" + "吃了第" + j + "个包子");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

5.守护线程

线程分为用户线程(如main)和守护线程(如gc)
守护线程,当所有的非守护线程都结束后,即使它没有执行完,也会强制结束。一般是为工作线程服务的

package com.zjh;

public class Thread04 {
    public static void main(String[] args) throws InterruptedException {
        T2 t2 = new T2();
        t2.setDaemon(true);
        t2.start();
        for (int i = 0; i < 10; i++) {
            System.out.println("主线程工作" + i);
            Thread.sleep(1000);
        }
    }
}
class T2 extends Thread{
    @Override
    public void run() {
        for (; ;) {
            System.out.println("守护线程");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

一旦main线程结束,T2线程不再工作

6.线程状态

6.1线程状态(API定义六种)

imgimg

  1. NEW 线程对象被创建
  2. Runnable 线程调用了start()方法后进入该状态,该状态包含了三种情况
    1. 就绪状态 :等待cpu分配时间片
    2. 运行状态:进入Runnable方法执行任务
  3. Blocked 没获取到锁时的阻塞状态
  4. WAITING 调用wait()、join()等方法后的状态
  5. TIMED_WAITING 调用 sleep(time)、wait(time)、join(time)等方法后的状态
  6. TERMINATED 线程执行完成或抛出异常后的状态

6.2线程状态(操作系统定义5种)

img

  1. 初始状态:创建线程对象时的状态
  2. 可运行状态(就绪状态):调用start()方法后进入就绪状态,也就是准备好被cpu调度执行
  3. 运行状态:线程获取到cpu的时间片,执行run()方法的逻辑
  4. 阻塞状态: 线程被阻塞,放弃cpu的时间片,等待解除阻塞重新回到就绪状态争抢时间片
  5. 终止状态: 线程执行完成或抛出异常后的状态

7.线程同步

局限性:导致程序的执行效率要降低

7.1线程安全

线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据。
解释:当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式有个高尚的名称叫互斥锁,即能达到互斥访问目的的锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后。
保证在任何一个时刻,只能有一个线程访问该对象。

7.2synchronized

synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能)

7.2.1synchronized应用方式

  • 修饰非静态方法,作用于当前实例加锁,默认锁对象为this
  • 修饰代码块,指定加锁对象,对给定对象加锁
  • 修饰静态方法,作用于当前类对象加锁,默认锁对象:当前类.class

注意:用synchronized修饰实例对象或者代码块时,多个线程需要访问的是同一个实例对象(一把锁),否则仍然线程不安全,此时就应该使用的是静态方法(修饰类对象)

7.3Lock显式锁

  • 显式锁:显式的开启关闭
  • 只能锁代码

ReentrantLock实现Lock接口
synchronized和ReentrantLock都是可重入锁
可重入锁:可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class)
ReentrantLock的使用

private static final ReentrantLock LOCK = new ReentrantLock();

private static void m() {
    LOCK.lock();
    try {
        log.info("begin");
      	// 调用m1()
        m1();
    } finally {
        // 注意锁的释放
        LOCK.unlock();
    }
}

锁的部分放在try中
锁的释放放在finally中

7.4抢票问题

基本需求:3个售票口一起卖10张票

7.4.1代码1.0 超卖 重卖

package com.zjh;

public class Ticket {
    public static void main(String[] args) {
        SellTicket sellTicket = new SellTicket();
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
        new Thread(sellTicket).start();
    }
}
//Runnable方式
class SellTicket implements Runnable {
    private int ticketNum = 100;//让多个线程共享 ticketNum
    @Override
    public void run() {
        while (true) {
            if (ticketNum <= 0) {
                System.out.println("售票结束...");
                break;
            }

            //休眠50毫秒, 交叉运行
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"
                    + " 剩余票数=" + (--ticketNum));
        }

        }
}

img

解决:加锁 synchronized

7.4.2代码2.0 解决超卖重卖 Runnable 同步方法

package com.zjh;

public class Ticket {
    public static void main(String[] args) {
        SellTicket sellTicket = new SellTicket();
        Thread thread = new Thread(sellTicket);
        Thread thread1 = new Thread(sellTicket);
        Thread thread2 = new Thread(sellTicket);
        thread.setName("0");
        thread1.setName("1");
        thread2.setName("2");
        thread.start();
        thread1.start();
        thread2.start();

    }
}
//Runnable方式
class SellTicket implements Runnable {
    private static int ticketNum = 100;//让多个线程共享 ticketNum
    @Override
    public void run() {
        while (ticketNum > 0) {
            sell();
        }

        }
    public synchronized void sell(){
        if (ticketNum <= 0) {
            System.out.println("售票结束...");
            return;
        }

        //休眠50毫秒, 交叉运行
        try {
            Thread.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"
                + " 剩余票数=" + (--ticketNum));
    }
}

注意不能直接在run方法上加synchronized

7.4.2代码2.0 解决超卖重卖 Runnable 同步代码块

package com.zjh;

public class Ticket {
    public static void main(String[] args) {
        //Runnable方式
        SellTicket sellTicket = new SellTicket();
        Thread thread = new Thread(sellTicket);
        Thread thread1 = new Thread(sellTicket);
        Thread thread2 = new Thread(sellTicket);
        thread.setName("0");
        thread1.setName("1");
        thread2.setName("2");
        thread.start();
        thread1.start();
        thread2.start();
    }
}
//Runnable方式
class SellTicket implements Runnable {
    private static int ticketNum = 100;//让多个线程共享 ticketNum
    @Override
    public void run() {
            while (ticketNum > 0) {
                synchronized (this){
                    if (ticketNum <= 0) {
                        System.out.println("售票结束...");
                        return;
                    }

                    //休眠50毫秒, 交叉运行
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"
                            + " 剩余票数=" + (--ticketNum));
                }
            }
        }
}

注意:对于Runnable实现方式,只要在创建线程时new一个SellTicket类,然后在几个Thread类中都放同一个SellTicket类,就可以保证锁的唯一性,因此同步代码块中的synchronized可以直接放this,同步方法也可以使用非静态,而对于Thread实现方式则有所不同

7.4.3代码3.0 解决超卖重卖 Thread 同步方法

package com.zjh;

public class Ticket {
    public static void main(String[] args) {

        //Thread方式
        new SellTicket0().start();
        new SellTicket0().start();
        new SellTicket0().start();

    }
}

//Thread方式
class SellTicket0 extends Thread{
    private static int ticketNum = 100;//让多个线程共享 ticketNum
    private static Object object = new Object();
    @Override
    public void run() {
        while (ticketNum > 0) {
            sell();
        }
    }
    public  synchronized static void sell(){
        if (ticketNum <= 0) {
            System.out.println("售票结束...");
            return;
        }

        //休眠50毫秒, 交叉运行
        try {
            Thread.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"
                + " 剩余票数=" + (--ticketNum));
    }
}

注意:一定要使用synchronized static锁同步方法,即锁整个class对象,因为Thread方式实现方式是new三个不同的SellTicket0对象,必须用static保证SellTicket0对象的唯一性

7.4.3代码3.0 解决超卖重卖 Thread 同步代码块

package com.zjh;

public class Ticket {
    public static void main(String[] args) {
        //Thread方式
        new SellTicket0().start();
        new SellTicket0().start();
        new SellTicket0().start();

    }
}

//Thread方式
class SellTicket0 extends Thread{
    private static int ticketNum = 100;//让多个线程共享 ticketNum
    private static Object object = new Object();
    @Override
    public void run() {
        while (ticketNum > 0) {
            synchronized (object){
                if (ticketNum <= 0) {
                    System.out.println("售票结束...");
                    return;
                }

                //休眠50毫秒, 交叉运行
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("窗口 " + Thread.currentThread().getName() + " 售出一张票"
                        + " 剩余票数=" + (--ticketNum));
            }
        }
    }

}

注意:在此使用了一个private static Object object放进synchronized锁的对象,这样才可以保证锁的唯一性

8.死锁

多个线程都占用了对方的锁资源,但不肯相让,导致了死锁,在编程是一定要避免死锁的发生。

package xianchen06;

public class DeadLock {
    public static void main(String[] args) {
        DeadLockDemo a1 = new DeadLockDemo(true);
        DeadLockDemo a2 = new DeadLockDemo(false);
        a1.start();
        a2.start();
    }
}
class  DeadLockDemo extends Thread{
    static  Object o1 = new Object(); //保证多线程,共享一个对象,这里使用static
    static  Object o2= new Object();
    boolean flag;
    public  DeadLockDemo(boolean flag) {
        this.flag = flag;
    }
    public  void  run() {
        //1.如果为flag为 T,线程A就会先得到/持有01对象锁,然后尝试获取o2对象锁
        //2如果线程a1得不到02,就会Blocked
        //1.如果为flag为 F,线程B就会先得到/持有02对象锁,然后尝试获取o1对象锁
        //2如果线程B得不到01,就会Blocked
        if (flag) {
            synchronized (o1) {
                System.out.println(Thread.currentThread().getName()+" 进入1");
                synchronized (o2) { //这里获得li对象的监视权
                    System.out.println(Thread.currentThread().getName()+" 进入2");
                }
            }

        }
        else{
            synchronized (o2) {
                System.out.println(Thread.currentThread().getName()+" 进入3");
                synchronized (o1) { //这里获得li对象的监视权
                    System.out.println(Thread.currentThread().getName()+" 进入4");
                }
            }


        }
    }
}

产生死锁的必要条件:

  1. 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
  2. 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  4. 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。

9.线程通信

等待/通知机制,是指线程A调用了对象O的wait()方法进入等待状态,而线程B调用了对象O的notify()/notifyAll()方法,线程A收到通知后退出等待队列,进入可运行状态,进而执行后续操作。上述两个线程通过对象O来完成交互,而对象上的wait()方法和notify()/notifyAll()方法的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。

9.1wait notify

  • wait():使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒。 【进入WAITING】
  • wait(long):超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回。【进入TIMED_WAITING】
  • wait(long,int):对于超时时间更细力度的控制,单位为纳秒。【进入TIMED_WAITING】
  • notify():随机唤醒等待队列中等待同一共享资源的一个线程,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知一个线程。
  • notifyAll():使所有正在等待队列中等待同一共享资源的全部线程退出等待队列,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于JVM虚拟机的实现。(有多个线程需要用时推荐使用)

注意:

9.1.1wait、notify/notifyAll和sleep的区别与联系

  1. wait使当前线程阻塞,前提是必须先获得锁,所以只能在synchronized锁范围内里使用wait、notify/notifyAll方法,而sleep可以在任何地方使用。
  2. notify和wait的顺序不能错,只能notify被已经wait的线程

9.1.2sleep和wait的区别

  • 当线程执行sleep方法时,不会释放当前的锁(如果当前线程进入了同步锁),也不会让出CPU。sleep(milliseconds)可以用指定时间使它自动唤醒过来,如果时间不到只能调用interrupt方法强行打断。
  • 当线程执行wait方法时,会释放当前的锁,然后让出CPU,进入WAITING状态。只有当notify/notifyAll被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized代码块的代码或是中途遇到wait() ,再次释放锁。

9.2线程通信案例【操作系统PV操作】

9.2.2管程法(用变量控制)

需求:生产100只鸡,最多库存是10只鸡,边生产边消费

package com.zjh;

//解决线程同步,管程法
/*
    生产者只管生产
    消费者只管消费
    鸡: 实体类
    容器 :

    容器添加数据.
    要判断容器是否满 , 满了等待消费者消费
    没有满,生产者生产,通知消费者消费

    容器减少数据
    判断还有没有数据, 没有数据的话,等待生产者生产
    消费完毕,通知生产者生产
 */
public class PV_tube {
    public static void main(String[] args) {
        SynContainer synContainer = new SynContainer();
        new Productor(synContainer).start();
        new Consumer(synContainer).start();
    }
}
//生产者
class Productor extends Thread{
    //需要向容器中加入产品
    SynContainer container;
    public Productor(SynContainer container){
        this.container = container;
    }
    @Override
    public void run() {
        for (int i = 1; i < 100; i++) {
            //生产者添加产品
            container.push(new Chicken(i));
            System.out.println("生产者生产了"+i+"鸡");
        }
    }
}

//消费者
class Consumer extends Thread{
    SynContainer container;
    public Consumer(SynContainer container){
        this.container = container;
    }
    @Override
    public void run() {
        for (int i = 1; i < 100; i++) {
            //消费者拿走产品
            Chicken chicken = container.pop();
            System.out.println("消费者消费了"+chicken.id+"鸡");
        }
    }
}
//缓冲区-->容器
class SynContainer{

    //容器,最多10个
    Chicken[] chickens = new Chicken[10];

    //容器的计数器
    int num = 0;

    //生产者放入产品
    public synchronized void push(Chicken chicken) {

        //假如容易已经满了,就不用放,等待消费者消费
        if (num>=chickens.length){
            //等待消费者消费
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
        //假如容器没有满, 生产者继续生成
        chickens[num] = chicken;
        System.out.println("容器此时有多少个元素"+num);
        num++;
        //通知消费者消费
        this.notifyAll();
    }

    //消费者拿走产品
    public synchronized Chicken pop(){
        //假如容器空的,等待
        if (num<=0){
            //等待生产
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
        //取走产品
        num--;
        Chicken chicken = chickens[num];
        //通知生产者生产
        this.notifyAll();
        return chicken;
    }


}
//产品->鸡
class Chicken {
    int id;

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

9.2.3信号灯法(用标志量控制)

基本需求:
演员说话,观众等待
观众说话,演员等待
竞争资源:电视,只有一个,可以用信号量标志法,flag初始值为true,使得play先进行,watch后进行

package com.zjh;

public class PV_flag {
    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("节目:快乐大本营播放中");
                System.out.println();
            }else {
                this.tv.play("广告:抖音,记录美好生活");
            }
        }
    }
}
//消费者
class Watcher extends Thread{
    TV tv;
    public Watcher(TV tv){
        this.tv = tv;
    }
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}
//电视
class TV{
    //演员说话 , 观众等待
    //观众观看 , 演员等待
    boolean flag = true;
    //说话
    String voice;
    //表演
    public synchronized void play(String voice){
        //演员等待
        if (!flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("表演了"+voice);
        this.voice = voice;
        //通知观众观看
        this.notifyAll();
        this.flag = !this.flag;
    }


    //观看
    public synchronized void watch(){
        //观众等待
        if (flag){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("观众听到了: "+voice);
        //通知演员说话
        this.notifyAll();
        this.flag = !this.flag;
    }

}

9.3park()与unpark()【少用】

优点:

  1. 以thread为操作对象更符合阻塞线程的直观定义。
  2. 操作更精准,可以准确地唤醒特定线程(notify随机唤醒一个线程,notifyAll唤醒所有等待的线程),增加了灵活性。
  3. park/unpark的设计原理核心是“许可”(permit):park是等待一个许可,unpark是为某线程提供一个许可。permit不能叠加,也就是说permit的个数要么是0,要么是1。也就是不管连续调用多少次unpark,permit也是1个。线程调用一次park就会消耗掉permit,再一次调用park又会阻塞住。如果某线程A调用park,那么除非另外一个线程调用unpark(A)给A一个许可,否则线程A将阻塞在park操作上。
  4. unpark可以先于park调用。在使用park和unpark的时候可以不用担心park的时序问题造成死锁。相比之下,wait/notify存在时序问题,wait必须在notify调用之前调用,否则虽然另一个线程调用了notify,但是由于在wait之前调用了,wait感知不到,就造成wait永远在阻塞。
  5. park和unpark调用的时候不需要获取同步锁。

10.释放锁

10.1不会释放锁的操作

  • Thread.sleep()
  • Thread.yield()
  • suspend()挂起

10.2释放锁的操作

  • 正常执行结束
  • 线程在同步代码块,同步方法中遇到break,return。
  • 线程出现了未处理的Error或Exception,导致异常结束
  • Thread.wait()
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Jiong-952

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

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

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

打赏作者

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

抵扣说明:

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

余额充值