【javaEE】——synchronized使用和单例模式(线程安全)03

目录

一、线程安全

1.1synchronized的使用

死锁的四个必要条件:

1.2Java 标准库中的线程安全类

1.3volatile 关键字

1.4 wai和notify

二、多线程案例

2.1 单例模式(懒汉、饿汉模式)

2.2 线程安全的单例模式(!!!!安全的懒汉模式(重要))

2.3 阻塞队列(生产者-消费者模型)

2.4  定时器

三、线程池


一、线程安全

操作系统中,由于线程的调度是随机性的(抢占式执行),由此带来了一些安全方面的问题。

 线程不安全的原因:

  • 1.线程之间的抢占式执行(无法确定顺序,具有随机性)
  • 2.多个线程 修改 同一个变量(规避:一个线程只改一个变量)
  • 3.原子性:操作只有一个步骤,多步骤互相排斥,加锁即保证了原子性(同步互斥,不能同时进行两个操作,A作业时B不能进行打断)
  • 4.内存可见性(和原子性类似):一个线程对共享变量值的修改,能够及时地被其他线程看到.(一个线程频繁读,另一个频繁写数据,则会存在可见性问题)
  • 指令重排序:代码执行顺序也会影响线程安全

给方法直接加synchronized关键字进行加锁,进入此方法自动加锁,离开方法,自动解锁,便可保证下面代码自增结果始终为100000

当一个线程加锁成功,其他线程尝试枷锁便会出发阻塞等待(BLOCKED),持续到占用所得线程释放锁为止。

class Counter {
    public int count;
    synchronized public void increase() {
        count++;
    }
}

public class thread {
    private static Counter counter = new Counter();  //创建一个实例counter

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() ->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() ->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();

        t1.join();  //进行一个线程等待(俩线程执行完毕,再打印)
        t2.join();

        System.out.println(counter.count); //100000
    }
}

执行计数器++,的步骤:

  • 1.先把内存中count的值加载到cpu寄存器中
  • 2.再给寄存器中的值+1
  • 3.再把寄存器的值写回到count种

synchronized可保证原子性、可见性、有序但不能禁止指令重排;

volatile保证可见性、禁止指令重排但不保证原子性

此处的有序并不代表指令重排序


1.1synchronized的使用

Java中每个类都是继承自Object,new出来的实例包含了自己安排的属性,也包含了“对象头”,对象的一些元数据。(加锁操作也就是给对象头里设置标志位)。两个线程针对同一个变量进行加锁才具有竞争,若是不同变量则不需竞争

  • 进入 synchronized 修饰的代码块, 相当于 加锁
  • 退出 synchronized 修饰的代码块, 相当于 解锁

监视器锁:monitor lock

  • 1.直接修饰普通方法:(针对某个对象(this)加锁)
    synchronized public void increase() {
        count++;
    }
  • 2.修饰一个代码块:需要显式指定那个对象加锁,(java中任意对象都可作为锁对象)
    public void increase() {
        synchronized(this) {  //锁对象需要手动指定
            count++;
        }
    }
  • 3.修饰一个静态方法:对类对象进行加锁
    public static void func() {
        synchronized (Counter.class) {  //针对类对象(Counter是类名)进行加锁
 
        }
    }

连续进行两次锁,则会造成死锁的状态:

  • 外层锁:进入方法则开始枷锁,加锁成功,因为当前锁无占用
  • 内层锁:进入代码块,开始加锁,加锁失败(外层占用锁,需要其释放才可加锁)
    synchronized public void increase() {
        synchronized(this) {  //锁对象需要手动指定
            count++;
        }
    }

 为了解决死锁的问题,引入了可重入锁(记录当前锁被占用的线程,记录一个加锁次数)

 在可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息。

  • 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.;
  • 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)
static class Counter {
    public int count = 0;
    synchronized void increase() {
        count++;
    }
    synchronized void increase2() {
        increase();
    }
}

死锁的四个必要条件:

  • 互斥使用:一个锁被占用后,其他线程不能占用(原子性)
  • 不可抢占:一个所被占用后,其他线程不能抢走
  • 请求和保持:一个线程占据多把锁之后,除非进行显式的释放锁,否则锁只能被该线程持有
  • 环路等待:等待关系(实际中,要尽量避免出现循环等待,给固定的顺序,就可避免死锁)

1.2Java 标准库中的线程安全类

Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施:

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • StringBuilder

 线程安全的 :

  • Vector (不推荐使用)
  • HashTable (不推荐使用)
  • ConcurrentHashMap
  • StringBuffer  (StringBuffer 的核心方法都带有 synchronized)
  • String(线程安全,非synchronized,是不可变对象(没有提供public 的修改属性操作)),不可变对象和常量/final没有联系

1.3volatile 关键字

volatile 修饰的变量, 能够保证 "内存可见性".。不会引起线程阻塞。

代码在写入 volatile 修饰的变量的时候:

  • 改变线程工作内存中volatile变量副本的值
  • 将改变后的副本的值从工作内存刷新到主内存

代码在读取 volatile 修饰的变量的时候:

  • 从主内存中读取volatile变量的最新值到线程的工作内存中
  • 从工作内存中读取volatile变量的副本

直接访问工作内存(实际是 CPU 的寄存器或 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况。加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了.(缓存是介于CPU和寄存器之间的,空间比寄存器大,速度比CPU快)

static class Counter {
    public int flag = 0;
}
public static void main(String[] args) {
    Counter counter = new Counter();
    Thread t1 = new Thread(() -> {
    while (counter.flag == 0) {
        // do nothing
    }
    System.out.println("循环结束!");
    });

    Thread t2 = new Thread(() -> {
    Scanner scanner = new Scanner(System.in);
    System.out.println("输入一个整数:");
    counter.flag = scanner.nextInt();
});
    t1.start();
    t2.start();
}
// 执行效果
// 当用户输入非0值时, t1 线程循环不会结束. (这显然是一个 bug)

t1 读的是自己工作内存中的内容;当 t2 对 flag 变量进行修改, 此时 t1 感知不到 flag 的变化.

1.4 wai和notify

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序具有随机性。

wai和notify都是 Object 对象

wait 做的事情:

  • 释放当前的锁
  • 使当前执行代码的线程进行等待. (把线程放到等待队列中)
  • 满足一定条件时被唤醒, 重新尝试获取这个锁.
public class waitnotifyDemo {
    public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        synchronized (object) {   //加入锁,object为锁对象。则不会出现IllegalMonitorStateException非法锁异常的结果
            System.out.println("wait等待前");
            object.wait();
            System.out.println("wait等待后");
        }
    }
}

notify 方法是唤醒等待的线程:

  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
  • 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。

使用notify()方法唤醒线程:

public class waitnotifyDemo {
    private static Object locker = new Object();  //创建一个锁对象
   public static void main(String[] args) throws InterruptedException {
       Thread t1 = new Thread(() ->{
           //进行wait
           synchronized (locker) {
               try {
                   System.out.println("wait 之前");
                   locker.wait();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println("wait 之后");
           }
       });
       t1.start();
       Thread.sleep(1000);

       Thread t2 = new Thread(() ->{
           //进行notify
           synchronized (locker) {
               System.out.println("notify 之前");
               locker.notify();
               System.out.println("notify 之后 ");
           }
       });
       t2.start();
   }
}

notify 和 notifyAll:

notify 只唤醒等待队列中的一个线程. 其他线程还是乖乖等着;notifyAll 一下全都唤醒, 需要这些线程重新竞争锁

wait 和 sleep 的对比(面试题):

wait 和 sleep 完全是没有可比性的,因为wait是用于线程之间的通信的,sleep是让线程阻
塞一段时间,唯一的相同点就是都可以让线程放弃执行一段时间。

  • 1. wait 需要搭配 synchronized 使用. sleep 不需要.
  • 2. wait 是 Object 的方法 sleep 是 Thread 的静态方法.

二、多线程案例

2.1 单例模式(懒汉、饿汉模式)

单例模式:设计模式(有固定的套路,是在基础框架上进行优化)之一,可保证某个类在程序中只存在一个实例(不会创建多个实例)。这一点在很多场景上都需要. 比如 JDBC 中的 DataSource 实例就只需要一个.

(1)饿汉模式:类加载的同时, 创建实例.

  • static 修饰的成员,叫做“类成员”==>类属性/类方法 
  • 不加static修饰的成员,是“实例成员”==>实例属性 / 实例方法

Java程序中,一个类对象只存在一份,故而保证了类的static 成员也只有一个

类对象 !=  对象  

类对象(类名.class):就是.class文件被JVM加载到内存之后表现出来的形式

类:相当于实例的模板,类中可以包含很多个对象(实例).

对象(实例):new对象

class Singleton {
    // 1.使用static创建一个实例,并立即进行实例化New 操作
    // instance 对应的实例是该类唯一的实例
    private static Singleton instance = new Singleton();  //创建一个类成员instance(类加载的时候就创建实例:饿汉模式)
// 2.把构造方法设为私有的,防止其他操作中不小心new 到singleton(饿汉模式只能new一次)
    private Singleton() {}
    //提供一个方法确保外面能拿到唯一的实例(私有属性提供公开接口则外部可以进行调用)
    public static Singleton getInstance() {
        return instance;
    }
}

public class singleTon {
    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();
//        Singleton instance2 = new Singleton();  //不能进行编译,因为static修饰的类对象 只能有唯一的一个实例
    }
}

(2)懒汉模式:类加载的时候不创建实例. 第一次使用的时候才创建实例.(调用getInstance才创建实例);此模式中,getInstance只读取了变量的内容,若多个线程只读取同一变量不进行修改,则线程是安全的。

//懒汉模式:非立即实例化,需要时再实例化
class Singleton2 {
    private static Singleton2 instance = null;  //不用实例化
    private Singleton2() {}  //构造方法私有
    public static Singleton2 getInstance() {  //提供接口供外部调用
        if (instance == null) {
            instance = new Singleton2();  //需要使用实例时再创建实例
        }
        return instance;
    }
}

public class singtonDemo2 {
    public static void main(String[] args) {
        Singleton2 instance = Singleton2.getInstance();
    }
}

2.2 线程安全的单例模式(!!!!安全的懒汉模式(重要))

真正需要解决问题的是需要实现线程安全的模式。在上述模式中,懒汉模式涉及到读写操作,存在安全问题。加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候. 因此后续使用的时候, 不必再进行加锁了.

线程安全版本的单例模式:

  • 1.饿汉模式:类加载时就创建(线程安全的,只涉及读操作)
  • 2.懒汉模式:首次调用实例才创建(非安全,体现在首次调用,若多个线程同时调用则不安全,需加锁,并只在首次调用加锁)

 只需给懒汉模式加锁即可保证线程安全:

线程未初始化之前,多线程调用getInstance涉及到读写操作(不安全),而初始化之后(instance != null,if 条件不成立 )getInstance操作只剩下读操作(不存在线程安全的问题,故不需要再加锁),故加锁之前应该有前提条件

同时给实例instance加入volatile保证内存可见性(避免多次读取时,数据的错误),


//懒汉模式:非立即实例化,需要时再实例化
class Singleton2 {
    private static volatile Singleton2 instance = null;  //不用实例化,但要加入内存可见性
    private Singleton2() {}
    public static Singleton2 getInstance() {
        if (instance == null) {   //在未初始化之前进行加锁,若初始化了,只需进行读操作(不存在安全问题)
            synchronized (Singleton2.class) {  //指定锁对象(类名.class)
                if (instance == null) {
                    instance = new Singleton2();  //需要使用实例时再创建实例
                }
            }
        }
        return instance;  //返回instance(也即是读操作,不进行修改)
    }
}

public class singtonDemo2 {
    public static void main(String[] args) {
        Singleton2 instance = Singleton2.getInstance();
    }
}

当多线程首次调用 getInstance, 发现 instance 为 null, 于是继续往下执行来竞争锁,其中竞争成功的线程, 再完成创建实例的操作;当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例.


2.3 阻塞队列(生产者-消费者模型)

阻塞队列(线程安全)是一种特殊的队列. 遵守 "先进先出" 的原则.

  • 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
  • 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.

阻塞队列的一个典型应用场景就是 "生产者消费者模型"(典型的开发模型).

开发中的阻塞队列不是一个简单的数据结构,而是一个/一组专门的服务器程序,它在阻塞队列的基础上提供更多的功能(数据持久化存储、支持多个数据通道、多节点容灾冗余备份、管理面板、方便配置参数...),也称为“消息队列”。

生产者消费者模型:通过一个容器(中间场所)来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.阻塞队列只是单纯的存储数据,不参与计算,因此抗压能力较强

  • 1) 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力.(削峰填谷)
  • 2) 阻塞队列也能使生产者和消费者之间 解耦(解耦合)

(1)Java标准库中的阻塞队列

  • BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
  • put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
  • BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.
 public static void main(String[] args) throws InterruptedException {
    BlockingDeque<String> queue = new LinkedBlockingDeque<>();
    queue.put("hello");
    String str = queue.take();
}

(2)生产者消费者模型代码实现(队列+线程安全+阻塞)

队列可以基于数组和链表实现,此处基于数组实现更为简单(队尾进 ,对头出)

 实现循环队列??如何区分空?还是满?:
1.浪费一个空间
插入:tail+1=head:满
删除:tail=head:空
2. 创建一个变量size,记录当前元素个数(常用方法)
出:size--  ,size=0时为空
入:size++,size=array.length满

代码实现:先实现出入功能,再添加锁、最后实现阻塞(入put的阻塞是队列满了,出take的阻塞是队列为空),针对那个对象,就对那个对象wait

import java.util.Queue;

class blockQueue {
    private int[] data = new int[1000];
    private int size = 0;  //有效元素个数
    private int head = 0;  //头下标
    private int tail = 0;   //尾下标
    private Object locker = new Object(); // 创建一个专门的锁对象

    public void put(int value) throws InterruptedException {  //入队列
        synchronized (locker) {
            if(size == data.length) {
                locker.wait();  //,针对那个对象,就对那个对象wait(此处的等待由take中唤醒,也即是出队列之后,就可放入元素)
//                return;
            }
            data[tail] = value;
            tail++;
            if (tail >=data.length) { // 当达到数组的容量进行循环操作(从头开始)
                tail = 0;
            }
            size++;
            locker.notify();   //唤醒take中的阻塞等待
        }
    }

    public Integer take() throws InterruptedException {  //出队列
        synchronized (locker) {  //保证线程安全(此处锁对象用this或者locker都可以)
            if (size == 0) {
                locker.wait();  //针对那个对象,就对那个对象wait
//                return null; //队列为空,返回一个非法值(int返回类型不能是null)
            }
            int ret = data[head];
            head++;
            if (head >= data.length) {
                head = 0;
            }
            size--;
            locker.notify();  //唤醒put中等待的对象this
            return ret;
        }
    }
}

public class myBlockQueue {
    public static void main(String[] args) throws InterruptedException {
        //实现简单的生产者消费者模型
        blockQueue queue = new blockQueue();
        Thread producer = new Thread(() ->{
            int num = 0;
            while (true) {
                System.out.println("生产数量:"+num);
                try {
                    queue.put(num);
                    num++;
                    Thread.sleep(1000);  //当生产者速度慢了,消费者也得跟着生产者的速度进行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();

        Thread customer = new Thread(() ->{
            while (true) {
                int num = 0;
                try {
                    num = queue.take();
                    System.out.println("消费数量:"+num);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        customer.start();
    }
}

2.4  定时器

(1)标准库中的定时器

标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule ( 包含两个参数. 第一个参数:指定即将要执行的任务代码, 第二个参数:指定时间 /单位:ms )

Timer 内部有专门的线程,负责执行注册的任务:

  • 描述任务:创建一个专门的类来表示定时器中的任务(TimerTask)
  • 组织任务:使用一定的数据结构把任务放到一起,进行组织(使用堆stack 的数据结构)
  • 执行时间到了的任务

import java.util.Timer;
import java.util.TimerTask;

public class timeDemo {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello timer");
            }
        },3000);
        System.out.println("main");
    }
}

(2)定时器的构成

  • 描述一个任务:runnable + time
  • 一个带优先级的阻塞队列  PriorityBlockingQueue

因为阻塞队列中的任务都有各自的执行时刻 (delay). 最先执行的任务一定是 delay 最小的. 使用带优先级的队列就可以高效的把这个 delay 最小的任务找出来.

  • 队列中的每个元素是一个 Task 对象.
  • Task 中带有一个时间属性, 队首元素就是即将执行的任务
  • 同时有一个 worker 线程一直扫描队首元素, 看队首元素是否需要执行

import java.util.concurrent.PriorityBlockingQueue;
class MyTask {   //创建一个任务类MyTask(定义好任务和时间,构造函数、执行方法run、和获取时间的公开接口)
    private Runnable runnable; //指代任务
    private long time;     //时间
    public MyTask(Runnable runnable, long after) {  //构造函数:快捷键alt+insert
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + after;   //设置一个绝对时间,超过此时间就可执行任务
    }
    public void run() {
        runnable.run();
    }
    public long getTime() {
        return time;
    }
}

//创建一个定时器类 ,内部包含一个带优先级的阻塞队列queue,和schedule方法,用于存放任务和时间
// 以及 类的构造函数 (创建线程,如何执行任务)
class myTimer {
    //定时器内部需要存放多个任务
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    public void schedule(Runnable runnable,long delay) {   // schedule的主要任务是往阻塞队列中插入元素
        MyTask task = new MyTask(runnable,delay);   //task描述一个任务,包含任务内容和时间
        queue.put(task);
    }

    public myTimer() {  //构造函数
        Thread t = new Thread(() ->{    //创建一个线程
            while (true) {
                try {
                    MyTask task = queue.take();  //1.取队首元素
                    long curTime = System.currentTimeMillis();
                    if(curTime < task.getTime()) {  //时间未到.把任务塞回到阻塞队列
                        queue.put(task);
                    }else {
                        task.run(); // 时间到了,执行任务
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}
public class classdemo {
    public static void main(String[] args) {
        myTimer mytimer = new myTimer();
        mytimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello timer");
            }
        },3000);
        System.out.println("hello main");
    }
}

(1)问题1:以上代码中,MyTask没有指定比较规则,导致报错:

优先级队列要进行比较Comparable

 改进后的代码:

import java.util.concurrent.PriorityBlockingQueue;
class MyTask implements Comparable<MyTask>{    //实现Comparable接口
    private Runnable runnable; //指代任务
    private long time;     //时间
    public MyTask(Runnable runnable, long after) {  //构造函数:快捷键alt+insert
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + after;   //设置一个绝对时间,超过此时间就可执行任务
    }
    public void run() {
        runnable.run();
    }
    public long getTime() {
        return time;
    }

    @Override
    public int compareTo(MyTask o) {   //添加比较方法即可
        return (int) (this.time - o.time);   //时间小的在前
    }
}

(2)问题2:循环若不加任何限制,执行会非常快:只需指定一下等待时间即可解决wait(time),time时间到达后会自动唤醒。

比如第一个任务设定的是 1 min 之后执行某个逻辑. 但是这里的 while (true) 会导致每秒钟访问队
首元素几万次. 而当前距离任务执行的时间还有很久呢.(造成资源浪费,没必要)

此处指定等待时间,只能用wait(可被唤醒),sleep不能被中途唤醒;同时,修改 MyTimer 的 schedule 方法, 每次有新任务(马上执行)到来时唤醒一下 worker 线程.(synchronized代码块)

class myTimer {
    //定时器内部需要存放多个任务
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    public void schedule(Runnable runnable,long delay) {   // schedule的主要任务是往阻塞队列中插入元素
        MyTask task = new MyTask(runnable,delay);   //task描述一个任务,包含任务内容和时间
        queue.put(task);
//每次插入任务成功后,都唤醒扫描线程一下,检查队首的任务是否要执行了(若未到,则计算等待时间)
        synchronized (locker) {
            locker.notify();
        }
    }

    private Object locker = new Object();
    public myTimer() {  //构造函数
        Thread t = new Thread(() ->{    //创建一个线程
            while (true) {
                try {
                    MyTask task = queue.take();  //1.取队首元素
                    long curTime = System.currentTimeMillis();
                    if(curTime < task.getTime()) {  //时间未到.把任务塞回到阻塞队列
                        queue.put(task);
                        synchronized (locker) {   //指定一个等待时间,时间到达后自动唤醒
                            locker.wait(task.getTime() -curTime);
                        }
                    }else {
                        task.run(); // 时间到了,执行任务
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

整体代码:


import java.util.concurrent.PriorityBlockingQueue;
class MyTask implements Comparable<MyTask>{   //创建一个任务类MyTask(定义好任务和时间,构造函数、执行方法run、和获取时间的公开接口)
    private Runnable runnable; //指代任务
    private long time;     //时间
    public MyTask(Runnable runnable, long after) {  //构造函数:快捷键alt+insert
        this.runnable = runnable;
        this.time = System.currentTimeMillis() + after;   //设置一个绝对时间,超过此时间就可执行任务
    }
    public void run() {
        runnable.run();
    }
    public long getTime() {
        return time;
    }

    @Override
    public int compareTo(MyTask o) {
        return (int) (this.time - o.time);   //时间小的在前
    }
}

//创建一个定时器类 ,内部包含一个带优先级的阻塞队列queue,和schedule方法,用于存放任务和时间
// 以及 类的构造函数 (创建线程,如何执行任务)
class myTimer {
    //定时器内部需要存放多个任务
    private PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    public void schedule(Runnable runnable,long delay) {   // schedule的主要任务是往阻塞队列中插入元素
        MyTask task = new MyTask(runnable,delay);   //task描述一个任务,包含任务内容和时间
        queue.put(task);
//每次插入任务成功后,都唤醒扫描线程一下,检查队首的任务是否要执行了(若未到,则计算等待时间)
        synchronized (locker) {
            locker.notify();
        }
    }

    private Object locker = new Object();
    public myTimer() {  //构造函数
        Thread t = new Thread(() ->{    //创建一个线程
            while (true) {
                try {
                    MyTask task = queue.take();  //1.取队首元素
                    long curTime = System.currentTimeMillis();
                    if(curTime < task.getTime()) {  //时间未到.把任务塞回到阻塞队列
                        queue.put(task);
            //  此处synchronized代码块是避免盲等,造成资源浪费
                        synchronized (locker) {   //指定一个等待时间,时间到达后自动唤醒
                            locker.wait(task.getTime() -curTime);
                        }
                    }else {
                        task.run(); // 时间到了,执行任务
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}


public class classdemo {
    public static void main(String[] args) {
        myTimer mytimer = new myTimer();
        mytimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello timer");
            }
        },3000);
        System.out.println("hello main");
    }
}

三、线程池

  • 因为进程的频繁创建销毁开销大,引入进程池或线程解决。
  • 而线程虽比进程轻量,到那时创建销毁频率过大,开销仍然不小,故引入线程池或协程解决  

把线程提前创建好,放入池子(用户态实现),需要时直接取就不必再申请了,而线程用完之后不用销毁,放回池子便于下次继续使用。线程池最大的好处就是减少每次启动、销毁线程的损耗

创建销毁涉及用户态和内核态的切换(切换到内核态,创建出对应的PCB);若只是在池里的操作,则只是在用户态完成操作(更高效)

  • 1.若任务场景比较稳定则临时工少一点(主要为核心线程数)
  • 2.若任务场景波动较大,则可多设置一些临时工(不需要时销毁即可)

自己写的代码,一般都是在应用程序中应用,常称为”用户态“代码。纯用户态的操作效率比内核态处理的操作效率更高。(进入和内核态则不可控,效率受到一定的影响)

(1)标准库中的线程池

  • 使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
  • 返回值类型为 ExecutorService
  • 通过 ExecutorService.submit 可以注册一个任务到线程池中.

关于线程池的常用参数:

  • BlockingQueue<Runnable>  workQueue 阻塞队列(任务队列): 组织线程池要执行的任务
  • ThreadFactory threadFactory:  线程工厂,设置不同线程的创建方式
  • RejectedExecutionHandler handler:  拒绝策略(队列满了之后如何操作(忽略新任务?阻塞等待?丢弃旧任务?))

线程池中线程 的个数如何设置????(应通过性能测试,找到合适的值 且CPU占用也合理的平衡点)

线程越多,整体的运行速度越快,但CPU占用也越高(反之CPU占用越低),但是平时应用过程应保持CPU占用留有冗余空间,以应对突发情况

Executors 本质上是 ThreadPoolExecutor 类的封装.

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

public class threadpoolDemo {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(10);  //创建一个固定线程数目的线程池,指定线程个数
//        Executors.newCachedThreadPool();  //创建一个会自动扩容的线程池
//        Executors.newSingleThreadExecutor();  //创建只有一个线程的线程池
//        Executors.newScheduledThreadPool();  //创建一个带有定时器功能的线程池
        for (int i = 0; i < 50; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello ThreadPool");
                }
            });
        }
    }
}

(2)实现线程池

  •  描述任务:使用 Worker 类描述一个工作线程. 使用 Runnable 描述一个任务.
  • 组织任务:BlockingQueue 组织所有的任务
  • 描述工作线程:  工作线程不停的从 BlockingQueue 中取任务并执行.
  • 组织线程: 指定一下线程池中的最大线程数 maxWorkerCount; 当当前线程数超过这个最大值时, 就不再新增线程了.
  • 实现线程:往线程池里添加任务(submit·)
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

class MyThreadPool {
    //描述一个任务直接使用Runnable
    // 使用一个数据结构队列queue来组织若干个任务
    private BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();
    static class worker extends Thread {  //描述一个工作线程,其任务是从任务队列中取任务并执行
        private BlockingDeque<Runnable> queue = null;
        public worker (BlockingDeque<Runnable> queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    Runnable runnable = queue.take();  //获取任务
                    runnable.run();  //执行任务
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    private List<Thread> workers = new ArrayList<>();  //创建一个数据结构组织若干个线程

    public MyThreadPool(int n) {   //在构造方法中创建若干个线程,放到上述数组workers中
        for (int i = 0; i < n; i++) {
            worker worker = new worker(queue);
            worker.start();
            workers.add(worker);
        }
    }

    public void submit(Runnable runnable) {   //创建一个方法,允许程序员往线程池放任务
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class threadpoolDemo1 {
    public static void main(String[] args) {
        MyThreadPool pool = new MyThreadPool(10);
        for (int i = 0; i < 50; i++) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello threadpool");
                }
            });
        }
    }
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值