J2EE 知识点总结_上

发布于:2022-12-02 ⋅ 阅读:(657) ⋅ 点赞:(0)

申明: 未经许可,禁止以任何形式转载,若要引用,请标注链接地址
全文共计19757字,阅读大概需要5分钟
欢迎关注我的个人公众号:不懂开发的程序猿

基础概念

Java:跨平台的语言(能运行在Windows,Mac OS,Linux等操作系统之上)

Write Once, Run Anywhere!

JVM:跨语言的平台(能为Java,Python,Kotlin等语言编译字节码)

JDK(Java Development Kit Java开发工具包):是提供给Java开发人员使用的,其中包含了java的开发工具,也包括了 JRE。所以安装了JDK,就不用在单独安装JRE了。

JRE(Java Runtime Environment Java运行环境) 包括Java虚拟机(JVM Java Virtual Machine)和Java程序所需的核心类库等, 如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。

JDK = JRE + 开发工具集(例如Javac编译工具等)

JRE = JVM + Java SE标准类库

正数的原码、反码、补码都相同

负数的反码:是对原码按位取反(中间态)

负数的补码:其反码加1。计算机底层都是使用的数值的补码保存数据的。

“&”和“&&”的区别:

单&时,左边无论真假,右边都进行运算;

双&时,如果左边为真,右边参与运算,如果左边为假,那么右边不参与运算。

javadoc文档解析

javadoc -encoding UTF-8 -charset UTF-8 -d myhello -author -version HelloWorld.java

java入门问题1——javadoc解析时,cmd窗口报错:编码GBK的不可映射字符
问题1描述:在cmd中运行命令,javadoc -d myhello -author -version HelloJava.java 时,报错编码GBK的不可映射字符。

解决方法:经过网页查询,且前提为使用编码UTF-8(其他编码对应更改就可以),将cmd中的命令改为javadoc -encoding UTF-8 -charset UTF-8 -d myhello -author -version HelloJava.java

上述问题得到解决,但出现了新的问题。

问题2描述:javadoc错误-找不到可以文档化的公共或受保护的类。

解决方法:在源文件中的class前加入public后,可以成功运行。

数组

一维数组的声明方式: type var[] 或 type[] var;

java.util.Arrays常用工具类

boolean equals(int[] a,int[] b) 判断两个数组是否相等。

String toString(int[] a) 输出数组信息。

void fill(int[] a,int val) 将指定值填充到数组之中。

void sort(int[] a) 对数组进行排序。

int binarySearch(int[] a,int key) 对排序后的数组进行二分法检索指定的值。

int[] copyOfRange(int[] original, int from, int to)

选择排序 :

(直接)简单选择排序:

每次都从一组无序数组中选一个最小的,然后和第一个对调。

堆排序:

1)根据初始数组去构造初始堆

1.1)建初始堆的过程:先将一个无序数组按照从上到小,从左到右排列成完全二叉树。

然后再从最后一个非叶子结点开始,从右到左、从下到上调整为大顶堆。

2)交换第一个和最后一个元素,输出最后一个元素(最大值),然后把剩下的元素调整为大顶堆,再交换第一个和最后一个元素,输出最后一个元素再调整剩下的元素为大顶堆,重复这个操作,直至整个数组排序完成。

交换排序 :

冒泡排序

比如说从小到大排序:依次比较相邻的两个元素,大的放右边。重复n-1轮

快速排序

一次划分的过程:有2个指针,low指针指向头元素,high指针指向尾元素,一般取第一个元素为界点(标准)元素,为了减少数据移动,将标准元素暂存在R[0]中(此时的low指针指向空)最后再放入最终位置。开始high指针从后往前移动,找比界点元素小的元素,放在low指针的位置(此时的high指针为空),然后low指针从前往后移动,找比界点元素大的元素,放在high指针的位置。重复上述的过程知道low指针和high指针位置重合,把界点元素放在该位置。

重复划分过程。

插入排序

直接插入排序

从一组无序数组的第2位开始,每次只和它前面的数进行比较,直接插入在正确的位置

Shell排序 (缩小增量排序)

比如有8个元素一组的无序数组,

第一趟(d=4):第5个位置元素和第1个位置元素比大小,小的话就交换位置(大的放后面),第6个位置元素和第2个位置元素比大小,第7个位置元素和第3个位置元素比大小,第8个位置元素和第4个位置元素比大小

第二趟(d=2):第4个位置元素和第2个位置元素比大小,小的话就交换位置(大的放后面),第6个位置元素和第4个位置元素比大小,第8个位置元素和第6个位置元素比大小,大的放后面

然后:第3个位置元素和第1个位置元素比大小,小的话就交换位置(大的放后面),第5个位置元素和第3个位置元素比大小,第7个位置元素和第5个位置元素比大小,大的放后面

第三趟(d=1):后一个位置和前一个位置元素比大小,小的话就交换位置(大的放后面)

折半插入排序

折半插入排序是对直接插入排序的一种改良方式,

二路归并排序

将一组无序数组,分为左右一半,不停的进行递归拆分,递归深度为log2n,直到分为单个元素不可再分为止

我们将单个元素进行排序合成一个有序序列

计数排序

一组有确定范围的无序数组,申请一个大小为arr.length+1长度的数组,下标从arr[0]~arr[length],默认初始化为0,开始计每一个数出现的次数,就在对应下标的数的位置填出现的次数,然后遍历输出这个数组。

桶式排序

是计数排序的升级版。原理是:假设输入的数据服从均匀分布,将数据分配到有限数量的桶中,再对每个桶分别进行排序。分配的原则是利用函数映射关系,整个算法的高效与否就在于这个映射函数的确定。

思路:

1、根据待排序集合中的数据,确定映射规则和桶的数量;

2、遍历待排序数组,将每一个元素根据映射规则,移动到对应的桶里;

3、对每一个桶中的元素进行排序

4、依次输出每个桶中的数据,得到整个有序集合

步骤:

1、找到待排序数组中的最大值amx,和最小值min

2、桶的数量为:(max-min)/ arr.length + 1,桶的下标是从0开始

3、映射函数的对映规则:也就是存放桶的下标=(arr[i] - min)/ arr.length

4、这时候,每个桶里的数据是无序,但是随着桶的下标递增,桶里的数据是递增

5、再对每个桶里的数据进行排序。桶内排序可以选择比较排序或者其他排序

6、遍历桶数组

基数排序

先把无序数组变成位数都一样,不足的高位补0,然后先按照低位排好序,再按照高位排好序,依次类推,直到最高位,最后遍历输出数组就行

十大排序算法的代码具体实现,详情见《数据结构》专栏

面向对象

重载(Overload)的概念

在同一个类中,允许存在一个以上的同名方法,只要它们的参数个数或者参数 类型不同即可。

重载的特点: 与返回值类型无关,只看参数列表,且参数列表必须不同。(参数个数或参数类 型)。调用时,根据方法参数列表的不同来区别。

Override重写(覆盖):

表示子类中的方法可以与父类中的某个方法的名称和参数完全相同,通过子类创建的实例对象调用这个方法时,将调用子类中的定义方法(子类重写父类中的方法),是面向对象编程的中多态性的一种表现。

要求:

  1. 子类重写的方法必须和父类被重写的方法具有相同的方法名称、参数列表
  2. 子类重写的方法的返回值类型不能大于父类被重写的方法的返回值类型
  3. 子类重写的方法使用的访问权限不能小于父类被重写的方法的访问权限
    1. 子类不能重写父类中声明为private权限的方法
  4. 子类方法抛出的异常不能大于父类被重写方法的异常

注意: 子类与父类中同名同参数的方法必须同时声明为非static的(即为重写),或者同时声明为 static的(不是重写)。因为static方法是属于类的,子类无法覆盖父类的方法

overwrite重写:

java官方文档没有该词的出现,所以java中就没有它的存在,但是也有人把overwrite解释为override。

在**C++**中将Override和overwrite进行了区分。

Override(覆盖):

是指派生类函数覆盖基类函数,特征是:

(1)不同的范围(分别位于派生类与基类);

(2)函数名字相同;

(3)参数相同;

(4)基类函数必须有virtual 关键字。

Overwrite(重写):

是指派生类的函数屏蔽了与其同名的基类函数,规则如下:

(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏。

(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏。

构造器的作用:

创建对象;

给对象进行初始化

总结:属性赋值过程

赋值的位置: ① 默认初始化 ② 显式初始化 ③ 构造器中初始化 ④ 通过“对象.属性“或“对象.方法”的方式赋值

赋值的先后顺序: ① - ② - ③ - ④

JavaBean

JavaBean是一种Java语言写成的可重用组件。

所谓JavaBean,是指符合如下标准的Java类:

类是公共的

有一个无参的公共的构造器

有属性,且有对应的get、set方法

用户可以使用JavaBean将功能、处理、值、数据库访问和其他任何可以 用Java代码创造的对象进行打包,并且其他的开发者可以通过内部的JSP 页面、Servlet、其他JavaBean、applet程序或者应用来使用这些对象。用 户可以认为JavaBean提供了一种随时随地的复制和粘贴的功能,而不用关 心任何改变。

package com.jerry.java;

/**
 * @author jerry_jy
 * @create 2022-09-27 10:32
 */
public class JavaBean {//类是公共的
    private String name;//有属性,且有对应的get、set方法
    private int age;
    public JavaBean(){//有一个无参的公共的构造器

    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

在这里插入图片描述

对于class的权限修饰只可以用public和default(缺省)。

​ public类可以在任意地方被访问。

​ default类只可以被同一个包内部的类访问。

多态性

是面向对象中最重要的概念,在Java中的体现: 对象的多态性:父类的引用指向子类的对象

Person p = new Student(); //Person类型的变量p,指向Student类型的对象

Object o = new Person();//Object类型的变量o,指向Person类型的对象

o = new Student(); //Object类型的变量o,指向Student类型的对象

子类可看做是特殊的父类,所以父类类型的引用可以指向子类的对象:向上转型(upcasting)

一个引用类型变量如果声明为父类的类型,但实际引用的是子类 对象,那么该变量就不能再访问子类中添加的属性和方法

Student m = new Student();

m.school = “pku”; //合法,Student类有school成员变量

Person e = new Student();

e.school = “pku”; //非法,Person类没有school成员变量

属性是在编译时确定的,编译时e为Person类型,没有school成员变量,因而编译错误。

虚拟方法调用(多态情况下)(Virtual Method Invocation)

子类中定义了与父类同名同参数的方法,在多态情况下,将此时父类的方法称为虚拟方法,父 类根据赋给它的不同子类对象,动态调用属于子类的该方法。这样的方法调用在编译期是无法确定的。

instanceof 操作符

x instanceof A:检验x是否为类A的对象,返回值为boolean型。

要求x所属的类与类A必须是子类和父类的关系,否则编译错误。

如果x属于类A的子类B,x instanceof A值也为true。

Object类是所有Java类的根父类

如果在类的声明中未使用extends关键字指明其父类,则默认父类 为java.lang.Object类

public class Person { … } 等价于: public class Person extends Object { … }

例:method(Object obj){…} //可以接收任何类作为其参数

Person o=new Person();

method(o);

==操作符与equals方法:

==操作符

基本类型比较值:只要两个变量的值相等,即为true。

引用类型比较引用(是否指向同一个对象):只有指向同一个对象时,==才返回true。

用“==”进行比较时,符号两边的数据类型必须兼容(可自动转换的基本 数据类型除外),否则编译出错

equals():所有类都继承了Object,也就获得了equals()方法。还可以重写。

只能比较引用类型,其作用与“==”相同,比较是否指向同一个对象。

格式:obj1.equals(obj2)

特例:当用equals()方法进行比较时,对类File、String、Date及包装类 (Wrapper Class)来说,是比较类型及内容而不考虑引用的是否是同一个对 象;原因:在这些类中重写了Object类的equals()方法。

面试题:==和equals的区别

1 == 既可以比较基本类型也可以比较引用类型。对于基本类型就是比较值,对于引用类型 就是比较内存地址

2 equals的话,它是属于java.lang.Object类里面的方法,如果该方法没有被重写过默认也 是==;我们可以看到String等类的equals方法是被重写过的,而且String类在日常开发中 用的比较多,久而久之,形成了equals是比较值的错误观点。

3 具体要看自定义类里有没有重写Object的equals方法来判断。

4 通常情况下,重写equals方法,会比较类中的相应属性是否都相等。

toString() 方法

toString()方法在Object类中定义,其返回值是String类型,返回类名和它 的引用地址。

在进行String与其它类型数据的连接操作时,自动调用toString()方法

包装类(Wrapper)的使用

在这里插入图片描述

在这里插入图片描述

代码演示:

        // 包装类的使用
        int i = 5;
        Integer integer = new Integer(i);//把基本数据类型转化为包装类,调用new Integer()
        System.out.println(integer.getClass().getName());//java.lang.Integer

        int i1 = integer.intValue();//包装类转化为基本数据类型,调用包装类的方法:xxxValue()
        System.out.println(i1);//5

        int j = 1;
        String s = String.valueOf(j);// 基本数据类型转化为String类,方式一:调用String.valueOf()
        System.out.println(s.getClass().getName());//java.lang.String

        String s1 = j + ""; // 基本数据类型转化为String类,方式二:+ ""
        System.out.println(s1.getClass().getName());//java.lang.String

        String s2 = "123";
        int i2 = Integer.parseInt(s2); // String类转化为基本数据类型,调用Integer.parseInt()
        System.out.println(i2);//123

        Integer integer1 = new Integer(2);
        String s3 = integer1.toString();// 包装类转化为String类,调用包装类对象的toString()方法
        System.out.println(s3.getClass().getName());//java.lang.String

        Integer i3 = new Integer("456");// String类转化为包装类
        System.out.println(i3.getClass().getName()); //java.lang.Integer

垃圾回收机制关键点

垃圾回收机制只回收JVM堆内存里的对象空间。

对其他物理连接,比如数据库连接、输入流输出流、Socket连接无能为力

现在的JVM有多种垃圾回收实现算法,表现各异。

垃圾回收发生具有不可预知性,程序无法精确控制垃圾回收机制执行。

可以将对象的引用变量设置为null,暗示垃圾回收机制可以回收该对象。

程序员可以通过System.gc()或者Runtime.getRuntime().gc()来通知系统进行垃圾回收,会有
一些效果,但是系统是否进行垃圾回收依然不确定。

垃圾回收机制回收任何对象之前,总会先调用它的finalize方法(如果覆盖该方法,让一
个新的引用变量重新引用该对象,则会重新激活对象)。

永远不要主动调用某个对象的finalize方法,应该交给垃圾回收机制调用。

如果想让一个类的所有实例共享数据,就用类变量!

因为不需要实例就可以访问static方法,因此static方法内部不能有this。(也 不能有super ? YES!)

static修饰的方法不能被重写

单例(Singleton)设计模式-饿汉式

class Singleton {
    // 1.私有化构造器
    private Singleton() {
    }
    // 2.内部提供一个当前类的实例
	// 4.此实例也必须静态化
    private static Singleton single = new Singleton();
    // 3.提供公共的静态的方法,返回当前类的对象
    public static Singleton getInstance() {
        return single;
    }
}

单例(Singleton)设计模式-懒汉式(线程不安全)

class Singleton {
    // 1.私有化构造器
    private Singleton() {
    }
    // 2.内部提供一个当前类的实例
    // 4.此实例也必须静态化
    private static Singleton single;
    // 3.提供公共的静态的方法,返回当前类的对象
    public static Singleton getInstance() {
        if(single == null) {
            single = new Singleton();
        }
        return single;
    }
}

工厂设计模式

interface Car {
    void run();
}
class Audi implements Car {
    public void run() {
        System.out.println("奥迪在跑");
    }
}
class BYD implements Car {
    public void run() {
        System.out.println("比亚迪在跑");
    }
}
//工厂类
class CarFactory {
    //方式一
    public static Car getCar(String type) {
        if ("奥迪".equals(type)) {
            return new Audi();
        } else if ("比亚迪".equals(type)) {
            return new BYD();
        } else {
            return null;
        }
    }
//方式二
// public static Car getAudi() {
// return new Audi();
// }
//
// public static Car getByd() {
// return new BYD();
// }
}
public class Client02 {
    public static void main(String[] args) {
        Car a = CarFactory.getCar("奥迪");
        a.run();
        Car b = CarFactory.getCar("比亚迪");
        b.run();
    }
}

对main方法的语法理解:

public static void main(String[] args) {}

由于Java虚拟机需要调用类的main()方法,所以该方法的访问权限必须是 public,又因为Java虚拟机在执行main()方法时不必创建对象,所以该方法必须 是static的,该方法接收一个String类型的数组参数,该数组中保存执行Java命令 时传递给所运行的类的参数。

抽象方法:只有方法的声明,没有方法的实现。以分号结束:

比如:public abstract void talk();

接口(interface)是抽象方法和常量值定义的集合

接口中的所有成员变量都默认是由public static final修饰的。

接口中的所有抽象方法都默认是由public abstract修饰的。

Java 8中,你可以为接口添加静态方法和默认方法。

静态方法:使用 static 关键字修饰。可以通过接口直接调用静态方法,

默认方法:默认方法使用 default 关键字修饰。可以通过实现类对象来调用。

异常处理

详情见博客《Java2EE练习及面试题_chapter07异常处理》

多线程

JDK1.5之前创建新执行线程有两种方法:

继承Thread类的方式

  1. 定义子类继承Thread类。
  2. 子类中重写Thread类中的run方法。
  3. 创建Thread子类对象,即创建了线程对象。
  4. 调用线程对象start方法:启动线程,调用run方法。

代码演示:

package com.jerry.java;

/**
 * @author jerry_jy
 * @create 2022-10-01 17:33
 */
public class ThreadTest {
    public static void main(String[] args) {
        //3.创建Thread子类对象,即创建了线程对象。
        MyThread myThread = new MyThread();
        //4.调用线程对象start方法:启动线程,调用run方法。
        myThread.start();
    }
}
//1.定义子类继承Thread类。
class MyThread extends Thread {
    public MyThread() {
        super();
    }
// 2.子类中重写Thread类中的run方法。
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("子线程:" + i);
        }
    }
}

注意点:

  1. 如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
  2. run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU 调度决定。
  3. 想要启动多线程,必须调用start方法。
  4. 一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上 的异常“IllegalThreadStateException”。

实现Runnable接口的方式

  1. 定义子类,实现Runnable接口。
  2. 子类中重写Runnable接口中的run方法。
  3. 通过Thread类含参构造器创建线程对象。
  4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
  5. 调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。

代码演示:

package com.jerry.java;

/**
 * @author jerry_jy
 * @create 2022-10-01 17:40
 */
public class RunnableTest {

    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        //3.通过Thread类含参构造器创建线程对象。
        //4.将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
        Thread thread = new Thread(myRunnable);
        //5.调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。
        thread.start();
    }
}

//1.定义子类,实现Runnable接口。
class MyRunnable implements Runnable{

    //2.子类中重写Runnable接口中的run方法。
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("子线程:"+i);
        }
    }
}

多线程的创建–>继承方式和实现方式的联系与区别:

区别

​ 继承Thread:线程代码存放Thread子类run方法中。

​ 实现Runnable:线程代码存在接口的子类的run方法。

实现方式的好处

​ 避免了单继承的局限性

​ 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线 程来处理同一份资源。

Thread类的有关方法

void start(): 启动线程,并执行对象的run()方法
run(): 线程在被调度时执行的操作
String getName(): 返回线程的名称
void setName(String name):设置该线程名称
static Thread currentThread(): 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
static void yield():线程让步
	暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
	若队列中没有同优先级的线程,忽略此方法
join() :当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止
    低优先级的线程也可以获得执行
static void sleep(long millis)(指定时间:毫秒)
	令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
	抛出InterruptedException异常
stop(): 强制线程生命期结束,不推荐使用
boolean isAlive():返回boolean,判断线程是否还活着

线程的优先级

线程的优先级等级
	MAX_PRIORITY:10 
	MIN _PRIORITY:1 
	NORM_PRIORITY:5 
涉及的方法
	getPriority() :返回线程优先值
	setPriority(int newPriority) :改变线程的优先级
说明
    线程创建时继承父线程的优先级
	低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用

补充:线程的分类

Java中的线程分为两类:一种是守护线程,一种是用户线程

​ 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开

​ 守护线程是用来服务用户线程的,通过在start()方法前调用 thread.setDaemon(true)可以把一个用户线程变成一个守护线程

​ Java垃圾回收就是一个典型的守护线程。

​ 若JVM中都是守护线程,当前JVM将退出。

线程的生命周期

JDK中用Thread.State类定义了线程的几种状态

要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类 及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五 种状态:

​ 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建 状态

​ 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已 具备了运行的条件,只是没分配到CPU资源

​ 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线 程的操作和功能

​ 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中 止自己的执行,进入阻塞状态

​ 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

在这里插入图片描述

在这里插入图片描述

Synchronized的使用方法

Java对于多线程的安全问题提供了专业的解决方式:同步机制

1. 同步代码块:
synchronized (对象){
// 需要被同步的代码;
}
2. synchronized还可以放在方法声明中,表示整个方法为同步方法。
例如:
public synchronized void show (String name){.
}

在这里插入图片描述

同步机制中的锁

同步锁机制:

在《Thinking in Java》中,是这么说的:对于并发工作,你需要某种方式来防 止两个任务访问相同的资源(其实就是共享资源竞争)。 防止这种冲突的方法 就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须 锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁 之时,另一个任务就可以锁定并使用它了。

synchronized的锁是什么?

​ 任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)

​ 同步方法的锁:静态方法(类名.class)、非静态方法(this)

​ 同步代码块:自己指定,很多时候也是指定为this或类名.class

单例设计模式之懒汉式(线程安全)

package com.jerry.java;

/**
 * @author jerry_jy
 * @create 2022-10-01 19:27
 */
public class SingletonTest {
    //    单例设计模式之懒汉式(线程安全)
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton1 == singleton2);//true
    }
}

class Singleton {
    // 2.内部提供一个当前类Singleton的实例singleton
    // 4.此实例也必须静态化static
    private static Singleton singleton;
    // 1.私有化构造器
    private Singleton() {
    }
    // 3.提供公共的静态的方法getInstance,返回当前类的对象return singleton
    public static Singleton getInstance() {
        if (singleton == null) {
            //synchronized同步监视器:每次只new 一个当前类的对象
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

Lock(锁)

class A{
    private final ReentrantLock lock = new ReenTrantLock();
    public void m(){
        lock.lock();
        try{
			//保证线程安全的代码;
        }
        finally{
            lock.unlock();
        }
    }
}

注意:如果同步代码有异常,要将unlock()写入finally语句块

synchronized 与 Lock 的对比

  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是 隐式锁,出了作用域自动释放

  2. Lock只有代码块锁,synchronized有代码块锁和方法锁

  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有 更好的扩展性(提供更多的子类)

优先使用顺序:

Lock --> 同步代码块(已经进入了方法体,分配了相应资源)–> 同步方法 (在方法体之外)

线程通信

wait() 与 notify() 和 notifyAll()

​ wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当 前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。

​ notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待

​ notifyAll ():唤醒正在排队等待资源的所有线程结束等待.

这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报 java.lang.IllegalMonitorStateException异常。

因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁, 因此这三个方法只能在Object类中声明。

wait() 方法

​ 在当前线程中调用方法: 对象名.wait()

​ 使当前线程进入等待(某对象)状态 ,直到另一线程对该对象发出 notify (或notifyAll) 为止。

​ 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)

​ 调用此方法后,当前线程将释放对象监控权 ,然后进入等待

​ 在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行。

notify()/notifyAll()

​ 在当前线程中调用方法: 对象名.notify()

​ 功能:唤醒等待该对象监控权的一个/所有线程

​ 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)

示例题目:使用两个线程打印 1-100。线程1, 线程2 交替打印

package com.jerry.java;

/**
 * @author jerry_jy
 * @create 2022-10-02 17:26
 */
public class Communication implements Runnable {
    int i = 1;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                notify();
                if (i <= 100) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                    i++;

                    try {
                        //使得调用如下wait()方法的线程进入阻塞状态
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    break;
                }
            }
        }
    }

    public static void main(String[] args) {
        Communication communication = new Communication();
        Thread thread1 = new Thread(communication);
        Thread thread2 = new Thread(communication);

        thread1.setName("线程一");
        thread2.setName("线程二");

        thread1.start();
        thread2.start();
    }
}

经典例题:生产者/消费者问题

package com.jerry.java;

/**
 * @author jerry_jy
 * @create 2022-10-02 17:45
 */
public class ProductTest {
    /*
    生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处
    取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图
    生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通
    知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如
    果店中有产品了再通知消费者来取走产品。
    这里可能出现两个问题:
    生产者比消费者快时,消费者会漏掉一些数据没有取到。
    消费者比生产者快时,消费者会取相同的数据。
     */
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Productor productor = new Productor(clerk);
        Consumer consumer = new Consumer(clerk);
        Thread thread1 = new Thread(productor);
        Thread thread2 = new Thread(consumer);

        thread1.start();
        thread2.start();

    }
}

class Clerk {
    private int product = 0;

    public synchronized void addProduct() {
        if (product > 20) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            product++;
            System.out.println("生产者生产了第" + product + "个产品");
            notifyAll();
        }
    }

    public synchronized void getProduct() {
        if (product <= 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println("消费者取走了第" + product + "个产品");
            product--;
            notifyAll();
        }
    }

}

class Productor implements Runnable {
    Clerk clerk;

    public Productor(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println("生产者开始生产产品");
        while (true) {
            try {
                Thread.sleep((int) Math.random() * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.addProduct();
        }
    }
}

class Consumer implements Runnable {
    Clerk clerk;

    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep((int) Math.random() * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.getProduct();
        }
    }
}

JDK5.0新增线程创建方式

新增方式一:实现Callable接口

与使用Runnable相比, Callable功能更强大些

​ 相比run()方法,可以有返回值

​ 方法可以抛出异常

​ 支持泛型的返回值

​ 需要借助FutureTask类,比如获取返回结果

package com.jerry.java;

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

/**
 * @author jerry_jy
 * @create 2022-10-02 18:46
 */
public class CallableTest {
    public static void main(String[] args) {

        //3.创建Callable接口实现类的对象
        MyCallable myCallable = new MyCallable();
        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask<>(myCallable);
        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        Thread thread = new Thread(futureTask);
        thread.start();

        //6.获取Callable中call方法的返回值
        //get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
        try {
            Object o = futureTask.get();
            System.out.println("总和为:" + o);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

//1.创建一个实现Callable的实现类
class MyCallable implements Callable {

    //2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum;
    }
}

新增方式二:使用线程池

JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors

package com.jerry.java;

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

/**
 * @author jerry_jy
 * @create 2022-10-02 18:54
 */
public class ThreadPool {
    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        //设置线程池的属性
        System.out.println(service.getClass());//class java.util.concurrent.ThreadPoolExecutor
        service1.setCorePoolSize(15);
        System.out.println(service1.getMaximumPoolSize());//10

        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        service.execute(new NumberThread1());//适合适用于Runnable

        //3.关闭连接池
        service.shutdown();
    }
}

class NumberThread implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread1 implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

–end–