多线程及其工具类使用

为什么要使⽤线程

在程序中完成某⼀个功能的时候,我们会将他描述成任务,这个任务需要在线程中完成.

串⾏与并发

如果在程序中,有多个任务需要被处理,此时的处理⽅式可以有串⾏和并发:

  • 串⾏(同步):所有的任务,按照⼀定的顺序,依次执⾏。如果前⾯的任务没有执⾏结束,后⾯的任务等待。
  • 并发(异步):将多个任务同时执⾏,在⼀个时间段内,同时处理多个任务。

并发的原理

⼀个程序如果需要被执⾏, 必须的资源是CPU和内存。 在内存上开辟空间, 为程序中的变量进⾏数据的存储; 同时需要CPU处理程序中的逻辑。 现在处于⼀个硬件过剩的时代, 但是即便是硬件不发达的时代, 并发任务也是可以实现的。 以单核的CPU为例, 处理任务的核⼼只有⼀个, 那就意味着, 如果CPU在处理⼀个程序中的任务, 其他所有的程序都得暂停。 那么并发是怎么实现的呢?

其实所谓的并发, 并不是真正意义上的多个任务同时执⾏。 ⽽是CPU快速的在不同的任务之间进⾏切换。 在某⼀个时间点处理任务A, 下⼀个时间点去处理任务B, 每⼀个任务都没有⽴即处理结束。 CPU快速的在不同的任务之间进⾏切换, 只是这个切换的速度⾮常快, ⼈类是识别不了的, 因此会给⼈⼀种“多个任务在同时执⾏”的假象。
因此, 所谓的并发, 其实就是CPU快速的在不同的任务之间进⾏切换的⼀种假象。

进程和线程

  • 进程, 是对⼀个程序在运⾏过程中, 占⽤的各种资源的描述。
  • 线程, 是进程中的⼀个最⼩的执⾏单元。 其实, 在操作系统中, 最⼩的任务执⾏单元并不是线程, ⽽是句柄(Handle)。 只不过句柄过⼩, 操作起来⾮常的麻烦, 因此线程就是我们可控的最⼩的任务执⾏单元。

相同点: 进程和线程都是为了处理多个任务并发⽽存在的。
不同点: 进程之间是资源不共享的, ⼀个进程中不能访问另外⼀个进程中的数据。 ⽽线程之间是资源共享的, 多个线程可以共享同⼀个数据。 也正因为线程之间是资源共享的, 所以会出现临界资源的问题。

概念总结

  • 程序:⼀个可执⾏的⽂件
  • 进程:⼀个正在运⾏的程序.也可以理解成在内存中开辟了⼀块⼉空间
  • 线程:负责程序的运⾏,可以看做⼀条执⾏的通道或执⾏单元,所以我们通常将进程的⼯作理解成线程的⼯作
  • 进程中可不可以没有线程? 必须有线程,⾄少有⼀个.
  • 当有⼀个线程的时候我们称为单线程(唯⼀的线程就是主线程).
  • 当有⼀个以上的线程同时存在的时候我们称为多线程.
  • 多线程的作⽤:为了实现同⼀时间⼲多件事情(并发执⾏).

线程的⽣命周期

线程的状态

线程的⽣命周期, 指的是⼀个线程对象, 从最开始的创建, 到最后的销毁, 中间所
经历的过程。 在这个过程中, 线程对象处于不同的状态。

  • New: 新⽣态, ⼀个线程对象刚被实例化完成的时候, 就处于这个状态。
  • Runnable: 就绪态, 处于这个状态的线程, 可以参与CPU时间⽚的争抢。
  • Run: 运⾏态, 某⼀个线程抢到了CPU时间⽚, 可以执⾏这个线程中的逻辑
  • Block: 阻塞态, 线程由于种种原因, 暂时挂起, 处于阻塞(暂停)状态。 这个状态的线程, 不参与CPU时间⽚的争抢。
  • Dead: 死亡态, 线程即将被销毁。

线程的⽣命周期图

在这里插入图片描述

理解多线程

对线程并发执⾏的说明

简单理解(cpu单核):从宏观上看,线程有并发执⾏,从微观上看,并没有,在线程完成任务时,实际⼯作的是cpu,我们将cpu⼯作描述为时间⽚(单次获取cpu的时间,⼀般在⼏⼗毫秒).cpu只有⼀个,本质上同⼀时间只能做⼀件事,因为cpu单次时间⽚很短,短到⾁眼⽆法区分,所以当cpu在多个线程之间快速切换时,宏观上给我们的感觉是多件事同时在执⾏.

注意:
1.cpu是随机的,线程之间本质上默认是抢cpu的状态,谁抢到了谁就获得了时间⽚,就⼯作,所以多个线程的⼯作也是默认随机的.
2.在使⽤多线程时,并不是线程数越多越好,本质上⼤家共同使⽤⼀个cpu,完成任务的时间并没有减少.要根据实际情况创建线程,多线程是为了实现同⼀时间完成多件事情的⽬的.⽐如我们⽤⼿机打开⼀个app时,需要滑动界⾯浏览,同时界⾯的图⽚需要下载,对于这两个功能最好同时进⾏,这时可以使⽤多线程.

示例代码

public static void main(String[] args) {
    new Test();
    /*
    * ⼿动运⾏垃圾回收器
    */
    System.gc();
    System.out.println("main");
}
class Test{
	@Override
    protected void finalize() throws Throwable {
    	System.out.println("finalize");
    }
}
  • 代码演示的是主线程和垃圾回收线程在同时⼯作时的状态
  • 什么叫任务区?
    我们将线程⼯作的地⽅称为任务区.
    每⼀个线程都有⼀个任务区,任务区通过对应的⽅法产⽣作⽤.
  • JVM默认是多线程吗? ⾄少要有两个线程:
    • 主线程:任务区:main函数
    • 垃圾回收线程:任务区:finalize函数

代码说明:
1.gc()⽅法:之前讲过,是垃圾回收器
原理:当执⾏gc时,会触发垃圾回收机制,开启垃圾回收线程,执⾏finalize⽅法
2.finalize()⽅法:垃圾回收线程的任务区
正常情况下,这个函数是由系统调⽤的,重写只是为了更好的观察多线程的发⽣
当Test对象被释放的时候,会⾃动的调⽤finalize⽅法
3.线程和任务的关系
任务区结束,线程随着任务的结束⽽结束,线程随着任务的开始⽽开始.当线程还在⼯作的时候,进程不能结束.
对于主线程来说:当main函数结束时,主任务区结束,主线程结束
对于垃圾回收线程:当finalize函数结束,垃圾回收任务结束,垃圾回收线程结束

创建线程

java将线程⾯向对象了,形成的类就是Thread,在Thread类内部执⾏任务的⽅法叫run()

线程对象的实例化

在Java中, 使⽤Thread类来描述⼀个线程。 实例化⼀个线程, 其实就是⼀个Thread对象。

使⽤Thread类创建线程对象

  • 线程对象刚刚被实例化的时候, 线程处于新⽣态,还没有线程的功能。 如果需要
    让这个线程执⾏他的任务, 需要调⽤ start() ⽅法, 使线程进⼊到就绪态, 争抢
    CPU时间⽚。

  • 为什么通过调⽤start()⽅法开启线程,⽽不是通过⼿动调⽤run()?

    答:因为线程获取cpu是随机的,run是线程的任务区,代表功能.如果⼿动执⾏run,此
    时线程可能没有拿到cpu,⽆法⼯作,操作失败.通过start,让线程处于就绪状态,随时
    拥有抢cpu的能⼒,当抢到cpu后,再⾃动执⾏run,实现并发的任务.

  • 为什么要使⽤Thread类的⼦类对象?

    答:我们实现的实际功能,Thread类作为系统类不能提前知晓,所以⽆法将功能代码放⼊
    Thread的run⽅法⾥.如果想实现⾃⼰的功能,可以写Thread类的⼦类,重写run⽅法,这
    也是为什么Thread的run⽅法是⼀个空⽅法.

内存分析

在这里插入图片描述

继承Thread类

  • 继承⾃Thread类, 做⼀个Thread的⼦类。 在⼦类中, 重写⽗类中的run⽅法,在这个重写的⽅法中, 指定这个线程需要处理的任务。
  • Thread.currentThread() : 可以⽤在任意的位置, 获取当前的线程。如果是Thread的⼦类, 可以在⼦类中, 使⽤this获取到当前的线程。
  • 当我们⼿动调⽤run的时候,他失去了任务区的功能,变成了⼀个普通的⽅法. 当run作为⼀个普通⽅法时,内部对应的线程跟调⽤他的位置保持⼀致.
  • 结果分析:
    主线程和两个⼦线程之间是随机打印的,他们是抢cpu的关系.
  • 通过创建Thread⼦类的⽅式实现功能,线程与任务绑定在了⼀起,操作不⽅便

我们可以将任务从线程中分离出来,哪个线程需要⼯作,就将任务交给谁,操作⽅便,灵活-使⽤Runnable接⼝

使⽤Runnable接⼝

  • 在Thread类的构造⽅法中, 有⼀个重载的构造⽅法, 参数是 Runnable 接⼝。因此, 可以通过Runnable接⼝的实现类对象进⾏Thread对象的实例化。
  • 这⾥Thread内部默认有⼀个run,⼜通过runnable传⼊⼀个run,为什么优先调⽤的是传⼊的run?

如果该线程是使⽤独⽴的 Runnable 运⾏对象构造的,则调⽤该 Runnable 对象的 run ⽅法;否则,该⽅法不执⾏任何操作并返回。

优缺点对⽐

  • 继承的⽅式: 优点在于可读性⽐较强, 缺点在于不够灵活。 如果要定制⼀个线程, 就必须要继承⾃Thread类, 可能会影响原有的继承体系。
  • 接⼝的⽅式: 优点在于灵活, 并且不会影响⼀个类的继承体系。 缺点在于可读性较差。

⽤的⽐较多的⽅式是使⽤接⼝的⽅式。

线程名字的设置

每⼀个线程, 都有⼀个名字。 如果在实例化线程的时候不去设定名字, 那么这个线程会拥有⼀个默认的名字。

  • 设置线程的名字, 使⽤⽅法 setName(String name)
Thread thread = new Thread(() -> {
	System.out.println("⼦线程的逻辑");
});
// 设置线程的名字
thread.setName("⼦线程的名字");
  • Thread类对象, 在进⾏实例化的时候, 可以同时设置线程的名字。
// 使⽤接⼝的⽅式进⾏线程的实例化
Thread thread = new Thread(() -> {}, "线程的名字");
}
  • 如果使⽤继承Thread类的⽅式进⾏的实例化, 可以添加⼀个构造⽅法, 进⾏实例化对象的同时进⾏名称的设置。 在构造⽅法中, 使⽤ super(String) 进⾏⽗类⽅法的调⽤。
public class MyThread extends Thread {
    public MyThread(String name) {
    	super(name);
    }
	@Override
    public void run() {
    	System.out.println("⼦线程的逻辑");
    }
}

设置线程名字, 可以使⽤上述三种⽅式, 但是获取线程线程的名字, 只有⼀个⽅法, 就是 getName()

线程的礼让

线程礼让, 就是当前已经抢到CPU资源的正在运⾏的线程, 释放⾃⼰持有的CPU资
源, 回到就绪状态, 重新参与CPU时间⽚的争抢。

Thread.yield();//强制退出运行态并进入就绪态

线程同步

临界资源问题

  • 临界资源

在⼀个进程中, 多个线程之间是可以资源共享的。 如果在⼀个进程中的⼀个资源同时被多个线程访问, 这个资源就是⼀个临界资源。
如果多个线程同时访问临界资源, 会对这个资源的值造成影响。

  • 临界资源问题

多个线程同时访问⼀个资源的情况下, ⼀个线程在操作这个资源的时候, 将值取出进⾏运算, 在还没来得及进⾏修改这块空间的值之前, 值⼜被其他的线程取⾛了。此时就会出现临界资源的问题, 造成这个资源的值出现不是我们预期的值。

解决

临界资源问题出现的原因就是多个线程在同时访问⼀个资源, 因此解决⽅案也很简单, 就是不让多个线程同时访问即可。
在⼀个线程操作⼀个资源的时候, 对这个资源进⾏“上锁”, 被锁住的资源, 其他的线程⽆法访问。

类似多个⼈去公共卫⽣间, 每⼀个⼈在进到卫⽣间的时候, 都会从⾥⾯进⾏反锁。 此时, 其他⼈如果也需要使⽤这个卫⽣间, 就得在⻔外等待。

线程锁

  • 线程锁, 就是⽤来“锁住”⼀个临界资源, 其他的线程⽆法访问。 在程序中, 可以分为对象锁和类锁

  • 对作为锁的对象的要求:

    • 1.必须是对象
    • 2.必须保证被多个线程共享
  • 对象锁: 任何的普通对象或者this, 都可以被当做是⼀把锁来使⽤。 但是需要注意, 必须要保证不同的线程看到的锁, 需要是同⼀把锁才能⽣效。 如果不同的线程看到的锁对象是不⼀样的, 此时这把锁将没有任何意义。

  • 注意: 不能直接使⽤匿名对象作为锁,因为这样每次都是在重新new,要保证锁是被⼤家共享.

  • 类锁: 可以将⼀个类做成锁, 使⽤类.class (类的字节码⽂件对象)来作为锁。因为类的字节码⽂件的使⽤范围太⼤,所以⼀般我们不使⽤他作为锁,只有在静态⽅法中.

synchronized

如果在⼀个⽅法中, 所有的逻辑, 都需要放到同⼀个同步代码段中执⾏。 这样的⽅法, 可以直接做成同步⽅法。

同步⽅法

  • ⾮静态同步⽅法,使⽤的对象锁(this)

    是某个对象实例内,synchronized aMethod(){}可以防⽌多个线程同时访问这个对象的synchronized⽅法(如果⼀个对象有多个synchronized⽅法,只要⼀个线程访问了其中的⼀个synchronized⽅法,其它线程不能同时访问这个对象中任何⼀个synchronized⽅法)。这时,不同的对象实例的synchronized⽅法是不相⼲扰的。也就是说,其它线程照样可以同时访问相同类的另⼀个对象实例中的synchronized⽅法

  • 静态同步⽅法,使⽤的类锁(当前类的.class⽂件)

    是某个类的范围,synchronized static aStaticMethod{}防⽌多个线程同时访问这个类中的synchronized static ⽅法。它可以对类的所有对象实例起作⽤。静态同步函数在进内存的时候不会创建对象,但是存在其所属类的字节码⽂件对象,属于class类型的对象,所以静态同步函数的锁是其所属类的字节码⽂件对象。

同步代码块

synchronized关键字⽤于⽅法中的某个区块中,表示只对这个区块的资源实⾏互斥访问。

同步代码段, 是来解决临界资源问题最常⻅的⽅式。 将⼀段代码放⼊到同步代码段中, 将这段代码上锁。

第⼀个线程抢到了锁标记后, 可以对这个紧接资源上锁, 操作这个临界资源。 此时其他的线程再执⾏到synchronized的时候, 会进⼊到锁池, 直到持有锁的线程使⽤结束后, 对这个资源进⾏解锁。 此时, 处于锁池中的线程都可以抢这个锁标记, 哪⼀个线程抢到了, 就进⼊到就绪态, 没有抢到锁的线程, 依然处于锁池中。

  • 同步代码块⼉的构成:
synchronized(锁(对象)){
//同步的代码
}
  • 同步代码块⼉的特点:

    1.可以保证线程的安全

    2.由于每次都要进⾏判断处理,所以降低了执⾏效率(这里是与 不同步 对比)

⽐较同步代码块⼉和同步函数

  • 同步代码块⼉使⽤更加的灵活,只给需要同步的部分代码同步即可,⽽同步函数是给这个函数内的所有代码同步.

  • 由于处于同步的代码越少越好,所以最好使⽤同步代码块⼉

  • 什么时候使⽤同步代码块⼉或者同步⽅法:

    1.多个线程共享⼀个数据
    2.⾄少有两个线程

synchronized在继承中的使⽤

synchronized关键字是不能继承的,也就是说,基类的⽅法synchronized f(){}在继承类中并不⾃动是synchronized f(){},⽽是变成了f(){}。继承类需要你显式的指定它的某个⽅法为synchronized⽅法;

线程通信

package communication.test;
/**
 * 实例:打印机打印
 * 实现功能:不断输⼊不断输出
 * 总结:需要给输⼊任务和输出任务同时加⼀把锁,保证两个任务之间是同步的给两
 * 个任务加⼀把锁:可以是desc或者Object.class
 * */
public class Demo1 {
    public static void main(String[] args) {
//        创建数据对象
        Data data = new Data();
//        创建输入输出任务
        Input input = new Input(data);
        Output output = new Output(data);
//        创建线程
        Thread in = new Thread(input);
        Thread out = new Thread(output);
//        开始线程
        in.start();
        out.start();
    }
}
//数据类
class Data{
    String name;
    String sex;
}
//输入任务
class Input implements Runnable{
    Data data;
    int i = 0;
    public Input(Data data) {
        this.data = data;
    }

    @Override
    public void run() {
        while (true){
            synchronized (data) {
                if (i == 0) {
                    data.name = "张三";
                    data.sex = "男";
                } else {
                    data.name = "李华";
                    data.sex = "⼥";
                }
                i = (i + 1) % 2;
            }
        }
    }
}
//输出任务
class Output implements Runnable{
    Data data;
    public Output(Data data) {
        this.data = data;
    }

    @Override
    public void run() {
        while (true){
            synchronized (data) {
                System.out.println(data.name+"\t"+data.sex);
            }
        }
    }
}

package communication.test1;
/**
 * 实例:打印机打印
 * 功能实现:⼀次输⼊⼀次输出
 * 总结:在上⾯线程通信基本实现的基础上改进代码,通过分别给输⼊和输出线程设
 * 置wait和notify状态,实现⼀次输⼊⼀次输出
 * */
public class Demo {
    public static void main(String[] args) {
//        创建数据对象
        Data data = new Data();
//        创建输入输出任务
        Input input = new Input(data);
        Output output = new Output(data);
//        创建线程
        Thread in = new Thread(input);
        Thread out = new Thread(output);
//        开始线程
        in.start();
        out.start();
    }
}
//数据类
class Data{
    String name;
    String sex;

    boolean flag = false; //用于执行唤醒等待的切换
}
//输入任务
class Input implements Runnable{
    Data data;
    int i = 0;
    public Input(Data data) {
        this.data = data;
    }

    @Override
    public void run() {
        while (true){
            synchronized (data) {
                if (data.flag == true){
                    //让当前的线程等待
                    //wait⽅法要在同步下使⽤,因为要使⽤同步锁
                    try {
                        data.wait();
                        //当执⾏这⾏代码的时候,这⾥对应的是哪个线程,就操作的是哪个线程
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if (i == 0) {
                    data.name = "张三";
                    data.sex = "男";
                } else {
                    data.name = "李华";
                    data.sex = "⼥";
                }
                i = (i + 1) % 2;

                //状态切换
                data.flag = !data.flag;
                //唤醒输出线程
                data.notify();
            }
        }
    }
}
//输出任务
class Output implements Runnable{
    Data data;
    public Output(Data data) {
        this.data = data;
    }

    @Override
    public void run() {
        while (true){
            synchronized (data) {
                if (data.flag == false){
                    try {
                        data.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                System.out.println(data.name+"\t"+data.sex);


                data.flag = !data.flag;
                data.notify();
            }
        }
    }
}


package communication.test2;
/**
 * 实例:打印机打印
 * 功能:对⼀次输⼊⼀次输出代码的改进
 * 总结:进⾏了代码优化
 * ⾯向对象的精髓:谁的活⼉谁⼲,不是你的活⼉不要⼲
 * 将数据准备的活⼉从输⼊任务输出任务提出来,放⼊数据类
 * */
public class Demo {
    public static void main(String[] args) {
        Data2 data = new Data2();
        Input input = new Input(data);
        Output output = new Output(data);

        Thread in = new Thread(input);
        Thread out = new Thread(output);

        in.start();
        out.start();
    }
}
class Data2{
    String name;
    String sex;

    boolean flag = false;

    public void setData(String name,String sex){
        if (flag == true){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.name = name;
        this.sex = sex;
        flag = !flag;
        notify();
    }
    public void getData(){
        if (flag == false){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(name+"\t"+sex);
        flag = !flag;
        notify();
    }
}
class Input implements Runnable{
    Data2 data;

    public Input(Data2 data) {
        this.data = data;
    }

    @Override
    public void run() {
        int i=0;
        while (true) {
            synchronized (data) {
                if (i == 0) {
                    data.setData("张三", "男");
                }else {
                    data.setData("李华", "⼥");
                }
                i=(i+1)%2;
            }
        }
    }
}
class Output implements Runnable{
    Data2 data;

    public Output(Data2 data) {
        this.data = data;
    }
    @Override
    public void run() {
        while (true){
            synchronized (data) {
                data.getData();
            }
        }
    }
}

⽣产者消费者模式

⽣产者消费者问题是研究多线程程序经典问题之⼀,它描述是有⼀块缓冲区作为仓库,⽣产者可以将产品放⼊仓库,消费者则可以从仓库中取⾛产品。在Java中⼀共有四种⽅法⽀持同步,其中前三个是同步⽅法,⼀个是管道⽅法。

(1)Object的wait() / notify()⽅法 (2)Lock和Condition的await() / signal()⽅法
(3)BlockingQueue阻塞队列⽅法 (4)PipedInputStream /PipedOutputStream

单⽣产者消费者

package communication.procon;

/**
 * 单⽣产者单消费者
 * 需要的线程:两个---⼀个⽣产线程⼀个消费线程
 * 需要的任务:两个---⼀个⽣产任务⼀个消费任务
 * 需要数据:⼀份---产品
 */
public class ProducerConsumer {
    public static void main(String[] args) {
        Product product = new Product();
        Producter producter = new Producter(product);
        Consumer consumer = new Consumer(product);

        Thread pro = new Thread(producter);
        Thread con = new Thread(consumer);
        pro.start();
        con.start();
    }
}

//数据类——产品
class Product{
    String name;//名字
    double price;//价格
    int number;//数量
    //标识--控制唤醒等待
    boolean flag = false;

    public synchronized void setProduct(String name,double price){
        while (flag){//if (flag==true){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        this.name = name;
        this.price = price;
        number++;
        System.out.println(Thread.currentThread().getName()+"生产了\t名字:"+this.name+"\t价格:"+this.price+"\t当前数量:"+this.number);

        flag = !flag;
        notifyAll();//notify();
    }
    public synchronized void getProduct(){
        while (!flag){//if (flag==false){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        number--;
        System.out.println(Thread.currentThread().getName()+"消费了 \t名字:"+this.name+"\t价格:"+this.price);

        flag = !flag;
        notifyAll();//notify();
    }
}
//生产者任务
class Producter implements Runnable{
    Product product;

    public Producter(Product product) {
        this.product = product;
    }

    @Override
    public void run() {
        while (true) {
            product.setProduct("辣条", 10);
        }
    }
}
//消费者任务
class Consumer implements Runnable{
    Product product;

    public Consumer(Product product) {
        this.product = product;
    }

    @Override
    public void run() {
        while (true) {
            product.getProduct();
        }
    }
}

多⽣产者多消费者

将单⽣产者单消费者代码不做更改,直接再添加⼀个⽣产线程,⼀个消费线程,就形成了多⽣产者多消费者?//error

  • 出现的错误1

    • 错误描述:当有两个⽣产线程,两个消费线程同时存在的时候,有可能出现⽣产⼀次,消费多次或者⽣产多次消费⼀次的情况.
    • 原因:当线程被重新唤醒之后,没有判断标记,直接执⾏之后的代码
    • 解决办法:将标记判断的if写入while循环 确保状态正确才执行之后代码
  • 出现的错误2

    • 问题描述:继续运⾏程序,会出现死锁的情况(4个线程同时处于等待状态)
    • 原因:唤醒的是本⽅的线程,最后导致所有的线程都处于等待状态.
    • 解决办法:将notify改成notifyAll.保证将对⽅的线程唤醒

Lock锁

  • 为什么使⽤Lock锁?

在我们使⽤synchronized进⾏同步的时候,锁对象是Object类的对象,使⽤wait,notify⽅法都来⾃Object类,但是咱们知道并不是所有的对象都会⽤到同步,所以这样⽤法不太合理,⽽且锁相关的功能很多,Lock就是将锁⾯向对象的结果.不光是将锁⾯向对象了,同时将wait,notify等⽅法也做了⾯相对象处理.形成了Condition接⼝.当我们想实现多⽣产者多消费者模式时,可以使⽤Lock实现同步,同时配合Condition接⼝实现唤醒等待.

  • ⽐较synchronized和Lock

    1.synchronized:从jdk1.0就开始使⽤的同步⽅法-称为隐式同步
    synchronized(锁对象){//获取锁 我们将锁还可以称为锁旗舰或者监听器
    //同步的代码
    }//释放锁
    2.Lock:从jdk1.5开始使⽤的同步⽅法-称为显示同步

  • Lock本身是接⼝,要通过他的⼦类创建对象⼲活⼉

    常⽤⼦类:ReentrantLock

    • ⾸先调⽤lock()⽅法获取锁
    • 进⾏同步的代码块⼉
    • 使⽤unlock()⽅法释放锁

当进⾏多⽣产者多消费者的功能时,使⽤Lock,其他的都使⽤synchronized

  • 效率:Lock⾼于synchronized

唤醒等待机制

Object类中⼏个⽅法如下:

  • wait()

    • 等待,让当前的线程,释放⾃⼰持有的指定的锁标记,进⼊到等待队列。
    • 等待队列中的线程,不参与CPU时间⽚的争抢,也不参与锁标记的争抢。
  • notify()

    • 通知、唤醒。唤醒等待队列中,⼀个等待这个锁标记的随机的线程。
    • 被唤醒的线程,进⼊到锁池,开始争抢锁标记。
  • notifyAll()

    • 通知、唤醒。唤醒等待队列中,所有的等待这个锁标记的线程。
    • 被唤醒的线程,进⼊到锁池,开始争抢锁标记。

wait和sleep的区别

  • sleep()⽅法,在休眠时间结束后,会⾃动的被唤醒。 ⽽wait()进⼊到的阻塞态,需要被notify/notifyAll⼿动唤醒。

  • wait()会释放⾃⼰持有的指定的锁标记,进⼊到阻塞态。sleep()进⼊到阻塞态的时候,不会释放⾃⼰持有的锁标记。

  • ⽆论是wait()⽅法,还是notity()/notifyAll()⽅法,在使⽤的时候要注意,⼀定要是⾃⼰持有的锁标记,才可以做这个操作。否则会出现IllegalMonitorStateException 异常。

  • 为什么wait,notify⽅法要使⽤锁调⽤?

在这里插入图片描述

死锁

出现的情况有两种

  • 所有的线程处于等待状态

    • ⼤家都处于等待状态,没有⼈获取cpu使⽤
  • 锁之间进⾏嵌套调⽤

    • 多个线程, 同时持有对⽅需要的锁标记, 等待对⽅释放⾃⼰需要的锁标记。此时就是出现死锁。 线程之间彼此持有对⽅需要的锁标记, ⽽不进⾏释放, 都在等待。

线程其他内容

线程的停⽌

线程的停⽌:3种

  • 1.通过⼀个标识结束线程
  • 2.调⽤stop⽅法—因为有固有的安全问题,所以系统不建议使⽤.
  • 3.调⽤interrupt⽅法----如果⽬标线程等待很⻓时间(例如基于⼀个条件变量),则应使⽤ interrupt ⽅法来中断该等待。

线程的休眠

线程休眠, 就是让当前的线程休眠指定的时间。 休眠的线程进⼊到阻塞状态, 直到
休眠结束。 阻塞的线程, 不参与CPU时间⽚的争抢。

注: 线程休眠的时间单位是毫秒。

try {
    // 线程休眠
    Thread.sleep(1000);
}
catch (InterruptedException e) {
    e.printStackTrace();
}

线程的合并

将⼀个线程中的任务, 合并⼊到另外⼀个线程中执⾏, 此时, 合并进来的线程有限
执⾏。 类似于: 插队。

注意:优先级只⽐main线程的⾼.对其他的线程没有影响.

// 先开启
vip.start();
// 再合并
try {
    vip.join();
} catch (InterruptedException e) {
    e.printStackTrace();
}

线程的优先级设置

设置线程的优先级, 可以决定这个线程能够抢到CPU时间⽚的概率。 线程的优先级范围在 [1, 10], 默认的优先级是5。 数值越⾼, 优先级越⾼。 但是要注意, 并不是优先级⾼的线程⼀定能抢到CPU时间⽚, 也不是优先级的线程⼀定抢不到CPU时间⽚。 线程的优先级只是决定了这个线程能够抢到CPU时间⽚的概率。 即便是优先级最低的线程, 依然可以抢到CPU时间⽚。

// 设置线程的优先级, 必须在这个线程启动之前
thread0.setPriority(1);
thread1.setPriority(10);
thread0.start();
thread1.start();

守护线程

守护线程, ⼜叫后台线程。 是⼀个运⾏在后台, 并且会和前台线程争抢CPU时间⽚的线程。

  • 守护线程依然会和前台线程争抢CPU时间⽚, 实现并发的任务。
  • 在⼀个进程中, 如果所有的前台线程都结束了, 后台线程即便任务没有执⾏结束, 也会⾃动结束。
// 将⼀个线程设置为守护线程(先设置再开启)
thread.setDaemon(true);
// 开启线程
thread.start();

在这里插入图片描述

线程池

线程池, 其实就是⼀个容器, ⾥⾯存储了若⼲个线程。使⽤线程池, 最主要是解决线程复⽤的问题。 之前使⽤线程的时候, 当我们需要使⽤⼀个线程时, 实例化了⼀个新的线程。 当这个线程使⽤结束后, 对这个线程进⾏销毁。 对于需求实现来说是没有问题的, 但是如果频繁的进⾏线程的开辟和销毁,其实对于CPU来说, 是⼀种负荷, 所以要尽量的优化这⼀点。可以使⽤复⽤机制解决这个问题。 当我们需要使⽤到⼀个线程的时候, 不是直接实例化, ⽽是先去线程池中查找是否有闲置的线程可以使⽤。 如果有, 直接拿来使⽤; 如果没有, 再实例化⼀个新的线程。 并且, 当这个线程使⽤结束后, 并不是⻢上销毁, ⽽是将其放⼊到线程池中, 以便下次继续使⽤。

线程池的开辟

在Java中, 使⽤ThreadPoolExecutor类来描述线程池, 在这个类的对象实例化的时
候, 有⼏个常⻅的参数:

  • BlockingQueue

    • ArrayBlockingQueue
    • LinkedBlockingQueue
    • SynchronouseQueue
  • RejectedExecutionHandler

    • ThreadPoolExecutor.AbortPolicy : 丢弃新的任务,并抛出异常
    • RejectedExecutionException
    • ThreadPoolExecutor.DiscardPolicy : 丢弃新的任务,但是不会抛出异常
    • ThreadPoolExecutor.DiscardOldestPolicy : 丢弃等待队列中最早的任务
    • ThreadPoolExecutor.CallerRunsPolicy : 不会开辟新的线程,由调⽤的线程来处理

线程池的⼯作原理

线程池中的所有线程, 可以分为两部分: 核⼼线程 和 临时线程
核⼼线程:
核⼼线程常驻于线程池中, 这些线程, 只要线程池存在, 他们不会被销毁。 只有当线程池需要被销毁的时候, 他们才会被销毁。

临时线程:
就是临时⼯。 当遇到了临时的⾼密度的线程需求时, 就会临时开辟⼀些线程, 处理⼀些任务。 这些临时的线程在处理完⾃⼰需要处理的任务后, 如果没有其他的任务要处理, 就会闲置。 当闲置的时间到达了指定的时间之后, 这个临时线程就会被销毁。

任务分配逻辑:

  1. 当需要处理并发任务的时候, 优先分配给核⼼线程处理。
  2. 当核⼼线程都已经分配了任务, ⼜有新的任务出现时,会将这个新的任务存⼊等待队列。
  3. 当等待队列被填满后, 再来新的任务时, 会从开辟⼀个临时线程,处理这个新的任务。
  4. 当临时线程加核⼼线程数量已经到达线程池的上限,再来新的任务的时候,就会触发拒绝访问策略。

线程池的常⽤⽅法

在这里插入图片描述

线程池的⼯具类

线程池的开辟, 除了可以使⽤构造⽅法进⾏实例化, 还可以通过Executors⼯具类进⾏获取。 实际应⽤中, ⼤部分的场景下, 可以不⽤前⾯的构造⽅法进⾏线程池的实例化, ⽽是⽤Executors⼯具类中的⽅法进⾏获取。

  • RejectedExecutionException
  • ThreadPoolExecutor.DiscardPolicy : 丢弃新的任务,但是不会抛出异常
  • ThreadPoolExecutor.DiscardOldestPolicy : 丢弃等待队列中最早的任务
  • ThreadPoolExecutor.CallerRunsPolicy : 不会开辟新的线程,由调⽤的线程来处理

线程池的⼯作原理

线程池中的所有线程, 可以分为两部分: 核⼼线程 和 临时线程
核⼼线程:
核⼼线程常驻于线程池中, 这些线程, 只要线程池存在, 他们不会被销毁。 只有当线程池需要被销毁的时候, 他们才会被销毁。

临时线程:
就是临时⼯。 当遇到了临时的⾼密度的线程需求时, 就会临时开辟⼀些线程, 处理⼀些任务。 这些临时的线程在处理完⾃⼰需要处理的任务后, 如果没有其他的任务要处理, 就会闲置。 当闲置的时间到达了指定的时间之后, 这个临时线程就会被销毁。

任务分配逻辑:

  1. 当需要处理并发任务的时候, 优先分配给核⼼线程处理。
  2. 当核⼼线程都已经分配了任务, ⼜有新的任务出现时,会将这个新的任务存⼊等待队列。
  3. 当等待队列被填满后, 再来新的任务时, 会从开辟⼀个临时线程,处理这个新的任务。
  4. 当临时线程加核⼼线程数量已经到达线程池的上限,再来新的任务的时候,就会触发拒绝访问策略。

线程池的常⽤⽅法

[外链图片转存中…(img-ZyfN7wgZ-1628306171709)]

线程池的⼯具类

线程池的开辟, 除了可以使⽤构造⽅法进⾏实例化, 还可以通过Executors⼯具类进⾏获取。 实际应⽤中, ⼤部分的场景下, 可以不⽤前⾯的构造⽅法进⾏线程池的实例化, ⽽是⽤Executors⼯具类中的⽅法进⾏获取。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值