赞
踩
文章结构:
序号 | 总体范畴 | 章节 | 细分内容 |
---|---|---|---|
1 | 基础 | 1/2/3/4 | JDK、语言基础、流程控制 |
2 | 面向对象 | 6/7/8 | 封装、继承、多态、抽象、接口 |
3 | 容器 | 5/12 | 泛型、容器 |
4 | 异常 | 9 | |
5 | IO流&文件 | 10/18 | IO、文件、网络编程 |
6 | 多线程 | 11 | |
7 | Java 8新特性 | 13 | 注解、反射、内部类、Lambda表达式 |
(1)“类”是对某一类事物的描述,是一个抽象的、概念上的定义;
(2)在Java语言中,类是将数据和方法封装在一起的一种数据结构,其中数据表示类的属性,方法表示类的行为;
(3)定义一个类实际上是定义类的属性和方法,在使用类之前,必须先定义它,然后才可以利用所定义的类来声明相应的变量,并创建对象,这与声明一个基本类型变量(如 int x
)实质上是一个概念,只是基本数据类型是系统定义好的。
(1)“对象”是相对于“类”而言的,类是某一同类事物的描述,而对象则是实际存在的、属于该类事物的具体个体,因为对象也称为类的实例(instance);
封装是指把变量和方法包装在用一个类内,以限定外界对该类成员的访问,从而达到保护数据的一种技术。
通过 new 关键字创建一个普通的对象过程:
(1)检测类是否被加载:当虚拟机执行到new时,会先去常量池中查找这个类的符号引用。如果能找到符号引用,说明此类已经被加载到方法区(方法区存储虚拟机已经加载的类的信息),可以继续执行;如果找不到符号引用,就会使用类加载器执行类的加载过程,类加载完成后继续执行。
(2)为对象分配内存:类加载完成以后,虚拟机就开始为对象分配内存,具体的分配内存有两种情况:
(3)为分配的内存空间初始化零值:对象的内存分配完成后,还需要将对象的内存空间都初始化为零值,这样能保证对象即使没有赋初值,也可以直接使用。
(4)对对象进行其他设置:分配完内存空间,初始化零值之后,虚拟机还需要对对象进行其他必要的设置,设置的地方都在对象头中,包括这个对象所属的类,类的元数据信息,对象的hashcode,GC分代年龄等信息。
(5)执行 init 方法:执行完上面的步骤之后,在虚拟机里这个对象就算创建成功了,但是对于Java程序来说还需要执行init方法才算真正的创建完成,因为这个时候对象只是被初始化零值了,还没有真正的去根据程序中的代码分配初始值,调用了init方法之后,这个对象才真正能使用。
(1)当一个对象被创建之后,在调用该对象的方法时,也可以不定义对象的引用变量,而直接调用这个对象的方法,这样的对象成为匿名对象。例如:
定义对象的引用变量 stu :
Student stu = new Student();
stu.setName("张三");
改写为匿名对象的形式:
new Student().setName("张三");
(2)使用匿名对象的两种场景:
在面向对象的程序设计语言中,有一些方法的含义相同,但带有不同的参数,这些方法使用相同名字,这就叫方法的重载(Overload)。
换句话说,重载是指在同一个类内具有相同名称的多个方法,这些同名方法如果参数个数不同,或者参数个数相同但类型不同,则这些同名的方法就具有不同的功能,这就是方法的重载。
方法的重载中参数的类型是关键,仅仅参数的变量名不同是不行的,也不允许参数个数或参数类型完全相同而只有返回值类型不同的重载。
方法重载的条件:
条件一:在同一个类中
条件二:方法名相同
条件三:形参列表不同(个数,类型)
什么是方法的重写?子类继承父类以后,继承过来的方法不能满足子类当前的业务需求,子类有权利对这个方法进行重新编写,这就是方法的重写(又称为方法的覆盖)。
方法重写的条件:
条件一:两个类要有继承关系
条件二:重写后的方法和之前的方法具有:相同的返回值类型、相同的方法名、相同的形参列表
条件三:重写的方法的访问权限不能更低
条件四:重写之后的方法抛出的异常类型不能大于父类抛出的异常类型
(1)构造方法是一种特殊的、与类名相同的方法,分为无参构造方法和有参构造方法,作用是专门用于创建对象时,完成初始化工作;
(2)若一个类没有声明构造方法,依然可以创建新的对象,并能正确执行程序,这是因为如果省略构造方法,Java编译器会自动为该类生成一个默认的构造方法,程序在创建对象时会自动调用默认的构造方法。默认的构造方法没有参数,在其方法体中也没有任何代码,即什么都不做。
(3)构造方法的特殊性主要包括如下几方面:
static 称为静态修饰符,它可以修饰类中的成员。没被 static 修饰的成员称为实例成员,实例成员属于个别对象所有,彼此之间不能共享。而被 static 修饰的成员称为静态成员,也称为类成员。
(1)静态变量:用 static 修饰的成员变量称为静态变量
静态变量是隶属于类的变量,而不是属于任何一个类的具体对象;
静态对象不需要实例化,就可以直接使用;当然也可以通过实例对象来访问静态变量,例如:
类名.静态变量名;
对象名.静态变量名;
静态变量是一个公共的储存单元,不是保存在某个对象实例的内存空间中,而是保存在类的内存空间的公共存储单元中。
对于类的任何一个具体对象而言,静态变量是一个公共的存储单元,类的任何一个对象访问它时,取到的都是一个相同的数值。
由于静态变量是所有对象的公共存储空间,所以使用静态变量的另一个优点是可以节省大量的内存空间,尤其是大量创建对象的时候。
(2)静态方法:用 static 修饰的方法属于类的静态方法,又称为类方法
static 是属于类的,他在内存中的代码段将被所有对象所共用
static 方法只能访问 static 成员变量或调用 static 成员方法,不能直接访问非静态的成员,但可以通过创建对象的方法间接地访问非静态成员。
调用静态方法,可以直接使用类名调用,也可以用某一个具体的对象名来调用,例如:
类名.静态方法名();
对象名.静态方法名();
(3)静态初始化器(“静态代码块”):由 static 修饰的一对花括号 “{ }” 的语句组
定义:Java 运行环境提供了一个系统的垃圾回收器线程,负责自动回收那些没有被引用的对象所占用的内存。垃圾回收的好处:
(1)继承的定义:类的继承是使用已有的类为基础,派生出新的类。被继承的类称为父类或超类,由继承而得到的类称为子类。
(2)继承关键字:extends,例如:
class Subclass extends Superclass {
...
}
(3)继承的实质:通过类继承的方式,便能开发出新的类,而不需要编写相同的程序代码,实质上是实现代码的复用,提高开发效率。
(4)继承的特点:
(1)新定义的类,它可以从父类那里继承所有“非私有成员”作为自己的成员。
(2)子类的每个对象也是其父类的对象,这是继承性的“即是”性质;反之,父类的对象不一定是它子类的对象。
(3)Java 程序在执行子类的构造方法之前,会先调用父类中的无参构造方法,其目的是为了要帮助继承自父类的成员做初始化操作。
(4)在子类的构造方法中,通过 super() 语句来调用父类特定的构造方法,但 super() 语句必须写在子类构造方法的第一行,否则编译时将出现错误信息。
(1)用 protected 修饰的成员可以被该类自身、与它在同一个包中的其他类、在其他包中该类的子类三种类所引用。
(2)将成员声明为 protected 的最大好处是可以同时兼顾成员的安全性与便利性,因为它只能供父类与子类或同一包中的类来访问,而其他类则无法访问它。
(1)定义:
方法的重载:重载是指在同一个类内定义多个名称相同但参数个数或类型不同的方法,Java。
方法的覆盖:覆盖是指在子类中定义名称、参数个数与类型均与父类中完全相同的方法,用以重写父类中同名方法的功能。
(2)子类中不能覆盖父类中声明为 final 或 static 的方法。
(1)向上转型:创建父类类型的变量指向子类对象,即将子类对象赋值给父类类型的变量,这种技术称为“向上转型”。
(2)向下转型:将父类对象通过强制转换为子类型,再赋值给子类对象的技术。向下转型就是将较抽象的类转换为较具体的类。
(1)修饰成员变量:如果用 final 来修饰成员变量,则说明该变量是最终变量,不可再改变,即“常量”,程序的其他部分都可以访问,但不能修改。
static final
两个修饰符所限定,它实际的含义就是常量,所以在程序中通常用 static 和 final 一起来指定一个常量,且这样的常量只能在定义时被赋值;(2)修饰成员方法:如果用 final 来修饰成员方法,则该成员方法不能再被子类所覆盖,即该方法为最终方法
(3)修饰类:如果一个类被 final 修饰符所修饰,则说明这个类不能再被其他类所继承,即该类不可能有子类,这种类被称为最终类。例如:class String
。
(1)Object 类是所有类的来源,如果某个类没有使用 extends 关键字,则该类默认为 java.lang.Object
类的子类。
(2)Object 类常用的方法:
boolean equals(Object obj)
:判断两个对象变量所指向的是否为同一个对象String toString():
:将调用 toString() 方法的对象转换成字符串final Class getClass()
:返回运行时对象所属的类Object clone()
:返回调用该方法的对象的一个副本(1)对于字符串变量来说:
注意:对于字符串的操作,Java 程序在执行时会维护一个字符串池,对于一些可共享的字符串对象,会现在字符串池中查找是否有相同的字符串内容(字符相同),如果有就直接返回,而不是直接创建一个新的字符串对象,以减少内存的占用。当在程序中直接使用 “ ” 括起来的一个字符串时,该字符串就会在字符串池中。
(2)对于非字符串变量来说:== 和 .equals() 方法都用来比较其所指对象在堆内存中的首地址,换句话来说,== 和 .equals() 方法都是用来比较两个类类型的变量是否指向同一个对象。
多态的定义:“多态”即“对象的多种形态”,指同一个实体同时具有多种形式,即同一个对象,在不同时刻,代表的对象不一样。例如:
Map<Integer, String> map = new HashMap()<>;
注:多态的规则:
Animal a = new Cat();
Java实现多态,靠的是父类(或接口定义)的引用变量,可以指向子类(或具体实现类的实例对象),而程序调用的方法在运行期才动态绑定,就是引用变量所指向的具体实例对象的方法,也就是内存里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。
(1)可替换性(substitutability)。多态对已存在代码具有可替换性。例如,多态对圆Circle类工作,对其他任何圆形几何体,如圆环,也同样工作。
(2)可扩充性(extensibility)。多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。实际上新加子类更容易获得多态功能。例如,在实现了圆锥、半圆锥以及半球体的多态基础上,很容易增添球体类的多态性。
(3)接口性(interface-ability)。多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。
(4)灵活性(flexibility)。它在应用中体现了灵活多样的操作,提高了使用效率。
(5)简化性(simplicity)。多态简化对应用软件的代码编写和修改过程,尤其在处理大量对象的运算和操作时,这个特点尤为突出和重要。
在小吴看来,Java中的“抽象”是一种设计理念,旨在将类或者方法的共性抽出来,做一个模版,供后续使用。例如:
Java语言包含抽象类和抽象方法:
(1)抽象类:以 abstract 修饰符所修饰的类,称为抽象类;
abstract class ClassName {
...
}
(2)抽象方法:以 abstract 修饰符所修饰的方法,称为抽象方法;
public abstract void run();
抽象代码示例:
public abstract class Animal {
public abstract void eat();
}
public class Cat extends Animal {
public void eat() {
System.out.println("小猫爱吃鱼");
}
}
public class Dog extends Animal {
public void eat() {
System.out.println("小狗爱吃骨头");
}
}
(1)抽象类的作用优点类似“模版”,它可以作为父类被它所有的子类所共享,其目的是根据它的格式来创建和修改新的子类。
(2)抽象类不能直接创建对象,只能通过抽象类派生出新的子类,再由子类来创建对象。
(3)抽象类的子类必须实现父类中所有的抽象方法,或者将自己也声明为抽象的。
(4)由于抽象类是需要被继承的,所以抽象类不能用 final 来修饰。
(5)一个类不能即是最终类,又是抽象类,即关键字 abstract 与 final 不能同时使用。
(1)抽象方法主需要声明,不需要实现。即用 “;” 结尾,而不是用 “{ }” 结尾。
(2)抽象方法声明中,修饰符 static 和 abstract 不能同时使用。
(3)抽象类不一定包含抽象方法,但是包含抽象方法的类,一定要声明为抽象类。
public interface InterfaceName {
...
}
(1)官方解释:Java 接口是一系列方法的声明,是一些方法特征的集合。接口中只有方法的特征,而没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的功能。
(2)我的理解:
public interface List<E> extends Collection<E> {
int size();
boolean isEmpty();
Iterator<E> iterator();
...
}
(1)接口中的“抽象方法”只需做声明,不用定义其处理数据的方法体;
(2)数据成员都是静态的,且必须赋初值,即数据成员必须是静态常量;
(3)接口中的成员属性和成员方法,都是公开的,所以在定义接口时若省略了 public 修饰符,在实现抽象方法时则不能省略该修饰符;
(4)由接口的定义可以看出,接口实际上就是一种特殊的抽象类。
(1)接口被用来描述一种抽象;
(2)接口被用来实现解耦;
(3)解决 Java 语言无法多继承的问题。
定义一个接口时,可通过 extends 关键字声明该接口是某个已存在接口的子接口,它将继承父接口的常量、抽象方法和默认方法。
接口继承的特点:
(1)一个接口可以有一个以上的父接口,他们之间用 “,” 间隔,形成父接口列表;
(2)新接口将继承所有父类口中的常量、抽象方法和默认方法,但不能继承父接口中的静态方法,也不能被实现类所继承;
(3)如果类实现的接口继承自另外一个接口,那么该类必须实现在接口继承链中定义的所有方法。
(1)容器类:
容器类是 Java 以类库的形式供用户开发程序时可直接使用的各种数据结构。
(2)容器框架:
Java 容器框架中,有两个名称分别为 Collection 和 Set 的接口。从 JDK 5 开始,容器框架全部采用泛型实现。
Java 容器框架结构由两棵接口树构成:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7SHKqiU8-1688804917583)(C:\Users\吴启仁\AppData\Roaming\Typora\typora-user-images\image-20230309235849791.png)]
(1)Collection 是一个接口,是高度抽象出来的集合,它包含了集合的基本操作,比如添加、删除、清空、遍历(读取)、是否为空、获取大小、是否保护某元素。
(2)Collection 接口的定义如下:
public interface Collection<E> extends Iterable<E> {
...
}
(1)列表接口 List 是 Collection 的子接口;
(2)List 是一种包含有序元素的线性表,其中元素必须按顺序存放,且可重复,也可以是空值 null,List 接口使用下标来访问元素;
(3)元素之间的顺序关系可以由添加到列表的先后来决定,也可以由元素值的大小来决定。
(4)实现 List 接口的类主要由两个:链表类 LinkedList 和数组列表类 ArrayList,他们都是线性表。
(5)List 集合的接口定义:
public interface List<E> extends Collection<E> {
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
...
}
(1)ArrayList 集合是 List 接口的实现类之一;
(2)ArrayList 非线程安全,底层是一个 Object[] 动态数组,默认容量为 10,每次扩容 1.5 倍;
(3)ArrayList 集合中的元素通过索引访问,元素可以为 null,元素可以相同;
(4)ArrayList 集合源码分析:
集合定义:
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
...
}
主要成员变量:
private static final int DEFAULT_CAPACITY = 10;
// ArrayList的默认长度是多少
private static final Object[] EMPTY_ELEMENTDATA = {};
// ArrayList的默认空元素链表
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// ArrayList存放的数据
transient Object[] elementData; // non-private to simplify nested class access
// ArrayList的长度
private int size;
构造函数
// 构造一个初始化容量为10的空列表 public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } // 初始化一个指定大小容量的列表 public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } // 构造一个包含指定集合的元素列表, 按照它们由集合迭代器返回的顺序 public ArrayList(Collection<? extends E> c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } }
扩容机制
// 增加元素的方法 public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } //判断当前数组是否是默认构造方法生成的空数组,如果是的话minCapacity=10反之则根据原来的值传入下一个方法去完成下一步的扩容判断 private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } //minCapacitt表示修改后的数组容量,minCapacity = size + 1 private void ensureCapacityInternal(int minCapacity) { //判断看看是否需要扩容 ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); }
(1)LinkedList 集合是 List 接口的实现类之一;
(2)LinkedList非线程安全,底层是链表,双向循环链表;
(3)LinkedList 允许元素为 null,且允许元素重复;
(1)ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。
(2)对于随机访问 get 和 set,ArrayList觉得优于LinkedList,因为LinkedList要移动指针。
(3)对于新增和删除操作 add 和 remove,LinedList比较占优势,因为ArrayList要移动数据。
(1)Set 接口是 Collection 接口的子接口;
(2)Set 集合无序、不可包含重复元素;
(3)HashSet 是基于 HashMap 实现的,TreeSet 是基于 TreeMap 实现的。
(1)HashSet 集合是 Set 接口的实现类之一;
(2)HashSet 是底层是基于哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null;
(3)HashSet 要求放入的对象必须实现 HashCode()
方法,放入的对象,是以 hashcode 码作为标识的,而具有相同内容的 String 对象,hashcode 是一样,所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例 。
(4)HashSet 集合源码:
集合定义:
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {
...
}
构造函数:
private transient HashMap<E,Object> map; //默认构造器 public HashSet() { map = new HashMap<>(); } //将传入的集合添加到HashSet的构造器 public HashSet(Collection<? extends E> c) { map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); addAll(c); } //明确初始容量和装载因子的构造器 public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<>(initialCapacity, loadFactor); } //仅明确初始容量的构造器(装载因子默认0.75) public HashSet(int initialCapacity) { map = new HashMap<>(initialCapacity); }
注:通过上面的源码,我们发现了HashSet就是一个皮包公司,它就对外接活儿,活儿接到了就直接扔给HashMap处理了。因为底层是通过HashMap实现的,这里简单提一下:HashMap的数据存储是通过数组+链表/红黑树实现的,存储大概流程是通过hash函数计算在数组中存储的位置,如果该位置已经有值了,判断key是否相同,相同则覆盖,不相同则放到元素对应的链表中,如果链表长度大于8,就转化为红黑树,如果容量不够,则需扩容。
(1)TreeSet 集合是 Set 接口的实现类之一;
(2)TreeSet 是给予二叉树实现的,Treeset中的数据是自动排好序的,不允许放入null值;
(3)TreeSet可以确保集合元素处于排序状态,TreeSet支持两种排序方式:自然排序和定制排序。
CompareTo (Object obj)
方法来比较元素之间大小关系,然后将元素按照升序排列;int compare(T o1,T o2)
方法。(1)Map 提供了一种映射关系,其中的元素是以键值对(key-value)的形式存储的,能够实现根据 key 快速查找 value;
(2)Map 中的键值对以 Entry 类型的对象实例形式存在;
(3)键(key)不可重复,值(value) 可以重复,一个value值可以和很多 key 值形成对应关系,每个键最多只能映射到一个值。
(4)Map支持泛型,形式如:Map<K,V>
(5)Map中使用put(K key,V value)方法添加
(1)接口是 Map 接口中的一个内部接口,定义 Map 集合的键值对,其定义如下:
interface Entry<K,V> {
K getKey();
V getValue();
...
}
(2)为什么要设计这个内部接口?
Map 中存放的元素均为键值对,所以每一个键值对必然存在一个映射关系,Map 中采用 Entry 内部类来表示一个映射项,映射项包含 Key 和 Value
(1)Set<K> keySet();
是 Map 集合的一个内部接口,返回值是个只存放key值的Set集合;
(2)HashMap 集合中 keySet() 方法的定义:
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
(3)keySet() 方法的使用举例:
public class Demo {
public static void main(String[] args) {
Map<Integer, String> map = new HashMap<>();
map.put(1, "第一");
map.put(2, "第二");
map.put(3, "第三");
map.put(4, "第四");
map.put(5, "第五");
Set<Integer> set = map.keySet();
System.out.println(set); // 结果:[1, 2, 3, 4, 5]
}
}
(1)是 Map 接口中的一个抽象方法,返回 Map 接口的“键值对” Set 集合,Set 集合里面的类型是Map.Entry;
(2)该方法由 Map 接口的实现类去重写,entrySet() 方法的定义如下:
Set<Map.Entry<K, V>> entrySet();
(3)entrySet() 在 HashMap 类中的重写:
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
(4)entrySet() 方法的使用举例:
public class Demo { public static void main(String[] args) { Map<Integer, String> map = new HashMap<>(); map.put(2, "第二"); map.put(4, "第四"); map.put(1, "第一"); map.put(5, "第五"); map.put(3, "第三"); // 将Map中的元素,以“键-值”的形式存储到一个Set集合中,无序存放 Set<Map.Entry<Integer, String>> entries = map.entrySet(); for (Map.Entry<Integer, String> entry : entries) { System.out.print(entry + " "); // 结果:1=第一 2=第二 3=第三 4=第四 5=第五 } Iterator<Map.Entry<Integer, String>> iterator = entries.iterator(); List<Integer> list = new ArrayList<>(); while (iterator.hasNext()) { int key = iterator.next().getKey(); list.add(key); } // 将Map的key放在一个集合中 System.out.println(Arrays.toString(list.toArray())); // 结果:[1, 2, 3, 4, 5] // 将key排序之后,查找对应的value list.sort((o1, o2) -> o2 - o1); for (int i = 0; i < map.size(); i++) { String value = map.get(list.get(i)); System.out.print(value + " "); // 结果:第五 第四 第三 第二 第一 } } }
(1)HashMap 集合类是 Map 接口的实现类之一,也是最常用的 Map 类,底层基于哈希表实现;
(2)HashMap 集合类的 key 值和 value 值都可以为null,但是一个 HashMap 只能有一个 key 值为 null 的映射,因为key值不可重复。
(1)HashMap 底层是基于数组和链表来实现的,即 HashMap 底层实现还是数组,只是数组的每一项都是一条链表。
(2)JDK 1.8前后 HashMap 的变化:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d2BOp7pl-1688804917584)(C:\Users\吴启仁\AppData\Roaming\Typora\typora-user-images\image-20230314225712373.png)]
定义:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
...
}
构造函数
// 构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } // 构造一个带指定初始容量和默认加载因子 (0.75) 的空 HashMap public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } //构造一个带指定初始容量和加载因子的空 HashMap public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); }
注:
在这里提到了两个参数:初始容量,加载因子,这两个参数是影响HashMap性能的重要参数;
”初始容量“是创建哈希表时的容量,容量表示哈希表中桶的数量,
”加载因子“是哈希表在其容量自动增加之前可以达到多满的一种尺度,它衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。系统默认负载因子为0.75,一般情况下我们是无需修改的。
HashMap 和 TreeMap 都是 Java 中常用的 Map 接口实现类。
(1)HashMap 是基于哈希表实现的,它是非线程安全的,在多线程环境下需要使用 Collections.synchronizedMap(new HashMap()) 来进行包装。它的键值对是无序的。
(2)TreeMap 是基于红黑树实现的,它是线程安全的,键值对是有序的,它按照键的自然顺序或者比较器顺序来排序。
(3)总的来说 HashMap 更快,而 TreeMap 更适用于需要排序和有序的情况。
LinkedHashMap 和 HashMap 都是 Java 中用于存储键值对的映射表,LinkedHashMap 是 HashMap 的子类,继承了父类的主要方法。主要区别在于:
(1)顺序:LinkedHashMap 会按照元素插入的顺序遍历,而 HashMap 则没有特定的顺序。
(2)实现:LinkedHashMap 内部使用链表来维护元素的顺序,而 HashMap 使用数组+链表的结构。
(3)性能:在需要保证顺序的场景下使用LinkedHashMap会有较差的性能,因为需要维护链表。而HashMap没有这样的限制,性能会更高。
(4)常用用途:HashMap 通常用于简单的键值映射,而LinkedHashMap 则多用于缓存和 LRU 算法中。
(1)程序是含有指令和数据的文件,被存储在磁盘或其他的数据存储设备中,也就是说,程序是静态的代码;
(2)当程序在执行时,将会被操作系统载入内存中,并启动它的工作,然后就变成了所谓的进程。
(1)进程是程序的一次执行过程,是系统运行程序的基本单位,进程是动态的;
(2)进程是操作系统资源分配和处理器调度的基本单位,拥有独立的代码、内部数据和运行状态;
(3)频繁的切换进程状态,会消耗大量的系统资源;
(4)系统运行一个程序即是一个进程从创建、运行到消亡的过程;
(5)每个进程之间是相互独立的,一起执行他们和先后执行他们是没有什么差别的。
(1)多任务是指在一个系统中可以同时运行多个进程,即有多个独立运行的任务,每一个任务对应一个进程;
(2)多任务是由操作系统将系统资源分配给各个进程,每个进程在CPU上交替运行着。
(1)线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位;
(2)一个进程在其执行过程中可以产生多个线程,形成多条执行路径;
(3)线程不能独立存在,必须存在于线程之中;
(4)一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qpFCo7yJ-1688804917585)(C:\Users\吴启仁\AppData\Roaming\Typora\typora-user-images\image-20230202222234402.png)]
(1)多线程(multi-thread)是指同一个进程中同时存在几个执行体,按几条不同的执行路径同时工作的情况;
(2)执行一个线程不必等待另一个线程执行完成后才进行,所有线程都可以发生在同一时刻;
使用多线程,本质上就是提升程序性能。度量性能的指标有很多,但是有两个指标是最核心的,它们就是延迟和吞吐量。
(1)延迟:发出请求到收到响应这个过程的时间;延迟越短,意味着程序执行得越快,性能也就越好。
(2)吞吐量:在单位时间内能处理请求的数量;吞吐量越大,意味着程序能处理的请求越多,性能也就越好。
这两个指标内部有一定的联系(同等条件下,延迟越短,吞吐量越大),但是由于它们隶属不同的维度(一个是时间维度,一个是空间维度),并不能互相转换。
所谓提升性能,从度量的角度,主要是降低延迟,提高吞吐量。这也是使用多线程的主要目的。
每个Java程序都有一个默认的主线程,对于应用程序来说其主线程是 main() 方法执行的线程。要想实现多线程,必须在主线程中创建新的线程对象。
新建的线程在它的一个完整生命周期内通常要经历五种状态:
(1)新建(newborn):当一个 Thread 类或其子类的对象被声明并创建,但还未被执行的这段时间里,处于一种特殊的新建状态中。特征:
(2)就绪(runnable):处于新建状态的线程被启动后,将进入线程队列排队等待CPU资源,此时它已具备了运行的条件,也就是出于就绪状态。特征:
(3)执行(running):当就绪状态的线程被调度并获取CPU资源的时候,便进入执行状态。特征:
(4)阻塞(blocked):一个正在执行的线程如果在某些特殊情况下,将让出CPU并暂时中止自己的执行,线程处于这种不可执行的状态,就被称为阻塞状态。下面几种情况可使得一个线程进入阻塞状态:
(5)消亡(dead):导致线程消亡的原因有两个:
注:当线程处于消亡状态、并且没有该线程对象的引用时,垃圾回收期会从内存中删除该线程对象。
(1)在多线性的系统中,每个线程都被赋予了一个执行优先级,优先级决定了线程被CPU执行的优先顺序。优先级高的线程可以在一段时间内获得比优先级低的线程更多的执行时间。
(2)Java 语言的线程优先级从低到高以整数 1 ~ 10 表示,共分为 10 级。Thread 类有三个关于线程优先级的静态常量:
MIN_PRIORITY:表示最小优先级,通常为 1;
MAX_PRIORITY:表示最高优先级,通常为 10;
NORM_PRIORITY:表示普通优先级,默认为5。
注:如果想要改变线程的优先级,可以通过调用线程对象的 setPriority()
方法来进行设置。
具体操作:继承 Thread 类,并重写父类的 run() 方法
public class MyThread extends Thread { // 演唱会,需要卖票100000张 private int ticket = 100000; @Override public void run() { while (true) { synchronized (MyThread.class) { if (ticket > 0) { System.out.println(Thread.currentThread().getName() + " 卖票: No." + ticket); ticket--; } else { break; } } } } }
public class MyThreadTest {
public static void main(String[] args) {
// 启动3个线程th1,th2,th3;共同卖100000张票
MyThread window = new MyThread();
Thread th1 = new Thread(window, "窗口1");
Thread th2 = new Thread(window, "窗口2");
Thread th3 = new Thread(window, "窗口3");
th1.start();
th2.start();
th3.start();
}
}
具体操作:实现 Runnable 接口,并重写接口的 run() 方法
public class MyRunnable implements Runnable { // 演唱会,需要卖票100000张 private int ticket = 100000; @Override public void run() { while (true) { synchronized (MyThread.class) { if (ticket > 0) { System.out.println(Thread.currentThread().getName() + " 卖票: No." + ticket); ticket--; } else { break; } } } } }
public class MyRunnableTest {
public static void main(String[] args) {
// 启动3个线程th1,th2,th3;共同卖100000张票
MyRunnable window = new MyRunnable();
Thread th1 = new Thread(window, "窗口1");
Thread th2 = new Thread(window, "窗口2");
Thread th3 = new Thread(window, "窗口3");
th1.start();
th2.start();
th3.start();
}
}
具体操作:实现 Callable 接口,并重写接口的 call() 方法
public class MyCallable implements Callable<String> { // 演唱会,需要卖票100000张 private int ticket = 100000; @Override public String call() throws Exception { while (true) { synchronized (MyThread.class) { if (ticket > 0) { System.out.println(Thread.currentThread().getName() + " 卖票: No." + ticket); ticket--; } else { break; } } } return "票卖完了..."; } }
public class MyCallableTest { public static void main(String[] args) { Callable<String> callable = new MyCallable(); MyCallable window = new MyCallable(); FutureTask<String> futureTask1 = new FutureTask<>(window); FutureTask<String> futureTask2 = new FutureTask<>(window); FutureTask<String> futureTask3 = new FutureTask<>(window); new Thread(futureTask1).start(); new Thread(futureTask2).start(); new Thread(futureTask3).start(); try { System.out.println("窗口1返回结果:" + futureTask1.get()); System.out.println("窗口2返回结果:" + futureTask2.get()); System.out.println("窗口3返回结果:" + futureTask3.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } }
具体操作:系统启动时创建线程池,使用时直接调用。是实际中用的最多的方法。
public class MyThreadPool implements Runnable{ // 演唱会,需要卖票100000张 private int ticket = 100000; @Override public void run() { while (true) { synchronized (test12_thread.Thread.MyThread.class) { if (ticket > 0) { System.out.println(Thread.currentThread().getName() + " 卖票: No." + ticket); ticket--; } else { System.out.println("票卖完了..."); break; } } } } }
public class MyThreadPoolTest {
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(3);
// 提交5个任务,让线程池去执行
pool.submit(new MyThreadPool());
pool.submit(new MyThreadPool());
pool.submit(new MyThreadPool());
pool.submit(new MyThreadPool());
pool.submit(new MyThreadPool());
// 销毁线程池
pool.shutdown();
}
}
(1)降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
(2)提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
(3)提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
(1)ThreadPoolExecutor
类的源码
线程池的真正实现类是 ThreadPoolExecutor
public class ThreadPoolExecutor extends AbstractExecutorService {
...
}
(2)ThreadPoolExecutor 类的构造方法
构造方法1:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler);
}
构造方法2:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler);
}
构造方法3:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler);
}
构造方法4:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.acc = System.getSecurityManager() == null ? null : AccessController.getContext(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
构造方法参数解读
(1)任务队列(workQueue)
任务队列是基于阻塞队列实现的,即采用 生产者 - 消费者
模式,在 Java 中需要实现 BlockingQueue 接口。但 Java 已经为我们提供了 7 种阻塞队列的实现:
注意:有界队列和无界队列的区别:如果使用有界队列,当队列饱和时并超过最大线程数时就会执行拒绝策略;而如果使用无界队列,因为任务队列永远都可以添加任务,所以设置 maximumPoolSize 没有任何意义。
(2)线程工厂(threadFactory)
线程工厂指定创建线程的方式,需要实现 ThreadFactory 接口,并实现 newThread(Runnable r) 方法。该参数可以不用指定,Executors 框架已经为我们实现了一个默认的线程工厂:
// The default thread factory static class DefaultThreadFactory implements ThreadFactory { private static final AtomicInteger poolNumber = new AtomicInteger(1); private final ThreadGroup group; private final AtomicInteger threadNumber = new AtomicInteger(1); private final String namePrefix; DefaultThreadFactory() { SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-"; } public Thread newThread(Runnable r) { Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) t.setDaemon(false); if (t.getPriority() != Thread.NORM_PRIORITY) t.setPriority(Thread.NORM_PRIORITY); return t; } }
(3)拒绝策略(handler)
当线程池的线程数达到最大线程数时,需要执行拒绝策略。拒绝策略需要实现 RejectedExecutionHandler 接口,并实现 rejectedExecution(Runnable r, ThreadPoolExecutor executor)
方法。不过 Executors 框架已经为我们实现了 4 种拒绝策略:
// (1)创建线程池 ThreadPoolExecutor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); // (2)向线程池提交任务 threadPool.execute(new Runnable() { @Override public void run() { ... // 线程执行的任务 } }); // (3)关闭线程池 threadPool.shutdown(); // 设置线程池的状态为SHUTDOWN,然后中断所有没有正在执行任务的线程 threadPool.shutdownNow(); // 设置线程池的状态为 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表
Executors 已经为我们封装好了 4 种常见的功能线程池,如下:
(1)定长线程池(FixedThreadPool)
特点:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。
应用场景:控制线程最大并发数。
创建方法源码:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
使用示例:
// 1. 创建定长线程池对象 & 设置线程池线程数量固定为3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task = new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
fixedThreadPool.execute(task);
(2)定时线程池(ScheduledThreadPool )
特点:核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列。
应用场景:执行定时或周期性的任务。
创建方法源码:
private static final long DEFAULT_KEEPALIVE_MILLIS = 10L; public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { return new ScheduledThreadPoolExecutor(corePoolSize); } public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue()); } public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory) { return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory); } public ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory) { super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue(), threadFactory); }
使用示例:
// 1. 创建 定时线程池对象 & 设置线程池线程数量固定为5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task = new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // 延迟1s后执行任务
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// 延迟10ms后、每隔1000ms执行任务
(3)可缓存线程池(CachedThreadPool)
特点:无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。
应用场景:执行大量、耗时少的任务。
创建方法的源码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
使用示例:
// 1. 创建可缓存线程池对象
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task = new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
cachedThreadPool.execute(task);
(4)单线程化线程池(SingleThreadExecutor)
特点:只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列。
应用场景:不适合并发但可能引起 IO 阻塞性及影响 UI 线程响应的操作,如数据库操作、文件操作等。
创建方法的源码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
使用示例:
// 1. 创建单线程化线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2. 创建好Runnable类线程对象 & 需执行的任务
Runnable task =new Runnable(){
public void run() {
System.out.println("执行任务啦");
}
};
// 3. 向线程池提交任务
singleThreadExecutor.execute(task);
(5)功能线程池的比较
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JeoJ0ipN-1688804917586)(C:\Users\吴启仁\AppData\Roaming\Typora\typora-user-images\image-20230330234436576.png)]
(1)Executors 的 4 个功能线程池虽然方便,但现在已经不建议使用了,而是建议直接通过使用 ThreadPoolExecutor
的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
例如,在实例项目中,创建线程池:
/** * 多数据中心用于像各个边缘站点发送数据 * * @authors00422198 * @since 2022-10-12 */ public class MultiDCRestDispatcher { private static final Logger LOGGER = LoggerFactory.getLogger(MultiDCRestDispatcher.class); private static Supplier<MultiDCRestDispatcher> INSTANCE = Suppliers.memoize(MultiDCRestDispatcher::new); private static final int POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2 + 2; private static final int MAX_POOL_SIZE = 100; private static final ExecutorService CALL_EDGE_SITES_EXECUTOR = new ThreadPoolExecutor(POOL_SIZE, MAX_POOL_SIZE, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new NamedThreadFactory("multi-dc-rest-dispatch")); public static MultiDCRestDispatcher getInstance() { return INSTANCE.get(); } }
(2)其实 Executors 的 4 个功能线程有如下弊端:
(1)“降低延迟,提高吞吐量”的方法?
基本上有两个方向:
注:前者属于算法范畴,后者则是和并发编程息息相关了。
计算机主要硬件类型:一个是 I/O,一个是 CPU。在并发编程领域,提升性能本质上就是提升硬件的利用率,就是提升 I/O 的利用率和 CPU 的利用率。
(2)创建多少线程合适?
答:创建多少线程合适,要看多线程具体的应用场景。
(1)简单场景:因为线程功能简单,每个线程都包含了运行时所需要的数据和方法,这样的线程在运行时不需要外部的数据和方法,就不必关心其他线程的状态或行为,称这样的线程为独立的、不同步的或是异步执行的。
(2)复杂场景:当应用问题的功能增强、关系复杂,存在多个线程之间共享数据时,若线程仍以异步方式访问共享数据时,有时是不安全或不符合逻辑的。
当一个线程对共享的数据进行操作时,应使之成为一个“原子操作”,即在没有完成相关操作之前,不允许其他线程打断它,否则就会破坏数据的完整性,必然会得到错误的处理结果,这就是线程的同步。
被多个线程共享的数据,在同一时刻只允许一个线程处于操作之中,这就是同步控制中的线程间互斥问题。
为了使临界代码对临界资源的访问成为一个不可被中断的原子操作,Java 技术利用对象的 “互斥锁” 机制来实现线程间的互斥操作。
在 Java 语言中每个对象都有一个互斥锁与之相连。
一个对象的互斥锁只有一个,利用对一个对象互斥锁的争夺,可以实现不同线程的互斥效果。
当一个线程获得互斥锁后,则需要该互斥锁的其他线程只能处于等待状态。
例如:当线程 A 获得了一个对象的互斥锁后,线程 B 若也想获得该对象的互斥锁,就必须等待线程A完成规定的操作并释放出互斥锁后,才能获得该对象的互斥锁,并执行线程 B 中的操作。
Java 语言使用 synchronized
关键字来表示同步资源,这里的资源可以说一种类型的数据,也就是对象,也可以是一个方法,还可以是一段代码。
首先判断对象或方法的互斥锁是否存在:
(1)互斥锁若在,就获得互斥锁,然后就可以执行紧随其后的临界代码段或方法体;
(2)互斥锁若不在(已被其他线程拿走),就进入等待状态,知道获得互斥锁。
注:当被 Synchronized 限定的代码段执行完,就会自动释放对象或方法的互斥锁。
(1)格式一:同步语句
Synchronized(对象) {
// 临界代码段
}
(2)格式二:同步方法
public synchronized 返回值类型 方法名() {
// 方法体
}
(1)synchronized 锁定的通常是临界代码,这些代码之间是串行执行的,不再是相互交替穿插并发执行,因而保证了 synchronized 代码块操作的原子性。
(2)synchronized 代码块中的代码数量越少越好,包含的范围越小越好,否则就失去多线程并发执行的很多优势。
(3)任何时刻,一个对象的互斥锁只能被一个线程拥有。
(4)临界代码中的共享变量应定义为 private 型,否则其他类的方法可能直接访问和操作该共享变量,这样 synchronized 的保护就失去了意义。
(5)如果 synchronized 用在类声明中,则表示该类中的所有方法都是 synchronized 的。
synchronized
和 java.util.concurrent.locks.Lock
是 Java 中两种用来实现线程同步的方式。
(1)synchronized
是 Java 的关键字,它可以修饰方法和代码块。当一个线程访问一个对象的同步方法或同步代码块时,其他线程不能访问这个对象的其他同步方法或同步代码块。
(2) Lock
是 java.util.concurrent.locks
包中的一个接口,它提供了更多的灵活性。比如可以尝试获取锁而不会阻塞线程、可以重试获取锁的次数以及可以提供公平锁和非公平锁。
总的来说,如果你需要基本的线程同步,可以使用 synchronized
关键字,如果需要更高级的线程同步,可以使用 Lock
接口。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uGRx6ZcY-1688804917586)(C:\Users\吴启仁\AppData\Roaming\Typora\typora-user-images\image-20230401155228226.png)]
多线程的执行往往需要相互之间的配合,为了更有效地协调不同的线程的工作,需要在线程间建立沟通渠道,通过线程间的“对话”类解决线程间的同步问题,而不是紧紧依靠互斥机制。
(1)public final void wait()
:如果一个正在执行同步代码(synchronized)的线程A执行了 wait()
调用在对象 x 上,该线程暂停执行而进入对象 x 的等待队列,并释放已获得的对象 x 的互斥锁。线程A到一直等到其他线程在对象 x 上调用 notify()
或 notifyAll()
方法,才能够在重新获得对象 x 的互斥锁后继续执行(从 wait()
语句后继续执行)。
(2)public void notify()
:唤醒正在等待该对象互斥锁的第一个线程;
(3)public void notifyAll()
:唤醒正在等待该对象互斥锁的所有线程,具有最高优先级的线程首先被唤醒并执行。
注意:
wait()
、notify()
、notifyAll()
方法,该线程必须已经获得对象 x 的互斥锁。wait()
、notify()
、notifyAll()
只能在同步代码块里调用。(1)相同点:
都可以让线程进入休眠状态。
都可以相应interrupt中断请求。
(2)不同点:
wait() 必须配合 synchronized 使用,而 sleep() 不用;
wait() 属于 Object 对象的方法,而 sleep() 是属于 Thread 线程的方法;
sleep() 不释放锁,而 wait() 要释放锁;
sleep() 必须要传递一个数值型的参数,而 wait() 可以不传参;
sleep() 让线程进入到 TIMED_WAITING 状态,而无参的 wait() 方法让线程进入了 WAITING 状态;
一般情况下,sleep() 只能等待超过时间之后才能恢复执行,而 wait() 可以接收 notify() 、notifyAll() 之后就可以执行。
待定。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。