Java SE - 10 - 多线程

发布于:2023-01-06 ⋅ 阅读:(394) ⋅ 点赞:(0)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


1. 线程的概述

程序: 为了完成某一个特定的功能,利用某种语言来编写一段指令的集合。

进程: 某个程序运行一次的过程,正在运行的某个程序。

线程: 进程可以分化成线程,是某个程序内部的一种执行路径。

并发: 同一个时刻内,多个指令在同一cpu上执行。

并行: 同一个时刻内,多个指令在多个cpu上执行。

多线程的优点:

a、提高了程序的响应

b、提高了cpu的执行效率

c、改善了程序结构

何时才能用到多线程:

a、两个或两个以上的任务时

b、需要在后台运行的任务

c、需要实现一些等待任务

多线程的继承图:

在这里插入图片描述

2.多线程的创建与使用

2.1 方式一:继承的方式开启线程

实现步骤:

1.创建一个子类,继承Thread类。

2.子类重写父类的run()方法。

3.在run方法中编写线程执行的功能。

4.在调用处,创建子类对象。

5.调用start()方法开启线程。

代码演示:

public class Demo01 {
    public static void main(String[] args) {
        // 4.创建子类对象
        MyThread myThread = new MyThread();
        // 5.开启线程
        myThread.start();

        for (int i = 0; i < 100; i++) {
            System.out.println("PHP ---" + i);
        }
    }
}

// 1.创建子类,继承Thread类
class MyThread extends Thread{
    // 2.重写run方法
    @Override
    public void run() {
        // 3.编写线程要执行的功能
        for (int i = 0; i < 100; i++) {
            System.out.println("Java ---" + i);
        }
    }
}

**注意事项 : **
a、启动线程是调用start()方法而非run()方法,调用run()方法和调用普通方法没有差别。
b、一个对象只能开启一个线程,重复调用start()方法,会报出java.lang.IllegalThreadStateException异常
c、run()方法是由JVM自动调用的,什么时候调用,执行过程都是由操作系统的cpu调度决定的。

2.2 方式二:实现Runnable接口方式开启线程

实现步骤 :

1.创建子类,实现Runnable接口

2.重写run()方法

3.在run()方法中编写线程的功能

4.在调用处创建子类对象

5.创建Thread实例对象,将子类对象构造参数传入

6.开启线程

—————————————或————————————————

1.创建Thread对象

2.在Thread对象的构造方法中传入一个匿名内部类对象,来实现Runnable接口

3.重写run()方法,在run()方法中编写线程的功能

4.开启线程

代码演示:

public class Demo01 {
    public static void main(String[] args) {
        // 4.在调用处创建子类对象
        myRunnable myRunnable = new myRunnable();

        // 5.创建Thread实例对象,将子类对象构造参数传入
        Thread thread = new Thread(myRunnable);

        // 6.开启线程
        thread.start();

        for (int i = 0; i < 100; i++) {
            System.out.println("PHP ");
        }
    }
}
// 1.创建一个子类,实现Runnable接口
class myRunnable implements Runnable{

    // 2.重写run()方法
    @Override
    public void run() {
        // 3.在run()方法中编写线程的功能
        for (int i = 0; i < 100; i++) {
            System.out.println("Java ");
        }
    }
}


// 匿名内部类对象来实现Runnable接口
public class Demo02 {
    public static void main(String[] args) {
        // 1.创建Thread对象
        // 2.在Thread对象的构造方法中传入一个匿名内部类对象,来实现Runnable接口
        Thread thread = new Thread(new Runnable() {

            // 3.重写run()方法,在run()方法中编写线程的功能
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("Java -- " + i);
                }
            }
        });
        // 4.开启线程
        thread.start();

        for (int i = 0; i < 100; i++) {
            System.out.println("Java -- " + i);
        }
    }
}

接口实现的好处
a、避免了单继承的局限性
b、多个线程可以共享一个接口的实现类对象,非常适合多个线程处理同一个资源。

2.3 方式三:实现Callable接口

实现步骤:

1.创建一个自定义的类,实现Callable接口

2.重写call()方法,在call方法中编写线程执行的功能

3.在调用出,创建自定义类的实例对象

4.创建FutureTask对象,将Callable实现类对象,作为构造参数传入

5.创建Thread对象,将FutureTask作为构造参数传入

6.开启线程

代码示例:

public class Demo01 {
    public static void main(String[] args) {
        // 3.在调用出,创建自定义类的实例对象
        myCall myCall = new myCall();

        // 4.创建FutureTask对象,将Callable实现类对象,作为构造参数传入
        FutureTask<String> task = new FutureTask<>(myCall);

        // 5.创建Thread对象,将FutureTask作为构造参数传入
        Thread thread = new Thread(task);

        // 6.开启线程
        thread.start();


        for (int i = 0; i < 100; i++) {
            System.out.println("PHP -- " + i);
        }
    }
}

// 1.创建一个自定义的类,实现Callable接口
class myCall implements Callable<String>{
    // 2.重写call()方法,在call方法中编写线程执行的功能
    @Override
    public String call() throws Exception {
        for (int i = 0; i < 100; i++) {
            System.out.println("Java -- " + i);
        }
        return "Java结束~~";
    }
}

使用Callable创建线程的好处:
a、call()方法可以有返回值
b、实现Callable接口,可以支持泛型
c、方法可以向上抛异常
d、可以借助FutureTask类的一些功能,比如获取方法的返回值。

3.线程常用的方法

-- 有关名称设置
   String getName() : 获取当前线程的名称
   void setName(String name) : 设置当前名称
    
-- 线程的优先级
    void setPriority() : 设置线程的优先级,默认是5 范围1-10
    int getPriority() : 获取线程的优先级
    
-- 设置线程的守护状态
    void setDaemon(boolean on) : 将该线程标记为守护线程或用户线程 

-- 设置当前线睡眠
    static void sleep(long millis) 
          在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 
    
-- 其它方法
    yield():线程让步 暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程 若队列中没有同优先级的线程,忽略此方法 
	start():开启一个线程
	run():线程被调度是执行的操作
	stop(): 强制停止某个线程(该方法已过时)
	jion():当某个程序执行流中调用其他线程的 join() 方法时,调用线程将 被阻塞,直到 join() 方法加入的 join 线程执行完为止

代码演示:

public class Demo01 {
    public static void main(String[] args) {
        // currentThread():返回当前线程的引用
        System.out.println("main线程:" + Thread.currentThread()); // main线程:Thread[main,5,main]

        // 创建MyThread对象
        MyThread myThread = new MyThread();


        // setName(): 设置当前线程名称
        myThread.setName("MyThread线程");

        // getName():返回当前线程名称
        System.out.println(myThread.getName()); // MyThread线程

        // sleep():使当前线程放弃一段时间的cpu控制权,待时间结束后,充型争夺cpu控制权
        try {
            myThread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // join():当调用该方法时,当前线程阻塞,直加入join方法线程完为之
        try {
            myThread.join(1000, 10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // yield()暂缓
        // run()当线程启动后,所执行的操作


        //  start():开启当前线程
        myThread.start();
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        // currentThread():返回当前线程的引用
        System.out.println("MyThread线程:" + currentThread()); // MyThread线程:Thread[Thread-0,5,main]

        for (int i = 0; i < 100; i++) {
            System.out.println(i);
        }
    }
}

4.线程的同步问题

4.1 多线程存在的问题

引发的问题:

1.多个线程执行的不确定性,引发了结果的不稳定

2.多个线程操作同一个共享数据,会引发操作的不完整性,会破坏数据

卖票案例:

/**
 * 卖票,多个线程同时操纵一个数据源
 */
public class Demo01 {
    public static void main(String[] args) {
        myRannable myRannable = new myRannable();

        Thread t1 = new Thread(myRannable,"窗口一:");
        Thread t2 = new Thread(myRannable,"窗口二:");
        Thread t3 = new Thread(myRannable,"窗口三:");

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

// 1.定义一个类,实现Rannable接口
class myRannable implements Runnable{
    int ticket = 100;
    @Override
    public void run() {
        // 一直卖票
        while (true){
            // 判断是否还有票
            if (ticket <= 0){
                break;
            }

            // 让问题更突出
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 开始买票
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "号票");
            ticket--;

        }
    }
}

导致的问题:

  1. 多线程出现了安全问题
  2. 问题的原因: 当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有 执行完,另一个线程参与进来执行。导致共享数据的错误。
  3. 解决办法: 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以 参与执行。

4.2 解决多线程安全问题方式一 : 同步代码块

使用格式:

synchronized(同步锁对象){
    需要同步的代码块;
}

注意事项:
	1.同步代码块中放的是对共享数据操作的代码。
	2.锁,锁指的是同一个对象,注意是同一个对象,对于this要谨慎使用。

对上述代码中的run()方法进行改造:

class myRannable implements Runnable{
    int ticket = 100;
    Object object = new Object(); // 同步锁
    @Override
    public void run() {
        // 一直卖票
        while (true){
            synchronized (object){
                // 判断是否还有票
                if (ticket <= 0){
                    break;
                }

                // 让问题更突出
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 开始买票
                System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "号票");
                ticket--;
            }
        }
    }
}

4.3 解决多线程安全问题方式二 :同步方法

使用格式:

[权限修饰符] synchronized 返回值类型 方法名(参数列表){
    同步代码;
}

注意事项:
	1.同步方法分为静态同步方法和非静态同步方法。
	2.静态同步方法的锁是this,非静态同步方法的锁是该类的字节码文件:类.class

对上述代码进行修改:

class myRannable implements Runnable{
    int ticket = 100;
    Object object = new Object(); // 同步锁
    @Override
    public void run() {
        // 一直卖票
        while (true){
            if (sell()) break;
        }
    }

    private synchronized boolean sell() {
        // 判断是否还有票
        if (ticket <= 0){
            return true;
        }

        // 让问题更突出
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 开始买票
        System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "号票");
        ticket--;
        return false;
    }
}

4.4 解决多线程安全方式三 :Lock锁机制

使用的方法:

-- 常用的方法
	创建锁的构造方法:ReentrantLock()
	开启锁:lock()
	释放锁:unlock()

-- 使用的步骤
    在成员位置创建锁对象:Lock lock = new ReentrantLock();
	在同步代码块前后加锁和解锁
    开启锁:lock()
	释放锁:unlock()

对上述代码进行改进:

class myRannable implements Runnable{
    int ticket = 100;

    // 创建Lock锁对象
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        // 一直卖票
        while (true){

            // 加锁
            lock.lock();
            // 判断是否还有票
            if (ticket <= 0){
                break;
            }

            // 让问题更突出
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 开始买票
            System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "号票");
            ticket--;

            // 释放锁
            lock.unlock();
        }
    }
}

4.5 死锁

概述: 线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行

产生的原因: 1. 资源有限 2. 同步嵌套

public class DeathLockDemo {
    public static void main(String[] args) {
        // 定义两个锁
        Object a = new Object();
        Object b = new Object();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (a){
                    synchronized (b){
                        System.out.println("11111");
                    }
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (b){
                    synchronized (a){
                        System.out.println(2222);
                    }
                }
            }
        }).start();
    }
}

解决方式:

​ a、尽量避免嵌套使用同步锁
​ b、减少同步资源的定义
​ c、专门的算法和规则

5.线程的声明周期

新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建 状态
就绪: 处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已 具备了运行的条件,只是没分配到CPU资源
运行: 当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线 程的操作和功能
阻塞: 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中 止自己的执行,进入阻塞状态
死亡: 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

线程执行的示意图:

在这里插入图片描述

6.线程通信

使用的方法:

void wait() :导致当前线程等待,直到另一个线程调用该对象的 
void notify() 唤醒正在等待对象监视器的单个线程 
void notifyAll() 唤醒正在等待对象监视器的所有线程

代码演示:

/**
 * main主程序入口
 */
public class Demo01 {
    public static void main(String[] args) {
        BaoZi baoZi = new BaoZi();
        Cook cook = new Cook("厨师:", baoZi);
        Customer customer = new Customer("顾客:", baoZi);

        cook.start();
        customer.start();
    }
}

/**
 * 包子类:锁,用来控制生产者消费者对cpu的执行权的
 */
public class BaoZi implements Serializable {
    private static final long serialVersionUID = -758549433910957718L;
    private String seek; // 包子馅
    private boolean flag = false; // 是否有包子

    public BaoZi() {
    }

    public BaoZi(String seek, boolean flag) {
        this.seek = seek;
        this.flag = flag;
    }

    public String getSeek() {
        return seek;
    }

    public void setSeek(String seek) {
        this.seek = seek;
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        BaoZi baoZi = (BaoZi) o;
        return flag == baoZi.flag &&
                Objects.equals(seek, baoZi.seek);
    }

    @Override
    public int hashCode() {
        return Objects.hash(seek, flag);
    }

    @Override
    public String toString() {
        return "BaoZi{" +
                "seek='" + seek + '\'' +
                ", flag=" + flag +
                '}';
    }
}


/**
 * 生产者
 */
public class Cook extends Thread implements Serializable {

    private static final long serialVersionUID = -7753692006936253909L;
    private BaoZi baoZi;

    public Cook() {
    }

    public Cook(String name, BaoZi baoZi) {
        super(name);
        this.baoZi = baoZi;
    }

    public BaoZi getBaoZi() {
        return baoZi;
    }

    public void setBaoZi(BaoZi baoZi) {
        this.baoZi = baoZi;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Cook cook = (Cook) o;
        return Objects.equals(baoZi, cook.baoZi);
    }

    @Override
    public int hashCode() {
        return Objects.hash(baoZi);
    }

    @Override
    public String toString() {
        return "Cook{" +
                "baoZi=" + baoZi +
                '}';
    }

    @Override
    public void run() {
        // 让厨师一直做
        int count = 0;
        while (true){
            synchronized (baoZi){
                // 判断是否有包子
                if (baoZi.isFlag()){
                    try {
                        baoZi.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                if (count % 2 == 0){
                    baoZi.setSeek("素馅");
                }else {
                    baoZi.setSeek("肉馅");
                }

                // 执行完if就是没有包子
                System.out.println(this.getName() + "正在做" + baoZi.getSeek() +"的包子~~~");
                try {
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(this.getName() + "做完包子正在售卖~~" );

                // 包子制作好了
                baoZi.setFlag(true);

                // 唤醒顾客来买包子
                baoZi.notify();
            }
        }
    }
}


/**
 * 消费者模式:
 */
public class Customer extends Thread implements Serializable {
    private static final long serialVersionUID = -9084887748655208949L;
    private BaoZi baoZi;

    public Customer() {
    }

    public Customer(String name, BaoZi baoZi) {
        super(name);
        this.baoZi = baoZi;
    }

    public BaoZi getBaoZi() {
        return baoZi;
    }

    public void setBaoZi(BaoZi baoZi) {
        this.baoZi = baoZi;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Customer customer = (Customer) o;
        return Objects.equals(baoZi, customer.baoZi);
    }

    @Override
    public int hashCode() {
        return Objects.hash(baoZi);
    }

    @Override
    public String toString() {
        return "Customer{" +
                "baoZi=" + baoZi +
                '}';
    }

    // 重写run方法
    @Override
    public void run() {
        // 保证一直在吃包子
        while (true){
          synchronized (baoZi){
              // 判断是否有包子
              if (!baoZi.isFlag()){
                  // 就让顾客等待
                  try {
                      baoZi.wait();
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }

              // 执行完if证明,厨师已经制作好包子
              System.out.println(this.getName() + "正在吃:" + baoZi.getSeek() +"的包子~~");
              try {
                  Thread.sleep(3000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println(this.getName() + "已经吃完,还要继续买包子~~~");
              System.out.println("-------------------------------------------------------");
              // 吃完包子
              baoZi.setFlag(false);

              // 唤醒厨师继续制作包子
              baoZi.notify();
          }
        }
    }
}

7.线程池

7.1 Executors工具类创建线程池

常用的方法:

-- 创建线程池对象
    static ExecutorService newCachedThreadPool() 
          创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。 
    static ExecutorService newFixedThreadPool(int nThreads) 
          创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。 
-- 提交线程任务
    <T> Future<T> submit(Callable<T> task) 
          提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 FutureFuture<?> submit(Runnable task) 
          提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future<T> Future<T> submit(Runnable task, T result) 
          提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future-- 关闭线程池
    void shutdown() 
          启动一次顺序关闭,执行以前提交的任务,但不接受新任务。 

代码示例:

public class Demo01 {
    public static void main(String[] args) {
        // 1.调用newCachedThreadPool()方法创建ExecutorService线程池对象
        ExecutorService executorService = Executors.newCachedThreadPool();

        // 2.创建线程,利用submit()方法提交线程
        for (int i = 0; i < 100; i++) {
            final int j = i;
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "java" + j);
                }
            });
        }

        // 3.关闭线程池shutdown()
        executorService.shutdown();
    }
}

7.2 自定义线程池对象

定义方式:

public ThreadPoolExecutor(
    int corePoolSize, // 核心线程的数量
    int maximumPoolSize, // 线程池容量 = 核心线程数量 + 临时线程的数量 
    long keepAliveTime,  // 临时线程的存活时间
    TimeUnit unit,  // 时间单位
    BlockingQueue<Runnable> workQueue, // 阻塞队列 :阻塞线程的队列
    ThreadFactory threadFactory, // 线程池工厂:为线程池提供对象
    RejectedExecutionHandler handler // 拒绝任务策略
) 

TimeUnit 类(时间单位)

  • 概述:表示给定单元粒度的时间段,它提供在这些单元中进行跨单元转换和执行计时及延迟操作的实用工具方法。TimeUnit 不维护时间信息,但是有助于组织和使用可能跨各种上下文单独维护的时间表示形式。毫微秒定义为千分之一微秒,微秒为千分之一毫秒,毫秒为千分之一秒,一分钟为六十秒,一小时为六十分钟,一天为二十四小时。

  • DAYS   
    HOURS   
    MICROSECONDS      
    MILLISECONDS      
    MINUTES       
    NANOSECONDS      
    SECONDS 
    

BlockingQueue 阻塞队列

  • 概述:BlockingQueue 方法以四种形式出现,对于不能立即满足但可能在将来某一时刻可以满足的操作,这四种形式的处理方式不同:第一种是抛出一个异常,第二种是返回一个特殊值(nullfalse,具体取决于操作),第三种是在操作可以成功前,无限期地阻塞当前线程,第四种是在放弃前只在给定的最大时间限制内阻塞。所有 BlockingQueue 都可用于传输和保持提交的任务。可以使用此队列与池大小进行交互:

    ①如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。

    ②如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。

    ③如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。

  • 排队的三种通用的策略:

    	1.直接提交。工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
        
    	2.无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。 
    
        3.有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。
    
  • 使用方式:

    -- 构造方法
       new ArrayBlockingQueue<String>(队列的长度); 
    -- 特殊的方法:
        E put(E e) : 向队列中添加元素,当队列中的元素已满,新元素添加不进去会阻塞。
        E take() : 获取队列中的元素,若队列没有元素,则会阻塞。
    

ThreadFactory线程工厂

  • 概述:根据需要创建新线程的对象。使用线程工厂就无需再手工编写对 new Thread 的调用了,从而允许应用程序使用特殊的线程子类、属性等等。

  • -- 获取线程工厂的方式一:
        class SimpleThreadFactory implements ThreadFactory {
            public Thread newThread(Runnable r) {
                return new Thread(r);
            }
        }
    -- 获取方式二:调用Executors工具类中的方法
        defaultThreadFactory() : 该方法提供了更有用的简单实现,即在返回线程前将已创建线程的上下文设置为已知的值
    

RejectedExecutionHandler 任务拒接策略

  • 概述:当 Executor 已经关闭,并且 Executor 将有限边界用于最大线程和工作队列容量,且已经饱和时,在方法 execute(java.lang.Runnable) 中提交的新任务将被拒绝。在以上两种情况下,execute 方法都将调用其 RejectedExecutionHandler 的 RejectedExecutionHandler.rejectedExecution(java.lang.Runnable, java.util.concurrent.ThreadPoolExecutor) 方法。

  • 四种预定义的处理程序策略:

    	1.在默认的 ThreadPoolExecutor.AbortPolicy 中,处理程序遭到拒绝将抛出运行时 RejectedExecutionException。用于被拒绝任务的处理程序,它将抛出 RejectedExecutionException.// 直接拒绝并抛出异常 
    
    	2.ThreadPoolExecutor.DiscardPolicy 中,不能执行的任务将被删除。用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。  // 直接拒绝并不报错
    
    	3.ThreadPoolExecutor.DiscardOldestPolicy 中,如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)。用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试 execute;如果执行程序已关闭,则会丢弃该任务。// 也就是随机的将阻塞队列中的一个线程任务移除队列,将队列外面等待时间最长的线程任务添加到队列当中。
    
    	4.ThreadPoolExecutor.CallerRunsPolicy 中,线程调用运行该任务的 execute 本身。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。用于被拒绝任务的处理程序,它直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务。//通俗的说请其它的线程对象,来完成该线程池完成不了的线程
    

创建线程池的演示:

public class Demo01 {
    public static void main(String[] args) {
        // 1.创建时间单位
        TimeUnit minutes = TimeUnit.MINUTES;
        // 2.创建阻塞队列
        ArrayBlockingQueue<Runnable> runnables = new ArrayBlockingQueue<Runnable>(3);
        // 3.拒绝任务策略
        RejectedExecutionHandler hanndler= new ThreadPoolExecutor.DiscardPolicy();
        // 4.线程池工厂
        ThreadFactory threadFactory = new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r);
            }
        };

        // 4.创建线程池对象
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                2, // 核心线程数量
                5,  // 线程池容量 = 核心线程数量 + 临时线程数量
                2, // 临时线程存活时间
                minutes, // 临时线程存活时间单位
                runnables,  // 阻塞队列
                threadFactory, // 线程池工厂
                hanndler // 拒绝策略
        );

        // 5.使用线程
        for (int i = 0; i < 10; i++) {
            final int j =i;
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "....." + j);
                }
            });
        }

        // 6.关闭线程池
        executor.shutdown();

    }
}

8.有关线程安全性相关的问题及解决方式

8.1 原子性操作产生的问题

原子性概述: 所谓的原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的 干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体。例如:int i = 1;该操作语句是不可再分割的语句,其具有原子性。

原子性操作在多线程中所产生的问题:

当有两个线程A、B同时操作一个共享数据时,当线程A操作共享数据时,而此时B没有得到及时的更新就会出现问题。

1.共享数据存储在堆中,而两个线程分别都开辟了属于自己的栈内存。堆内存是唯一的。

2.每一个线程在使用共享数据时,都会从堆中拷贝一份数据到自己的栈中。

3.此时线程若访问变量就首先会访问自己栈中所拷贝的数据。

4.若此时某一个线程修改的共享数据,而另一个线程只是查看自己拷贝的数据,并没有去访问共享的数据,则会出现线程安全问题。

案例:假设有一对情侣结婚,彩礼有十万块,钱存在银行。男生偷偷取走1万,女生多久会发现

public class Demo01 {
    public static void main(String[] args) {
        Money money = new Money();
        Boy boy = new Boy("男孩", money);
        Girl girl = new Girl("女孩", money);

        boy.start();
        girl.start();
    }
}

// 彩礼
class Money{
     int money = 100000;
}

// 1.创建男孩线程
class Boy extends Thread{
    private Money money;

    public Boy() {
    }
    public Boy(String name, Money money) {
        super(name);
        this.money = money;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 拿走一万,原子性操作
        money.money = 90000;
    }
}

// 2.创建女孩线程
class Girl extends Thread{
    private Money money;

    public Girl() {
    }
    public Girl(String name, Money money) {
        super(name);
        this.money = money;
    }

    @Override
    public void run() {
        // 一直查找
        while (money.money == 100000){}

        // 循环结束,发现金额减少
        System.out.println("结婚基金已经不是十万了");
    }
}

结论: 女孩虽然知道结婚基金是十万,但是当基金的余额发生变化的时候,女孩无法知道最新的余额。

解决问题的方式一:同步代码块、锁机制解决

public class Demo02 {
    public static void main(String[] args) {
        Money money = new Money();
        Boy boy = new Boy("男孩", money);
        Girl girl = new Girl("女孩", money);

        boy.start();
        girl.start();
    }
}

// 彩礼
class Money{
    public static Object lock = new Object();
    int money = 100000;
}

// 1.创建男孩线程
class Boy extends Thread{
    private Money money;

    public Boy() {
    }
    public Boy(String name, Money money) {
        super(name);
        this.money = money;
    }

    @Override
    public void run() {
        synchronized (Money.lock){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 拿走一万,原子性操作
            money.money = 90000;
        }
    }
}

// 2.创建女孩线程
class Girl extends Thread{
    private Money money;

    public Girl() {
    }
    public Girl(String name, Money money) {
        super(name);
        this.money = money;
    }

    @Override
    public void run() {
       synchronized (Money.lock){
           // 一直查找
           while (money.money == 100000){}

           // 循环结束,发现金额减少
           System.out.println("结婚基金已经不是十万了");
       }
    }
}

解决方式二:volatile关键字解决

Volatile关键字 :

强制线程每次在使用的时候,都会看一下共享区域最新的值

或是说

被修饰的变量,线程对象在使用时一定是取用共享区域中的真实值而不是自己线程栈中的临时值;

public class Demo03 {
    public static void main(String[] args) {
        Money money = new Money();
        Boy boy = new Boy("男孩", money);
        Girl girl = new Girl("女孩", money);

        boy.start();
        girl.start();
    }
}

// 彩礼
class Money{
    // 在共享数据上添加volatile关键字
    volatile int money = 100000;
}

// 1.创建男孩线程
class Boy extends Thread{
    private Money money;

    public Boy() {
    }
    public Boy(String name, Money money) {
        super(name);
        this.money = money;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 拿走一万,原子性操作
        money.money = 90000;
    }
}

// 2.创建女孩线程
class Girl extends Thread{
    private Money money;

    public Girl() {
    }
    public Girl(String name, Money money) {
        super(name);
        this.money = money;
    }

    @Override
    public void run() {
        // 一直查找
        while (money.money == 100000){}

        // 循环结束,发现金额减少
        System.out.println("结婚基金已经不是十万了");
    }
}

8.2 非原子性操作可能产生的问题

非原子性概述: 可以分割的代码块,比如自增、自减运算。

非原子性操作可能产生的问题:

当有两个线程A、B同时操作一个共享数据时,当线程A,B同时操作共享数据时,就会发生如下过程:

1.共享数据存储在堆中,而两个线程分别都开辟了属于自己的栈内存。堆内存是唯一的。

2.每一个线程在使用共享数据时,都会从堆中拷贝一份数据到自己的栈中。

3.当A线程取用数据时,将数据拷贝到自己的栈中,对数据进行操作。此时线程B也取用数据,对数据进行操作。

4.线程A操作完数据将结果数据返回给共享共享数据,而此时线程B操作的数据还是旧的数据,这时就出现线程安全问题。

案例:送冰淇凌问题,创建100个线程,每个线程送出100个冰淇淋

public class Demo {
    public static void main(String[] args) {
        //创建任务对象
        Target target = new Target();
        //创建100次线程对象 100个线程对象每个人送100次
        for (int i = 0; i < 100; i++) {
            new Thread(target).start();
        }
    }
}

class Target implements Runnable {
    private int count = 0; //送冰淇淋的数量

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            count++;//非原子性的操作
            System.out.println("已经送了" + count + "个冰淇淋");
        }
    }
}

在这里插入图片描述

解决方式一:同步代码块、锁机制

public class Demo {
    public static void main(String[] args) {
        //创建任务对象
        Target target = new Target();
        //创建100次线程对象 100个线程对象每个人送100次
        for (int i = 0; i < 1000; i++) {
            new Thread(target).start();
        }
    }
}
class Target implements Runnable{
    private int count = 0; //送冰淇淋的数量
    private Object obj = new Object();//锁对象
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            //解决方案1: 对原子性的操作进行上锁
            synchronized (obj){
                count++;//非原子性的操作
                System.out.println("已经送了" + count + "个冰淇淋");
            }
        }
    }
}

解决方式二:CAS算法和自旋

在这里插入图片描述

非原子性操作的类:

非原子性操作的类 : atomic包下的一些非原子性操作的类 完成 非原子性在多线程环境下的安全问题;
这些类的内部实现类 : CAS算法和自旋操作 --> 规避了不安全的情况
使用原子性类:AtomicXxxxx:
	概述:Java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。(CAS算法 + 自旋)
	因为变量(共享数据)的类型有很多种,所以在Atomic包里一共提供了13个类,属于4种类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性(成员变量)。

本次我们只讲解使用原子的方式更新基本类型,使用原子的方式更新基本类型Atomic包提供了以下3个类:
1. AtomicBoolean:原子更新布尔类型
2. AtomicInteger:原子更新整型
3. AtomicLong:原子更新长整型

AtomicInteger:原子更新整型

构造方法
	AtomicInteger() : 创建一个初始值为0AtomicInteger对象,默认值是 0
	AtomicInteger(int value) :创建一个初始值为value的AtomicInteger对象
成员方法
	int get(): 获取值
	int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。b=a++; //b是返回值
	int incrementAndGet():以原子方式将当前值加1,注意,这里返回的是自增后的值。b=++a;//b是返回值
	int addAndGet(int data):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。 //a += data;
	int getAndSet(int value):以原子方式设置为newValue的值,并返回旧值。

解决上述案例出现的问题:

public class Demo01 {
    public static void main(String[] args) {
        //创建任务对象
        Target target = new Target();
        //创建100次线程对象 100个线程对象每个人送100次
        for (int i = 0; i < 1000; i++) {
            new Thread(target).start();
        }
    }
}
class Target implements Runnable{
    //private volatile int count = 0; //送冰淇淋的数量
    private AtomicInteger count = new AtomicInteger();
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            //count++;//非原子性的操作
            count.incrementAndGet();
            System.out.println("已经送了" + count + "个冰淇淋");
        }
    }
}

8.3 乐观锁、悲观锁

乐观锁: 以乐观的角度看待线程安全问题,假设这个不会有线程同时修改数据,所以不加锁机制。在某个线程对共享数据进行修改时,我们只是需要检查当前数据是否又被其它线程更改过。如果有,则重新获取最新的值。若没有,则直接对共享数据进行修改。

**悲观锁: ** 就是以悲观的角度看待线程安全问题,假设所有的线程都有可能修改数据,所以强制添加锁的机制。每一次只能有一个线程对数据进行操作。

同步与CAS算法自旋的区别:

--相同点:在多线程情况下,都可以保证共享数据的安全性。
--不同点:
    synchronized总是从最坏的角度出发,认为每次获取数据的时候,别人都有可能修改。所以在每 次操作
共享数据之前,都会上锁。(悲观锁)
 	CAS是从乐观的角度出发,假设每次获取数据别人都不会修改,所以不会上锁。只不过在修改共享数据的时候,会
检查一下,别人有没有修改过这个数据。
	如果别人修改过,那么我再次获取现在最新的值。
	如果别人没有修改过,那么我现在直接修改共享数据的值.(乐观锁)

9.线程安全的类(并发工具类)

9.1 集合板块

单列集合 : Vector<E> : 同步方法完成线程安全的保障
双列集合 :
	Hashtable<K,V> : 同步方法完成线程安全的保障
		锁对象会把整张hash表全部锁住,当一个线程操作hash表,其他线程对象就不可以操作
	ConcurrentHashMap<K,V> : 同步方法完成线程安全的保障
		锁对象不会锁整张表,而是锁上一个hash表的索引位置;
使用集合在多线程场景下:
单列集合 Vector<E>
双列集合 ConcurrentHashMap<K,V>

9.2 CountDownLatch线程中的计数器

常用的方法:

-- 构造方法 :
	public CountDownLatch(int count) : 传入几就代表要等待几个线程对象
-- 成员方法 :
	void await() : 无限等待 //如果计数器不是0,那么就一直等待
	void countDown() :递减锁存器的计数 每调用一次countDown 那么计数器就减一
					//当计数器减到0的时候,会自动唤醒在等待的线程

代码演示:

// 三个孩子吃饺子,吃完了,妈妈洗碗的过程。
public class Demo05 {
    public static void main(String[] args) {
        //创建线程对象,创建计数器对象
        CountDownLatch count = new CountDownLatch(3);
        ChildThread1 thread1 = new ChildThread1("金吒",count);
        ChildThread2 thread2 = new ChildThread2("木吒",count);
        ChildThread3 thread3 = new ChildThread3("哪吒",count);
        MotherThread motherThread = new MotherThread("李夫人",count);
        //启动线程
        motherThread.start();
        thread1.start();
        thread2.start();
        thread3.start();
    }
}
//创建4个线程
class ChildThread1 extends Thread{
    //属性
    private CountDownLatch count;
    public ChildThread1(String name, CountDownLatch count) {
        super(name);
        this.count = count;
    }
    //编写线程任务
    @Override
    public void run() {
        //孩子线程吃饺子
        for (int i = 1; i <= 20; i++) {
            System.out.println(getName() + " 正在吃第 " + i + " 个饺子");
        }
        //吃完说一声
        count.countDown();//计数器-1
    }
}
class ChildThread2 extends Thread{
    //属性
    private CountDownLatch count;
    public ChildThread2(String name, CountDownLatch count) {
        super(name);
        this.count = count;
    }
    //编写线程任务
    @Override
    public void run() {
        //孩子线程吃饺子
        for (int i = 1; i <= 10; i++) {
            System.out.println(getName() + " 正在吃第 " + i + " 个饺子");
        }
        //吃完说一声
        count.countDown();//计数器-1
    }
}
class ChildThread3 extends Thread{
    //属性
    private CountDownLatch count;
    public ChildThread3(String name, CountDownLatch count) {
        super(name);
        this.count = count;
    }
    //编写线程任务
    @Override
    public void run() {
        //孩子线程吃饺子
        for (int i = 1; i <= 15; i++) {
            System.out.println(getName() + " 正在吃第 " + i + " 个饺子");
        }
        //吃完说一声
        count.countDown();//计数器-1
    }
}
//妈妈线程
class MotherThread extends Thread{
    //属性
    private CountDownLatch count;
    public MotherThread(String name, CountDownLatch count) {
        super(name);
        this.count = count;
    }
    //线程任务
    @Override
    public void run() {
        //等待
        try {
            count.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //代码能下来说明 计数器归零
        System.out.println(getName() + " 开始洗碗了~ ");
    }
}

9.3 Semaphore

常用的方法:

Semphore : 提供通行证的类 -> 可以控制同时执行的线程数量
构造方法 :
	Semaphore(int permits) //int permits: 通行证的数量
成员方法 :
	获取通行证
	void acquire()
	释放通行证
	void release()

使用场景 : 可以控制访问特定资源的线程数量。

通车案例:

实现步骤:

1,需要有人管理这个通道

2,当有车进来了,发通行许可证

3,当车出去了,收回通行许可证

4,如果通行许可证发完了,那么其他车辆只能等着

代码示例:

public class Demo06 {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();
        for (int i = 0; i < 100; i++) {
            new Thread(mr).start();
        }
    }
}
class MyRunnable implements Runnable {
    //1.获得管理员对象,
    private Semaphore semaphore = new Semaphore(2);
    @Override
    public void run() {
        //2.获得通行证
        try {
            semaphore.acquire();
            //3.开始行驶
            System.out.println("获得了通行证开始行驶");
            Thread.sleep(2000);
            System.out.println("归还通行证");
            //4.归还通行证
            semaphore.release();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到