Java SE(五)多线程 单例模式 枚举

线程的相关概念
  1. 程序(program):一个固定的运行逻辑和数据的集合,是一个静态的状态,一般存储在硬盘中

  2. 进程(process):一个正在运行的程序,是一个程序的一次运行,是一个动态的概念,一般存储在内存中。

线程(thread):一条独立的执行路径。多线程,在执行某个程序的时候,该程序可以有多个子任务,每个线程都可以独立的完成其中一个任务。在各个子任务之间,没有什么依赖关系,可以单独执行

进程和线程的关系:

进程是用于分配资源的单位
一个进程中,可以有多条线程;但是一个进程中,至少有一条线程
线程不会独立的分配资源,一个进程中的所有线程,共享同一个进程中的资源
  1. 并行(parallel):多个任务(进程、线程)同时运行。在某个确定的时刻,有多个任务在执行, 条件:有多个cpu,多核编程

  2. 并发(concurrent):多个任务(进程、线程)同时发起。不能同时执行的(只有一个cpu),只能是同时要求执行。就只能在某个时间片内,将多个任务都有过执行。一个cpu在不同的任务之间,来回切换,只不过每个任务耗费的时间比较短,cpu的切换速度比较快,所以可以让用户感觉就像多个任务在同时执行。(实际开发中的多线程程序,都是并发运行机制)

多线程的实现方式

多线程实现的第一种方式:继承方式

1.继承Thread类

  1. 步骤:

    1. 定义一个类,继承Thread类
    2. 重写自定义类中的run方法,用于定义新线程要运行的内容
    3. 创建自定义类型的对象
    4. 调用线程启动的方法:start方法

    ​ 使该线程开始执行;Java 虚拟机调用该线程的 run 方法

  2. 图示
    image

4.使用匿名内部类实现多线程

// 1. 自定义出一个类,继承Thread线程父类
public class MyThread extends Thread{
    // 2. 重写从父类Thread继承到的run方法功能
    @Override
    public void run(){
        for(int i = 1; i <= 10; i++){
            System.out.println("run---"+i);
        }
    }
}
public class TestThread {
    // main方法就是一个线程,名称叫做: 主线程
    public static void main(String[] args) {
        // 在main方法这个线程中, 再创建出另外第一个线程
        // 注意 : 1)每一条线程都是独立的代码执行路径,彼此之间互不干扰
        // 2) 多线程都是并发的执行机制,互相竞争CPU资源,因此导致多线程程序执行结果
        // 具有很大的随机性, 哪条线程抢到CPU资源成功,哪条线程执行
        // 3. 创建出一个自定义的线程类对象
        MyThread my = new MyThread();
        // 4. 调用start()方法, 开启线程
        my.start();

        // 5. 在main线程中,继续写一个10次循环,与my线程中run方法中代码形成竞争运行关系
        for(int i = 1; i <= 10; i++){
            System.out.println("main---" + i);
        }
    }
}

匿名内部类实现多线程

public class Demo01_匿名内部类实现多线程 {
    public static void main(String[] args) {
        /*
            匿名内部类语法结构:
            new 父类或者父接口(){// 大括号表示父类的子类或者接口的实现类实现过程
                 将方法进行重写;
            }

            上述的语法结构整体 : 是一个匿名内部类对象
            匿名内部类作用 : 作为一个类的子类对象或者是一个接口的实现类对象存在
         */

        // 1. 创建出一个Thread线程类的一个子类对象
        new Thread(){
            @Override
            public void run(){
                for(int i = 1; i <= 50; i++){
                    System.out.println("线程1---" + i);
                }
            }
        }.start();

        // 2. 创建出第二个Thread线程类的子类对象
        new Thread(){
            @Override
            public void run(){
                for(int i = 1; i <= 50; i++){
                    System.out.println("线程2---" + i);
                }
            }
        }.start();

        // 3. main方法中的循环
        for(int i = 1; i <= 50; i++){
            System.out.println("main---" + i);
        }
    }
}

多线程实现的第二种方式:实现方式

  1. 实现Runnable接口:Runnable接口的实现类对象,表示一个具体的任务,将来创建一个线程对象之后,让线程执行这个任务

  2. 步骤:

    1. 定义一个任务类,实现Runnable接口

    2. 重写任务类中的run方法,用于定义任务的内容

    3. 创建任务类对象,表示任务

    4. 创建一个Thread类型的对象,将任务对象作为构造参数传递,用于执行任务类对象

      Thread(Runnable able);

​ 5.调用线程对象的start方法,开启新线程

		调用的就是Thread类构造方法中传递的able线程任务中的run方法
  1. 使用匿名内部类实现多线程
// 1. 自定义出一个线程类,作为Runnable线程接口的实现类
public class MyRunnable implements Runnable{
    // 2. 重写从父接口继承到的run抽象方法,将需要独立运行的代码写在run中
    @Override
    public void run() {
       for(int i = 1; i <= 20; i++){
           System.out.println("runnable---" + i);
       }
    }
}
public class TestRunnable {
    public static void main(String[] args) {
        // 3. 创建出一个自定义的线程类对象(Runnable接口的直接实现类)
        MyRunnable my = new MyRunnable();
        // 注意 : 因为Runnable接口中,只有一个run方法, 没有开启线程的方式
        // 借助一下Thread线程类的功能
        // 4. Thread构造方法中:
        // Thread(Runnable target): 在Thread类的构造方法中传递一个Runnable的接口
        // 实际传递的是接口的实现类对象,my就符合规则
        // 效果 : 当通过Thread类中的start方法开启线程的时候, 运行的就是构造参数中的线程类
        // run方法功能
        Thread t1 = new Thread(my);
        // 5. t1.start开启线程:
        t1.start();

        for(int i = 1; i <= 20; i++){
            System.out.println("main---" + i);
        }
    }
}

Runnable匿名内部类实现多线程

public class Demo01_匿名内部类实现多线程 {
    public static void main(String[] args) {
        /*
            匿名内部类语法结构:
            new 父类或者父接口(){// 大括号表示父类的子类或者接口的实现类实现过程
                 将方法进行重写;
            }

            上述的语法结构整体 : 是一个匿名内部类对象
            匿名内部类作用 : 作为一个类的子类对象或者是一个接口的实现类对象存在
         */

        // 1. 创建出一个Thread线程类的一个子类对象
        new Thread(){
            @Override
            public void run(){
                for(int i = 1; i <= 50; i++){
                    System.out.println("线程1---" + i);
                }
            }
        }.start();

        // 2. 创建出第二个Thread线程类的子类对象
        new Thread(){
            @Override
            public void run(){
                for(int i = 1; i <= 50; i++){
                    System.out.println("线程2---" + i);
                }
            }
        }.start();

        // 3. Runnable匿名内部类实现多线程
        Runnable able = new Runnable(){
            @Override
            public void run() {
                for(int i = 1; i <= 50; i++){
                    System.out.println("Runnable线程3---" + i);
                }
            }
        };

        Thread t1 = new Thread(able);
        t1.start();

        // 4. main方法中的循环
        for(int i = 1; i <= 50; i++){
            System.out.println("main---" + i);
        }
    }
}

多线程第三种实现:实现Callable接口

  1. 相关方法介绍:
V call(): 计算结果,如果无法计算结果,则抛出一个异常

FutureTask(Callable<V> callable): 创建一个 FutureTask,一旦运行就执行给定的 Callable

V get(): 如有必要,等待计算完成,然后获取其结果
  1. 实现思路:

    如果创建Thread对象,执行Runnable任务,需要Runnable对象

    Runnable是一个接口,有一个特殊的实现类

    FutureTask是Runnable的一个实现类

    FutureTask在创建对象的时候,需要传递一个Callable的对象

    Callable是一个接口,所以我们需要一个Callable的实现类

  2. 实现步骤:

  1. 定义一个类MyCallable实现Callable接口

  2. 在MyCallable类中重写call()方法

  3. 创建MyCallable类的对象

  4. 创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数

  5. 创建Thread类的对象,把FutureTask对象作为构造方法的参数

  6. 使用Thread类的对象调用start方法启动线程

  7. FutureTask对象用get方法,就可以获取线程结束之后的结果。

Callable接口实现过程中,涉及到的类型之间的关系:

1.Runnable线程的顶层父接口
2.JDK提供了Runnable实现类 FutureTask
3.创建出一个线程任务: 可以使用Thread t1 = new Thread(Runnable able); 于是,FutureTask对象可以作为实际参数传递进来
4.FutureTask(Callable callable) : FutureTask构造方法没有空参数的, 构造参数需要Callable接口的实现类对象
5.自定义出一个类MyCallable实现Callable接口, 将唯一抽象方法V call()进行重写
call() 方法的存在意义,就相当于run()方法一样,都是独立的线程通道中执行的代码
不同:
1)run() 方法没有任何返回值结果, 也不能抛出任何异常
2)call() 方法可以有V(泛型)返回值类型, 也可以抛出异常
6.FutureTask中的get()方法, 获取到Callable接口中的call方法的返回值结果

注意 : FutureTask就相当于Callable接口与Thread线程之间的一个桥梁, 用于把两者关联起来,让线程可以执行Callable中的call方法

import java.util.concurrent.Callable;
// 1. 自定义出一个Callable接口的实现类,可以在Callable接口上直接确定出泛型
// 可以不确定Callable接口泛型, 那么call方法返回值结果类型就是Object
public class MyCallable implements Callable<Integer> {

    // 2. 重写call方法功能, 将需要单独运行的代码设计在call方法中
    @Override
    public Integer call() throws Exception {
        int i;
        for(i = 1; i <= 20;i++) {
            System.out.println("callable---" + i);
        }
        return i;
    }
}


import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class TestCallable {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 3. 创建出一个Callable实现类对象
        MyCallable my = new MyCallable();
        // 4. 创建出一个FutureTask对象,就是一个Runnable实现类对象
        // 可以作为Thread线程类的构造方法参数
        FutureTask<Integer> ft = new FutureTask<>(my);
        // 5. 创建出一个线程对象
        Thread t1 = new Thread(ft);
        t1.start();
        // 6. FutureTask类中有一个方法功能 : get,获取到call方法的返回值结果
        Integer i = ft.get();
        System.out.println(i);
        //封装时:  MyCallable--->FutureTask--->Thread--->start()开启线程
        //解析执行时:  start执行--->FutureTask--->调用MyCallable中call
    }
}
Thread线程常用方法
线程API之线程名称
Thread(Runnable r , String name): //传递一个Runnable的任务对象,还可以传递一个name的线程名称
void setName(String name): //将此线程的名称更改为等于参数name
String getName(): //返回此线程的名称
static Thread currentThread(): //返回对当前正在执行的线程对象的引用,那个线程执行这行代码,就返回哪个线程
public class Demo01_线程名称相关方法 {
    public static void main(String[] args) {
        Thread t1 = new Thread(){// {}就是Thread类的子类实现,子类可以直接继承使用
            // 父类Thread中方法功能
            @Override
            public void run(){
               for(int i = 1; i <= 10; i++){
                   // 1. getName() : 表示获取到当前线程的名称
                   System.out.println(getName() + i);
               }
            }
        };
        // 2. setName(String name) : 表示给线程设置一个名字
        t1.setName("小强");
        t1.start();

        // 3. 通过Thread线程类的构造方法给线程设置名字
        Thread t2 = new Thread("小花"){// {}就是Thread类的子类实现,子类可以直接继承使用
            // 父类Thread中方法功能
            @Override
            public void run(){
                for(int i = 1; i <= 10; i++){
                    System.out.println(getName() + i);
                }
            }
        };
        t2.start();

        // 4. 创建出一个Runnable实现类对象
        Runnable able = new Runnable(){
            @Override
            public void run() {
                for(int i = 1; i <= 10; i++){
                    // static currentThread(): 获取当前正在运行的线程对象本身
                    // 第37行代码如果正在运行, 那么一定是在一个线程中运行
                    System.out.println(Thread.currentThread().getName() + i);// 代码37行
                }
            }
        };

        // 5. Thread类的构造方法给Runnable接口的实现类任务设置线程名称
        Thread t3 = new Thread(able,"小able");
        t3.start();
    }
}
线程API之线程休眠
static void sleep(long millis): //使当前正在执行的线程停留(暂停执行)指定的毫秒数

说明: 参数是毫秒值, 1000毫秒 = 1秒, 当线程执行到了sleep方法,休眠指定的毫秒数, 在休眠时间段内, 不再竞争CPU资源, 当休眠时间结束, 可以继续进行CPU资源竞争

实际开发场景中使用 : 例如系统每天凌晨1点都会到指定路径下,下载当前的对账文件(到指定目录下进行文件的读取), 每次文件下载的过程中

先判断文件是否存在

a : 存在, 继续判断文件的大小, length() 返回文件的字节大小

b: 不存在, 先不要报错, 给3次机会(循环), 每次机会之间给1秒钟的停顿时间(休息时间), 1秒钟之后(使用sleep方法,让线程停止1秒,再继续运行),进行第二次文件是否存在验证

public class Demo02_线程休眠方法 {
    public static void main(String[] args) throws InterruptedException {
        new Thread("线程1"){
            @Override
            public void run(){
                for(int i = 1; i <= 5; i++){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(getName() + i);
                }
            }
        }.start();

        for(int i = 1; i <= 5; i++){
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + i);
        }
    }
}
线程API之线程优先级
  1. final int getPriority()
    返回此线程的优先级
    
  2. final void setPriority(int newPriority)
    更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10
    Thread类提供的优先级常量(静态常量)
    MIN_PRIORITY = 1;		最低
    NORM_PRIORITY = 5;		默认
    MAX_PRIORITY = 10;		最高
    

    注意 : 哪怕给线程设定了优先级,没有办法保证,哪些线程一定最先运行; 线程优先级别高的, 获取CPU资源的概率大, 但线程的执行仍然具有很大的随机性

public class Demo03_线程优先级 {
    public static void main(String[] args) {
        Thread t1 = new Thread("最低优先级"){
            @Override
            public void run(){
                for(int i = 1; i <= 10; i++){
                    System.out.println(getName() + i);
                }
            }
        };

        t1.setPriority(Thread.MIN_PRIORITY);

        Thread t2 = new Thread("最高优先级"){
            @Override
            public void run(){
                for(int i = 1; i <= 10; i++){
                    System.out.println(getName() + i);
                }
            }
        };

        t2.setPriority(Thread.MAX_PRIORITY);

        Thread t3 = new Thread("普通优先级"){
            @Override
            public void run(){
                for(int i = 1; i <= 10; i++){
                    System.out.println(getName() + i);
                }
            }
        };

        t3.setPriority(Thread.NORM_PRIORITY);

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

// 获取线程优先级
        System.out.println(t1.getName()+"--"+t1.getPriority());
        System.out.println(t2.getName()+"--"+t2.getPriority());
        System.out.println(t3.getName()+"--"+t3.getPriority());
    }
}
线程API之线程礼让
 static void yield()
线程让步,暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程若队列中没有同优先级的线程,忽略此方法
public class Demo04_礼让线程 {
    public static void main(String[] args) {
        Thread t1 = new Thread("礼让线程"){
            @Override
            public void run(){
                for(int i = 1; i <= 10; i++){
                    System.out.println(getName() + "--" + i);
                }
            }
        };
        // 将t1设置成礼让线程,让同等线程优先级别的其他线程先运行,礼让线程先等着
        // 等别人运行完毕,礼让线程再运行,无法100%礼让,只能说礼让会尽量后执行
        t1.yield();

        t1.setPriority(Thread.MAX_PRIORITY);

        Thread t2 = new Thread("不让线程1"){
            @Override
            public void run(){
                for(int i = 1; i <= 10; i++){
                    System.out.println(getName() + "--" +i);
                }
            }
        };

        t2.setPriority(Thread.MAX_PRIORITY);

        Thread t3 = new Thread("不让线程2"){
            @Override
            public void run(){
                for(int i = 1; i <= 10; i++){
                    System.out.println(getName() + "--" +i);
                }
            }
        };

        t3.setPriority(Thread.MAX_PRIORITY);
        t1.start();
        t2.start();
        t3.start();
    }
}
线程API之线程中断
void interrupt(): 中断这个线程休眠,等待状态。
public class Demo05_线程休眠中断 {
    public static void main(String[] args) {
        Thread t3 = new Thread("不让线程2"){
            @Override
            public void run(){
                for(int i = 1; i <= 10; i++){
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(getName() + "--" +i);
                }
            }
        };

        t3.start();
        t3.interrupt();
    }
}
线程API之后台线程
1.后台线程也叫作守护线程。守护线程是用来提供服务的。
2.守护线程的特点:如果一个程序的运行中,没有非守护线程了,只有守护线程了,那么守护线程会过一段时间后自动停止
3.final void setDaemon(boolean on) : //参数设置为true,将线程设置为守护线程
	将此线程标记为daemon线程或用户线程。当运行的唯一线程都是守护进程线程时,Java虚拟机将退出

   类比守护线程存在:
   下象棋: 
类比非守护线程 : 将/帅, 保证自己可以正常,顺序的运行即可
类比守护线程 : 兵,炮,马,车..., 都可以理解成守护线程, 所有这些存在都是为了守护
帅/将
public class Demo06_守护线程 {
    public static void main(String[] args) {
        Thread t1 = new Thread("守护线程"){
            @Override
            public void run(){
                for(int i = 1; true; i++){
                    System.out.println(getName() + "--" +i);
                }
            }
        };
        // setDaemon(true) : 表示将t1线程设置为守护线程
        t1.setDaemon(true);

        Thread t2 = new Thread("非守护"){
            @Override
            public void run(){
                for(int i = 1; i <= 10; i++){
                    System.out.println(getName() + "--" +i);
                }
            }
        };
        t1.start();
        t2.start();
    }
}
多线程中的线程安全问题
线程安全电影买票案例

要求 : 影院在进行影票销售,卖的票<葫芦娃救爷爷>,一共有票100张,卖票, 可以多渠道购买票,渠道如下 : 美团团,猫眼眼,影院,三个渠道一起销售100张票,一张票只能一个渠道销售

分析:

  1. 美团团,猫眼眼,影院三个销售渠道,模拟成3个线程

  2. 三个渠道共享销售100张票

  3. 销售票的功能应该写在run方法中,三个线程开启,就是为了卖票

public class SaleTickets implements Runnable{
    // tickets表示剩余的票数,因为总票一共100张,初始值为100
    // 定义成成员变量的原因, 就是为了让三个渠道可以共享100张票
    static int tickets = 100;
    // run中的代码表示销售过程
    @Override
    public void run() {
        while(tickets > 0){
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "现在正在销售第"+
                    tickets-- + "张票");
        }
    }
}
public class TicketsTest {
    public static void main(String[] args) {
        SaleTickets st = new SaleTickets();
        Thread t1 = new Thread(st,"美团");
        Thread t2 = new Thread(st,"猫眼");
        Thread t3 = new Thread(st,"影院");

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

销售结果: 出现了多线程处理数据的安全问题
image

线程安全问题的发生原因

某段代码在没有执行完成的时候,cpu就可能被其他线程抢走,结果导致当前代码中的一些数据发生错误

原因:没有保证某段代码的执行的完整性、原子性

希望:这段代码要么全都执行,要么全都没有执行
image

同步代码块
  1. 同步代码块:

使用一种格式,达到让某段代码执行的时候,cpu不要切换到影响当前代码的代码上去

这种格式,可以确保cpu在执行A线程的时候,不会切换到影响A线程执行的其他线程上去

​ 2.使用格式

 synchronized (锁对象) {
  需要保证完整性、原子性的一段代码(需要同步的代码)
}

说明 :

synchronized : 同步的概念,关键字

小括号中,可以设计任意一个对象, 但是保证这个对象是唯一的, 这个对象的作用就像一把锁, 将同步代码块中的代码锁住

将需要保证完整执行代码,设计在同步代码块中

​ 3.使用同步代码块之后的效果:

当cpu想去执行同步代码块的时候,需要先获取到锁对象,获取之后就可以运行代码块中的内容;

当cpu正在执行当前代码块的内容时,cpu可以切换到其他线程,但是不能切换到需要相同锁对象的线程上。

当cpu执行完当前代码块中的代码之后,就会释放锁对象,cpu就可以运行其他需要当前锁对象的同步代码块了

同步代码块解决安全问题的执行原理:
image

public class SaleTickets implements Runnable{
    // tickets表示剩余的票数,因为总票一共100张,初始值为100
    // 定义成成员变量的原因, 就是为了让三个渠道可以共享100张票
    static int tickets = 100;
    // run中的代码表示销售过程
    @Override
    public void run() {
        while(tickets > 0){
            synchronized ("abc"){// 把一张票的完整销售过程,设计在同步代码块中
                 if(tickets > 0){
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                System.out.println(Thread.currentThread().getName() + "现在正在销售第"+
                            tickets-- + "张票");
                }
            }
         }
       }
    }
public class TicketsTest {
    public static void main(String[] args) {
        SaleTickets st = new SaleTickets();
        Thread t1 = new Thread(st,"美团");
        Thread t2 = new Thread(st,"猫眼");
        Thread t3 = new Thread(st,"影院");

        t1.start();
        t2.start();
        t3.start();
    }
}
同步方法
  1. 同步代码块:在某段代码执行的时候,不希望cpu切换到其他影响当前线程的线程上去,就在这段代码上加上同步代码块

  2. 如果某个方法中,所有的代码都需要加上同步代码块,使用同步方法这种【简写格式】来替代同步代码块

  3. 同步方法的格式:

 权限修饰符 [静态修饰符] synchronized 返回值类型 方法名称(参数列表) {
  		需要同步的方法体
}

4.同步方法的锁对象

如果是非静态的方法,同步方法的锁对象就是this,当前对象,哪个对象调用这个同步方法,这个同步方法使用的锁就是哪个对象

如果是静态的方法,同步方法的锁对象就是当前类的字节码对象,类名.class(在方法区的一个对象),哪个类在调用这个同步方法,这个同步方法使用的锁就是哪个类的字节码对象
public class SaleTickets implements Runnable {
    // tickets表示剩余的票数,因为总票一共100张,初始值为100
    // 定义成成员变量的原因, 就是为了让三个渠道可以共享100张票
    static int tickets = 100;

    // run中的代码表示销售过程
    @Override
    public void run() {
        while (tickets > 0) {
            sale2();
        }
    }

    // 定义出一个同步方法 : 方法的修饰符上使用同步关键字synchronized修饰而已
    /*
          修饰符 synchronized 返回值类型 方法名(参数列表){
               需要保证完整代码逻辑,设计在同步方法方法体中;
          }

          1. 如果方法是非静态方法,那么同步方法的锁对象就是this关键字
          2. 如果方法是静态方法,那么同步方法的锁对象就是当前类对应的class字节码对象类对应的class字节码对象 : 一个类如果要运行, 需要进入到方法区, 类.class字节码文件对象就表示类.class文件对象, 在内存中唯一

     */
    public synchronized void sale() {
        // synchronized (this){
        if (tickets > 0) {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "现在正在销售第" +
                    tickets-- + "张票");
        }
    }
    //}

    public static synchronized void sale2() {
        //synchronized (SaleTickets.class) {
            if (tickets > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "现在正在销售第" +
                        tickets-- + "张票");
            }
        //}
    }
}
Lock锁
  1. 概述: 虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

  2. Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作

    Lock锁提供了获取锁和释放锁的方法

void lock(): 获得锁

void unlock(): 释放锁
  1. Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化。

ReentrantLock构造方法: ReentrantLock(): 创建一个ReentrantLock的实例

import java.util.concurrent.locks.ReentrantLock;

public class LockSaleTickets implements Runnable{
    static int tickets = 100;
    ReentrantLock reen = new ReentrantLock();
    @Override
    public void run() {
        while(tickets > 0){
            //synchronized ("abc"){
            try{
                // 获取锁
                reen.lock();
                if(tickets > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                System.out.println(Thread.currentThread().getName() + "现在正在销售第"+
                            tickets-- + "张票");
                }
            }finally{
                // 释放锁,只要获取锁,不管线程运行过程中是否有异常出现,必须要释放锁资源
                reen.unlock();
            }
            //}
        }
    }
}
public class LockTestTickets {
    public static void main(String[] args) {
        LockSaleTickets lst = new LockSaleTickets();

        Thread t1 = new Thread(lst,"美团");
        Thread t2 = new Thread(lst,"猫眼");
        Thread t3 = new Thread(lst,"影院");

        t1.start();
        t2.start();
        t3.start();
    }
}
死锁现象
  1. 死锁的发生: 线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行
    image
public class DeadLock {
    public static void main(String[] args) {
        Object o = new Object();
        Object o1 = new Object();

        new Thread("线程1"){
             @Override
             public void run(){
                synchronized (o){
                    System.out.println(getName() + "已经获取到了A锁,需要B锁");
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (o1){
                      System.out.println(getName() + "已经获取到了A锁和B锁,成功了");
                    }
                }
             }
        }.start();

        new Thread("线程2"){
            @Override
            public void run(){
                synchronized (o1){
                    System.out.println(getName() + "已经获取到了B锁,需要A锁");
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (o){
                      System.out.println(getName() + "已经获取到了A锁和B锁,成功了");
                    }
                }
            }
        }.start();
    }
}

死锁诊断(jstack工具的使用)

1.运行处死锁代码

2.打开命令运行窗口
image

3.执行命令 : jps ,表示输出JVM中运行的进程状态信息,从而获取pid,即进程编号
image

4.指定命令: jstack 指定进程pid, 查看某个Java进程内的某个线程堆栈信息
image

5.命令执行后的效果:
image

线程状态
  1. 线程是一个动态的概念,有创建的时候,也有运行和变化的时候,必须也就有消亡的时候,所以从生到死就是一个生命周期。在声明周期中,有各种各样的状态,这些状态可以相互转换。

  2. 别名:线程的状态图、线程的证明周期图、线程的状态周期

状态罗列:

新建态:刚创建好对象的时候,刚new出来的时候

就绪态:线程准备好了所有运行的资源,只差cpu来临

运行态:cpu正在执行的线程的状态

阻塞态:线程主动休息、或者缺少一些运行的资源,即使cpu来临,也无法运行

死亡态:线程运行完成、出现异常、调用方法结束
image

​ 3.JDK中对于线程状态的描述:

Thread类中,内部的枚举类型: State, 用于表示一个线程可以出现的所有状态

因为线程的状态场景目前是固定的, 因此使用enum枚举类型表示个数固定的对象存在方式
image

线程状态解释说明:

NEW

​ 一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程象,没有线程特征。

RUNNABLE

​ 当我们调用线程对象的start方法,那么此时线程对象进入了RUNNABLE状态。那么此时才是真正的在JVM进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令与CPU的调度,那么我们把这个中间状态称之为可执行状态(RUNNABLE)也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的度。

BLOCKED

​ 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。

WAITING

​ 一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有两种,分别是调用Object.wait()、join()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。

TIMED_WAITING

​ 一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:Thread.sleep(long),Object.wait(long)、join(long)。

TERMINATED

​ 一个完全运行完成的线程的状态。也称之为终止状态、结束状态

线程池
线程池概述

提到池,大家应该能想到的就是水池。水池就是一个容器,在该容器中存储了很多的水。那么什么是线程池呢?线程池也是可以看做成一个池子,在该池子中存储很多个线程。

线程池存在的意义:

​ 1.系统创建一个线程的成本是比较高的,因为它涉及到与操作系统交互,当程序中需要创建大量生存期很短暂的线程时,频繁的创建和销毁线程对系统的资源消耗有可能大于业务处理效率

​ 2.统资源的消耗,这样就有点"舍本逐末"了。针对这一种情况,为了提高性能,我们就可以采用线程池。线程池在启动的时,会创建大量空闲线程,当我们向线程池提交任务的时,线程池就会启动一个线程来执行该任务。等待任务执行完毕以后,线程并不会死亡,而是再次返回到线程池中称为空闲状态。等待下一次任务的执行。
image

Executors创建线程池

1、步骤:获取线程池对象;创建任务类对象;将任务类对象提交到线程池中

2、获取线程池对象:

工具类:Executors:生成线程池的工具类,根据需求生成指定大小的线程池

 ExecutorService  Executors.newFixedThreadPool(int nThreads)://创建一个指定线程数量的线程池

3、创建任务类对象:Runnable的实现类对象,用于定义任务内容

将任务类对象提交到线程池中

ExecutorService://是一个接口,不需要手动创建这个接口的实现类对象,使用方法获取到的就是这个接口的实现类对象,一定可以调用这个接口中的方法

 submit(Runnable r)://可以将一个任务类对象,提交到线程池中,如果有空闲的线程,就可以马上运行这个任务,如果没有空闲线程,那么这个任务就需要等待。

 shutDown()://结束线程池,已经提交的全部保证完成,不准继续提交了

 shutDownNow()://结束线程池,已经开始运行的,保证完成;但是还没有运行的,已经提交的,不给运行了,作为返回值范围;对于没有提交的,不准提交。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo01_ThreadPool {
    public static void main(String[] args) {
        // 1. 创建出一个具有指定线程个数的线程池
        // 方法的返回值结果类型 ExecutorService接口,主要功能就是可以操作线程池中的线程使用
        ExecutorService es = Executors.newFixedThreadPool(2);
        // 2. 创建出一些线程任务
        Runnable able1 = new Runnable(){
            @Override
            public void run() {
                for(int i = 1; i <= 5; i++){
                    System.out.println(Thread.currentThread().getName() + "--"+i);
                }
            }
        };
        Runnable able2 = new Runnable(){
            @Override
            public void run() {
                for(int i = 1; i <= 5; i++){
                    System.out.println(Thread.currentThread().getName() + "--"+i);
                }
            }
        };
        Runnable able3 = new Runnable(){
            @Override
            public void run() {
                for(int i = 1; i <= 5; i++){
                    System.out.println(Thread.currentThread().getName() + "--"+i);
                }
            }
        };
        Runnable able4 = new Runnable(){
            @Override
            public void run() {
                for(int i = 1; i <= 5; i++){
                    System.out.println(Thread.currentThread().getName() + "--"+i);
                }
            }
        };
        // 3. 使用submit(Runnable able) : 将参数中线程任务提交到线程池进行运行
        // 1) 如果有空闲线程, 马上运行  2) 没有空闲线程, 排队等待
        es.submit(able1);
        es.submit(able2);
        es.submit(able3);
        es.submit(able4);

        // 4. 发现 : 线程池使用完毕代码没有停, 线程池中的线程以队列方式一直等待给新的线程任务
        // 因此程序没有停止
        // 解决方案 : 当所有线程任务执行完毕,销毁线程池
        // ExecutorService :
        // 1) shutdown() : 将所有排队任务都完成之后,再销毁线程池
        // 2) shutdownNow() : 现在马上停止线程池, 正在运行的线程保证完成, 排队的,还没有执行的
        // 不执行了
        // es.shutdown();
        es.shutdownNow();
    }
}
单例模式

单例模式设计思路:

  1. 私有构造方法

  2. 在本类中创建出一个唯一的私有对象

  3. 为其他的外类提供唯一对象的公共方法方式

饿汉式

定义一个类型, 这个类型一旦加载,马上将这个类型的唯一对象给你

类型的唯一对象马上就要给出,不能等待,非常饥渴,称为饿汉式

实现步骤:

  1. 构造方法私有

  2. 在当前类中创建出唯一的一个对象, 使用private static 修饰

  3. 提供对外的访问唯一对象的公共方式

// 饿汉式单例模式: 因为单例模式必须要准守的就是构造方法私有化, 外类不能创建对象
// 因此,类型中所有成员变量,方法,都是static静态修饰
public class SingletonHunery {
    // 1. 构造方法私有化 : 为了不让直接new对象
    private SingletonHunery(){}

    // 2. 在本类中创建出一个唯一的对象: 私有化为了让其他外类中,不能为sh赋值为null
    // 于是私有修饰,不让外类直接访问
    private static SingletonHunery sh = new SingletonHunery();

    // 3. 给唯一的私有成员对象提供对外的公共的访问方式
    public static SingletonHunery getInstance(){
        return sh;
    }
}
public class TestSingleton {
    public static void main(String[] args) {
        SingletonHunery s = SingletonHunery.getInstance();
        SingletonHunery s1 = SingletonHunery.getInstance();
        System.out.println(s == s1);// true

        // 给SingletonHunery对应的对象赋值为null,不安全的,要的是一个对象,不是null
        // SingletonHunery.sh = null;
    }
}
懒汉式

懒汉式 : 定义出一个类型, 先不创建对象, 什么时间需要这个唯一的对象,才去给你创建

能不创建就不去创建

实现过程:

  1. 创建私有的构造,为了不能在其他类型中创建对象

  2. 在类型中声明一个私有的,静态的类型对象

  3. 提供对外的公共访问唯一对象的方法

    1. 为了不重复创建对象,方法中需要判断对象的值不为null
    2. 为了多线程对象唯一,需要在方法中添加同步代码块,保证多线程也只有一个对象
    3. 因为线程安全的同步代码块性能差,因此在同步外格外添加对象不为null判断,只能为null的时候,才会进入到同步代码块中对象的创建,以后都不会再进入同步

市面上也将这种双层判断对象不为null的方式成为 : 双重判断锁

public class SingletonLazy {
    // 1. 构造方法私有化
    private SingletonLazy(){}

    // 2. 在成员变量位置上做当前类型唯一对象的声明
    private static SingletonLazy sl;

    // 3. 提供出一个唯一的对象创建,对外提供公共的访问到唯一对象方式
    public static SingletonLazy getInstance(){
        // 4. 问题: 线程不安全,因此添加一个同步代码块保证单例模式在多线程下成立
        // 如果直接在getInstance方法中使用一个同步代码块保证线程安全,但是执行效率很低
        // 每一次同步代码块必须经过三个步骤 : 1) 判断锁  2) 获取锁  3) 同步结束,归还锁
        // 为了进一步的提高代码效率,因此在同步代码块之外,再进行一次sl非空验证
        // 此种经典的懒汉式实现 : 双重判断锁
        if(sl == null){
            synchronized (SingletonLazy.class){
                if(sl == null){
                    sl = new SingletonLazy();
                }
            }
        }
        return sl;
    }
}
public class TestSingleton {
    public static void main(String[] args) {
        // 1. 饿汉式单例模式验证
        SingletonHunery s = SingletonHunery.getInstance();
        SingletonHunery s1 = SingletonHunery.getInstance();
        System.out.println(s == s1);// true

        // 给SingletonHunery对应的对象赋值为null,不安全的,要的是一个对象,不是null
        // SingletonHunery.sh = null;

        // 2. 懒汉式单例模式的验证
        SingletonLazy sl = SingletonLazy.getInstance();
        SingletonLazy sl2 = SingletonLazy.getInstance();
        System.out.println(sl == sl2);// true

        /*
            懒汉式和饿汉式比较:
            1) 饿汉式: 拿空间换时间,因为饿汉式的类型一旦进入内存,马上创建出一个唯一对象
               , 这个对象不管后面是否使用,已经占有内容, 但是后面获取对象速度快
            2) 懒汉式 : 拿时间换空间,因为懒汉式类型进入内存, 没有创建对象,内存节省, 用对象的时候
            ,采取创建, 但是逻辑上需要双重判断锁, 因此执行时间上长

            总体来讲: 饿汉式更好,因为时间过了就没了,内存空间可以有优化和空充余地
         */
    }
}
老汉式

私有构造

创建一个唯一对象方式 : 类中只有一个对象,并且还是public static final修饰

public class SongletonOldMan {
    // 1. 构造方法私有
    private SongletonOldMan(){}

    // 2. 在成员位置创建出唯一的一个对象
    public static final SongletonOldMan SO = new SongletonOldMan();
}
public class TestSingleton {
    public static void main(String[] args) {
        // 3. 老汉式单例模式验证
        SongletonOldMan m1 = SongletonOldMan.SO;
        SongletonOldMan m2 = SongletonOldMan.SO;
        System.out.println(m1 == m2);// true
    }
}
枚举的定义特点以及常用方法
枚举的概述
  1. 枚举类型使用 enum 关键字定义

定义格式:

public enum 枚举名{  
	枚举项1,枚举项2,枚举项3;
}

对比类和接口的定义:

public class 类名{}
public interface 接口名{}
  1. 枚举的使用场景

有些情况下,类型中可以创建的对象的个数是固定的,就可以使用枚举类型

举例 : 星期类型----> 只有周一到周日,一共只有7天---->星期类型的对象只能有7个,7个对象分别对应周一到周日

​ 月份类型-----> 只有12个月---->对应只能创建12个对象,对应1月到12月

​ 季节类型-->只有春夏秋冬

  1. 枚举类型,源文件.java , 编译后的文件.class
枚举的特点
  1. 所有枚举类都是Enum的子类

  2. 我们可以通过"枚举类名.枚举项名称"去访问指定的枚举项

  3. 每一个枚举项其实就是该枚举的一个对象, 枚举项写作方式: 枚举对象名称,枚举对象名称,...最后一个对象;

  4. 枚举类的第一行上必须是枚举项,最后一个枚举项后的分号是可以省略的,但是如果枚举类有其他的东西,这个分号就不能省略。建议不要省略

  5. 枚举也是一个类,也可以去定义成员变量

  6. 枚举类可以有构造器,但必须是private的,它默认的也是private的.

  7. 枚举类也可以有抽象方法,但是枚举项必须重写该方法

    案例 : 使用枚举类型, 模拟星期使用, 星期类型一共可以创建出7个对象,表示星期一---星期日

    枚举类型实现代码

// 枚举类型使用 : 如果类型创建的对象个数固定, 可以直接使用枚举类型实现
// 枚举类型实际上就是多例模式
public enum WeekdayEnum {
    /*
     public static final WeekdayOldMan MON = new WeekdayOldMan();
     public static final WeekdayOldMan TUE = new WeekdayOldMan();
     public static final WeekdayOldMan WEN = new WeekdayOldMan();
     */
    // 1. 枚举项 : 就是枚举类型的对象,默认使用public static final修饰的对象成员变量
    // 切记 : 枚举项必须在枚举类型有效行第一行
    // 枚举项实际上调用枚举类型中指定构造方法, 默认调用空参数构造
    // 如果调用时有参数构造, 就需要通过构造方法为成员变量赋值
    MON("星期一"){
        @Override
        public void show() {
            System.out.println("枚举类型实现的星期一");
        }
    },TUE("星期二"){
        @Override
        public void show() {
            System.out.println("枚举类型实现的星期二");
        }
    },WEN("星期三"){
        @Override
        public void show() {
            System.out.println("枚举类型实现的星期三");
        }
    };

    // 2. 枚举类型中: 可以有普通成员变量
    private String weekdayName;

    // 3. 枚举类型中 : 可以有普通方法
    public String getWeekdayName() {
        return weekdayName;
    }

    public void setWeekdayName(String weekdayName) {
        this.weekdayName = weekdayName;
    }

    // 4. 枚举类型中: 可以有构造, 因为枚举类型中可以定义出成员变量
    // 注意 : 枚举类型中构造方法都是私有的
    // private WeekdayEnum(){}

    // 构造方法可以重载
     private WeekdayEnum(String weekdayName){
        this.weekdayName = weekdayName;
     }

     // 5. 枚举类型中 : 可以直接定义抽象方法
     // 但是需要在枚举项中将抽象方法进行重写
     public abstract void show();
}

老汉式实现代码

public abstract class WeekdayOldMan {
    public static final WeekdayOldMan MON = new WeekdayOldMan("Old星期一"){
        @Override
        public void show() {
            System.out.println("老汉式输出的星期一");
        }
    };
    public static final WeekdayOldMan TUE = new WeekdayOldMan("Old星期二"){
        @Override
        public void show() {
            System.out.println("老汉式输出的星期二");
        }
    };
    public static final WeekdayOldMan WEN = new WeekdayOldMan("Old星期三"){
        @Override
        public void show() {
            System.out.println("老汉式输出的星期三");
        }
    };

    // private WeekdayOldMan(){}
    private WeekdayOldMan(String weekdayName){
        this.weekdayName = weekdayName;
    }

    private String weekdayName;

    public String getWeekdayName() {
        return weekdayName;
    }

    public void setWeekdayName(String weekdayName) {
        this.weekdayName = weekdayName;
    }

    public abstract void show();
}

测试类代码

public class TestEnum {
    public static void main(String[] args) {
        // 1. 老汉式多例模式
        WeekdayOldMan m1 = WeekdayOldMan.MON;
        WeekdayOldMan m2 = WeekdayOldMan.MON;
        System.out.println(m1 == m2);// true
        System.out.println(m1.getWeekdayName());// Old星期一
        m1.show();// 老汉式输出的星期一

        // 2. 枚举类型测试枚举项就是枚举类型对象
        WeekdayEnum enumM1 = WeekdayEnum.MON;
        WeekdayEnum enumM2 = WeekdayEnum.MON;
        System.out.println(enumM1 == enumM2);// true
        System.out.println(enumM1.getWeekdayName());// 星期一
        enumM1.show();// 枚举类型实现的星期一
    }
}
枚举类型中的常用方法

Java中所有枚举类型,父类Enum抽象类

ordinal(): //获取枚举类型中的枚举序数,序数根据定义的枚举项,从0开始,返回值int类型
compareTo(E o) : //比较枚举项之间的顺序大小,方法调用枚举项的序数减去参数枚举项的序数
name() : //将枚举项转换成String类型
toString() : //将枚举项转换成String类型
static values() : //将一个枚举类型中的所有枚举项获取到,返回值类型枚举类型的数组
import java.util.Arrays;

public class EnumMethod {
    public static void main(String[] args) {
        // 1. ordinal(): 获取枚举类型中的枚举序数,序数根据定义的枚举项,从0开始,返回值int
        //类型
        System.out.println(WeekdayEnum.TUE.ordinal());// 1
        // 2. compareTo(E o) : 比较枚举项之间的顺序大小,方法调用枚举项的序数减去参数枚举项的序数
        //                         0           -                  2
        System.out.println(WeekdayEnum.MON.compareTo(WeekdayEnum.WEN));// -2
        // 3. name() : 将枚举项转换成String类型
        // toString(): 将枚举项转换成String类型
        System.out.println(WeekdayEnum.TUE.name());// TUE
        System.out.println(WeekdayEnum.TUE.toString());// TUE

        // 4. static values() : 表示获取到一个枚举类型中的所有枚举项
        // 返回值结果 : 枚举类型的数组
        WeekdayEnum[] arr = WeekdayEnum.values();
        System.out.println(Arrays.toString(arr));
    }
}
posted @ 2021-10-28 20:03  昊子豪  阅读(64)  评论(0编辑  收藏  举报