Java基础复习
一. java入门
java特点
1.面向对象
基本特点:类,对象
三大特性:
-
封装:内部细节对外部调用透明,外部调用无需修改或者关心内部实现
-
继承:继承基类的方法,并做出自己的改变和/或扩展
-
多态:父类引用指向子类对象,可以掉父类对象
2.健壮性
3.跨平台
JDK JRE JVM
JDK:
Java Develpment Kit java 开发工具
JRE:
Java Runtime Environment java运行时环境
JVM:
java Virtual Machine java 虚拟机
类的加载3种方式
1.由 new 关键字创建一个类的实例(静态加载)在由运行时刻用new 方法载入
2.调用 Class.forName()方法(动态加载)通过反射加载类型,并创建对象实例
3.调用某个 ClassLoader 实例的 loadClass() 方法(动态加载)
通过该 ClassLoader 实例的 loadClass() 方法载入。应用程序可以通过继承 ClassLoader 实现自己的类装载器。
加载:类加载过程的一个阶段:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象
验证:目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
准备:为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
解析:主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析(这里涉及到字节码变量的引用,如需更详细了解,可参考《深入Java虚拟机》)。
初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)
这便是类加载的5个过程,而类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例,在虚拟机提供了3种类加载器,引导(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器)
启动(Bootstrap)类加载器
启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <java_home>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。
扩展(Extension)类加载器
扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<java_home>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。
系统(System)类加载器
也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
双亲委派机制工作过程:
如果一个类加载器收到了类加载器的请求.它首先不会自己去尝试加载这个类.而是把这个请求委派给父加载器去完成.每个层次的类加载器都是如此.
因此所有的加载请求最终都会传送到Bootstrap类加载器(启动类加载器)中.只有父类加载反馈自己无法加载这个请求(它的搜索范围中没有找到所需的类)时.子加载器才会尝试自己去加载。
双亲委派模型的优点:
java类随着它的加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jart之中.无论哪一个类加载器都要加载这个类.最终都是双亲委派模型最顶端的Bootstrap类加载器去加载.因此Object类在程序的各种类加载器环境中都是同一个类.相反.如果没有使用双亲委派模型.由各个类加载器自行去加载的话.如果用户编写了一个称为“java.lang.Object”的类.并存放在程序的ClassPath中.那系统中将会出现多个不同的Object类.java类型体系中最基础的行为也就无法保证.应用程序也将会一片混乱.
二.Java基本语法
1.变量:用于在内存中保存数据
2.数据类型
3.强制类型转换
4.引用类型: String 连接"+"
面试题:为什么String设计为不可变?
(1)字符串常量池的需要:当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。
(2)保证了hash码的唯一性,允许String对象缓存HashCode
(3)避免安全问题:在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。
5.运算符
算数运算符:+ - * /
逻辑运算符:& | ! && || ^
位运算符:<< 左移乘2 >> 右移除2
三元运算符: (条件表达式)?表达式1:表达式2
6.流程控制
(1)if-else
(2)如何从键盘获取scanner
(3)switch-case
- switch 语句中的变量类型可以是: byte、short、int 或者 char。从 Java SE 7 开始,switch 支持字符串 String 类型了,同时 case 标签必须为字符串常量或字面量。
switch(expression){
case value :
//语句
break; //可选
case value :
//语句
break; //可选
//你可以有任意数量的case语句
default : //可选
//语句
}
(4)for循环
public class Test2 {
public static void main(String[] args) {
for (int i = 100; i < 1000;i++) {
int ge = i % 10;
int shi = i % 100 /10;
int bai = i / 100;
if (i == ge*ge*ge + shi*shi*shi + bai*bai*bai) {
System.out.println("水仙花数:" + i);
}
}
}
}
(5)while循环
(6)do-while
while是先判断在做,而do-while先做在判断,至少执行一次
(7)特殊关键字的使用:break(结束当前循环)、continue(结束当前循环)
7.数组
1.一维数组
2.获取数组长度 遍历
public class ArrayTest1 {
public static void main(String[] args) {
int[] num = {1,4,3,2,8,6};
System.out.println(num.length);//获取长度
//遍历数组的每个元素
for (int s: num){
System.out.print(s + " ");
}
}
}
排序:Array.sort();
查找:Array.binarySearch();
打印:Array.toString();
复制:Array.copyof();
3.多维数组
4.随机数
公式:Math.random()*(n-m + 1)+m,生成大于等于m小于n的随机数;
Math.random();//产生一个[0,1)之间的随机数
5.数组复制,数组翻转
String[] arr = {"詹姆斯","浓眉","科比","威少","哈登"};
//复制数组
String[] arr1 = new String[arr.length];
for (int i = 0; i < arr1.length; i++) {
arr1[i] = arr[i];
}
//数组反转方式1
for (int i = 0; i < arr.length / 2; i++) {
String temp = arr[i];
arr[i] = arr[arr.length - i -1];
arr[arr.length - i -1] = temp;
}
//反转方式二
for (int i = 0, j = arr.length -1; i < j; i++,j--) {
String temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
//遍历
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + "\t");
}
6.查找
(1)线性查找
public static void main(String[] args) {
String[] arr = {"詹姆斯","浓眉","科比","威少","哈登"};
//查找
Scanner scanner = new Scanner(System.in);
System.out.println("请出入:");
String dest = scanner.next();
boolean isFlag = true;
for (int i = 0; i < arr.length; i++) {
if (dest.equals(arr[i])) {
System.out.println("找到之指定元素位置为:" + i);
isFlag = false;
break;
}
}
if (isFlag) {
System.out.println("对不起,没有找到!");
}
}
}
(2)二分查找
public class ArrayTest3 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
//二分查找
//使用二分法查找前提是必须有序
int[] arr2 = {-1,2,4,5,6,7,8,9,11,12};
System.out.println("请出入你要找的值:");
int index = scanner.nextInt();
int head = 0;
int end = arr2.length - 1;
boolean isFlag1 = true;
while (head <= end) {
int mid = (head + end) / 2;
if (index == arr2[mid]) {
System.out.println("恭喜找到了,位置为:" + mid);
isFlag1 = false;
break;
} else if (arr2[mid] > index) {
end = mid - 1;
} else {
head = mid +1;
}
}
if (isFlag1) {
System.out.println("对不起,没有找到");
}
}
}
7.数组异常
(1)数组角标越界的异常:ArrayIndexOutOfBoundsException
(2)空指针异常:NullPointerException
8.时间相关的类
1、Date类 .getTime();计算毫秒
2、SimpleDateFormat类 格式化时间 .format();返回的是String字符串
3、Calendar接口
日历字段之间的转换提供了一些方法
.get(Calendar.YEAR);
.get(Calendar.MONTH);// 默认是当前月份减一 从0开始的
.get(Calendar.DAY_OF_MONTH);
.get(Calendar.DAY_OF_WEEK);
Calendar calendar = Calendar.getInstance();
Date date = calendar.getTime();
4、Runtime运行时时间 .freeMemory(); 当前的系统剩余空间
5、System.exit(0);退出程序,参数是0是正常退出
System.gc();调用垃圾回收器,不一定能够起来,只是起到一个促进的作用
三.对象与类
- 类是对一类事物的描述,是抽象的、概念上的定义
- 对象是实际存在该类事物的每个个体
1.对象内存解析
本地方法栈:存放C++
程序计数器:存放当前程序运行的位置
栈:当前函数运行时的局部变量,存储对象引用地址指向堆
方法区:在JDK1.7之前为永久代, 用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。
堆:用来存对象
本地方法栈,程序计数器,栈是线程私有的(每个线程会单独开辟这样一个内存)
堆和方法区是共享的
2.JVM的GC
3.属性与局部变量的对象
- 属性直接定义在类的一对{}内,而局部变量声明在方法内、方法形参、代码块内、构造器内部的变量。
- 属性可以在声明时指明权限问题,局部变量不可以使用权限修饰符。
- 属性加载到堆空间中的,而局部变量加载到栈空间。
4.方法重载
重载和重写
重载: 发生在同一个类中,方法名必须相同,参数类型不同
重写: 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于
等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方
法
5.构造器
构造器作用:创建对象
6.java Bean
-
JavaBean是一种Java语言写成的可重用组建
-
所谓JavaBean,是符合以下标准的Java类
-
- 类是公共的
- 有一个无参的公共构造器
- 有属性,且有对应的get、set方法
四.继承
1.继承:
继承基类的方法,并做出自己的改变和/或扩展
- 如果子类的构造器没有显示地调用超类的构造器,则会自动地超类默认(没有参数)的构造器,若父类只存在带惨构造则不会有默认的无参构造器。
- 如果父类没有无参构造器,并且子类的构造器又没有显示地调用父类的其他构造器,则Java编译期将报告错误。
inal 修饰符的主要作用为:确保被修饰的对象在子类中不会改变语义。
2.重写
-
定义:
-
- 在子类中可以根据需要对父类中继承来的方法进行改造,称之为方法重置、覆盖
-
要求:
-
- 子类重写的方法 必须和父类被重写方法具有相同的方法名称、参数列表
- 子类重写方法的返回值类型不能大于父类被重写的方法返回值
- 子类重写父类的方法使用的访问权限不能小于父类被重写的方法访问权限(子类不能重写父类声明为private权限方法)
- 子类方法抛出异常不能大于父类被重写方法异常
-
注意:
-
- 子类和父类中同名参数的方法必须同时声明为非static的活着同时声明为static
3.equals
对于==:
基本数据类型的变量,则直接比较其存储的 “值”是否相等;
引用类型的变量,则比较的是所指向的对象的地址
对于equals:
注意:equals方法不能作用于基本数据类型的变量
如果没有对equals方法进行重写,则比较的是引用类型的变量所指向的对象的地址;
诸如String、Date等类对equals方法进行了重写的话,比较的是所指向的对象的内容。
4.多态
总结:对象的多态性:是指父类的引用指向子类的对象。(或者子类 的对象赋给父类的引用)
注意:在我们编译期,只能调用父类中声明的方法,但是在运行期中,我们实际执行的是子类的重写父类的方法
小结:编译看左边、运行看右边。
5.Object类的使用
1.==
- 如果比较的是基本数据类型变量,比较两个变量保存的数据是否相等
- 如果比较的引用数类型变量,比较的是对象的地址值是否相同
2.equals
为什么需要重写equals()方法?因为如果直接调用equals方法的话是使用Object类的equals的,是比较地址是否相等。而我们需要进行比较两个引用的内容是否相等是需要重写equals方法。
3.toString
- 当我要输出一个对象引用时,实际上就是调用当前对象的toString
- 使用toString时是指,返回“实体内容”信息
6.关键字Static
-
static可以使用来修饰:属性、方法、代码块、内部类
-
static修饰属性:静态变量(不使用static修饰的称实例变量)
-
- 静态变量随着类加载而加载。可通过“类”静态变量的方式进行调用
- 静态变量的加载要早于对象的创建
- 由于类只会加载一次,则静态变量的内存中也只会保存一份,存在方法区的静态域中
-
static修饰方法:静态方法
-
- 随着类的加载而加载。可以通过“类”点调用静态方法即可。
- 静态方法中:只能调用静态的方法或属性;而非静态方法既可以调用非静态方法或属性,也可以调用静态的方法或属性。
- static使用细节:在静态的方法内不能使用this关键字、super关键
-
在开发中如何确定一个属性和方法是否要声明为static关键字?
-
- 属性是可以被多个对象所共享的,不会随着对象的不同而不同
- 操作静态属性的方法,通常设置为static;工具类中的方法使用static比如:Math、Arrays、Collection等
6.代码块
1)代码块的作用:用来初始化类、对象
(2)代码块如果修饰的话,只能使用static
(3)分类:静态代码块、非静态代码块
-
静态代码块:
-
- 内部可以有输出语句
- 随着类的加载而执行,而且只加载一次
- 作用:初始化类的信息
- 如果一个类总定义多个静态代码块,则按照顺序进行执行
-
非静态代码块:
-
- 内部可以有输出语句
- 随着对象的创建而执行
- 每创建一个对象,就执行一次非静态代码块
- 作用:可以在创建对象时,对对象的属性等进行初始化
7.关键字final
- final可以修饰的结构:类、方法、变量
- final修饰一个类时,此类不能被其它类继承
- final修饰变量时,此时的“变量”就称为一个常量,声明为final的变量就不能被改变了
- final修饰属性:可以考虑赋值的位置有(显示初始化、代码块中初始化、构造器中初始化)
- 修饰局部变量:当修饰形参时,表明型参是一个常量。当修饰方法时,给常量型参赋一个是参
- static final 用来修饰属性时:称为全局常量(static随着类加载而加载,final常量)
8.抽象类与抽象方法
-
abstract可以修饰的结构:类、方法(不能修饰:属性、构造器、私有方法、静态方法、final的类和方法)
-
abstract修饰类时:抽象类
-
- public class PersonTest {
public static void main(String[] args) {
Person1 p1 = new Person1();//一旦Person1类抽象了,就不可实例话
}
}
abstract class Person1 {}
- public class PersonTest {
-
修饰方法时:称为抽象方法(抽象方法只有方法的声明,没有方法体)
-
-
public abstract void eat();
-
注意1:包含抽象方法的类,一定是一个抽象类。反之,抽象类中可以没有抽象方法
-
注意2:若子类重写父类中所有抽象方法,此子类方可实例化;若子类没有重写父类中所有抽象方法,则子类也是一个抽象类,需要使用abstract修饰
-
9.接口
- Java不支持多重继承,所有提供类接口,这样就可以得到多重继承的效果。
- 接口的具体使用,体现多态性
- 接口,实际上可以看作一个规范
10.内部类
//返回一个实现类Comparable接口的类的对象
public Comparable getComparable(){
//创建一个实现类Comparable接口的类:局部内部类
//方式一
class MyComparable implements Comparable {
@Override
public int compareTo(Object o) {
return 0;
}
}
return new MyComparable();
}
public Comparable getComparable(){ return new Comparable() { @Override public int compareTo(Object o) { return 0; } }; }
五.异常处理
Java程序在这执行过程中所发生的异常事件可分为两类
- Error:Java虚拟机无法解决的严重问题。如JVM系统内部错误、资源耗尽等严重情况(stackoverFlowError和OOM)
- Exception:其它因编程错误或偶然的外在原素导致一般性问题。
throw表示方法内抛出某种异常对象,
而throws方法的定义上使用 throws 表示这个方法可能抛出某种异常,需要由方法的调用者进行异常处理。
六.集合
- Collection:代表一组对象,每一个对象都是它的子元素。
- List:有顺序的 collection,并且可以包含重复元素(顺序)。
- Set:不保证有序,同时不包含重复元素的Collection(唯一)。
- Map:可以把 键(key) 映射到 值(value) 的对象,键不能重复(键值对)。
1.String StringBuilder StringBuffer的区别?为什么String不可变?为什么要设计成这样?
String是final修饰的,不可变,每次操作都会产生新的String对象
StringBuffer是线程安全的,而StringBuilder是非线程安全的。都继承自AbstractStringBuilder类,都可以使用append
1.字符串常量池的需要
2.允许String对象缓存HashCode
3.安全性String被许多的Java类(库)用来当做参数URL,文件路径path
4.因为字符串是不可变的,所以是多线程安全的
2.说一说 Java 常见集合的数据结构以及其特点
List
ArrayList
:Object 数组(查询快,增删慢,线程不安全,效率高 )Vector
: Object数组(查询快,增删慢,线程安全,效率低 )LinkedList
: 双向链表,JDK1.6 之前是循环链表,JDK1.7 取消了循环(查询慢,增删快,线程不安全,效率高 )
Map
HashMap
: JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 存储元素的主体,链表则是主要为了解决哈希冲突而存在的,即 “拉链法” 解决冲突。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间(哈希表对键进行散列,Map结构即映射表存放键值对)LinkedHashMap
: LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得键值对的插入顺序以及访问顺序等逻辑可以得以实现。Hashtable
: 数组 + 链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的TreeMap
: 红黑树(平衡二叉排序树)
Set
HashSet
: 基于 HashMap 实现的,底层采用 HashMap 来保存元素(不保证有序,唯一)LinkedHashSet
: LinkedHashSet 继承与 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。TreeSet
: 红黑树,自平衡的排序二叉树(可实现自然排序,例如 a-z)
3.Collection和Collections的区别
- Collection是集合的上级接口,继承它的有 Set 和 List 接口
- Collections是集合的工具类,提供了一系列的静态方法对集合的搜索、查找、同步等操作
4.请简单说明一下什么是迭代器?
Iterator 提供了遍历及操作集合元素的接口,而 Collection接口实现 Iterable 接口,也就是说,每个集合都通过实现Iterable 接口中 iterator() 方法返回 Iterator 接口的实例,然后对集合的元素进行迭代操作。
5.请你说说Iterator和ListIterator的区别?
Iterator 可用来遍历 Set 和 List 集合,但是 ListIterator 只能用来遍历List。
- Iterator只能
remove()
元素,而ListIterator可以add()
、set()
、remove()
- Iterator只能使用
next()
顺序的向后遍历,ListIterator则向前previous()
和向后next()
遍历都可以- 还有一个额外的功能,ListIterator可以使用
nextIndex()
和previousIndex()
取得当前游标位置的前后index位置,Iterator没有此功能
- 还有一个额外的功能,ListIterator可以使用
5.阐述 ArrayList 分别与 Vector、LinkedList 的异同点
ArrayList 与 Vector
ArrayList 是现在 List 的一种主要实现类,而 Vector 已经是过时的 Java 遗留容器
- 同:两者都是使用 Object 数组方式存储数据,均可以实现扩容且允许直接按序号查询(索引)元素,但是插入元素要涉及数组元素移动等内存操作,所以两者查询数据快而插入数据慢
- 异:Vector中的方法由于添加了 synchronized 修饰,因此 Vector 是线程安全的容器,但性能上较 ArrayList 差
ArrayList 与 LinkedList
- 数据结构:ArrayList 是 Object 数组,LinkedList 是双向链表(JDK1.6 之前是循环链表,JDK1.7 取消了循环)
- 查询效率:ArrayList 支持高效的随机元素访问,即通过下标快速获取元素对象。而 LinkedList 不支持,所以 ArrayList 的查询效率更高
- 增删效率:ArrayList 底层是数组存储,所以插入和删除元素的时间复杂度与元素插入的位置有关,因为会涉及到元素的移动问题,例如追加在末尾,则时间复杂度为
O(1)
,若在首部插入,则时间复杂度为O(n)
,中间任意位置插入,时间复杂度为,为O((n - 1) / 2)
,平均时间复杂度还是O(n)
而 LinkedList采用的是链表存储,所以增删不会涉及到元素的移动,只需要修改指针即可,时间复杂度可以简单看为O(1)
,但是要是在指定位置增删元素的话,需要先移动到指定位置再插入,以这个角度看时间复杂度为O(n)
- 线程安全:ArrayList 和 LinkedListed 都是非线程安全的,如果遇到多个线程操作同一个容器的场景,则可以通过工具类 Collections 中的 synchronizedList 方法将其转换成线程安全的容器后再使用(这是对装潢模式的应用,将已有对象传入另一个类的构造器中创建新的对象来增强实现)。
- 内存消耗:LinkedListed 每一个元素都需要存放前驱和后继节点的地址,所以每一个元素都更加消耗空间,而 ArrayList 只要是在结尾会预留一定的容量空间,这是扩容所导致的不能充分填满数组的情况(除非使用方法瘦身)
6. Set 无序性是怎么理解的
无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。
7. HashSet 如何检查重复
当你把对象加入 HashSet时,HashSet 会先计算对象的 hashcode值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcode ,HashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals() 方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让加入操作成功。
8.HashMap 与 HashTable 、HashSet、HashMap 等的区别
HashMap 与 HashTable
- 数据结构:HashMap JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间,不过在转为红黑树前会判断,如果数组长度小于 64,还是会优先进行数组扩容(哈希表对键进行散列,Map结构即映射表存放键值对),而 HashTable 没有这种特殊的机构。
- 线程安全:HashMap 是非线程安全的,而 HashTable 属于线程安全的(方法添加了 synchronized 修饰 ),因此,HashMap 效率也会略高,通常认为,HashTable 类似 Vector 是一个Java遗留类,基本不做使用。想保证线程安全,可以考虑使用 ConcurrentHashMap。
- Null 的处理:HashMap 的键和值都可以存储为 null 的类型,但是只能有一个 null 类型的键,但是 null 类型的值可以有多个。HashTable 的键和值都不允许有 null 类型出现,否则会抛出空指针异常。
- 扩容机制:不指定初始值的时候,HashMap 初始值为 16,之后每次扩容,容量会成为原先的两倍,HashTable 初始值为 11,扩容会使得容量成为原先的 2n + 1。若指定了初始值,HashMap 会将其扩充为 2 的幂次方大小,而 HashTable 会直接使用你给的初始值。
HashMap 与 HashSet
- HashMap 实现了 Map 接口,HashSet实现了 Set 接口。
- HashMap 储存键值对,HashSet仅仅存储对象。
- 使用 put() 方法将元素放入 map 中 使用 add() 方法将元素放入 set 中,但 add() 方法实际调用的还是 HashMap 中的 put() 方法。
- HashMap 中使用键对象来计算 hashcode 值 HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说hashcode 可能相同,所以 equals() 方法用来判断对象的相等性,如果两个对象不同的话,那么返回 false。
- HashMap 比较快,因为是使用唯一的键来获取对象,HashSet 较 HashMap 来说比较慢。
HashMap 与 TreeMap
- 顺序问题:HashMap 中的元素是没有顺序的,TreeMap 中所有的元素都是有某一固定顺序的。
- 线程安全:HashMap 和 TreeMap 都不是线程安全的
- 继承问题:HashMap 继承 AbstractMap 类;覆盖了 hashcode() 和 equals() 方法,以确保两个相等的映射返回相同的哈希值。TreeMap 继承 SortedMap 类,保持键的有序顺序。
- 调优问题:HashMap 基于哈希表实现的,使用HashMap要求添加的键类明确定义了 hashcode() 和 equals() (可以重写该方法);为了优化HashMap的空间使用,可以调优初始容量和负载因子。而 TreeMap 基于红黑树实现,所以TreeMap 就没有调优选项,因为红黑树总是处于平衡的状态。
- 适用场景:HashMap 适用于 Map 插入,删除,定位元素,TreeMap适用于按自然顺序或自定义顺序遍历键(key)。
9. HashMap 的长度为什么是 2 的幂次方
HashSet因为底层使用哈希表(链表结合数组)实现,存储时key通过一些运算后得出自己在数组中所处的位置。我们在hashCoe方法中返回到了一个等同于本身值的散列值,但是考虑到int类型数据的范围:-2147483648~2147483647 ,着很显然,这些散列值不能直接使用,因为内存是没有办法放得下,一个40亿长度的数组的。所以它使用了对数组长度进行取模运算,得余后再作为其数组下标,indexFor( ) ——JDK7中,就这样出现了,在JDK8中 indexFor()就消失了,而全部使用下面的语句代替,原理是一样的。
// JDK8中
(tab.length - 1) & hash;
复制代码
// JDK7中
bucketIndex = indexFor(hash, table.length);
static int indexFor(int h, int length) {
return h & (length - 1);
}
复制代码
可以看到其本质计算方法都是 (length - 1) & hash
提一句,为什么取模运算时我们用 & 而不用 % 呢,因为位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快,这样就导致位运算 & 效率要比取模运算 % 高很多。
最关键的内容来了,如果我们用更容易理解的取余(%), length % hash == (length - 1) & hash
这个公式想要成立的前提,就必须满足 length 是 2 的 n 次方
简单的说:HashMap 的长度为什么是 2 的幂次方的原因就是,我们为了使用更加高效的 & 运算而不是 % 运算,但又为了保证运算的结果,仍然是取余操作。
10. 简单谈谈 HashMap 中的底层原理
JDK 1.8 之前
JDK1.8 之前 HashMap
底层是数组 + 链表,HashMap 会使用 hashCode 以及扰动函数处理 key ,然后获取一个hash 值,然后通过 (length- 1) & hash 判断当前元素应该存放的位置,如果这个位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
扰动函数在 4.3 中讲述的应该很清楚了
拉链法的解释,同样可以参考 003-HashMap源码分析(含散列表和红黑树介绍)
JDK 1.8
JDK 8 做了一些较大的调整,当数组中每个格子里的链表,长度大于阈值(默认为8)时,将链表转化为红黑树,就可以大大的减少搜索时间,不过在转为红黑树前会判断,如果数组长度小于 64,还是会优先进行数组扩容。
11.HashMap 中加载因子的理解
- 加载因子就是表示哈希表中元素填满的程度,当表中元素过多,超过加载因子的值时,哈希表会自动扩容,一般是一倍,这种行为可以称作rehashing(再哈希)。
- 加载因子的值设置的越大,添加的元素就会越多,确实空间利用率的到了很大的提升,但是毫无疑问,就面临着哈希冲突的可能性增大,反之,空间利用率造成了浪费,但哈希冲突也减少了,所以我们希望在空间利用率与哈希冲突之间找到一种我们所能接受的平衡,经过一些试验,定在了0.75f。
12.ConcurrentHashMap 和 Hashtable 的区别
HashTable 虽然也满足线程安全,但是类似 Vector, 是一个Java遗留类,基本不做使用。想保证线程安全,可以考虑使用 ConcurrentHashMap。
数据结构:JDK 1.7 中,ConcurrentHashMap 底层采用分段数组 + 链表实现,在 JDK 1.8 中,ConcurrentHashMap 中的数据结构与 HashMap 一致,都是数组 + 链表或红黑树。而 Hashtable 采用的是数组 + 链表的形式(数组为主体,链表用来解决哈希冲突)
线程安全:ConcurrentHashMap 在 JDK 1.7 的时候,有一个分段锁的概念,也就是对整个数组进行分割开来(这就是 Segment 的概念),每一把锁,只负责整个锁分段中的一部分,而如果多线程访问不同数据段的数据,锁的竞争也就不存在了,访问并法律也因此提高。而在 JDK 1.8 的时候,直接用 Node 数组 + 链表或红黑树实现,通过 synchronized(JDK 1.6 后优化了很多) 和 CAS 进行并发的控制。Hashtable 就是用一把锁 synchronized 来保证线程安全,效率不是很高,多线程下,很可能会陷入阻塞轮询状态。
- 注:虽然 JDK 1.8 的源码中还能看到 Segment ,但是主要也只是为了兼容旧版本了
七.多线程
1.并发、并行、串行的区别
串行在时间上不可能发生重叠,前一个任务没搞定,下一个任务就只能等着
并行在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行。
并发允许两个任务彼此干扰。统一时间点、只有一个任务运行,交替执行
2.进程和线程
进程是应用程序,线程是一条执行路径
进程有独立的内存空间,崩溃不会影响其他程序,
线程没有独立的空间,多个线程在同一个进程的空间,可能会影响其他线程
一个进程中,至少有一个线程
3.多线程创建方式
方式一:继承Thread
- 创建一个继承于Thread类的子类
- 重写Thread类的run() --> 将此线程执行的操作生命在run()中
- 创建Thread类的子类的对象
- 通过此对象调用start()方法开启线程
start()方法的作用
- 启动当前线程
- 调用当前线程的run()方法
/**
* 测试Thead中的常用方法:
* 1、start():启动当前线程;调用当前线程的run()
* 2、run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
* 3、currentThread():静态方法,返回执行当前代码的线程,相当于独独对线程this
* 4、getName():获取当前线程的名字
* 5、setName():设置当前线程的名字
* 6、yield():释放当前线程对CPU的执行权,给个机会
* 7、join():线程a执行线程b的join()方法,那就会将自己的执行权给线程b,等线程b执行完,线程a才开始执行,若三个线程,则哪个线程调用就会把谁阻塞
* 8、stop():结束线程,方法过期了
* 9、sleep(long millis):让线程睡眠,参数指定时间:毫秒
* 10、isAlive():判断线程是否存活
*/
class Thread1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0)
System.out.println(Thread.currentThread().getName()+":"+i);
// if (i % 20 == 0)
// this.yield(); //yield == this.yield(),但是在java14中已经不支持直接yield()了
}
}
public Thread1() {
}
public Thread1(String name) {
super(name);
}
}
public class ThreadMethodTest {
public static void main(String[] args) {
Thread1 thread1 = new Thread1("Thread:1");
// thread1.setName("子线程1");
thread1.start();
Thread.currentThread().setName("主线程");
for (int i = 0; i < 100; i++) {
if (i % 2 == 1)
System.out.println(Thread.currentThread().getName()+":"+i);
if (i == 40 ) {
try {
thread1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
线程的调度
- 调度策略
- 时间片。
- 抢占式:高优先级的线程抢占CPU。
- Java的调度方法
- 同优先级线程组成先进先出队列(先到先服务),使用时间片策略。
- 对高优先级,使用优先调度的抢占式策略。
- 线程的优先级等级
- 最高MAX_PRIORITY:10
- 最低MIN_PRIORITY:1
- 默认NORM_PRIORITY:5
- 涉及的方法
- getPriority():返回线程优先级
- setPriority(int new Priority):改变线程的优先级
- 说明
- 线程创建时继承父线程的优先级
- 低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调
方式二:实现Runnable接口
**
* 创建多线程的方式二:实现Runnable接口
* 1、创建实现了Runnable接口的类
* 2、实现类去实现Runnable中的抽象方法:run()
* 3、创建实现类的对象
* 4、将此对象作为参数传递到Thread类的构造器中,创建Thread类对象
* 5、通过Thread类的对象调用start()
*
* 方式二相当于将 继承Thread创建线程的方式一 中的Run方法摘出来放到Runnable接口中去,
* 然后在将Runnable放回Thread启动线程,来实现线程创建的第二种方式。
*
* 问题:线程thread启动start()方法为什么会使用到实现接口Runnable的类Runnable1中的run方法?
* 答:查看源码==> 我们由后往前观察,先查看start()方法,我们知道start()方法是使用run()方法开启的,
* 所以查看Thread的run方法,得target不为null则调用target的run()方法,再查看target的类型为Runnable,
* 然后查看两个类的调用,创建的线程thread中使用的是带参数runnable的构造方法,可知变量target的值就是类Runnable1,
* 至此可知Thread类的带参数runnable的构造方法,将Runnable1赋值给target,因为target不为null,
* 所以开启传递的Runnable1的run方法。
*
* 比较创建线程的两种方式
* 开发中:优先选择:实现Runnable接口的方式
* 原因:
* 1、实现的方式没有类的单继承性的局限性(接口的出现就是要打破继承的局限性)。
* 2、实现的方式更适合来处理多个线程有共享数据的情况。
*
* 联系:Thread也是实现Runnable接口的
* 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()方法中。
*
*/
class Runnable1 implements Runnable{
@Override
public void run() {
for (int i=1 ;i<=100; i++){
if (i%2 == 0){
System.out.println("遍历偶数"+i);
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
Runnable1 runnable1 = new Runnable1();
Thread thread = new Thread(runnable1);
thread.start();
}
}
4.线程的生命周期
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的声明周期中同城要经历如下的五种状态
- 新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
- 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源。
- 运行:当就绪的线程被调度并获得CPU资源时,便君如运行状态,run()方法定义了线程的操作和功能
- 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态。
- 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束。
5.线程同步
真正要问题:如何保护共享数据?
解决方法:
当一个线程a在操作ticket的时候,其他线程不能参与进来。知道进程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
在Java中,我们通过同步机制,来解决线程的安全问题。
方式一:同步代码块
非常像厕所,设置一个茅厕,肯定不希望有人闯进来,然后需要一个锁,这个锁,可以是木头棍、石头,甚至是一本书顶着都可以
synchronized(同步监视器){
//需要被同步的代码
}
//1、什么是同步的代码? 操作共享数据的代码,即为需要被同步的代码
//2、什么是共享数据? 多个线程共同操作的变量。比如:ticket就是共享数据,其中判断也算操作共享数据
//3、什么是同步监视器? 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
// 锁的要求:多个线程必须共用同一把锁。(不可能用自己的锁来标志,这个厕所没人用)
/**
* 例子:创建三个窗口买票,总共100张票
* 存在安全问题待解决:
* 1、三个线程开始都会卖第100张票
* 2、票不能按顺序卖出
*
* 1、什么是同步的代码? 操作共享数据的代码,即为需要被同步的代码
* 2、什么是共享数据? 多个线程共同操作的变量。比如:ticket就是共享数据,其中判断也算操作共享数据
* 3、什么是同步监视器? 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
* 4、在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器
*
*/
class windows implements Runnable {
private int ticket = 100;
Object obj = new Object();
@Override
public void run() {
while (true){
synchronized (obj){
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":票:"+ticket);
ticket--;
}else {
break;
}
}
}
}
}
public class WindowTest {
public static void main(String[] args) {
windows windows = new windows();
Thread thread1 = new Thread(windows);
Thread thread2 = new Thread(windows);
Thread thread3 = new Thread(windows);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
同步代码块解决继承Thread的线程安全问题
package com.zhang.day02;
/**
* 例子:创建三个窗口买票,总共100张票
* 存在安全问题待解决:
* 1、三个线程开始都会卖第100张票
* 2、票不能按顺序卖出
*
* 使用同步代码块解决继承Thread的方式解决线程安全问题
* 同步代码块重点需要找到对象是否唯一,唯一的话锁好定义,不唯一就需要使用static
* 同步代码块不可包含多,也不可包含少
*
*/
class windows2 extends Thread{
private static int ticket = 100;
//由于是继承Thread创建的对象也是三个,所以使用static
private static Object obj = new Object();
@Override
public void run() {
while (true){
//可以使用反射来当锁Class<windows2> windows2Class = windows2.class; 反射唯一
synchronized(obj){
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":票:"+ticket);
ticket--;
}else {
break;
}
}
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
windows2 w1 = new windows2();
windows2 w2 = new windows2();
windows2 w3 = new windows2();
w1.start();
w2.start();
w3.start();
}
}
方式二:同步方法
如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的
1、同步方法解决实现Runnable接口的线程同步安全问题
/**
* 同步方法解决实现Runnable接口的线程同步问题
* * 例子:创建三个窗口买票,总共100张票
* * 存在安全问题待解决:
* *1、三个线程开始都会卖第100张票 *
* *2、票不能按顺序卖出 *
* * 1、同步方法与同步代码块差不多,同步方法将要包围的代码,变成方法包围。
* * 2、它也有锁,不过它的锁是 this,this唯一就行,指的就是继承Runnable实现多线程的windows2。
*/
class windows2 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
show();
if (ticket <= 0) break;
}
}
public synchronized void show() {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":票:" + ticket);
ticket--;
}
}
}
public class WindowTest2 {
public static void main(String[] args) {
windows2 windows = new windows2();
Thread thread1 = new Thread(windows);
Thread thread2 = new Thread(windows);
Thread thread3 = new Thread(windows);
thread1.setName("窗口1");
thread2.setName("窗口2");
thread3.setName("窗口3");
thread1.start();
thread2.start();
thread3.start();
}
}
2、同步方法解决继承Thread的线程同步安全问题
/**
* 例子:创建三个窗口买票,总共100张票
* 存在安全问题待解决:
* 1、三个线程开始都会卖第100张票
* 2、票不能按顺序卖出
*
* 使用同步方法解决继承Thread的方式解决线程安全问题
*
*/
class windows4 extends Thread{
private static int ticket = 100;
@Override
public void run() {
while (true){
if (ticket <= 0)
break;
show();
}
}
private static synchronized void show(){
//由于同步方法中的锁是 this,且继承Thread实现多线程创建的对象不唯一,导致锁不唯一:w1 w2 w3
//解决:将他们创建的对象变成唯一,对方法设置成static,则方法的锁就是windows4.class
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":票:"+ticket);
ticket--;
}
}
}
public class WindowTest4 {
public static void main(String[] args) {
windows4 w1 = new windows4();
windows4 w2 = new windows4();
windows4 w3 = new windows4();
w1.start();
w2.start();
w3.start();
}
}
同步方法和同步方法块的总结
同步的方式,解决了线程的安全问题。---好处
操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。---局限性
6.死锁
一个共享资源不会出现死锁,但是共享资源大多时候是多个的)
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状,无法继续。
解决方法
- 专门的算法、原则
- 尽量减少同步资源的定义
- 尽量避免嵌套同步
/**
* 演示线程的死锁问题
*
* 1、死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,
* 都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
*
* 2、说明:
* 1)出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状,无法继续
* 2)我们使用同步时,要避免出现死锁。
*
*/
public class ThreadTest {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread(){
@Override
public void run() {
synchronized(s1){
s1.append("a");
s2.append("1");
System.out.println("等待获得s2锁");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(s2){
s1.append("b");
s2.append("2");
System.out.println("以获得s2锁:"+s1);
System.out.println("以获得s2锁:"+s2);
}
}
}
}.start();
new Thread(new Runnable(){
@Override
public void run() {
synchronized(s2){
s1.append("c");
s2.append("3");
System.out.println("等待获得s1锁");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(s1){
s1.append("d");
s2.append("4");
System.out.println("以获得s1锁:"+s1);
System.out.println("以获得s1锁:"+s2);
}
}
}
}).start();
}
}
//死锁的演示
class A{
public synchronized void foo(B b){
//主线程进入这个同步方法,则主线程拿到的锁就是a
System.out.println("当前线程名:"+Thread.currentThread().getName()+"进入了A实例的foo方法");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程名:"+Thread.currentThread().getName()+"企图调用B实例的last方法");
//使用b调用last需要的锁是b
b.last();
}
public synchronized void last(){
System.out.println("进入了A类的last方法内部");
}
}
class B{
public synchronized void bar(A a){
//副线程使用对象b调用bar方法,那设定的锁就是b
System.out.println("当前线程名:"+Thread.currentThread().getName()+"进入了B实例的bar方法");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("当前线程名:"+Thread.currentThread().getName()+"企图调用A实例的last方法");
//传递的对象是a,而且是使用a的last方法,所以锁是a
a.last();
}
public synchronized void last(){
System.out.println("进入了B类的last方法内部");
}
}
/**至此可以知道
* 主线程:调用 init()方法,而 init()方法用a调用 foo()方法,foo()是同步方法会将 调用的对象a 当成锁使用,所以 foo()方法获得 a对象锁
* foo()方法传递了参数b对象,而b调用了 last()方法,由于 last()方法也是同步方法,他会将调用他的对象 b作为锁使用,
* 于是出现了主线程开始需要a对象作为锁,里面需要b对象作为锁
*
* 副线程:副线程开启 run()方法,用b调用 bar()方法,而 bar()方法是同步方法,于是将b对象作为锁使用
* bar()方法里面使用传递对象b调用 last()方法,而 last()也是同步方法,于是将a对象作为锁使用
* 于是出现副线程开始需要b对象作为锁,里面需要a对象作为锁b
*
* 所以出现了死锁问题
* 问题:如何看出死锁?
* 答: 先将主线程与副线程分开看,一条一条看。
* 然后找出锁,同步方法是将调用对象作为锁使用。而静态类是将整个类作为锁使用。
*
*/
public class DeadLock implements Runnable{
A a = new A();
B b = new B();
public void init(){
//主线程会进入这个方法
Thread.currentThread().setName("主线程");
//调用a对象的foo方法
a.foo(b);
System.out.println("进入了主线程之后");
}
@Override
public void run() {
//副线程会进入这个方法
Thread.currentThread().setName("副线程");
//调用b对象的bar方法
b.bar(a);
System.out.println("进入副线程之后");
}
public static void main(String[] args) {
DeadLock dl = new DeadLock();
new Thread(dl).start();
dl.init();
}
}
7.Lock(锁)
- 接口时控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能由一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
- ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放锁
import java.util.concurrent.locks.ReentrantLock;
/**
* 解决线程安全问题的方式三:Lock锁 ----JDK 5.0新增
* 1、面试题:synchronized 与 Lock的异同?
* 相同:二者都可以解决线程安全难问题
* 不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器
* Lock需要手动的启动同步(Lock()),同时结束同步也需要手动的实现(unlock())
* (对于上一段的理解更倾向于:因为是先使用类定义锁,然后定义锁边界,所以边界的定义需要使用方法开启和结束,
* 如果使用同步方法,定义好了重新调用是多此一举。使用同步方法块的方式定义边界,远不如使用两个方法开启和结束简洁,
* 所以我并不认为是因为手动开启和结束的思想,造成两个方法定义边界的实现,而是认为因为先定义锁,然受定义锁边界这个思想造成手动开启和结束这个结果)
* 面试题:如何解决线程安全问题?有几种方式?三种同步方法,同步方法块和Lock锁
* 感受:1、不同于同步方法和同步方法块,他们会直接给出 厕所(需要同步的代码)的大小,只要将需要同步的代码放进去就行,
* Lock锁是将需要同步代码为主,用代码定大小,然后在代码的边界设置 厕所的边界(也就是使用lock()和unlock()方法定需要同步的代码边界)
* 2、同步方法和同步方法块,一个是将调用方法也就是this设置成锁,一个是自己定义锁。
* 而 Lock锁 与 同步方法 具有相似性,只不过 lock锁 是将高度上升到了类,而同步方法高度还是方法。也就是将锁定义为类
* 3、所以我们要推到之前的固有思想,Lock锁 将锁变成一个接口,而ReentrantLock实现了接口,于是它成了一个锁,然后使用锁定义了
* 需要同步的边界,也就是厕所。
* 对比:
* 同步方法和同步方法块:都是先将厕所(需要同步的代码的边界)定好,然后才是上锁
* Lock锁:先将锁打好,然后使用锁将厕所(需要同步的代码的边界)定好。
*
* 使用顺序:Lock ——> 同步代码块(已经进入了方法,分配了相应资源) ——> 同步方法(在方法体之外)
*/
class Window implements Runnable{
private int ticket = 100;
//1、实例化ReentrantLock
//可以使用带boolean值的构造器,true:就是公平的,就是按队列的方式先进先出,就不是争抢了,默认是false
private ReentrantLock lock= new ReentrantLock();
@Override
public void run() {
while (true){
try {
//2、调用锁定方法 lock()
lock.lock();
if (ticket>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket);
ticket--;
}
}finally {
//3、调用解锁方法 unlock()
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.start();
t2.start();
t3.start();
}
}
线程同步练习一
银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。
问题:该程序是否有线程安全问题,如果有,如何解决?
提示:
- 明确哪些代码是多线程运行代码,需写入run()方法
- 明确什么是共享数据。
- 明确多线程运行代码中哪些语句是操作共享数据的。
import java.util.concurrent.locks.ReentrantLock;
/**
* 银行有一个账户。
* 有两个储户分别向同一个账户存3000元,每次粗野你1000,存3次。每次存完打印账户余额。
* 分析:
* 1、是否是多线程问题?是,两个储户线程
* 2、是否有共享数据?有,账户
* 3、是否有线程安全问题?有,需要考虑如何解决线程安全问题。同步机制:有三种方式
*
*/
class Account{
private int balance;
private ReentrantLock Lock = new ReentrantLock(true);
public Account(int balance) {
this.balance = balance;
}
public void deposit(double amt){
if (amt>0){
Lock.lock();
balance += amt;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"存入:"+amt+"成功,余额:"+balance);
Lock.unlock();
}
}
}
class Customer extends Thread{
private Account account;
public Customer(Account account) {
this.account = account;
}
@Override
public void run() {
for (int i = 0; i < 30; i++) {
account.deposit(1000);
}
}
}
public class AcountTest {
public static void main(String[] args) {
Account account = new Account(0);
Customer c1 = new Customer(account);
Customer c2 = new Customer(account);
c1.setName("客户一");
c2.setName("客户二");
c1.start();
c2.start();
}
}
8.线程的通信
当执行 wait() 等待 方法时,将会释放锁
线程通信方法只能使用在同步方法或者同步方法块,lock锁的话另说
例题:使用两个线程打印1-100.线程1,线程2交替打印
/**
* 线程通信的例子:使用两个线程打印 1-100.线程1,线程2 交替打印
* 涉及到的3个方法:
* wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器(也就是锁)【重点:wait会失去锁】
* notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的线程【重点已经醒的线程不能再被唤醒,优先级】
* notifyAll():一旦执行此方法,就会唤醒所有被wait的一个线程。【重点全部】
* 说明:
* 1、wait(),notify(),notifyAll()三个方法必须使用在 同步方法 或者 同步方法块 中。
* 2、wait(),notify(),notifyAll()三个方法的调用者必须时 同步方法 或者 同步方法块 中的锁
* 3、由于对象,类等等都可以充当锁,而wait(),notify(),notifyAll()三个方法又是使用锁来调用,那三个方法定义在哪里?
* 他们定义在java.lang.Object类里面。
* 面试题:sleep() 和 wait()的异同?
* 1、相同点:一旦执行方法,都可以使得当前线程进入阻塞状态。
* 2、不同点:
* 1)声明位置不同:两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()
* 2)调用位置不同:调用的要求不同:sleep()可以在任何需要的场景下调用。wait()必须使用在同步代码或同步方法中
* 3)是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁
*/
class Number implements Runnable{
private int number = 1;
@Override
public void run() {
while (true){
synchronized(this){
notify();
if (number<=100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+number);
number++;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else break;
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程一");
t2.setName("线程二");
t1.start();
t2.start();
}
}
经典例题:生产者/消费者问题
生产者(Productor)将产品交给电源(Clerk),而消费者(Customer)从店员出取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产者生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
这里可能出现两个问题:
-
生产者比消费者快时,消费者会漏掉一些数据没有取到。
-
消费者比生产者快时,消费者会取相同的数据。
/** * 线程通信的应用:经典例题:生产者、消费者问题 * 生产者(Productor)将产品交给电源(Clerk),而消费者(Customer)从店员出取走产品, * 店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品, * 店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产者生产; * 如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。 * * 分析: * 1、是否是多线程问题? 是,生产者线程,消费者线程 * 2、是否有共享数据? 是,产品 * 3、如何解决线程的安全问题? 同步机制,有三中方法 * 4、是否涉及线程的通信? 是 * * 感悟: * 1、当执行while循环时,就应该意识到加入同步锁的两个线程再也不会终止, * 只有在极端条件下消费完或者生产到20,才会wait,这样可以将wait认为暂时结束程序, * 将共享数据交给另一线程操作。 * 2、两个方法都用synchronized修饰成了同步方法,下意识会以为是两个锁,真实是1个锁就是this */ //店员 class Clerk{ private int productCount = 0; public synchronized void produceProduct() { this.notify(); if (productCount < 20){ try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } productCount++; System.out.println(Thread.currentThread().getName()+":生产第"+productCount+"个产品"); }else { //产品数量等于20,让消费者线程等待,消费者开始消费产品 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } public synchronized void consumeProduct() { this.notify(); if (productCount > 0){ try { Thread.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":消费第"+productCount+"个产品"); productCount--; }else { //产品数量等于0,让消费者线程等待,生产者开始生产产品 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } //消费者 class Consumer extends Thread{ private Clerk clerk; public Consumer(Clerk clerk) { this.clerk = clerk; } @Override public void run() { System.out.println("消费者正在消费产品。。。。。"); while (true){ clerk.consumeProduct(); } } } //生产者 class Producer extends Thread{ private Clerk clerk; public Producer(Clerk clerk) { this.clerk = clerk; } @Override public void run() { System.out.println("生产者正在生产产品。。。。。"); while (true){ clerk.produceProduct(); } } } public class ProductTest { public static void main(String[] args) { Clerk clerk = new Clerk(); Consumer c1 = new Consumer(clerk); Producer p1 = new Producer(clerk); c1.setName("消费者1"); p1.setName("生产者1"); c1.start(); p1.start(); } }
总结:并不是我想的那样一定要交替进行,或者是一定要生产得到20,或者是消耗到0。或者说我并不用考虑到其中的过程,要考虑到其中的方向。到哪个节点需要做哪种操作。
比如:到20后需要使生产者线程wait,顺便将消费者线程唤醒;到0时需要使消费者线程wait,顺便将生产者线程唤醒。
9、JDK5.0新增线程创建方式
新增线程创建方式有两个
新增方式一:实现Callable接口
与使用Runnable相比,Callable功能更强大些
- 相比run()方法,可以有返回值
- 方法可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask类,比如获取返回结果
Future接口
- 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
- FutrueTask是Future接口的唯一的实现了
- FutureTask同时实现了Runnable、Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值
package com.zhang.day02;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 创建线程的方式三:实现Callable接口。 --- JDK 5.0新增
* 如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
* 1、call()可以又返回值
* 2、call()可以抛出异常,被外面的操作捕获,获取异常的信息
* 3、Callable是支持泛型的
*
* 感悟:其实实现Callable接口创建多线程差不多与Runnable差不多,Callable其实继承的接口就是Runnable和Callable,
* 只是为了获得返回值和抛出异常才加入实现Callable接口,如此多出来的操作就是使用FutureTask接收Callable的对象,
* 将此对象作为参数开启线程,也可以使用get方法获得返回值,以及获得它的异常。
*/
//1、创建一个实现Callable的实现类
class NumberThread implements Callable{
//2、实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if (i%2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3、常见Callable接口实现类的对象
NumberThread thread = new NumberThread();
//4、将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
FutureTask<integer> task = new FutureTask(thread);
//5、将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象给,并调用start方法
new Thread(task).start();
try {
Integer integer = task.get();
System.out.println(integer);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
感悟:和实现Runnable接口开启线程方式差不多,只是为了获得返回值和异常功能 。要使用Callable接口。如此就要将Callable对象变成参数放入FutureTask的构造器中获取FutureTask对象。如此FutureTask对象可以开启线程,获得返回值,以及处理异常的多功能操作。
新增方式二:使用线程池
背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完返回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
- 便于线程管理
- corePoolSize:核心池的大小
- maximumPoolSize:最大线程数
- keepAliveTime:线程没有任务时最多保持多长时间后会终止
线程池相关API
JDK 5.0起提供了线程池相关API:ExxecutorService 和 Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
- void executor(Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
- Futuresubmit(Callable task):执行任务,有返回值,一般用来执行Callable
- void shutdown():关闭练级池
Executors:工具里、线程池的工厂类,用来创建并返回不同类型的线程池
- Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n):创建一个可重用的固定线程数的线程池
- Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n):创建一个线程池,它可以安排在给定延迟后运行命令或者定期执行
线程池创建多线程
package com.zhang.day02;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 创建线程的方式四:使用线程池
*
* 好处:
* - 提高响应速度(减少了创建新线程的时间)
* - 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
* - 便于线程管理
* - corePoolSize:核心池的大小
* - maximumPoolSize:最大线程数
* - keepAliveTime:线程没有任务时最多保持多长时间后会终止
*
* 面试题:创建多线程有多少种方式?四种!
*/
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);
}
}
}
}
class NumberThread2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2 != 0){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1、提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
//设置线程池的属性(由于ExecutorService是接口,没有更多的操作,需要的是它的实现类),
//所以我们需要下转型,夺舍继承人,获得更多的方法
//那如何找到这个继承人,我们可以使用反射寻找,因为反射寻找的是类,而我们上面的是接口,
//用接口找继承人,相当于用血源找继承人
//System.out.println(service.getClass());
ThreadPoolExecutor servie1 = (ThreadPoolExecutor) service;//夺舍成功
//2、执行指定的线程操作。需要提供实现Runnable接口或Callable接口的实现类对象
service.execute(new NumberThread1()); //适合用于Runnable
service.execute(new NumberThread2()); //适合用于Runnable
//service.submit();//适用于Callable
//如果需要使用Callable接口实现多线程来接收参数的话,使用FutureTask接收submit()方法的返回值,获得的对象就可以进行操作
//3、关闭连接池
service.shutdown();
}
}
总结:线程池方式大大的缩减了,原生两种创建多线程的繁琐步骤,直接创建线程池,不用在new Thread()这样一个一个创建多线程。
而且也更好的管理线程,说到管理线程我们使用的是接口,所以管理线程的方法没有。这样就需要夺舍(下转型)它的实现类。如何寻找?使用反射,反射寻找的是类。所以使用接口service.getClass()可以获得实现类。然后夺舍(下转型),这样我们就有了很多的方法可以使用了,
注意execute()方法是适用于Runnable接口的,sumbit()才是适用于Callable()接口的。还有开启是要关闭的。
</java_home></java_home>