【多线程基础】线程状态 同步 协作 线程池 Lambda表达式

一、基本概念

  • 进程 Process
    • 进程就是执行程序的一次执行过程,它是一个动态的概念,是系统资源分配的单位
    • 通常再一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义,线程是CPU调度和执行的单位
  • 线程 Thread
    • 线程就是独立的执行路径
    • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,比如主线程,GC线程
    • main()称之为主线程,为系统的入口,用于执行整个程序
    • 在一个进程中,如果开辟了多个线程,线程的运行是由调度器安排调度的,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的
    • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
    • 线程会带来额外的开销,如CPU调度时间,并发控制开销
    • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致
  • 多线程


二、线程创建

1、继承 Thread 类

Thread 类实现了 Runnable 接口

1.1、实现步骤

  • 继承Thread类

  • 重写run方法

  • 创建实例调用start方法

public class ThreadTest extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println("我的线程==========="+i);
        }
    }

    public static void main(String[] args) {
        ThreadTest threadTest = new ThreadTest();
        threadTest.start();
        //threadTest.run();

        for (int i = 0; i < 2000; i++) {
            System.out.println("主线程"+i);
        }
    }
}

1.2、start 和 run 方法的区别?

  • start

    通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法

  • run

    run()方法 称为线程体。只是类的一个普通方法而已。如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。

1.3、多线程下载图片案例

package com.ajun;

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;

/**
 * @author ajun
 * Date 2021/6/27
 * @version 1.0
 */
public class WebDown extends Thread{
    private String url;
    private String file;

    public WebDown(String url,String file){
        this.url = url;
        this.file = file;
    }

    @Override
    public void run() {
        WebDownLoader webDownLoader = new WebDownLoader();
        webDownLoader.downLoader(url,file);
        System.out.println("下载了图片:"+file);
    }

    public static void main(String[] args) {
        WebDown t1 = new WebDown("https://img2020.cnblogs.com/blog/2429409/202106/2429409-20210623132753966-565643795.jpg", "1.jpg");
        WebDown t2 = new WebDown("https://img2020.cnblogs.com/blog/2429409/202106/2429409-20210618095622043-1605637963.jpg", "2.jpg");
        WebDown t3 = new WebDown("https://img2020.cnblogs.com/blog/2429409/202106/2429409-20210614212002983-1003721756.jpg", "3.jpg");

        t1.start();
        t2.start();
        t3.start();

    }
}

//下载器
class WebDownLoader{
    //下载方法
    public void downLoader(String url,String file){
        try {
            FileUtils.copyURLToFile(new URL(url),new File(file));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2、实现 Runnable 接口 (推荐)

2.1、实现步骤

  • 实现Runnable接口

    Runnable 接口只有run()方法

  • 实现run方法

  • 创建Thread时作为参数传入,调用start方法

public class RunnableTest implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 200; i++) {
            System.out.println("我的线程==========="+i);
        }
    }

    public static void main(String[] args) {
        //创建一个实现 Runnable 接口的类
        RunnableTest runnableTest = new RunnableTest();
        //创建线程,把实现Runnable接口的类对象作为参数传入
        new Thread(runnableTest).start();

        for (int i = 0; i < 2000; i++) {
            System.out.println("主线程"+i);
        }
    }
}

2.2、龟兔赛跑

public class Race implements Runnable {
    //胜利者
    private static String winner = "";
    //距离
    private int m = 1_0000_0000;

    @Override
    public void run() {
        for (int i = 1; i < m+1; i++) {
            //选手
            String player = Thread.currentThread().getName();
            //兔子每10米就休息
            if (player.equals("兔子") && i % 10 == 0) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //判断比赛是否结束
            if (gameOver(i)) {
                System.out.println(player + " ---> 跑了 " + i + " 米");
                break;
            }
        }
    }

    //判断比赛是否结束
    private boolean gameOver(int m1) {        
        if (winner != "") {//已经有胜利者
            return true;
        } else {//没有胜利者
            if (m1 >= m) {//已到达终点
                winner = Thread.currentThread().getName();
                System.out.println(winner + "赢了!");
                return true;
            }
        }
        return false;
    }

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

3、实现 Callable 接口 (了解)

3.1、步骤

3.2、改造多线程下载图片案例

public class WebDownCallable implements Callable<Boolean> {
    private String url;
    private String file;

    public WebDownCallable(String url, String file) {
        this.url = url;
        this.file = file;
    }

    @Override
    public Boolean call() throws Exception {
        WebDownLoader1 webDownLoader = new WebDownLoader1();
        webDownLoader.downLoader(url, file);
        System.out.println("下载了图片:" + file);
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        WebDownCallable t1 = new WebDownCallable("https://img2020.cnblogs.com/blog/2429409/202106/2429409-20210623132753966-565643795.jpg", "1.jpg");
        WebDownCallable t2 = new WebDownCallable("https://img2020.cnblogs.com/blog/2429409/202106/2429409-20210618095622043-1605637963.jpg", "2.jpg");
        WebDownCallable t3 = new WebDownCallable("https://img2020.cnblogs.com/blog/2429409/202106/2429409-20210614212002983-1003721756.jpg", "3.jpg");

        //创建执行服务:创建线程池
        ExecutorService service = Executors.newFixedThreadPool(3);

        //提交执行
        Future<Boolean> s1 = service.submit(t1);
        Future<Boolean> s2 = service.submit(t2);
        Future<Boolean> s3 = service.submit(t3);

        //得到返回值
        Boolean re1 = s1.get();
        Boolean re2 = s2.get();
        Boolean re3 = s3.get();

        System.out.println(re1);
        System.out.println(re2);
        System.out.println(re3);

        //关闭服务
        service.shutdownNow();
    }

}

//下载器
class WebDownLoader1 {
    //下载方法
    public void downLoader(String url, String file) {
        try {
            FileUtils.copyURLToFile(new URL(url), new File(file));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

三、Lambda表达式 (箭头函数)

1、介绍

  • λ 希腊字母表中排序第十一位的字母,英语名称为 Lambda (兰布达)
  • Lambda表达式 就是 箭头函数。是JDK1.8的一种新特性(语法糖)
  • 避免匿名内部类定义过多
  • 可以让代码看起来很简洁
  • 去掉了一堆没有意义的代码,留下核心的逻辑
  • 其实质属于函数式编程的概念
(params)-> expression[表达式]
(params) -> statement[语句]
(params) -> {statements}

2、函数式接口

  • 理解Functional interface(函数式接口)是学习java8 lamda表达式的关键所在

  • 函数式接口的定义:

    • 任何接口,如果只包含一个方法,那么它就是一个函数式接口

      public interface Runnable{
      	public abstract void run();
      }
      
    • 对于函数式接口,我们可以通过lamda表达式来创建该接口的对象

3、示例

3.1、定义接口

//接口
//只有一个方法
interface ILike{
    void lambda();
    //带参数
    //void lambda(int n);
}

3.2、传统实现类

public class LambdaTest {
    public static void main(String[] args) {
        ILike1 iLike1 = new ILike1();
        iLike1.lambda();//lambda1
    }
}

//传统实现类
class ILike1 implements ILike{
    @Override
    public void lambda() {
        System.out.println("lambda1");
    }
    //带参数
    //@Override
    //public void lambda(int a) {
    //    System.out.println("lambda1---"+a);
    //}
}

3.3、静态内部类

public class LambdaTest {
	//静态内部类
    static class ILike2 implements ILike{
        @Override
        public void lambda() {
            System.out.println("lambda2");
        }
    }

    public static void main(String[] args) {
        ILike2 iLike2 = new ILike2();
        iLike2.lambda(3); //lambda2       
    }
}    

3.4、局部内部类

public class LambdaTest {
    public static void main(String[] args) {
        //局部内部类
        class ILike3 implements ILike{
            @Override
            public void lambda() {
                System.out.println("lambda3");
            }
        }
        
        ILike3 iLike3 = new ILike3();
        iLike3.lambda(); //lambda3       
    }
}   

3.5、匿名内部类

public class LambdaTest {
    public static void main(String[] args) {
        //匿名内部类  
        //new 接口 { 实现方法 }
        ILike iLike4 = new ILike() {
            @Override
            public void lambda() {
                System.out.println("lambda4");
            }
        };        
        
        iLike4.lambda(); //lambda4       
    }
}   

3.6、Lambda表达式

  • 完整写法
public class LambdaTest {
    public static void main(String[] args) {        
        //Lambda表达式          
        ILike i5 = () -> {
            System.out.println("lambda5");
        };       
        
        i5.lambda(); //lambda5
    }
}  
  • 简写 (省花括号)

    方法中只有一条执行语句时省略花括号

public class LambdaTest {
    public static void main(String[] args) {        
        //方法中只有一条执行语句时
        //花括号可以省略
        ILike i5 = () -> System.out.println("lambda6");        
        
        i5.lambda(); //lambda6
    }
} 
  • 简写(只有一个参数时省略小括号)
public class LambdaTest {
    public static void main(String[] args) {        
        //只有一个参数时省略小括号
        ILike i5 = a -> System.out.println("lambda7---"+a);        
        
        i5.lambda(5); //lambda7---5
    }
} 

四、线程状态

1、状态


  • state
public enum State {
    NEW,//新线程,未启动
    RUNNABLE,//可运行(包含:就绪状态 和 运行状态)
    BLOCKED,//阻塞
    WAITING,//等待
    TIMED_WAITING,//超时等待
    TERMINATED;//中止
}

2、线程方法

  • getName()
  • setName()

2.1、线程停止

2.2、线程休眠 sleep()

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

2.3、线程礼让 yield()

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让CPU重新调度,礼让不一定成功,看CPU心情

2.4、线程强制执行 join()

  • Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
  • 可以想象成插队
public class ThreadJoinTest implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println("线程VIP来了"+i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadJoinTest joinTest = new ThreadJoinTest();
        Thread thread = new Thread(joinTest);
        thread.start();

        for (int i = 0; i < 100; i++) {
            if(i==30){
                thread.join();
            }
            System.out.println("main"+i);
        }
    }
}

2.5、线程优先级

  • java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程;线程调度器按照优先级决定该调度哪个线程来执行
  • 线程的优先级用数字来表示,范围从1~10
    • Thread.MIN_PRIORITY = 1
    • Thread.MAX_PRIORITY= 10
    • Thread.NORM_PRIORITY = 5
  • 使用 getPriority()setPriority() 来获取或改变优先级

2.6、守护线程setDeamon()

setDeamon(true)
  • 线程分为用户线程和守护线程
  • 虚拟机必须确保用户线程执行完毕
  • 虚拟机不用等待守护线程执行完毕
  • 如后台记录操作日志,监控内存,垃圾回收

五、线程同步

1、介绍

  • 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候我们就需要线程同步,线程同步其实是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕,下一个线程在使用
  • 由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问的冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制syncronized,当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后释放锁即可,存在一下问题
  • 一个线程持有锁会导致其他所有需要此锁的进程挂起
    • 在多线程竞争的情况下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
    • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题

2、同步方法 和 同步块

  • 同步块

    synchronized(obj){ }

    • obj称之为同步监视器
    • obj可以是任何对象,但是推荐使用共享资源作为同步监视器
  • 同步方法

    • 在方法上添加synchronized关键字,锁的是对象本身
    • 同步方法中无需指定同步监视器,因为同步方法中的同步监视器就是this,就是这个对象本身,或者是class
  • 同步监视器的执行过程

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

3、三大不安全案例

3.1、买票

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

        new Thread(buyTicket,"小张").start();
        new Thread(buyTicket,"小王").start();
        new Thread(buyTicket,"小李").start();
    }
}

class BuyTicket implements Runnable{
    private int ticketNum = 10;
    boolean flag = true;
    @Override
    public void run() {
        while (flag){
            buy();
        }
    }

    //同步方法
    private synchronized void buy(){
        if(ticketNum < 1){
            flag = false;
            return;
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " 拿到了 "+ ticketNum--);
    }
}

解决办法:

把买票方法 buy 同步

3.2、取钱

public class BankTest {
    public static void main(String[] args) {
        Account account = new Account(100, "结婚基金");

        TakeMoney myWife = new TakeMoney(account, 80);
        TakeMoney my = new TakeMoney(account, 50);

        new Thread(myWife,"我的妻子").start();
        new Thread(my,"我").start();
    }

}

//账户
class Account {
    private int money;
    private String name;

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

    public int getMoney() {
        return money;
    }

    public void setMoney(int money) {
        this.money = money;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

//取钱
class TakeMoney implements Runnable {
    Account account;//账户
    int takeNum;//存走的钱
    int nowNum;//手里的钱

    public TakeMoney(Account account, int takeNum) {
        this.account = account;
        this.takeNum = takeNum;
    }

    @Override
    public void run() {
        //同步块:把账户 account 同步
        synchronized (account){
            String threadName = Thread.currentThread().getName();
            //判断是否有钱
            if (account.getMoney() - takeNum < 1) {
                System.out.println(threadName + " 钱不够了");
                return;
            }

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

            //卡内余额
            account.setMoney(account.getMoney() - takeNum);
            //手里的钱
            nowNum = nowNum + takeNum;
            System.out.println(account.getName() +" 余额为:"+account.getMoney());
            System.out.println(threadName+" 手里的钱为:"+nowNum);
        }
    }
}

解决办法:

同步块,把账户 account 同步

3.3、线程不安全集合

public class CollectionTest {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                //同步块:对集合 list 同步
                synchronized (list){
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        Thread.sleep(3000);
        System.out.println(list.size());
    }
}

解决办法:

同步块,对集合 list 同步

4、死锁

4.1、死锁分析

  • 多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源释放才能运行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有"两个以上的对象锁"时,就可能发生死锁现象

4.2、死锁产生的必要条件

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

4.3、死锁避免方法



5、释放锁

Java多线程运行环境中,在哪些情况下会使对象锁释放?

由于等待一个锁的线程只有在获得这把锁之后,才能恢复运行,所以让持有锁的线程在不再需要锁的时候及时释放锁是很重要的。在以下情况下,持有锁的线程会释放锁

  • 执行完同步代码块,就会释放锁。(synchronized)
  • 在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放。(exception)
  • 在执行同步代码块的过程中,执行了锁所属对象的wait()方法,这个线程会释放锁,进
    入对象的等待池。(wait)

除了以上情况以外,只要持有锁的线程还没有执行完同步代码块,就不会释放锁

在下面情况下,线程是不会释放锁的:

  • 执行同步代码块的过程中,执行了Thread.sleep()方法,当前线程放弃CPU,开始睡眠,进入堵塞状态,在睡眠中不会释放锁
  • 在执行同步代码块的过程中,执行了Thread.yield()方法,当前线程放弃CPU,回到就绪状态,但不会释放锁
方法 是否释放锁
wait() 释放
sleep() 不释放
yield() 不释放

6、Lock锁

  • 从JDK1.5开,java提供了更为强大的线程同步机制——通过显式定义同步锁对象来实现同步,同步锁使用lock对象来充当

  • java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象

  • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁,释放锁


class BuyTicket implements Runnable{

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

    private int ticketNum = 10;
    boolean flag = true;
    @Override
    public void run() {
        while (flag){
            buy();
        }
    }

    //synchronized
    private void buy(){
        lock.lock();//加锁
        try{
            //业务代码
            if(ticketNum < 1){
                flag = false;
                return;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 拿到了 "+ ticketNum--);
            
        }finally {
            lock.unlock();//解锁
        }
    }

}

7、Lock 与 synchronized

  • Lock是显式锁(手动开启和关闭,别忘记关闭锁) synchronized是隐式锁,出了作用域自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用lock锁,JVM将花费较少的时间来调度线程,性能更好,并且具有更好的扩展性(提供更多的子类)
  • 优先使用顺序
    Lock > 同步代码块 > 同步方法
比较 Lock synchronized
类型 java接口 内置关键字
开启释放 显式锁
手动 开启 和 关闭
隐式锁
出了作用域自动释放
代码锁 支持 支持
方法锁 不支持 支持
锁状态 可以判断是否获取到了锁 无法判断获取锁的状态
线程阻塞 Lock锁就不一定会等待下去 线程1(获得锁,阻塞),线程2(等待)
可重入性 可重入的,可以判断锁 可重入锁,不可以中断的
公平性 默认非公平的(可设置) 非公平的
性能 性能更好
JVM将花费较少的时间来调度
性能一般
JVM将花费较多的时间来调度管理
扩展性 有更好的扩展性(提供更多的子类) 不支持
适用场景 适合锁大量的同步代码 适合锁少量的代码同步问题

六、线程协作

1、生产者和消费者问题

  • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库产品取走消费
  • 如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止
  • 如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止

这是一个线程同步问题,生产者和消费者共享一个资源,并且生产者和消费者之间相互依赖,互为条件

  • 对于生产者
    • 没有生产产品之前,要通知消费者等待
    • 生产了产品之后,又要马上通知消费者消费
  • 对于消费者
    • 在消费之后,要通知生产者已经结束消费,需要生产新的产品以供消费

在生产者、消费者问题上,仅有 synchronized 是不够的

synchronized 可阻止并发更新同一个共享资源,实现了同步

synchronized 不能用来实现不同线程之间消息传递(通信)

2、Java 中解决线程间通信的方法


wait() 与 sleep() 区别

wait 在等待时会释放锁;sleep 在等待时不会释放锁(抱着锁睡觉)

3、解决方案

3.1、管程法

  • 生产者:负责生产数据的模块(可能是方法,对象,线程,数组)
  • 消费者::负责处理数据的模块(可能是方法,对象,线程,数组)
  • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个缓冲区

生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据

package com.ajun;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author ajun
 * Date 2021/6/28
 * @version 1.0
 * 生产者、消费者问题
 * 管程法
 */
public class PCTest {
    public static void main(String[] args) {
        //定义仓库
        Repository repository = new Repository();
        //生产者
        Provider provider = new Provider(repository);
        //消费者
        Consumer consumer = new Consumer(repository);

        new Thread(provider).start();
        new Thread(consumer).start();
    }
}

//产品
class Product {
    private int id;

    public int getId() {
        return id;
    }

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

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

//生产者
class Provider implements Runnable {
    //有仓库属性
    private Repository repository = new Repository();

    //构造器
    public Provider(Repository repository) {
        this.repository = repository;
    }

    @Override
    public void run() {
        //循环生产100件产品
        for (int i = 1; i < 101; i++) {
            System.out.println("生产了第 " + i + " 件产品");
            //存入仓库
            repository.push(new Product(i));
        }
    }
}

//消费者
class Consumer implements Runnable {

    //有仓库属性
    private Repository repository = new Repository();

    //构造器
    public Consumer(Repository repository) {
        this.repository = repository;
    }

    @Override
    public void run() {
        //循环消费100件产品
        for (int i = 1; i < 101; i++) {
            //从仓库取出
            Product product = repository.out();
            System.out.println("消费了第 " + product.getId() + " 件产品");
        }
    }
}

//仓库(缓存区)
class Repository {
    //存放产品的数组
    Product[] products = new Product[10];
    int count = 0;//计数器

    //生产者放入产品
    //放产品时需要线程同步
    public synchronized void push(Product product) {
        //判断仓库是否满了
        if(count == products.length){//满了
            try {
                this.wait();//等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //仓库没有满
        products[count] = product;//把产品放入仓库
        count++;//计器数加1

        //通知消费者可以消费了
        this.notifyAll();
    }

    //消费者取产品
    //取产品时需要线程同步
    public synchronized Product out() {
        //Product product = null;
        //判断仓库中是否有产品
        if(count == 0){//没有产品
            try {
                this.wait();//等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }/*else{//有产品
            count--;//计数器减1
            product = products[count];//取出产品

        }*/

        count--;//计数器减1
        Product product = products[count];//取出产品

        this.notifyAll();
        return product;
    }
}

在有 wait 和 notityAll 的同步时,不能用 ReentrantLock

wait 、notity 方法只能在 同步方法 或 同步代码块 中使用,否则会抛出 IllegalMonitorStateException 异常

3.2、信号灯法

package com.ajun;

/**
 * @author ajun
 * Date 2021/6/28
 * @version 1.0
 * 生产者、消费者问题
 * 信号灯法
 */
public class PCTest1 {
    public static void main(String[] args) {
        TV tv = new TV();
        Player player = new Player(tv);
        Watcher watcher = new Watcher(tv);
        new Thread(player).start();
        new Thread(watcher).start();
    }
}

//生产者:演员
class Player implements Runnable {

    private TV tv;

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

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            tv.play("第 " + (i + 1) + " 个节目");
        }
    }
}

//消费者:观众
class Watcher implements Runnable {

    private TV tv;

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

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            tv.watch();
        }
    }
}

//产品:节目
class TV {
    //具体节目
    String voice;
    //控制开关
    //节目未上映,演员生产,观众等待:false
    //节目已上映,观众观看,演员休息等待:true
    boolean flag = false;//未上映

    //表演
    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;
    }
}

七、线程池

1、介绍

  • 背景:经常创建和销毁,使用量特别大的资源,比如并发情况下的线程,对性能影响很大
  • 思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完毕放回池中,可以避免频繁的创建销毁,实现重复利用,类似生活中的公共交通工具
  • 好处
    • 提高了响应速度(减少了创建新线程的时间)
    • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
    • 便于线程管理(…)
      • corePoolSize: 核心池的大小
      • maximumPoolSize:最大线程数
      • keepAliveTime: 线程没有任务时最多保持多长时间后会终止

2、使用线程池

线程池工具类 Executors

2.1、execute()

没有返回值

执行 Runnable 线程类

package com.ajun;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author ajun
 * Date 2021/6/28
 * @version 1.0
 */
public class ThreadPoolTest {
    public static void main(String[] args) {
        //用线程池工厂类 Executors,创建线程池
        ExecutorService service = Executors.newFixedThreadPool(10);

        //执行线程
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());

        //关闭连接
        service.shutdown();
    }
}

//实现Runnable接口的类
class MyThread implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

2.2、submit()

有返回值 Future

执行 Callable 接口的类

package com.ajun;

import java.util.concurrent.*;

/**
 * @author ajun
 * Date 2021/6/28
 * @version 1.0
 */
public class ThreadPoolTest1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //用线程池工厂类 Executors,创建线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        
        //提交执行
        Future<Boolean> s1 = service.submit(new MyThread1());
        Future<Boolean> s2 = service.submit(new MyThread1());
        Future<Boolean> s3 = service.submit(new MyThread1());

        //得到返回值
        Boolean rs1 = s1.get();
        Boolean rs2 = s2.get();
        Boolean rs3 = s3.get();

        //关闭连接
        service.shutdown();
    }
}

class MyThread1 implements Callable<Boolean>{
    @Override
    public Boolean call() throws Exception {
        System.out.println(Thread.currentThread().getName());
        return true;
    }
}
posted @ 2021-07-27 10:37  土味儿  阅读(107)  评论(0)    收藏  举报