Skip to content

一、复习

1.1 哈希表原理

1、哈希表(HashMap)的底层是由数组 + 单链表 + 红黑树 组成

2、哈希表中使用数组的目的:根据下标访问操作的效率高,时间复杂度是O(1)。这里的数组是散列存储,根据 key 的哈希值来计算索引位置。index = hash(key) & (table.length-1)

3、万一两个或多个不同的(key,value)被映射到同一个 index 位置,称为哈希冲突。当哈希冲突的多个(key,value)要在同一个 table[index]位置存储的话,必须使用单链表或红黑树。

4、当 table[index]下的(key,value)的数量在 8 以下,使用 单链表。当 table[index]下的(key,value)的数量 达到 8 时,还要看 table.length 是否达到 64,如果达到 64,就会把 table[index]下单链表转为红黑树。

5、哈希表中的数组长度一定是 2 的 n 次方,每次扩容也是 2 倍扩容,保证一直是 2 的 n 次方。默认的初始长度为 16。数组的扩容有 2 种情况:

  • 正常情况:size >= threshold(size 是哈希表中所有键值对数量,不是某一个 table[index]桶的数量) 。threshold = table.length * loadfactor(loadfactor 的默认值是 0.75)。
  • 特殊情况:当 table[index]下的(key,value)的数量 达到 8,table.length 未达到 64,现在要往 table[index]位置继续添加(key,value),此时哈希表会扩容,试图分散 table[index] 的 键值对。

6、当 table[index]下的树的结点数量 小于等于 6 ,此时继续 put,触发扩容,会进行反树化。当 table[index]下的树的结点数量 比较少,此时 remove 时,遇到根结点、根的左结点、根的右结点、根的左左结点之一出现 null,会进行反树化。

7、key 的类型通常需要重写 hashCode 和 equals 方法,它们一起重写时要保证:

  • 两个“相等的 equals 为 true”的 key 对象,hashCode 一定是相同的
  • 两个“不相等的 equals 为 false”的 key 对象,hashCode 可能相同,可能不同
  • 两个 hashCode 不同的 key 对象,此时它们 equals 结果一定是 false

在哈希表结构中,先看 key 的 hashCode,如果 hashCode 不同,可能散列到不同的位置,也可能散列到相同的位置,但是此时无论它们在不在一个位置,都不需要调用 equals 方法了。如果 hashCode 相同,再次调用 equals 确定 key 是否重复。

1.2 Lambda 表达式

1、Lambda 表达式的作用

Lambda 表达式是一种语法糖,用于简化 匿名内部类实现函数式接口的代码。函数式接口是有且只有 1 个抽象方法必须重写。函数式接口通常有@FunctionalInterface 注解标记。

2、函数式接口的代表们

(1)Comparator<T>,抽象方法:int compare(T t1, T t2)

(2)Consumer<T>,抽象方法: void accept(T t) 有参无返回值

(3)Predicate<T>,抽象方法:boolean test(T t) 有参有返回值,返回值类型是 boolean

(4)Function<T,R>,抽象方法: R apply(T t) 有参有返回值

  • Operator 结尾,T apply(T t),参数类型与返回值类型相同
  • 包含 Unary,参数只有 1 个,且参数类型与返回值类型相同
  • 包含 Binary 或 Bi,参数有 2 个

(5)Supplier<T>,抽象方法:T get() 无参有返回值

.....

3、Lambda 表达式的语法格式

java
(形参列表) -> {方法体;}
  • 当形参的类型可以通过泛型自动推断的时候,类型可以省略
  • 当参数只有 1 个,且类型省略的情况下,()可以省略
  • 当{ 方法体} 只有 1 个语句时, {} 和这个语句的; 也可以省略,如果这个语句还是 return 语句,那么 return 也得省略。

4、方法引用

方法引用的作用是简化部分 Lambda 表达式。语法格式:

  • 类名::方法名
  • 对象名::方法名
  • 类名::new

当 Lambda 表达式的{方法体}只有 1 个语句,且是调用一个类的静态方法或一个对象的实例方法或在 new 一个对象,且调用方法用到的对象或参数全部来自于 Lambda 表达式的(形参列表),没有额外数据参与,那么就可以使用方法引用。

1.3 StreamAPI

第一步:创建 Stream

  • 针对数组:Arrays.stream(数组)
  • 针对 Collection 系列集合:集合.stream()
  • Stream.of(元素列表)
  • Stream.generate (...)Stream.iterate(...)

第二步:加工处理。这些方法的返回值类型一定 Stream

  • 过滤:filter
  • 去重:distinct
  • 跳过:skip
  • 限制几个:limit
  • 排序:sort
  • 查看:peek
  • 映射:map 和 flatMap

第三步:终结。这些方法的返回值类型一定不是 Stream

  • 匹配:allMatch、anyMatch、noneMatch
  • 最大值最小值:max、min
  • 统计数量:count
  • 遍历:forEach
  • 收集:collect 配合 Collectors 工具类

二、多线程

2.1 多线程的概念

  • 程序(Program):为了实现某个功能,或者为了完成某个任务,采用一种编程语言(可能 Java、可能是 C 等)编写的一段代码,然后被编译器或解释器处理为一组指令,这组指令可以被 CPU 执行。这个程序如果还未启动,它只占用硬盘的存储空间,不会占用内存等其他资源。

  • 进程(Process):应用程序一旦启动,操作系统 OS 就会给它创建一个进程,因为需要管理这个程序占用的各种资源,例如:内存资源,IO 资源,网络资源等。每一个进程有自己唯一的 ID。每一个进程是互相独立的。进程是操作系统分配和管理资源的最小单位。同一个程序被启动 2 次,会有 2 个进程。

  • 线程(Thread):一个应用程序至少有一条执行路径,即至少有 1 个线程。很多应用程序会同时拥有多条同时执行的执行路径,就称为多线程程序。

  • 串行(serial):一个任务完成再完成下一个

  • 并行(parallel):多个任务同时在多个 CPU 中一起执行

  • 并发(concurrent):一个 CPU 快速的在多个任务之间切换

image-20250721093753855

2.2 如何编写多线程的程序

共有 4 种方式:

  • 继承 Thread 类
  • 实现 Runnable 接口
  • 实现 Callable 接口(后期讲解)
  • 使用线程池(后期讲解)

2.2.1 继承 Thread 类

步骤:

  • 编写一个类(有名或匿名)继承 Thread 类

  • 必须重写 public void run() 方法,重写的快捷键 Ctrl + o

    • 你想让这个线程干什么,就在 run 方法里面写什么
  • 创建这个线程类的对象

  • 调用线程类对象的 start 方法,千万不要直接调用 run 方法。

image-20250721095324145

java
package com.atguigu.thread;

public class MyThread extends Thread{
    @Override
    public void run() {
        //例如:我想让一个线程,帮我打印1-100的偶数
        for(int i=2; i<=100; i+=2){
            try {
                Thread.sleep(10);//10毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("偶数even:" + i);
        }
    }
}
java
package com.atguigu.thread;

public class TestMyThread {
    //测试多线程的代码,不要用JUnit,否则会有很多问题
    //main方法也是一条执行路径,是主要的执行路径,称为主线程
    public static void main(String[] args) {
        MyThread my = new MyThread();
//        my.run();//错误,这个无法实现多线程并发执行
        my.start();


        //例如:我想让主线程,帮我打印1-100的奇数
        for(int i=1; i<=100; i+=2){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("奇数odd:" + i);
        }
    }
}

2.2.2 实现 Runnable 接口

步骤:

  • 编写一个类(有名或匿名)实现 Runnable 接口

  • 必须重写 public void run() 方法,重写的快捷键 Ctrl + I

    • 你想让这个线程干什么,就在 run 方法里面写什么
  • 创建这个线程类的对象,例如:my

  • 创建了一个 Thread 类的对象,例如:thread,把 my 作为参数传给 thread 的 target 属性

  • 调用 thread 的 start 方法

java
package com.atguigu.thread;

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        //例如:我想让一个线程,帮我打印1-100的偶数
        for(int i=2; i<=100; i+=2){
            try {
                Thread.sleep(10);//10毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("偶数even:" + i);
        }
    }
}
java
package com.atguigu.thread;

public class TestMyRunnable {
    public static void main(String[] args) {
        MyRunnable my = new MyRunnable();
//        my.start();//错误,因为MyRunnable,及其父类、父接口都没有start方法


        Thread thread = new Thread(my);
        thread.start();
        /*
        thread在这里就充当了代理的角色,或者担保人。
        new Thread(my) 这句代码,为thread对象的target属性赋值, target就相当于被代理者目标

        其实这里启动的是thread线程,线程调度器会去调用 thread的run方法。
        但是thread的run方法代码如下:
            public void run() {
                if (target != null) {
                    target.run();
                }
            }
       因为thread的target属性现在不为null,它target其实就是my。在thread的run方法中帮我们执行了target即my的run
         */

        //例如:我想让主线程,帮我打印1-100的奇数
        for(int i=1; i<=100; i+=2){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("奇数odd:" + i);
        }
    }
}

2.2.3 匿名内部类实现多线程

java
package com.atguigu.thread;

public class TestAnonymous {
    public static void main(String[] args) {
        //1、匿名内部类继承Thread类
/*        new Thread(){
            @Override
            public void run() {
                //例如:我想让一个线程,帮我打印1-100的偶数
                for(int i=2; i<=100; i+=2){
                    try {
                        Thread.sleep(10);//10毫秒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("偶数even:" + i);
                }
            }
        }.start();*/
        //类是匿名的,对象也是匿名的
       Thread t =  new Thread(){
            @Override
            public void run() {
                //例如:我想让一个线程,帮我打印1-100的偶数
                for(int i=2; i<=100; i+=2){
                    try {
                        Thread.sleep(10);//10毫秒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("偶数even:" + i);
                }
            }
        };
       t.start();
       //类是匿名的,对象是有名字的,是t

        //2、匿名内部类实现Runnable接口
        /*Runnable r = new Runnable(){
            @Override
            public void run() {
                //例如:我想让一个线程,帮我打印1-100的奇数
                for(int i=1; i<=100; i+=2){
                    try {
                        Thread.sleep(10);//10毫秒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("奇数odd:" + i);
                }
            }
        };
        Thread t2 = new Thread(r);
        t2.start();*/


        /*new Thread(new Runnable(){
            @Override
            public void run() {
                //例如:我想让一个线程,帮我打印1-100的奇数
                for(int i=1; i<=100; i+=2){
                    try {
                        Thread.sleep(10);//10毫秒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("奇数odd:" + i);
                }
            }
        }).start();*/

        new Thread(() ->{
                //例如:我想让一个线程,帮我打印1-100的奇数
                for(int i=1; i<=100; i+=2){
                    try {
                        Thread.sleep(10);//10毫秒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("奇数odd:" + i);
                }
            }
        ).start();//Lambda表达式写法
    }
}

2.3 Thread 类的部分方法

  • 线程名称的 get/set 方法

  • Thread 类的静态方法 currentThread() :获取当前线程对象。当前线程是指正在执行这句语句的线程对象。

  • Thread 类的静态方法 sleep(时间):线程休眠

  • Thread 类的非静态方法 join(时间)join():线程阻塞,加塞

  • Thread 类的非静态方法stop():线程停止

    • 不建议直接使用 stop 方法
    • 而是用变量或其他方法来控制线程的结束条件
  • Thread 类的非静态方法:setDaemon(true或false),设置当前线程为守护线程或非守护线程。

    • 什么是守护线程?守护线程是一种特殊的线程,它不会独立存在于 JVM 中,如果其他非守护线程结束了,守护线程自动结束。通常守护线程都是为其他线程服务的,例如:GC 线程、异常检测线程等

示例代码 1

java
package com.atguigu.thread;

/*
线程调度器只会调用 run方法,如果我的方法是别的,它不会帮我们自动调用,
我们必须手动调用。
 */
public class SubThread extends Thread{
    @Override
    public void run() {
        //打印我是xx线程
        System.out.println("我是" + getName() + "线程");
        //getName()从Thread继承的
    }
}
java
package com.atguigu.thread;

public class SubRunnable implements Runnable{
    @Override
    public void run() {
        //打印我是xx线程
//        System.out.println("我是" + getName() + "线程");
        //Object父类没有getName()
        //Thread类才有这个方法。

        Thread thread = Thread.currentThread();
        System.out.println("我是" + thread.getName() + "线程'");
    }
}
java
package com.atguigu.thread;

public class TestMethods  {
    public static void main(String[] args) {
        SubThread sub1 = new SubThread();
        SubThread sub2 = new SubThread();
        SubThread sub3 = new SubThread();
        SubThread sub4 = new SubThread();

        sub1.setName("老大");

        sub1.start();
        sub2.start();
        sub3.start();
        sub4.start();
        //有5个线程
        /*
        线程的默认名称是 Thread-编号,编号从0开始
         */
        Thread thread = Thread.currentThread();
        System.out.println("主线程的名字:" + thread.getName());

        SubRunnable sub5 = new SubRunnable();
       Thread t = new Thread(sub5);
       t.start();
    }
}

示例代码 2

java
package com.atguigu.thread;

public class TestSleep {
    public static void main(String[] args) {
        for(int i=1; i<=10; i++){
            System.out.println(i);
            try {
                Thread.sleep(1000);//休眠1秒 = 1000毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

示例代码 3

java
package com.atguigu.thread;

public class TestStop {
    public static void main(String[] args) {
        Thread t1 = new Thread(){
            @Override
            public void run() {
               for(int i=1; i<=10; i++){
                   System.out.println(i);
                   try {
                       Thread.sleep(1000);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
            }
        };
        t1.start();

        //主线程等5秒,让t1线程停下来
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t1.stop();//已过时,最好别用
    }
}
java
package com.atguigu.thread;

public class TestStop2 {
    private static boolean flag = true;
    public static void main(String[] args) {
        Thread t1 = new Thread(){
            @Override
            public void run() {
               for(int i=1; i<=10; i++){
                   System.out.println(i);
                   if(!flag) {
                       return;
                   }
                   try {
                       Thread.sleep(1000);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
            }
        };
        t1.start();

        //主线程等5秒,让t1线程停下来
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = false;
    }
}

示例代码 4

java
package com.atguigu.thread;

public class TestJoin {
    public static void main(String[] args) {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                for(int i=2; i<=100; i+=2){
                    System.out.println("偶数:" + i);

                }
            }
        };
        t1.start();

        Thread t2 = new Thread(){
            @Override
            public void run() {
                for(int i=1; i<=100; i+=2){
                    System.out.println("奇数:" + i);
                    try {
                        Thread.sleep(10000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t2.start();

        try {
            t1.join();//main线程要等t1执行完才能继续,如果t1不完事,main不继续
                    //此时与t2线程无关
            //t1插队到main前面
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        try {
//            t2.join();//main线程要等t2执行完才能继续,如果t2不完事,main不继续
            //此时与t1线程无关
            //t2插队到main前面
            //t1和t2还是竞争关系

            t2.join(30000);//t2只阻塞main线程30秒,main恢复自由
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //等偶数和奇数都打印完,接着打印26个字母
        for(char letter = 'a'; letter<='z'; letter++){
            System.out.println(letter);
        }
    }
}

示例代码 5

java
package com.atguigu.thread;

public class TestDaemon {
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                while(true){
                    System.out.println("我爱尚硅谷");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t.setDaemon(true);//让t称为守护线程
        t.start();

        for(int i=1; i<=10; i++){
            System.out.println(i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

以下是错误示例:

java
package com.atguigu.thread;

import org.junit.jupiter.api.Test;

public class TestJUnit {
    @Test
    public void test1(){
        Thread t = new Thread(){
            @Override
            public void run() {
                while(true){
                    System.out.println("我爱尚硅谷");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t.start();
        //在JUnit单元测试方法中t线程默认是守护线程

        /*for(int i=1; i<=10; i++){
            System.out.println(i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }*/
    }
}

2.4 线程安全

像 ArrayList 等集合,或第 1 代第 2 代的日期时间类等,StringBuilder 等可变字符串等,它们都是线程不安全的,当多个线程同时操作它们的同一个对象时,就会出现数据不一致等问题,就是线程不安全的现象。

面试题:哪几个集合是线程安全的?

List:Vector、Stack

Map:Hashtable、Properties、ConcurrentHashMap

2.4.1 如何解决线程安全问题

案例:卖票问题

现在有 100 张票,分为多个渠道(多个线程)同时卖票。

1、如何判断自己的代码有没有线程安全问题

  • 是不是存在多个线程
  • 多个线程有没有“共享”数据
  • 多个线程对共享数据是否有修改或写操作

只要上面的 3 个条件同时存在,那么你的代码一定有线程安全问题的隐患。

2、如何解决?

需要给 某段操作共享数据的代码 加锁。这段代码同一个时间只能被 1 个线程执行,不能被多个线程同时执行。

Java基础阶段,加锁的方式只有 1 种,同步锁 synchronized 。

3、synchronized 的原理

synchronized 比喻:厕所所长

synchronized 会选择一个 对象,这个对象就是厕所所长,它的职责是记录现在哪个线程 进入了 加锁的代码(比喻进入卫生间上厕所)。

只要有一个线程进入执行了这段代码,其他线程只能等待,等待这个线程出来,厕所所长就会擦除它的名字,下一个线程再进去。

4、如何使用 synchronized

(1)同步代码块的语法格式:

java
synchronized(所长对象){
	//需要加锁的代码
}

对于同步代码块来说,厕所所长(又称为监视器对象)是由编写同步代码块的程序员选择的。

(2)同步方法的语法格式:

java
【其他修饰符】 synchronized 返回值类型 方法名(【形参列表】)【throws 异常列表】{
    //整个方法体都需要加锁
}

对于同步方法来说,厕所所长是默认的,不可更改的:

  • 静态方法:所长是当前类的 Class 对象。每一个类被类加载器加载到 JVM 中之后,都会有唯一的 Class 对象来表示它。有 10 个类,就有 10 个 Class 对象,每一个类都有自己独立的 Class 对象,只要类相同,Class 对象就相同,类不同 Class 对象就不同。关于 Class 对象的更多细节,请看反射章节。
  • 非静态方法:所长是 this 对象

5、锁的代码范围如何确定?

单次线程任务代码,这一次任务代码是一个整体,具有不可拆分的原子性的特征。

6、选谁当厕所所长

原则:是不是多个线程 同时承认的同一个锁对象。至于锁对象的类型不重要。

2.4.2 示例代码

示例 1:有安全问题(ArrayList)

java
package com.atguigu.unsafe;

import java.util.ArrayList;

/*
需求:有一个ArrayList集合,现在有2个线程同时往集合中添加元素,
每一个线程都添加1000个元素,查看最后集合中有多少个元素?
 */
public class TestArrayList {
    private static ArrayList<Integer> list = new ArrayList<>();

    //    private static Vector<Integer> list = new Vector<>();
    public static void main(String[] args) {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                for (int i = 1; i <= 1000; i++) {

                        list.add(i);

                }
            }
        };
        t1.start();

        Thread t2 = new Thread() {
            @Override
            public void run() {
                for (int i = 1001; i <= 2000; i++) {
                       list.add(i);

                }
            }
        };
        t2.start();

        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("list的元素总个数:" + list.size());
    }
}

示例 2:没有安全问题(解决示例 1 问题)

java
package com.atguigu.unsafe;

import java.util.ArrayList;

/*
需求:有一个ArrayList集合,现在有2个线程同时往集合中添加元素,
每一个线程都添加1000个元素,查看最后集合中有多少个元素?
 */
public class TestArrayList {
    private static ArrayList<Integer> list = new ArrayList<>();

    //    private static Vector<Integer> list = new Vector<>();
    public static void main(String[] args) {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                for (int i = 1; i <= 1000; i++) {
                    synchronized (list) {
                        list.add(i);
                    }
                }
            }
        };
        t1.start();

        Thread t2 = new Thread() {
            @Override
            public void run() {
                for (int i = 1001; i <= 2000; i++) {
                    synchronized (list) {
                        list.add(i);
                    }
                }
            }
        };
        t2.start();

        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("list的元素总个数:" + list.size());
    }
}

示例 3:有安全问题(SimpleDateFormat)

java
package com.atguigu.unsafe;

import java.text.SimpleDateFormat;
import java.util.Date;

public class TestSimpleDateFormat{
    private static SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    public static void main(String[] args) {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                long start = System.currentTimeMillis();
                for(int i=1; i<=5; i++){
                    start += 1000;
                    Date date = new Date(start);

                        String str = sf.format(date);
                        System.out.println(str);

                }
            }
        };
        t1.start();

        Thread t2 = new Thread(){
            @Override
            public void run() {
                Date d = new Date(1000, 0,1);//过时的方法   2900-1-1
                long start = d.getTime();
                for(int i=1; i<=5; i++){
                    start += 1000;
                    Date date = new Date(start);

                        String str = sf.format(date);
                        System.out.println("\t\t" + str);

                }
            }
        };
        t2.start();


    }
}

示例 4:没有安全问题(解决示例 3 问题)

java
package com.atguigu.unsafe;

import java.text.SimpleDateFormat;
import java.util.Date;

public class TestSimpleDateFormat{
    private static SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    public static void main(String[] args) {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                long start = System.currentTimeMillis();
                for(int i=1; i<=5; i++){
                    start += 1000;
                    Date date = new Date(start);
                    synchronized (sf) {
                        String str = sf.format(date);
                        System.out.println(str);
                    }
                }
            }
        };
        t1.start();

        Thread t2 = new Thread(){
            @Override
            public void run() {
                Date d = new Date(1000, 0,1);//过时的方法   2900-1-1
                long start = d.getTime();
                for(int i=1; i<=5; i++){
                    start += 1000;
                    Date date = new Date(start);
                    synchronized (sf) {
                        String str = sf.format(date);
                        System.out.println("\t\t" + str);
                    }
                }
            }
        };
        t2.start();


    }
}

示例 5:有安全问题(卖票之继承 Thread)

image-20250721153433061

java
package com.atguigu.unsafe;

public class TicketTwo extends Thread{
//    int total = 10;//成员变量的实例变量。 每一个对象不共享。因为测试类中有多个TicketTwo的对象,所以实例不被多个线程共享
    private static int total = 10;
    @Override
    public void run() {
        while(total>0){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            total--;
            System.out.println(getName() +"卖出一张票,剩余:" + total);
        }
    }
}
java
package com.atguigu.unsafe;

public class TestTicketTwo {
    public static void main(String[] args) {
        TicketTwo t1 =new TicketTwo();
        TicketTwo t2 =new TicketTwo();
        TicketTwo t3 =new TicketTwo();

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

示例 6:没有安全问题(同步代码块)

java
package com.atguigu.safe.block;

public class TicketTwo extends Thread {
    //    int total = 10;//成员变量的实例变量。 每一个对象不共享。因为测试类中有多个TicketTwo的对象,所以实例不被多个线程共享
    private static int total = 1000;
    private static Object lock = new Object();

    @Override
    public void run() {
        while (true) {
//            synchronized ("李刚") {//"李刚“在内存中只有1份,可以给共享
//            synchronized (this) {//错误,因为此时有3个TicketTwo的对象
            synchronized (lock) {//同步代码块
                if(total>0) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    total--;
                    System.out.println(getName() + "卖出一张票,剩余:" + total);
                }else{
                    break;
                }
            }
        }
    }
}
java
package com.atguigu.safe.block;

public class TestTicketTwo {
    public static void main(String[] args) {
        TicketTwo t1 =new TicketTwo();
        TicketTwo t2 =new TicketTwo();
        TicketTwo t3 =new TicketTwo();

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

示例 7:没有安全问题(同步方法)

java
package com.atguigu.safe.method;

/*
public class TicketTwo extends Thread{
//    int total = 10;//成员变量的实例变量。 每一个对象不共享。因为测试类中有多个TicketTwo的对象,所以实例不被多个线程共享
    private static int total = 10;


//    (1)所长是谁?
//        run()是非静态方法,所长是this。这里this不合适
//    (2)锁的代码范围对不对?
//        不对
    @Override
    public synchronized void run() {
        while(total>0){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            total--;
            System.out.println(getName() +"卖出一张票,剩余:" + total);
        }
    }
}

*/
public class TicketTwo extends Thread{
    //    int total = 10;//成员变量的实例变量。 每一个对象不共享。因为测试类中有多个TicketTwo的对象,所以实例不被多个线程共享
    private static int total = 1000;

    @Override
    public void run() {
        while(total>0){
            saleOneTicket();
        }
    }

    //    (1)所长是谁?
//        saleOneTicket()是静态方法,所长是当前类的Class对象,TicketTwo.class
//    (2)锁的代码范围对不对?
//        对
    public static synchronized  void saleOneTicket(){
        if(total>0){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            total--;
            System.out.println(Thread.currentThread().getName() +"卖出一张票,剩余:" + total);
        }
    }
}
java
package com.atguigu.safe.method;

public class TestTicketTwo {
    public static void main(String[] args) {
        TicketTwo t1 =new TicketTwo();
        TicketTwo t2 =new TicketTwo();
        TicketTwo t3 =new TicketTwo();

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

示例 8:有安全问题(卖票之实现 Runnable)

java
package com.atguigu.unsafe;

public class TicketThree implements Runnable{
    int total = 10;//成员变量的实例变量。 每一个对象不共享。 如果多个线程使用同一个对象,那么可以共享实例变量。
    @Override
    public void run() {
        while(total>0){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            total--;
            System.out.println(Thread.currentThread().getName() +"卖出一张票,剩余:" + total);
        }
    }
}
java
package com.atguigu.unsafe;

public class TestTicketThree {
    public static void main(String[] args) {
        TicketThree three = new TicketThree();

        Thread t1 = new Thread(three);
        Thread t2 = new Thread(three);
        Thread t3 = new Thread(three);

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

示例 9:没有安全问题(同步代码块)

java
package com.atguigu.safe.block;

public class TicketThree implements Runnable{
    int total = 1000;//成员变量的实例变量。 每一个对象不共享。 如果多个线程使用同一个对象,那么可以共享实例变量。
    @Override
    public void run() {
        while(total>0){
            //同步代码块
            synchronized (this) {//这里this可以,因为TicketThree的对象现在是1个
                if (total > 0) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    total--;
                    System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + total);
                }
            }
        }
    }
}
java
package com.atguigu.safe.block;

public class TestTicketThree {
    public static void main(String[] args) {
        TicketThree three = new TicketThree();

        Thread t1 = new Thread(three);
        Thread t2 = new Thread(three);
        Thread t3 = new Thread(three);

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

示例 10:没有安全问题(同步方法)

java
package com.atguigu.safe.method;

/*public class TicketThree implements Runnable{
    int total = 10;//成员变量的实例变量。 每一个对象不共享。 如果多个线程使用同一个对象,那么可以共享实例变量。

   //(1)所长是谁? run()非静态,是this,这里this合适
    //(2)范围合适吗?不合适
    @Override
    public synchronized void run() {
        while(total>0){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            total--;
            System.out.println(Thread.currentThread().getName() +"卖出一张票,剩余:" + total);
        }
    }
}*/

public class TicketThree implements Runnable{
    int total = 1000;//成员变量的实例变量。 每一个对象不共享。 如果多个线程使用同一个对象,那么可以共享实例变量。

    @Override
    public void run() {
        while(total>0){
           saleOneTicket();
        }
    }

    //(1)所长是谁? run()非静态,是this,这里this合适
    //(2)范围合适吗?合适
    public synchronized void saleOneTicket(){
        if(total>0){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            total--;
            System.out.println(Thread.currentThread().getName() +"卖出一张票,剩余:" + total);
        }
    }
}
java
package com.atguigu.safe.method;

public class TestTicketThree {
    public static void main(String[] args) {
        TicketThree three = new TicketThree();

        Thread t1 = new Thread(three);
        Thread t2 = new Thread(three);
        Thread t3 = new Thread(three);

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

2.5 单例设计模式

单例:唯一的实例对象。某个类它的对象只有 1 个。

编写单例类的套路,模板,成为单例设计模式。

所谓设计模式,解决一类问题,大家总结出来的一套经验,套路。大约有 23 种设计模式,被各种编程语言公认是比较好的代码模板。

单例设计模式是 23 种当中最简单的,也是笔试题中考的最多。

问题 1:怎么限制这个类的对象只有 1 个?

(1)构造器必须私有化。==>注定只能在类的内部创建它唯一的对象

(2)给外界提供渠道能获取到这个类的 唯一对象

问题 2:在哪里,什么时候创建这个类的唯一对象?

  • 饿(恶)汉式单例设计模式:

    • 饿:饥不择食,着急
    • 恶:一言不合就开干
    • 急:无论这个类的对象你现在用不用,我上来(类加载时)就把这个对象创建好了。 总结:在类初始化时就创建它唯一的对象
    • 饿汉式是在类初始化时创建对象,而类初始化过程是一定线程安全的,因为底层已经加锁保证一个类只会被初始化一次。
  • 懒汉式单例设计模式

    • 懒:不到万不得已不会动
    • 慢:直到你明确来拿这个对象了,我再创建,不会提前创建
    • 懒汉式是在方法中创建对象的,所以要考虑线程安全问题。

示例代码 1:饿汉式

java
package com.atguigu.single;

public class SingleOne {
    public static final SingleOne INSTANCE = new SingleOne();
    private SingleOne(){
        System.out.println("SingleOne的唯一对象被创建了");
    }

    public static void method(){
        System.out.println("这是一个普通的静态方法");
    }
}
java
package com.atguigu.single;

public class TestSingleOne {
    public static void main(String[] args) {
        /*SingleOne s1 = SingleOne.INSTANCE;
        SingleOne s2 = SingleOne.INSTANCE;
        System.out.println(s1 == s2);//true比较地址值*/

        SingleOne.method();
    }
}

示例代码 2:饿汉式

java
package com.atguigu.single;

public enum SingleTwo {
    INSTANCE;

    SingleTwo(){
        System.out.println("SingTwo的对象被创建了");
    }

    public static void method(){
        System.out.println("这是一个普通的静态方法");
    }
}
java
package com.atguigu.single;

public class TestSingleTwo {
    public static void main(String[] args) {
       /* SingleTwo s1 = SingleTwo.INSTANCE;
        SingleTwo s2 = SingleTwo.INSTANCE;
        System.out.println(s1 == s2);//true*/

        SingleTwo.method();
    }
}

示例代码 3:饿汉式

java
package com.atguigu.single;

public class SingleThree {
    private static final SingleThree INSTANCE = new SingleThree();
    private SingleThree(){
        System.out.println("SingleThree对象被创建了");
    }

    public static SingleThree getInstance(){
        return INSTANCE;
    }

    public static void method(){
        System.out.println("这是一个普通的静态方法");
    }
}
java
package com.atguigu.single;

public class TestSingleThree {
    public static void main(String[] args) {
       /* SingleThree s1 =SingleThree.getInstance();
        SingleThree s2 =SingleThree.getInstance();
        System.out.println(s1 == s2);//true*/

        SingleThree.method();
    }
}

示例代码 4:懒汉式

java
package com.atguigu.single;


public class SingleFour {
    private static SingleFour instance;
    private SingleFour(){
        System.out.println("SingleFour对象被创建了");
    }

    public static synchronized SingleFour getInstance(){
        if(instance == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new SingleFour();
        }
        return instance;
    }

    public static void method(){
        System.out.println("这是一个普通的静态方法");
    }
}


/*
public class SingleFour {
    private static SingleFour instance;
    private SingleFour(){

    }

    public static  SingleFour getInstance(){
        if(instance == null) {//如果instance已经创建了,线程就不需要竞争锁了,直接返回instance对象,效率比较高
            synchronized (SingleFour.class) {//同步代码块
                if(instance == null) {//如果两个线程同时判断完instance == null都成立,但是只有1个线程进入到synchronized快中,先创建了。另一个线程进来之后,不能再创建第2个对象。这个条件可以避免创建多个对象。
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    instance = new SingleFour();
                }
            }
        }
        return instance;
    }
}*/
java
package com.atguigu.single;

public class TestSingleFour {
    public static void main(String[] args) {
       /* SingleFour s1 = SingleFour.getInstance();
        SingleFour s2 = SingleFour.getInstance();
        System.out.println(s1 == s2);
        //这是是单线程的,没有安全问题*/

        SingleFour.method();
        SingleFour s1 = SingleFour.getInstance();
    }
}
java
package com.atguigu.single;

public class TestSingleFour2 {
    private static SingleFour s1;
    private static SingleFour s2;
    public static void main(String[] args) {
        Thread t1 = new Thread(){
            @Override
            public void run() {
                s1 = SingleFour.getInstance();
            }
        };
        t1.start();
        Thread t2 = new Thread(){
            @Override
            public void run() {
                s2 = SingleFour.getInstance();
            }
        };
        t2.start();

        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("s1 = " + s1);
        System.out.println("s2 = " + s2);
        System.out.println(s1 == s2);
    }
}

示例代码 5:懒汉式

java
package com.atguigu.single;

public class SingleFive {
    private SingleFive(){
        System.out.println("SingleFive对象被创建了");
    }

    static {
        System.out.println("外部类的静态代码块");
    }

    private static class Inner{
        static SingleFive INSTANCE = new SingleFive();
        //把创建外部类对象的工作,交给内部类
        //这个对象是在初始化Inner类的时候才创建的

        static {
            System.out.println("内部类的静态代码块");
        }
    }

    public static SingleFive getInstance(){
        return Inner.INSTANCE;
    }

    public static void method(){
        System.out.println("这是一个普通的静态方法");
    }
}
java
package com.atguigu.single;

public class TestSingleFive {
    public static void main(String[] args) {
        SingleFive.method();
        SingleFive s1 = SingleFive.getInstance();
        SingleFive s2 = SingleFive.getInstance();
        System.out.println(s1 == s2);
    }
}