多线程编程基础

目录

一.概念

二.创建线程(Thread)

(1)继承Thread,重写run()

(2)实现Runnable接口

(3)使用匿名内部类,继承Thread

(4)使用匿名内部类,实现Runable

(5)使用Lambda表达式

三.Thread常见的类

(1)常见的构造方法

(2)Thread的常见属性

(3)中断线程

1.使用标志位来控制线程是否要停止。

2.使用Thread自带的标志位进行判定

(4)等待一个线程

(5)获取当前线程的引用 

四.线程的状态 

(1)观察线程的所有状态

 (2)线程状态转移

五.线程安全

(1) 出现线程安全问题的原因

(2)synchronized加锁

1.普通修饰方法

2.修饰代码块

3. synchronized特性

(3)死锁

​编辑

​编辑

(5)Java标准库中的线程安全类

(6)volatile关键字

 六.wait和notify

(1)wait

(2)notify:通知

七.多线程代码案例

(1)单例模式

1.饿汉模式

 2.懒汉模式

3.单例模式的线程安全模式(多线程)

(2)阻塞队列

1.生产者消费者模型

(3)定时器 

(4)线程池


一.概念

多进程编程已经可以解决并发编程的问题,但创建一个进程,消耗会比较大。(资源分配和回收)线程的出现让创建、销毁、调度的速度更快一些。

线程:轻量级进程

一个进程可以包含一个线程,也可以包含多个线程。只有第一个线程启动的时候开销比较大。同一个进程的多个线程之间,共用了进程的同一份资源(主要是内存和文件表述符)、

同一内存:线程1new的对象,在线程2,3,4里面都可以使用

同一文件描述符:线程1打开的文件在线程2,3,4里都可以使用

操作系统实际调度时,是以线程为单位进行调度的

出现安全问题:

线程模型,资源共享,多线程争抢同一资源容易触发。

进程模型,天然资源隔离,不容触发,进行进程间通信的时候,多进程访问同一个资源可能会出现问题。

二.创建线程(Thread)

Java中创建线程的写法有很多种

(1)继承Thread,重写run()

package thread;

import static java.lang.Thread.sleep;

class MyThread extends Thread {
    @Override
    public void run() {
        while (true) {
            System.out.println("hello Thread");
            try {
                Thread.sleep(1000);//使打印慢一些方便观察
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class ThreadDemo1 {
    public static void main(String[] args) {
        Thread t=new MyThread();
        t.start();
        //start里面没有调用run(),start是创建了一个线程,由新的线程来执行run()方法
        //主线程main()调用t.start(),创建出一个新的线程,新的线程调用run()
        //如果run()方法执行完毕,新的线程自然销毁
        while (true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

运行结果:

观察打印结果:操作系统调度线程的时候是“抢占式执行”,哪个线程先执行,哪个线程后执行不确定,取决于操作系统调度器的具体实现策略。

改成单线程:

 打印结果:

观察两次结果可知:没有调用start()就不能开启一个新的线程。

run()和start()的区别:

start()是真正创建了一个线程(从系统这里创建的),线程是独立的执行流。

run()只是描述了线程要做的事情,直接在main中调用run(),此时没有创建新线程,全程是main线程在执行。

可以用jdk自带的工具jconsole查看执行的进程,这里是本机的路径

 

 当前线程有许多,main和Thread0是我们在idea中创建的。其他的是jvm自带的线程。

(2)实现Runnable接口

package thread;
//Runnable作用是描述一个要执行的任务,run()方法就是任务的执行细节
class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("hello thread");
    }
}
public class ThreadDemo2 {
    public static void main(String[] args) {
        Runnable runnable=new MyRunnable();
        //这只是描述了一个任务
        Thread t=new Thread(runnable);//把任务交给线程来执行
        t.start();
    }
}

优点:

解耦合:目的就是为了让线程和线程要干的活之间分离开

未来如果要代码,不用多线程,使用多进程或者线程池,或者协程,此时代码改动比较小。

(3)使用匿名内部类,继承Thread

//使用匿名内部类来创建线程
public class ThreadDemo3 {
    public static void main(String[] args) {
        Thread t=new Thread(){

            @Override
            public void run() {
                System.out.println("hello");
            }
        };
        t.start();
    }
}

1)创建了一个Thread子类(子类没有名字)所以才叫做”匿名“

2)创建了子类的实例,并让t引用指向该实例

(4)使用匿名内部类,实现Runable

public class ThreadDemo4 {
    public static void main(String[] args) {
        Thread t=new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });
        t.start();
    }
}

这个代码本质和2相同,只不过是把实现Runnable任务交给匿名内部类的语法

此处是创建了一个类,实现Runnable,同时创建了类的实例,并且传给Thread的构造方法。

(5)使用Lambda表达式

推荐写法,也是最简单的写法

public class ThreadDemo5 {
    public static void main(String[] args) {
        Thread t=new Thread(()->{
            System.out.println("hello");
        });
        t.start();
    }
}

把任务用lambda表达式来描述,直接把lambda传给Thread构造方法。

lambda就是一个匿名函数(没有名字的函数),用一次就销毁。

三.Thread常见的类

(1)常见的构造方法

 Thread(String name)

public class ThreadDemo6 {
    public static void main(String[] args) {
        Thread t=new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println("hello");
                }
            }
        },"myThread");//创建的线程名字为myThread
        t.start();
    }
}

(2)Thread的常见属性

代码里面手动创建的线程,默认都是前台的,包括main默认也是前台的,其他的jvm自带的线程都是后台的。

isAlive()是在判断当前系统里面这个线程是不是真的有了:

在正真调用start()之前,调用isAlive就是false;调用start()之后,调用isAlive就是true。

如果内核里线程把run()执行完了,此时线程销毁,pcb随之释放,但是Thread这个对象不一定被释放,此时也是false。

xxx.start()会让内核创建一个PCB(),此时这个PCB才表示一个真正的线程。

 

如果t的run()还没有跑,isAlive()就是false;

如果t的run()正在跑,isAlive()就是true;

如果t的run()跑完了,isAlive()就是false;

注意:当调用了start()后,先执行run()还是下面的while()是不确定的,随机的。

(3)中断线程

中断的意思是,不是让线程立即就停止,而是通知线程应该要停止了。是否真的停止,取决于线程这里具体代码的写法。

1.使用标志位来控制线程是否要停止。

例:使用whlie(flag)来控制线程结束,当flag=false时,线程结束。

public class ThreadDemo8 {
    private static boolean flag=true;

    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            while (flag){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t.start();
        Thread.sleep(3000);
        //在主线程里就可以随时通过flag变量的取值,来操作t线程是否结束
        flag=false;
    }
}

2.使用Thread自带的标志位进行判定

Thread.currentThread()是Thread类的静态方法,通过这个方法可以获取到当前进程。(类似于this)

 interrupt会做两件事:

1.把线程内部的标志位(boolea)给设置成true

2.如果在进行sleep,就会触发异常,把sleep唤醒(但sleep唤醒时还会做一件事,把刚才设置的标志位再次设置称为flase),sleep完循环还会继续。

线程忽略终止请求:

public class ThreadDemo9 {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{//在t.run()中被使用,此处获取的就是t线程
            while (!Thread.currentThread().isInterrupted()){
                System.out.println("hello Thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
        Thread.sleep(3000);
        t.interrupt();//终止线程(终止t线程)
    }
}

运行结果:

调用interrupt,只是通知终止,不是线程一定要服从终止 。就像上面线程可以忽略终止操作。

线程立即响应终止请求:加上break

运行结果:

 线程稍后进行终止

(4)等待一个线程

线程是一个随机调动的过程。等待线程就是控制两个线程的结束顺序。

xxx.join()

 

如果开始执行join的时候,t线程已经结束了,join不会阻塞,就会立即返回。

(5)获取当前线程的引用 

在哪个线程中调用就能获取到哪个线程的实例。 

(6)休眠当前线程

 让线程休眠,本质上就是让这个线程不参加调度了(不去CPU上执行了)

四.线程的状态 

状态是根据当前线程调度的情况来描述的。

1.NEW 创建了Thread对象,但是还没有调用start(内核里还没有创建对应PCB)

2.TERMINATE  表示内核中的PCB已经执行完毕了,但是Thread对象还在。

3.RUNNABLE 可以运行的(正在CPU上执行的;在就绪队列里,随时可以去CPU上执行的)

4.WAITING

5.TIMED_WAITING

6.BLOCKED(4,5,6都是表示线程PCB正在阻塞队列中)

(1)观察线程的所有状态

 (2)线程状态转移

public class ThreadDemo11 {
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            for (int i = 0; i < 100000; i++) {
                //这个循环体内不做操作,也不sleep
            }
        });
        //启动之前,获取t的状态,就是new的状态
        System.out.println("start之前:"+t.getState());

        t.start();
        System.out.println("执行中的状态:"+t.getState());

        t.join();
        //线程执行完成之后就是terminated状态
        System.out.println("t结束之后:"+t.getState());
    }
}

五.线程安全

多线程的抢占式执行带来的随机性。

举例:

class  Counter{
    public int count=0;

    public void add(){
        count++;
    }
}

public class ThreadDemo12 {
    public static void main(String[] args) {
        Counter counter=new Counter();
        //添加两个线程,两个线程分别针对counter调用5w次的add方法
        Thread t1=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        //启动线程
        t1.start();
        t2.start();
        //等待两个线程结束
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //打印count的值
        System.out.println("count="+counter.count);

    }
}

结果:不是我们想象中要得到的10w,因为两个线程的并发执行顺序有很多种情况。

理想中的执行顺序:两次调用count++,依次增加两次

 举例一个其他的情况:

(1) 出现线程安全问题的原因

1.根本原因:抢占式执行,随机调度

2.代码结构:多个线程同时修改同一个变量(一个线程修改一个变量,没事;多个线程读取同一个变量,没事;多个线程修改不同的变量,也没事。)

3.原子性:非原子的,出现问题概率非常高

原子:不可拆分的基本单位(例如上述的count++非原子性,这里可以拆分为load,add,save三个操作)

针对线程安全问题,解决的最主要的手段就是从原子性入手,把非原子性的操作变成原子性的操作。(加锁)

4.内存可见性问题:一个线程对共享变量值的修改,可以被其他线程看到。

5.指令重排序(本质上是编译器优化的bug)

编译器认为我们写的代码不够好,在保证逻辑不变的情况下,自动调整代码的执行顺序,从而加快程序的执行效率。

(2)synchronized加锁

synchronized使用方法

1.修饰方法 

1)普通修饰方法

2)修饰静态方法

2.修饰代码块

1.普通修饰方法

修饰方法:进入方法就加锁,离开方法就解锁

对上面的代码的add()方法加锁synchronized

加了synchronized之后,进入方法就会加锁,出了方法就会解锁。如果两个线程同时尝试加锁,此时一个能获取锁成功,另一个只能阻塞等待(blocked),一直阻塞到刚才的线程释放锁,当前线程才能加锁成功。 

加锁本质是把并发变成串行。

一旦加锁后,代码执行速度一定会大打折扣。虽然加锁后,算的慢了,但还是比单线程快,加锁只是针对count++加锁了,除count++之外,还有for循环的代码,for循环代码是可以并发执行的,只是count++串行执行了。

修饰普通方法,锁对象就是this;修饰静态方法,锁对象就是类对象。(xxx.class);修饰代码块,显示/手动指定锁对象。

修饰静态方法和修饰一般方法同理。

2.修饰代码块

this这里可以指定任意对象,不一定非是this。进入代码块就加锁,出了代码块就解锁。

这样加完锁后,可以得到结果10_0000 

若两个线程一个加锁一个不加锁,就没有锁竞争了。例如写两个add方法,一个加锁,一个不加锁。代码执行和一开始都没有加锁是一样的。

3. synchronized特性

1)互斥:某个线程执行到某个对象的synchronized时,其他线程如果也执行到同一个对象synchronized就会阻塞等待

2)刷新内存(目前还存在争议)

3)可重入:synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。

一个线程针对同一个对象加锁两次,是否会有问题,如果没有问题,就叫可重入的。

锁对象是this,只要有线程调用add,进入add方法的时候,就会先加锁,紧接着又遇到了代码块,再次尝试加锁。此处是特殊情况,第二个线程和第一个线程本质是同一个线程 。

如果允许上述操作,这个锁是可以重入的。

如果不允许上述操作(第二次加锁会阻塞等待),就是不可重入的。(死锁)

在Java中为了避免死锁的出现,把synchronized设定成可重入的了。

(3)死锁

1.一个线程一把锁,连续加锁两次,如果锁是不可重入锁,就会死锁。

2.两个线程两把锁,t1和t2各自先针对锁A和锁B加锁,再尝试获取对方的锁。

举例:

//乐乐手上有可乐,文手上有芬达。乐乐给文说:你把芬达给我,我才给你可乐
//文对乐乐说:你把可乐给我,我才给你芬达
//这两个线程相互阻塞
public class ThreadDemo13 {
    public static void main(String[] args) {
        Object kele=new Object();
        Object fata=new Object();

        Thread lele=new Thread(()->{
            synchronized (kele){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (fata){
                    System.out.println("lele把可乐和芬达都拿到了");
                }
            }
        });
        Thread wen=new Thread(()->{
            synchronized (fata){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (kele){
                    System.out.println("wen把可乐和芬达都拿到了");
                }
            }
        });
        lele.start();
        wen.start();
    }
}

造成死锁:循环等待

线程1尝试获取到锁A和锁B,线程2尝试获取到锁B和锁A。线程1在获取B的时候等待线程2释放B;同时线程2在获取A的时候等待线程1释放A。

避免的方法:给锁编号,然后指定一个固定的顺序(比如从小到大)来加锁。任意线程加多把锁的时候,都让线程遵守上述顺序,此时循环等待自然破除。

针对死锁这样的问题要借助像jconsole这样的工具来进行定位,分析代码在哪里出现死锁。 

(5)Java标准库中的线程安全类

如果多个线程操作同一个集合类,就需要考虑到线程安全。

(6)volatile关键字

和内存可见性相关

import java.util.Scanner;

class MyCounter{
    public int flag=0;
}
public class ThreadDemo14 {
    public static void main(String[] args) {
        MyCounter myCounter=new MyCounter();
        Thread t1=new Thread(()->{
            while (myCounter.flag==0){

            }
            System.out.println("t1循环结束");
        });
        Thread t2=new Thread(()->{
            Scanner scanner=new Scanner(System.in);
            System.out.println("请输入一个整数:");
            myCounter.flag=scanner.nextInt();
        });
        t1.start();
        t2.start();
    }
}

 这个情况就是内存可见性问题,这是一个线程不安全问题。

 t1线程,汇编原理解释有两部:

1.load 把内存中flag的值读取到寄存器里。

2.cmp 把寄存器里的值和0比较,根据比较结果,决定下一步往哪里执行

上述循环,速度极快。在t2正真修改前,load的结果都是一样的,并且load和cmp相比速度慢非常多。编译器认为没有什么变化,jvm编译器优化,不再重复load,只读一次。而实际上是有t2进行修改的,但是jvm/编译器对于这种多线程的情况,判断可能存在误差。

内存可见性问题:一个线程对一个变量进行读取操作,同时另一个线程针对这个变量进行修改,此时读到的值,不一定是修改后的值,读线程没有感知到线程的变化。

volatile:告诉编译器,这个变量是“异变的”,每次都要重新读取这个变量的内存内容,不知道什么时候就变了,不能再进行激进的优化了。

 六.wait和notify

(1)wait

某个线程调用wait方法,就会进入阻塞(无论通过哪个对象wait)此时就处在WATING

wait的工作顺序:

1.先释放锁

2.进行阻塞等待

3.收到通知后,重新尝试获取锁,并且在获取锁后,继续往下执行。

wait要搭配synchroniced使用

wait()无参数版本:死等

wait(xxx)有参数版本:指定了等待的最大时间

(2)notify:通知

public class ThreadDemo17 {
    public static void main(String[] args) throws InterruptedException {
        Object object=new Object();
        Thread t1=new Thread(()->{
            System.out.println("t1:wait之前");
            try {
                synchronized (object) {
                    object.wait();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1:wait之后");
        });

        Thread t2=new Thread(()->{
            System.out.println("t2:notify之前");
            synchronized (object) {
                object.notify();
            }
            System.out.println("t2:notify之后");
        });
        t1.start();
        //此处的sleep 500大概率会让t1先执行wait
        //极端情况下,电脑特别卡,可能t2先执行notify
        Thread.sleep(500);
        t2.start();
    }
}

 notifyAll和notify相似,多个线程在wait的时候,notify随机唤醒一个,notifyAll所有线程都唤醒,这些线程再一起竞争锁。

例:有三个线程,分别打印ABC,设计使打印出的顺序为ABC

public class ThreadDemo18 {
    public static void main(String[] args) throws InterruptedException {
        Object locker1=new Object();
        Object locker2=new Object();

        Thread t1=new Thread(()->{
            System.out.println("A");
            synchronized (locker1){
                locker1.notify();
            }
        });

        Thread t2=new Thread(()->{
            synchronized (locker1){
                try {
                    locker1.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("B");
            synchronized (locker2){
                locker2.notify();
            }
        });

        Thread t3=new Thread(()->{
            synchronized (locker2){
                try {
                    locker2.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("C");
        });
       
        t2.start();
        t3.start();
        Thread.sleep(100);
        //保证t2,t3的wait都执行了,再来启动t1
        t1.start();
    }
}

七.多线程代码案例

(1)单例模式

单例模式---->单个实例(对象)

在有些场景中,有的特定的类,只能创建出一个实例,不该创建出多个实例。使用了单例模式后,就很难创建出多个实例。当创建多个实例的时候会编译报错。

1.饿汉模式

在类加载阶段,就急切的把实例创建出来了

//饿汉模式的单例模式实现
//此时保证Singleton这个类只能创建出一个实例
class Singleton{
    //创建实例
    private static Singleton instance=new Singleton();

    //如果需要使用这个唯一实例,同一通过Singleton.getInstance()方式来获取
    public static Singleton getInstance(){
        return instance;
    }

    //为了避免Singleton类不小心被复制多份
    //构造方法设为private,在类外面,就不能通过new创建这个Singleton的实例了
    private Singleton(){

    }
}

public class ThreadDemo19 {
    public static void main(String[] args) {
        Singleton s=Singleton.getInstance();
        Singleton s2=Singleton.getInstance();
      //  Singleton s3=new Singleton();编译报错,不能new多个实例
        System.out.println(s==s2);
    }
}

 

 2.懒汉模式

class SingletonLazy{
    private static SingletonLazy instance=null;

    public static SingletonLazy getInstance(){
        if(instance==null){
            instance=new SingletonLazy();
        }
        return instance;
    }

    private SingletonLazy(){}
}

public class ThreadDemo20 {
    public static void main(String[] args) {
        SingletonLazy s=SingletonLazy.getInstance();
        SingletonLazy s2=SingletonLazy.getInstance();
        System.out.println(s==s2);
    }
}

3.单例模式的线程安全模式(多线程)

class SingletonLazy {

    //volatile解决的是内存可见性和指令重排序问题,new的过程存在多步指令的问题
    private volatile static SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        //第一次new前需要加锁,后面的比较是读操作不存在修改不需要加锁
        if (instance == null) {//if:判断是否要加锁

            //load-cmp-new 加锁限制程序的运行顺序,保证多线程安全
            //加锁后,t2load的就是t1修改后的值,这样就触发不了if条件,也不再创建新的对象
            synchronized (SingletonLazy.class) {
                if (instance == null) {//if:是否创建对象
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

    private SingletonLazy() {}

    }


public class ThreadDemo20 {
    public static void main(String[] args) {
        SingletonLazy s=SingletonLazy.getInstance();
        SingletonLazy s2=SingletonLazy.getInstance();
        System.out.println(s==s2);
    }
}

(2)阻塞队列

阻塞队列也是特殊的队列,虽然也是先进先出的,但是带有特殊功能。

如果队列为空,执行出队列操作,就会阻塞,阻塞到另一个线程往队列里面添加元素(队列为空)为止;如果队列满了,执行入队列操作,也会阻塞,阻塞到另一个线程从队列取走元素位置。(队列不满)

消息队列:特殊的队列,相当于在阻塞队列的基础上,加上了消息类型,按照指定的类型进行先进先出

1.生产者消费者模型

1)实现了发送方和接收方之间的“解耦”。(解耦:降低耦合的过程)

开发中典型的场景:服务器之间的相互调用

 针对上述场景,使用生产者消费者模型,可以有效降低耦合。

此时A和B之间的耦合就降低了很多,A和B都不知道彼此,AB代码都与彼此无关,AB都只知道队列,此时新增一个C对于A毫无影响。

2)生产者消费者模型,第二个好处是可以做到“削峰填谷”,保证系统的稳定性。(比如“热搜”。多个客户端会发来大量请求,客户端的大量请求会在阻塞队列中按顺序依次执行,不会对服务器产生大的冲击)

queue提供的方法有三个:

入队列offer ;出队列poll;取队首元素peek

阻塞队列提供的方法主要是两个:两个方法都带有阻塞功能

入队列put;出队列take

简单了解阻塞队列的用法:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class ThreadDemo21 {
    public static void main(String[] args)  {
        BlockingQueue<Integer> blockingQueue=new LinkedBlockingQueue<>();

        //创建两个线程来作为生产者和消费者
        Thread customer=new Thread(()->{
            while (true){
                try {
                    Integer result=blockingQueue.take();
                    System.out.println("消费元素:"+result);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        customer.start();

        Thread producer=new Thread(()->{
            int count=0;
            while(true){
                try {
                    blockingQueue.put(count);
                    System.out.println("生产元素:"+count);
                    count++;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();
    }
}

 自己版本的阻塞队列

//自己写的阻塞队列
// 此处不考虑泛型,直接用int表示元素类型
class MyBlockingQueue {
    private int[] item = new int[1000];
    private int head = 0;
    private int tail = 0;
    private int size = 0;

    //入队列
    public void put(int value) throws InterruptedException {
        synchronized (this) {
            while (size == item.length) {
               // return;
                //队列满了,此时要产生阻塞
                this.wait();
            }
            item[tail] = value;
            tail++;
            // 对tail到末尾的处理
            // (1) tail=tail % item.length;
            if (tail >= item.length) {
                tail = 0;
            }
            size++;
            //这个notify唤醒take中的wait
            this.notify();
        }
    }

    //出队列
    public Integer take() throws InterruptedException {
        int result=0;
        synchronized (this) {
            while (size == 0) {
                //队列为空,也需要阻塞
                this.wait();
            }
             result = item[head];
            head++;
            if (head >= item.length) {
                head = 0;
            }
            size--;

            //唤醒put中的wait
          this.notify();
        }
        return result;
    }
}

public class ThreadDemo22 {
    public static void main(String[] args) throws InterruptedException {
     
        MyBlockingQueue queue=new MyBlockingQueue();
        Thread customer=new Thread(()->{
            while (true){
                try {
                    int result=queue.take();
                    System.out.println("消费:"+result);
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
       customer.start();

       Thread producer=new Thread(()->{
           int count=0;
           while(true){
               try {
                   System.out.println("生产:"+count);
                   queue.put(count);
                   count++;
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }
       });
       producer.start();
    }
}

 

(3)定时器 

类似于定闹钟,平时的闹钟有两种风格:

1.指定特定时刻,提醒

2.指定特定时间段后提醒

这里的定时器,不是提醒,而是执行一个实现准备好的方法/代码

schedule();这个方法的效果是给定计时器,注册一个任务,任务不会立即执行,而是在指定时间进行执行。

此方法的写法:

 自己写一个定时器:

1.让被注册的任务能够在指定时间被执行

2.一个定时器可以执行n个任务,n个任务会按照最初约定的时间按顺序执行

定时器里面的核心:

1.有一个扫描线程,负责判定时间/执行任务

2.还有一个数据结构,来保存所有被注册的任务(优先级队列是个好的选择)

此时扫描线程只需要扫一下队首元素即可,不必遍历整个队列。

此处的优先级队列会在多线程环境下使用(调用schedule是一个线程,扫描是另一个线程)

Java中提供的阻塞优先级队列:

PriorityBlockingQueue
import java.util.concurrent.PriorityBlockingQueue;
//使用这个类来表示一个定时器中的任务
class MyTask implements Comparable<MyTask>{
    //要执行的任务内容
    private Runnable runnable;
    //任务在啥时候执行(使用毫秒时间戳表示)
    private long time;

    public MyTask(Runnable runnable, long time) {
        this.runnable = runnable;
        this.time = time;
    }

    //获取当前任务的时间
    public long getTime() {
        return time;
    }

    //执行任务
    public void run(){
        runnable.run();
    }

    @Override
    public int compareTo(MyTask o) {
        //返回小于0,大于0,0
        //this比o小,返回小于0
        //this比o大,返回大于0
        //this和o相同,返回0
        //当前要实现的效果,是队首元素是时间最小的任务
        return (int) (this.time-o.time);
    }
}

//自己设计的简单的计时器
class MyTimer{
    //扫描线程
 private Thread t=null;

 //有一个阻塞优先级队列,来保存任务
    private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();

    public MyTimer() {
        t=new Thread(()->{
         while(true){
             try {
                 //取出队首元素。检查看队首元素的任务是否到时间了
                 //如果时间没到就把任务塞回到队列里面
                 //如果时间到了,就把任务进行执行
                 synchronized (this){
                     MyTask myTask=queue.take();
                     long curTime=System.currentTimeMillis();
                     if(curTime< myTask.getTime()){
                         //还没到点,先不必执行
                         //现在是13点,取出来的任务是14点执行
                         queue.put(myTask);
                         synchronized (this) {
                             this.wait(myTask.getTime() - curTime);
                         }
                     }else {
                         //时间到了,执行任务
                         myTask.run();
                     }
                 }
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
        });
        t.start();
    }

    //有一个schedule方法来注册任务
    //第一个参数是任务内容,第二个参数是任务在多少毫秒之后执行
    public void  schedule(Runnable runnable,long after){
        //注意这里时间要换算
        MyTask task=new MyTask(runnable,System.currentTimeMillis()+after);
        queue.put(task);
        synchronized (this) {
            this.notify();
        }
    }
}
public class ThreadDemo24 {
    public static void main(String[] args) {
    MyTimer myTimer=new MyTimer();
    myTimer.schedule(new Runnable() {
        @Override
        public void run() {
            System.out.println("任务1");
        }
    },1000);
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("任务2");
            }
        },2000);
    }
}

(4)线程池

使用线程池,来降低创建/销毁线程的开销(字符串常量池,数据库连接池.....)

事先把需要使用的线程创建好,放到“池”中,后面要使用的时候,直接从池中获取,如果用完了,也还给池。(比创建销毁更加高效)

创建一个线程池,池子里线程数目是10个。这个操作,使用某个类的某个静态方法,直接构造出一个对象来(相当于是new操作给隐藏到这样的方法后面了)

像这样的方法,就称为“工厂方法”,提供这个工厂方法的类,就称为“工厂类 ”。此处这个代码就使用了“工厂模式”这种设计模式。

工厂模式:使用普通的方法代替构造方法去创建对象。

 此处要注意:当前是往线程池里面放了1000个任务,1000个任务就是这10个线程平均分配,但这里并非是严格的平均。(每个线程都执行完一个任务后,再立即取下一个任务.....由于每个任务执行时间都差不多,因此每个线程做的任务数量就差不多)

 

线程池里面至少有两个大的部分:

1.阻塞队列,保存任务

2.若干个工作线程

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class MyThreadPool{
    //阻塞队列,此处不涉及时间,只有任务
    private BlockingQueue<Runnable> queue=new LinkedBlockingQueue<>();
    //n表示线程的数量
    public MyThreadPool(int n){
    //在这里创建出n个线程
        for (int i = 0; i <n; i++) {
            Thread t=new Thread(()->{
               while(true){
                   try {
                       Runnable runnable= queue.take();
                       runnable.run();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
            });
            t.start();
        }
    }
    //注册任务给线程池
    public void submit(Runnable runnable){
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
//自己实现一个线程池
public class ThreadDemo26 {
    public static void main(String[] args) {
    MyThreadPool pool=new MyThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            int n=i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello"+n);
                }
            });
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Roylelele

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

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

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

打赏作者

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

抵扣说明:

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

余额充值