面试八股文
- JAVA
- 1.java的特点和优点,为什么选择java?
- 多进程和多线程区别
- java常见异常
- JVM:垃圾回收算法,垃圾回收器,哪些情况下发生OOM?如何排查?
- java基本数据类型和引用类型
- 抽象类和接口的区别
- final关键字
- static修饰符的用法
- 请你说说String类,以及new string和使用字符串直接量的区别。
- String、StringBuffer、Stringbuilder有什么区别?
- 8.说说==与equals()的区别?
- hashCode()和equals()的区别,为什么重写equals()就要重写hashcode()?
- Java8的新特性
- 包装类(封装类)的自动拆装与自动装箱
- Java的异常处理机制
- 面向对象的理解
- 重载和重写的区别,构造方法能不能重写?
- 16.介绍一下访问修饰符
- 请你说说泛型和泛型擦除
- 类加载的过程
- class对象
- 反射的原理和应用
- 说说多线程
- 线程间通信
- 线程的状态以及切换
- wt()和sleep()区别
- threadlocal内部属性
- volatile
- 怎么保证线程安全
- 24.[synchronize的加锁方式](https://blog.youkuaiyun.com/ljn641028197/article/details/126910071)
- CopyOnWrite(写入时复制)
- 常见注解
- java集合类型
- HashMap和HashTable的区别
- HashMap实现方式
- HashMap扩容机制
- 为什么hashmap使用红黑树不用平衡二叉树,mysql为什么使用B+树
- HashMap、concurrentHashMap
- 为什么使用工厂
- 深拷贝和浅拷贝
- this的使用
- BIO、NIO和AIO区别
- 序列化(Serialization)
- 死锁
- 单例模式
- 常用linux命令
- 线程池
- 线程间通信
- 元组和列表区别
- 分布式和集群
- 多线程和多进程
- MySQL数据库
- sql注入
- 事务的ACID特性
- 数据库并发事务会带来哪些问题?
- 数据库隔离级别
- 数据库悲观锁和乐观锁的原理和应用场景分别有什么?
- 查询每个班级排名前三的学生
- MySQL优化了解吗?说一下从哪些方面可以做到性能优化?
- 删除数据前需要做什么操作,drop、truncate、delete的区别?
- MyISAM和InnoDB的区别
- 索引
- 1.聚集索引比非聚集索引性能高/主键索引比普通索引性能高
- 2.优化回表查询——组合索引、覆盖索引
- 3.索引条件下推(ICP,Index condition pushdown)
- 索引失效的六种情况
- int(8)还是char(>8)类型上建索引
- 三层B+树索引可以存储多少条数据
- 慢查询原因和优化
- Git的预备知识
- Git使用
- Gitee绑定SSH公钥,以后提交时免登录
- 使用码云创建自己的仓库
- Redis数据库
- 对Spring的理解
- 逻辑题
JAVA
1.java的特点和优点,为什么选择java?
1.吸收C++优点,去除C++中多继承、指针等复杂的使用,而且具有垃圾回收机制。
2.跨平台运行
为什么可以跨平台:利用不同系统上都有自己的一套JVM(JAVA虚拟机)将字节码编译成该平台能识别的机器码。
3.面向对象,易于开发和理解。
4.内含大量的库,简化编写工作。
5.支持网络编程。
多进程和多线程区别
资源隔离: 多进程有更好的资源隔离性。每个进程都有自己的内存空间,一个进程的崩溃不会影响其他进程。而多线程共享同一个进程的内存空间,一个线程的崩溃可能导致整个进程崩溃。
通信开销: 多进程通信需要更多的开销,因为进程之间需要通过IPC(进程间通信)机制进行通信。多线程可以更容易地共享数据,因为它们共享同一个进程的内存空间。
创建和销毁的开销: 创建和销毁进程通常比创建和销毁线程更加耗时和资源密集。
应用场景: 多进程更适合于CPU密集型任务,因为每个进程都有自己的内存空间,可以在不同的CPU核心上并行运行。多线程更适合于I/O密集型任务,因为它们可以共享内存,并在同一个进程中更有效地处理I/O操作。
java常见异常
1.编译时异常
编译时异常是编译阶段就出错的,所以必须处理,否则代码根本无法通过。
处理方法:①throw将方法内部出现的异常抛出去给本方法的调用者处理。
②try catch监视捕获异常,用在方法内部,可以将方法内部出现的异常直接捕获处理
2.运行时异常(RuntimeException)
java.lang.OutOfMemoryError(内存溢出错误):内存不足错误,程序运行过程中需要分配内存的时候,发现JVM中已经没有足够多的内存来进行分配
java.lang StackOverflowError(栈溢出错误):堆栈溢出错误,常发生于方法的无限递归调用
java.lang.ClassNotFoundException:找不到类异常。
算术异常:如除0导致算术异常
角标越界异常:如超过数组下标长度
类型转换异常:类型不匹配时转换抛出的异常,如父类转换为子类时抛出的异常
空指针异常:调用某对象的结果为空时
数字格式化异常:不符合转换格式的字符串被转换成数字
JVM:垃圾回收算法,垃圾回收器,哪些情况下发生OOM?如何排查?
垃圾判断算法:
(1)引用计数法:为对象创建计数器,每有一个地方引用它计数器值就加1,那么引用计数为0的对象就是垃圾。
(2)可达性分析法:通过一系列GC Roots的根对象出发,根据引用关系不断向下搜寻得到引用链,如果某个对象不在引用链中,说明不可达,需要被回收。GC Roots是JVM确定当前绝对不能被回收的对象
垃圾回收算法:
(1)标记-清除算法:根据可达性分析法对垃圾对象标记,然后统一回收所有被标记的对象。
(2)标记-整理算法:根据可达性分析法对垃圾对象标记,让所有非垃圾对象向一端移动,最后直接清理端边界以外的内存。
(3)复制算法:将内存分为大小相等两块,每次只使用其中一块,使用完后将活着的对象复制到另一块,然后把之前的那块一次清理掉。
(4)分代收集算法:
将堆分为新生代、老年代、元空间
因为新生代每次垃圾收集时只有少量对象存活,所以使用复制算法,成本较小。
刚创建的对象放在Eden区,区满后执行Minor(Young) GC——复制算法,将活的对象复制到survivor0并清空Eden,此后每次Eden区满了都使用Minor GC复制到survivor0直到survivor0也满了。然后对survivor0使用Minor GC复制到survivor1,此后Eden满了执行Minor GC都复制到survivor1,survivor1满了就Minor GC到survivor0,不断重复。当在两个存活区切换若干次后(自己设定)仍存活的对象将复制到老年代。
因为老年代的对象存活率高,所以使用标记清除/整理算法,避免复制大量对象和占用空间等。
随着Minor GC的进行,老年代的空间也将不够用,最终执行Major(Full) GC——标记清除/整理算法,如果老年代也满了就会抛出OOM(out of memory)。
永久代就是JVM的方法区。在这里都是放着一些被虚拟机加载的类信息,静态变量,常量等数据。
垃圾回收器:
serial(新生代,复制算法)/serial Old(老年代,标记-整理):单线程,工作时用户线程需要暂停
parNew、parallel scanvenge:多个线程进行回收,用户线程暂停,适用于吞吐量控制
CMS:并发实现用户线程和垃圾回收线程,适用于响应时间要求高
G1:将内存分为多个区,回收时则以分区为单位进行回收,这样它就可以用相对较少的时间优先回收包含垃圾最多区块。
常见的OOM:
1.java堆内存溢出:内存泄露(使用完的内存没有释放)或内存溢出(申请的堆大小过大),通过内存监控软件查找发生泄露的代码
2.java永久代溢出:大量class、常量等存在方法区,可以更改方法区大小
3.jvm栈溢出:存在死循环或递归深度过深导致的,可重新设置栈的大小
如何排查:
找到heap dump文件,他保存了某时刻jvm中堆的使用情况快照的镜像文件,可以实现开启heapdumponOOMError,这样有问题时能自动留下heap dump文件。用MemoryAnalyzer.exe分析,下面有problem suspect
看到某个地方产生了一千万个对象,找到对应的代码进行修改
java基本数据类型和引用类型
八种基本数据类型:
整型:byte(8位),short(16),int(32),long(64)
浮点型:float(32),double(64)
布尔型:boolean(1)
字符型:chart(16)
基本数据类型在声明后就会立即在栈上分配内存空间;
MySQL还有日期类型和枚举类型
其他数据类型都是引用类型:类、string、数组等,声明时不会分配内存空间,只是存储了一个内存地址。
抽象类和接口的区别
(相同点:都不能实例化,可以有抽象方法)
1.抽象类是对一类事物的抽象(是不是),接口是对类局部行为进行抽象(具不具备),所以一个类只能继承一个父类,但可以实现多个接口
2.关键字不同,抽象类:abstract class,接口:interface
抽象类需要继承extendds,接口需要实现implement
3.抽象类可以有定义和实现,接口只有定义
4.抽象类的成员变量可以是各种类型,接口的成员变量只能是public static final
final关键字
final可以修饰类、方法和变量。
修饰类:该类不可被继承
修饰方法:该方法不可被重写(继承父类的子类也不可覆盖)
修饰变量:如果是基本变量则值不可以改变,如果是引用变量则引用地址不能变。
static修饰符的用法
static修饰的都是类相关的,如类的成员方法、成员变量、内部类和代码块。
修饰类的成员变量(静态变量):静态变量和非静态变量的区别是静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。
修饰类的成员方法(静态方法):静态方法只能访问类的静态方法成员和静态成员变量;但是在非静态方法中可以访问静态成员方法和静态成员变量。
共同点:1. 属于类级别,无需创建对象就即可直接使用,使用方便(普通方法/实例方法必须调用创建对象,再使用对项目.方法名)。
2. 全局唯一,内存中唯一,静态变量可以唯一标识某些状态。
3. 类加载时候初始化,常驻在内存,调用快捷方便。
修饰代码块:类中可以有多个static代码块,在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次,因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。
static{
startDate = Date.valueOf("1946");
endDate = Date.valueOf("1964");
}
修饰内部类:在类的内部定义一个类,用static修饰。可以在不创建外部类对象的情况下被实例化
请你说说String类,以及new string和使用字符串直接量的区别。
String被final修饰(在定义的时候),所以不能被继承。创建字符串有两种方式
String str = “abc”:栈存放的是常量池里面的地址。
直接赋值时会首先检查字符串常量池中是否存在该字符串对象,如果已经存在,那么就不会在创建字符串常量池中再创建了,直接返回该字符串在字符串常量池中内存地址(存在str中),如果该字符串还不存在字符串常量池中,那么就会在字符串常量池中先创建该字符串的对象,然后再返回地址。
String str = new String(“abc”):栈存放的是堆里面的地址,堆里面存放的是常量池里面的地址。
在定义abc的时候,用new关键字在堆内存开辟一个内存区域,如果常量池有“abc”,则不再创建,但是同时 如果常量池里面没有 “abc”,那么它也会往常量池存放“abc”,然后再堆内存的区域中存放常量池里“abc”的地址。也就是说new的时候,实际是在内存中开辟两个地方”,一个在堆内存上(常量池地址),一个在常量池(“abc”)。
所以前者创建1个对象,后者创建2个对象。
String、StringBuffer、Stringbuilder有什么区别?
String和后两者区别在于String声明的是不可变对象,每次操作后会生成新String对象,然后指针指向新的String对象。后两者可在原有对象基础上进行操作,所以经常改变字符串内容情况下最好不要使用String。
StringBuffer线程安全,StringBuilder非线程安全,但后者性能高于前者(因为没有同步锁)。所以单线程使用后者,多线程使用StringBuffer。
8.说说==与equals()的区别?
==:
比较基本数据类型时,比较的是数值;
比较引用类型时,比较的是引用的内存地址。
equals:
没有重写时,比较的是两个对象内存地址是否相等,重写后(如string类就会对它重写)比较两个对象内容是否相等。
hashCode()和equals()的区别,为什么重写equals()就要重写hashcode()?
hashCode()是用来在散列存储结构中确定对象的存储地址的(用hash码表示)。equal是比较两个对象是否相等(默认比较地址),如果相等(一个对象),则hash码一定相等。反之hash码相等,equals()不一定相等,因为hash存储存在冲突情况。
由于二者具有联动关系,因此equals()重写时需要重写hashcode(),使结果保持相等。
Java8的新特性
1.Lambda表达式:Lambda允许把函数作为一个方法的参数。
2.接口改进:允许在接口中定义默认方法(用default修饰)。
3.Stream API:支持对元素流进行函数式操作,以及对集合进行批量操作。
4.Data Time API:加强对日期和时间的处理
包装类(封装类)的自动拆装与自动装箱
Java有8种基本数据类型,对应8种封装类(封装类就是把基本数据类型包装成引用数据类型,之后就可以调用一些方法)。
自动拆装:把封装类对象的值直接赋值给对应的基本数据类型。
自动装箱:把基本数据类型的数据直接赋值给对应的包装类。
Java的异常处理机制
try{
//可能抛出异常的代码
}
catch(异常类型1 异常对象名1){
//针对异常的处理代码
}
catch(异常类型2 异常对象名2){
//针对异常的处理代码
}
...
catch(异常类型n 异常对象名n){
//针对异常的处理代码
}
finally{
无论异常是否发生,都无条件执行的代码
}
try代码段:
包含在try中的代码段可能有多条语句会产生异常,但程序的一次执行过程中如果产生异常,只可能是这些异常中的某一个,该异常对象由Java运行时系统生成并抛出,try中产生异常语句,之后的语句都不会被执行,如果这次执行过程中没有产生异常,那么try中所有的语句都会被执行。
catch代码段:
捕获try中抛出的异常并在其代码段中做相应的处理,当try中代码产生的异常被抛出后,catch代码段按照从上到下的书写顺序将异常类型与自己参数所指向的异常类型进行匹配,若匹配成功程序转而表示异常被捕获,程序转而执行当前catch中的代码,后面所有的catch代码段都不会被执行,如果匹配不成功,交给下一个catch进行匹配,如果所有catch都不匹配,表示当前方法不具备处理该异常的能力。
finally代码段:
该代码段不是必须有的,但是如果有一定紧跟在最后一个catch代码段后面,作为异常处理机制的统一出口(常用于回收资源,而且有无异常都一定执行)。
面向对象的理解
面向对象的三大特征:封装、继承、多态
封装:隐藏类的成员变量和实现细节,不允许外部直接访问,只允许该类提供的方法进行操作。
继承:通过extends实现继承的类称为子类,子类能继承父类的方法和属性,实现了代码复用。
多态:需要有继承关系、方法的重写,父类引用指向子类的对象。
/*1.多态的前提:继承+重写*/
//1.创建父类
class Animal{
//3.创建父类的普通方法
public void eat(){
System.out.println("小动物Animal吃啥都行~");
}
}
//2.1创建子类1
class Cat extends Animal{
//4.1添加重写的方法
public void eat(){
System.out.println("小猫爱吃小鱼干~");
}
//5.1添加子类的特有功能
public void jump(){
System.out.println("小猫Cat跳的老高啦~");
}
}
//2.2创建子类2
class Dog extends Animal{
//4.2添加重写的方法
@Override
public void eat(){
System.out.println("小狗爱吃肉骨头~");
}
//5.2添加子类的特有功能
public void run(){
System.out.println("小狗Dog跑的老快啦~");
}
}
public class TestDemo {
public static void main(String[] args) {
//6.创建“纯纯的”对象用于测试
Animal a = new Animal();
Cat c = new Cat();
Dog d = new Dog();
a.eat();//小动物Animal吃啥都行~调用的是父类自己的功能
c.eat();//小猫爱吃小鱼干~调用的是子类重写后的功能
d.eat();//小狗爱吃肉骨头~调用的是子类重写后的功能
/*2.父类对象不可以使用子类的特有功能*/
//a.jump();//报错,Animal类里并没有这个方法
//a.run();//报错,Animal类里并没有这个方法
c.jump();//小猫Cat跳的老高啦~,子类可以调用自己的功能
d.run();//小狗Dog跑的老快啦~,子类可以调用自己的功能
//7.创建多态对象进行测试
/*3.口诀1:父类引用指向子类对象
* 解释:创建出来的子类对象的地址值,交给父类类型的引用类型变量来保存*/
Animal a2 = new Cat();//Cat类对象的地址值交给父类型变量a2来保存
Animal a3 = new Dog();//Dog类对象的地址值交给父类型变量a3来保存
//8.测试多态对象
/*4.口诀2:编译看左边,运行看右边
* 解释:必须要在父类定义这个方法,才能通过编译,把多态对象看作是父类类型
* 必须要在子类重写这个方法,才能满足多态,实际干活的是子类*/
a2.eat();//小猫爱吃小鱼干~,多态对象使用的是父类的定义,子类的方法体
}
}
重载和重写的区别,构造方法能不能重写?
重载:一个类中多个同名方法的存在,但要求形参列表不一致。
重写:父子类中的关系,指的是子类可以重写父类的方法,要求方法名和参数都相同。
前提知识:(1) 构造方法名与类名相同 , 且没有返回值,且不需要使用void修饰 。
(2) 作用:在构造方法中为创建的对象初始化赋值,
(3) 在创建一个对象的时候,至少要调用一个构造方法。
(4) 每个类都有构造方法。如果没有显式地为类定义构造方法,Java将会为该类提供一个默认构造方法,但是只要在一个Java类中定义了一个构造方法后,默认的无参构造方法即失效。
我们说构造方法是用来初始化对象的,那么它是怎样去初始化的呢,回想我们创建对象的语法
例 : Car car= new Car();
我们可以看到new关键字右边的这一块 ,这其实就是调用了Car类的构造方法来创建此对象的
构造方法也分为有参和无参的,如果没有显示的定义构造方法,默认是无参的
public class Car { //设计一个Car类
private String model; //型号
private String color; //颜色
//无参构造方法
public Car(){
}
//有参构造方法
public Car(String model,String color){
this.model=model;
this.color=color;
}
@Override
public String toString() { //重写toString用来输出对象
return "Car{" +
"model='" + model + '\'' +
", color='" + color + '\'' +
'}';
}
public static void main(String[] args) {
Car car1=new Car(); //调用无参构造方法初始化第一个对象
System.out.println(car1);
Car car2=new Car("宝马","白色"); //调用有参构造方法初始化第二个对象
System.out.println(car2);
}
}
所以构造方法不能重写,因为构造方法要求方法名与类名相同,而父类和子类的类名是不同的。(重写是方法名和参数相同,如果子类构造方法重写,那么一定与父类构造方法同名,因为和子类构造方法不同名)
16.介绍一下访问修饰符
java提供四种访问修饰符,修饰范围:public>protected>default>private。
请你说说泛型和泛型擦除
泛型:将类型作为参数传递(类型参数化),允许程序员在编写代码时使用一些以后才指定的类型 ,在实例化该类时将想要的类型作为参数传递,来指明这些类型。
未使用泛型前,我们对集合可以进行任意类型的 add 操作,因为不确定集合里存放的具体类型,取值时需要强制类型转换,否则拿到的都是Object。
所以加入泛型的好处是:明确指定集合接受哪些对象类型,类型转化错误在编译时就能发现,而不是运行时。
泛型擦除:Java是伪泛型,使用泛型时加上的类型在编译阶段会被擦除掉,在生成的字节码文件中是不包含泛型信息的,java的泛型基本上都是在编辑器这个层次上实现的。
参考:什么是泛型?- 泛型入门篇‘
类加载的过程
主要分为三步:
1.加载:通过全类名获取该类的二进制字节流,将字节流代表的静态存储结构转化为方法区运行时的数据结构,在内存生成该类的class对象,作为方法区数据的入口
2.连接:验证字节码是否合法(格式、语义、符号引用等),通过验证后分配内存,符号引用转换为直接引用
3.初始化:静态变量赋值操作和静态语句块执行
class对象
java中有两种:Class对象和实例对象。
class对象Class类没有公共的构造方法,Class对象是在类加载的时候由Java虚拟机以及通过调用类加载器中的 defineClass 方法自动构造的,因此不能显式地声明一个Class对象:
1、信息属性:从对象的作用看,Class对象保存每个类型运行时的类型信息,如类名、属性、方法、父类信息等等。在JVM中,一个类只对应一个Class对象。
2、普适性:Class对象是java.lang.Class类的对象,和其他对象一样,我们可以获取并操作它的引用。
3、运行时唯一性:每当JVM加载一个类就产生对应的Class对象,保存在堆区,类型和它的Class对象时一一对应的关系。一旦类被加载了到了内存中,那么不论通过哪种方式获得该类的Class对象,它们返回的都是指向同一个java堆地址上的Class对象引用。JVM不会创建两个相同类型的Class对象。
获取Class对象的三种方式:
//1、对象调用 getClass() 方法来获取,通常应用在:比如你传过来一个 Object
// 类型的对象,而我不知道你具体是什么类,用这种方法
Person p1 = new Person();
Class c1 = p1.getClass();
//2、类名.class 的方式得到,该方法最为安全可靠,程序性能更高
// 这说明任何一个类都有一个隐含的静态成员变量 class
Class c2 = Person.class;
//3、Class.forName(“全类名”): 将字节码文件加载进内存,用的最多,获取Class对象的一个引用,如果引用的类还没有被JVM加载,就立刻加载并初始化。
// 但可能抛出 ClassNotFoundException 异常
Class c3 = Class.forName("reflex.Person");
反射的原理和应用
映射:JVM将字节码内容不破坏原意情况下翻译成JVM可以看懂的语言
反射定义:在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个属性和方法;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
反射原理:加载完类之后,在堆中就产生了一个Class类型的对象(一个类只有一个Class对象),这个对象包含了类的完整结构信息,我们通过这个对象得到类的结构。
Package reflex;
public class Person {
//私有属性
private String name = "Tom";
//公有属性
public int age = 18;
//构造方法
public Person() {
}
//私有方法
private void say(){
System.out.println("private say()...");
}
//公有方法
public void work(){
System.out.println("public work()...");
}
}
使用反射的方法:
// 获取类的java对象
Class clz = Class.forName("reflex.Person");
// 获取构造方法的java对象
Constructor constructor = clz.getconstructor()
// 获取say方法的java对象
Method method = clz.getMethon("say",null);
// 通过反射实例化对象
Object object = constructor.newInstance();
// 通过反射调用say方法
Object distance = method.invoke(object,null);
动态语言:在运行过程中可以给对象增加或删除属性、方法。
应用场景:1.java是静态语言,通过反射可以让java完成动态语言的部分功能。
举例(假如我们有两个程序员,一个程序员在写程序的时候,需要使用第二个程序员所写的类,但第二个程序员并没完成他所写的类。那么第一个程序员的代码能否通过编译呢?这是不能通过编译的。利用Java反射的机制,就可以让第一个程序员在没有得到第二个程序员所写的类的时候,来完成自身代码的编译,运行时若第二个程序员的代码完成了,可以通过反射来调用他的代码)
2.可以调用私有方法、属性等。
说说多线程
1.线程是程序执行的最小单元,一个进程可以有多个线程,他们共享进程的内存空间和系统资源,但各个线程有自己的栈空间。
2.多线程创建和切换开销小,可以减少程序响应时间,提供cpu利用率,简化程序开发。
3.实现方式有三种:继承Thread类,实现runnable接口,实现callable接口,线程池,只有最后一种带返回值。
(1)继承Thread类:
线程间通信
方式一:共享内存,如volatile关键字
public class TestSync {
// 定义一个共享变量来实现通信,它需要是volatile修饰,否则线程不能及时感知
static volatile boolean notice = false;
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// 实现线程A
Thread threadA = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
notice = true;
}
});
// 实现线程B
Thread threadB = new Thread(() -> {
while (true) {
if (notice) {
System.out.println("线程B收到通知,开始执行自己的业务...");
break;
}
}
});
// 需要先启动线程B
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再启动线程A
threadA.start();
}
}
方式二:消息传递(使用Object类的wait() 和 notify() 方法)
public class TestSync {
public static void main(String[] args) {
//定义一个锁对象
Object lock = new Object();
List<String> list = new ArrayList<>();
// 线程A
Thread threadA = new Thread(() -> {
synchronized (lock) {
for (int i = 1; i <= 10; i++) {
list.add("abc");
System.out.println("线程A添加元素,此时list的size为:" + list.size());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (list.size() == 5)
lock.notify();//唤醒B线程
}
}
});
//线程B
Thread threadB = new Thread(() -> {
while (true) {
synchronized (lock) {
if (list.size() != 5) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程B收到通知,开始执行自己的业务...");
}
}
});
//需要先启动线程B
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//再启动线程A
threadA.start();
}
}
线程的状态以及切换
1.NEW:为初始状态,线程创建后还没调用start()方法
2.RUNABLE:运行状态
3.BLOCKED:线程正在等在监视器锁
4.WAITING:等待状态,线程正在等待其他线程的通知或中断
5.TIMED_WAITING:超时等待,在等待基础上增加了超时时间,到达超时时间后返回运行状态
6.TERMINATED:终止状态,线程运行结束
wt()和sleep()区别
1.wt()当前进程等待,释放锁,是Object方法;sleep()使当前线程休眠,没有释放锁,是Thread的静态方法(只对当前对象有效)。
2.返回条件不同:调用wt()进入等待的对象需要有notify()或notifyAll()唤醒。调用sleep()进入超时等待的线程需要在超时时间到达后自动返回。
threadlocal内部属性
线程变量,访问该变量的每个线程都会有这个变量的一个本地副本。当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。一般用于做连接管理。
具体过程为:新建一个threadlocal变量,然后在线程中threadlocal.set()设置变量,再通过threadlocal.get获取变量,这样的话每个线程中的threadlocal是互相独立的
public class ThreadLocalTest {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
threadLocal.set("本地变量1");
print("thread1");
System.out.println("线程1的本地变量的值为:"+threadLocal.get());
});
Thread thread2 = new Thread(() -> {
threadLocal.set("本地变量2");
print("thread2");
System.out.println("线程2的本地变量的值为:"+threadLocal.get());
});
thread1.start();
thread2.start();
}
public static void print(String s){
System.out.println(s+":"+threadLocal.get());
}
}
执行结果
由每个Thread维护一个ThreadLocalMap,该Map的key是这个ThreadLocal实例,value是存储的变量。
ThreadLocal的内存泄露问题:ThreadlocalMap中的Entry强引用了threadlocal,造成threadlocal无法被GC(垃圾回收),如果没有手动remove且该进程依然运行的情况下,会造成OOM,因为将key设置成弱引用。
volatile
对于某个共享变量,每个操作单元都缓存一个该变量的副本,当一个操作单元更新其副本时,其他的操作单元可能没有及时发现,进而产生缓存一致性问题。
一种类型修饰符,确保本条指令不会被编译器的优化而忽略。(因为编译器如果发现两次从i读数据的代码之间的代码没有对 i 进行过操作,那么就直接取本单元的副本)使用volatile关键字修饰后,系统每次用到他的时候都是直接从对应的内存当中提取。
根据规则,其作用是
在多处理器开发中保证共享变量的“可见性”和禁止指令重排(写操作必须在读操作之前执行)。
怎么保证线程安全
同步阻塞(悲观锁):使用ReentrantLock与synchronized来加锁,通过互斥实现同步。(性能损耗较大)
同步非阻塞(乐观锁):自旋CAS,先进行操作,比较替换值是否是期待值,是就替换,不是就失败。但是会产生ABA问题,解决方法是使用版本号。
自旋的含义是:一个无限循环中,执行一个 CAS 操作,当操作成功,返回 true 时,循环结束;当返回 false 时,接着执行循环,继续尝试 CAS 操作,直到返回 true。
无同步:
reentrantCode(Pure Code):可以在代码执行的任何时刻中断它,转而去执行另外一段代码,而在控制权返回后,原来的程序不会出现任何错误。
ThreadLocal:
24.synchronize的加锁方式
synchronize:加在三个不同的位置,对应三种不同的使用方式,区别是锁对象不同:
无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是当前对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
(1):加在静态方法上,锁是当前类的class对象。
当synchronized作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态成员的并发操作。需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁
(注意这里生成了两个对象哦)
public class AccountingSyncClass implements Runnable{
static int i=0;
/**
* 作用于静态方法,锁是当前class对象,也就是
* AccountingSyncClass类对应的class对象
*/
public static synchronized void increase(){
i++;
}
/**
* 非静态,访问时锁不一样不会发生互斥
*/
public synchronized void increase4Obj(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
//new新实例
Thread t1=new Thread(new AccountingSyncClass());
//new新实例
Thread t2=new Thread(new AccountingSyncClass());
//启动线程
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
由于synchronized关键字修饰的是静态increase方法,与修饰实例方法不同的是,其锁对象是当前类的class对象。注意代码中的increase4Obj方法是实例方法,其对象锁是当前实例对象,如果别的线程调用该方法,将不会产生互斥现象,毕竟锁对象不同,但我们应该意识到这种情况下可能会发现线程安全问题(操作了共享静态变量i)。
(2):加在实例方法上,则锁是当前的实例
public class AccountingSync implements Runnable{
//共享资源(临界资源)
static int i=0;
/**
* synchronized 修饰实例方法
*/
public synchronized void increase(){
i++;
}
@Override
public void run() {
for(int j=0;j<1000000;j++){
increase();
}
}
public static void main(String[] args) throws InterruptedException {
AccountingSync instance=new AccountingSync();
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();
t2.start();
//t.join含义:t线程调用该方法后会强占CPU资源,执行执行结束
t1.join();
t2.join();
System.out.println(i);
}
/**
* 输出结果:
* 2000000
*/
}
前提知识:Java 内存模型规定了所有的变量都存储在主内存中,每条线程有自己的工作内存。
线程的工作内存中保存了该线程中用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。
线程访问一个变量,首先将变量从主内存拷贝到工作内存,对变量的写操作,不会马上同步到主内存。
不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步。
上述代码中,我们开启两个线程操作同一个共享资源即变量i,由于i++;操作并不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于increase方法必须使用synchronized修饰,以便保证线程安全。此时我们应该注意到synchronized修饰的是实例方法increase,在这样的情况下,当前线程的锁便是实例对象instance,注意Java中的线程同步锁可以是任意对象。
(3):加在代码块上,其作用域是被修饰的整个代码块里面的内容,作用对象是括号中的对象或类
在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了,同步代码块的使用示例如下:
作用对象是对象的时候,锁的是当前对象,所有instance对象的线程执行到这里都要获取锁
public class AccountingSync implements Runnable{
static AccountingSync instance=new AccountingSync();
static int i=0;
@Override
public void run() {
//省略其他耗时操作....
//使用同步代码块对变量i进行同步操作,锁对象为instance
synchronized(instance){
for(int j=0;j<1000000;j++){
i++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(instance);
Thread t2=new Thread(instance);
t1.start();t2.start();
t1.join();t2.join();
System.out.println(i);
}
}
//this,当前实例对象锁,等同于上面代码
synchronized(this){
for(int j=0;j<1000000;j++){
i++;
}
}
//class对象锁,作用对象是类的时候,作用的是类及该类的所有对象
synchronized(AccountingSync.class){
for(int j=0;j<1000000;j++){
i++;
}
}
CopyOnWrite(写入时复制)
当向一个容器中添加元素的时候,不是直接在当前这个容器里面添加的,而是复制出来一个新的副本,在新的副本里面添加元素,添加完毕之后再将原容器的引用指向新的副本,这样就实现了写入时复制。这样可以做到读写并行,同时为了让读线程感受到这个变化,可以使用volatile关键字。
常见注解
@Transient
@transient 就是在给某个javabean上需要添加个属性,但是这个属性你又不希望给存到数据库中去,仅仅是做个临时变量,用一下。不修改已经存在数据库的数据的数据结构。
@Override
用于限定某个方法,如果写了@Override注解,编译器就会去检查该方法是否真的重写了父类的方法,如果的确实重写了,则编译通过,如果没有构成重写,则编译错误。
@Repository:将持久层(dao层或mapper层)注入spring容器
作用是访问数据库,向数据库发送sql语句,完成数据的增删改查任务
@Service:将业务层注入spring容器
模块之间的逻辑
@controller:将控制层注入spring容器
模块内的逻辑
放在控制层类的上面,表示这些类是controller层,运行时扫描得到控制层bean,里面的每个方法都对应Request中URL的映射,具体的映射要搭配@RequestMapping(路径)确定
@Component:普通pojo注入spring容器
没有从任何类继承、也没有实现任何接口,与其他架构没有关系的java对象。
java集合类型
1、List(有序、可重复)ArrayList、LinkedList
List里存放的对象是有序的,同时也是可以重复的,List关注的是索引,拥有一系列和索引相关的方法,查询速度快。因为往list集合里插入或删除数据时,会伴随着后面数据的移动,所有插入删除数据速度慢。
2、Set(无序、不能重复)HashSet
Set里存放的对象是无序,不能重复的,集合中的对象不按特定的方式排序,只是简单地把对象加入集合中。
3、Map(键值对、键唯一、值不唯一)HashMap
Map集合中存储的是键值对,键不能重复,值可以重复。根据键得到值,对map集合遍历时先得到键的set集合,对set集合进行遍历,得到相应的值。
HashMap和HashTable的区别
- HashMap是非线程安全的,Hashtable是线程安全的(synchronized锁)
- HashMap允许null作为键或值,Hashtable不允许,运行时会报NullPointerException
- HsahMap在数组+链表的结构中引入了红黑树,Hashtable没有
- HashMap初始容量为16,扩容*2,Hashtable初始容量为11,扩容+1
HashMap实现方式
HashMap是怎么实现的?
1.jdk1.7的HashMap是用数组+链表实现的
2.jdk1.8的HashMap是用数组+链表+红黑树实现的(数组长度大于8时,链表改为红黑树)
对于1.8来说,实现方法如下:
1.首先获取对象的hashCode()值,然后将hashCode值右移16位,然后将右移后的值与原来的hashCode做异或运算,返回结果。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
然后根据hash值计算机index
index = hash & (Length - 1)
2.判断table数组有没有初始化,若无则调用resize()方法进行初始化
3.根据index判断是否发生hash碰撞,
(1)如果未发生则直接放到对应的bucket中
(2)如果发生且节点已存在,则替换掉相应的value,
否则放入桶中存在的链表(尾插法放入,且如果链表长度超过threshold==8,则将链表转为树结构)或树结构
4.如果超过扩容的阈值则进行扩容,threshold = capacity * load factor,长度变为原数组两倍
HashMap扩容机制
底层:采用数组+链表(JDK1.7),采用数组+链表+红黑树(JDK1.8)。线程不安全。
容器:HashMap默认容器长度为16,扩容因子为0.75,以2的n次方扩容,最高可扩容30次。如第一次是长度达到160.75=12的时候开始扩容,162^1=32。
HashMap扩容分为两步:
扩容:创建一个新的Entry空数组,长度是原数组的2倍。
ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。
为什么要重新Hash呢,不直接复制过去呢?因为长度扩大以后,Hash的规则也随之改变。
Hash的公式—> index = HashCode(Key) & (Length - 1)
原来长度(Length)是8你位运算出来的值是2 ,新的长度是16你位运算出来的值明显不一样了,之前的所有数据的hash值得到的位置都需要变化。
为什么hashmap使用红黑树不用平衡二叉树,mysql为什么使用B+树
1.平衡二叉树解决了二叉查找树容易退化为链表的情况O(n),因为有时每棵树的左右子树高度差过大,增加查找时间。
2.平衡二叉树要求每个节点的左子树和右子树的高度差至多等于1,最坏的查找时间复杂度也为 O(logn)。
3.由于平衡二叉树的要求过于严格,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的规则,同时调整的时间复杂度较高O(logn),使平衡树的性能受到影响,于是提出红黑树(也是二叉排序树):
1.根、叶子节点黑色
2.红色节点的子节点都是黑色
3.任一结点到其每个叶子的所有路径都包含相同数目的黑色结点。
插入或删除数据时,只需要进行O(1)次数的旋转以及变色就能保证基本的平衡
红黑树是一种折中的方案,牺牲查找效率换取维持平衡状态成本,且时间复杂度O(log n)
4.由于mysql等数据库的数据存放在磁盘中,而红黑树的高度还是比较高,于是磁盘的IO便成为最大的性能瓶颈,于是提出了为磁盘等辅存设备设计的多路平衡查找树,与二叉树相比,B树的每个非叶节点可以有多个子树,使得树高大大减少,从而减少了磁盘IO操作,此外还能利用到访问的局部性原理,B树将键相近的数据存储在同一个节点,当访问其中某个数据时,数据库会将该整个节点读到缓存中;当它临近的数据紧接着被访问时,可以直接在缓存中读取,无需进行磁盘IO;换句话说,B树的缓存命中率更高:
- 设定m,作为m阶B树(阶是由一页的大小决定的,页/信息项)
- 每个结点最多有m个孩子
- 所有叶子结点都在同一层
5.但是B树未能解决元素遍历的效率低下问题,B+树也是多路平衡查找树,其与B树的区别在于:
- B树中每个节点(包括叶节点和非叶节点)都存储真实的数据,B+树中只有叶子节点存储真实的数据,非叶节点只存储键。(由于内部结点更小,所以盘块容纳的关键字数量越多,一次性读入内存能查询的关键字也越多,IO次数便降低了)
- B+树的叶节点之间通过双向链表链接。(只需遍历叶子节点就可以实现整棵树的遍历)
6.位图索引
对于性别,可取值的范围只有’男’,‘女’,并且男和女可能各站该表的50%的数据,这时添加B树索引还是需要取出一半的数据, 因此完全没有必要。相反,如果某个字段的取值范围很广,几乎没有重复,比如身份证号,此时使用B树索引较为合适。事实上,当取出的行数据占用表中大部分的数据时,即使添加了B树索引,数据库如oracle、mysql也不会使用B树索引,很有可能还是一行行全部扫描。
HashMap、concurrentHashMap
跟源码有关系
1.在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。
2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。
多线程应该使用ConcurrentHashMap,其主要通过:
- 容器为空,则volatile和CAS初始化(使用volatile保证当Node中的值变化时对于其他线程是可见的)
- 容器不空,节点空,使用CAS操作来保证 Node头节点能被能正确的写入。
- 节点不空,对table数组的头结点使用synchronized锁来保证写操作的安全
为什么使用工厂
1.把对象的创建和使用的过程分开。就是Class A 想调用 Class B ,那么A只是调用B的方法,而至于B的实例化,就交给工厂类。
2.创建对象B的代码很复杂时,可以放到工厂里统一管理。既减少了重复代码,也方便以后对B的创建过程的修改维护。
3.因为工厂管理了对象的创建逻辑,使用者并不需要知道具体的创建过程,只管使用即可,减少了使用者因为创建逻辑导致的错误。
适合:
- 对象的创建过程/实例化准备工作很复杂,需要初始化很多参数、查询数据库等。
- 类本身有好多子类,这些类的创建过程在业务中容易发生改变,或者对类的调用容易发生改变。
深拷贝和浅拷贝
浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 。如Object.assign(),
深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象。如JSON.parse(JSON.stringify())
区别:浅拷贝基本类型之间互不影响,引用类型其中一个对象改变了内容,就会影响另一个对象;深拷贝改变新对象不会影响原对象,他们之间互不影响。
this的使用
1.this.data; //在成员方法中使用,可以访问该类的属性,不加会出错
class MyDate{
public int year;
public int month;
public int day;
public void setDate(int year, int month,int day){
this.year = year;
this.month = month;
this.day = day;
}
public void PrintDate(){
System.out.println(this.year+"年 "+this.month+"月 "+this.day+"日 ");
}
}
public class TestDemo {
public static void main(String[] args) {
MyDate myDate = new MyDate();
myDate.setDate(2000,9,25);
myDate.PrintDate();
MyDate myDate1 = new MyDate();
myDate1.setDate(2002,7,14);
myDate1.PrintDate();
}
}
2.this.func() //这种是指在普通成员方法中使用this调用另一个成员方法
class Student{
public String name;
public void doClass(){
System.out.println(name+"上课");
this.doHomeWork();
}
public void doHomeWork(){
System.out.println(name+"正在写作业");
}
}
public class TestDemo2 {
public static void main(String[] args) {
Student student = new Student();
student.name = "小明";
student.doClass();
}
}
3.this() // 这种指在构造方法中使用this调用本类其他的构造方法
- this只能在构造方法中调用其他构造方法
- this要放在第一行
- 一个构造方法中只能调用一个构造方法
仔细分析:从主函数开始,new Flower()会在内存分配空间,初始化对象,初始化对象是调用构造函数,这里没有写任何参数,当然是调用默认构造函数,就是那个无参的构函数。这个无参的构造函数的第一行代码就是this(“hi”,122);这里的意思是该无参构造函数又去调用带两个参数的构造函数,来到带两个参数的构造函数,第一行代码是this(s);这行代码自动匹配带一个参数的构造函数,发现Flower(String ss)这个比较匹配,都是String类型的参数。然后调用了带有一个String类型参数的构造函数,打印:只有String类型的参数的构造函数 s = hi;然后回到上一级调用函数,就是带有两个参数的构造函数,打印输出:有String和int类型的参数的构造函数;再回到上一级,就是无参构造函数,打印:默认构造函数。此时构造函数已经初始化完成新建的对象,最后在主函数的最后一行代码中打印:petalCount=122 s=hi。
BIO、NIO和AIO区别
同步方法:调用一旦开始,调用者必须等到方法调用返回才能继续后续的行为。
异步方法:调用更像是一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作,而异步方法通常会在另外一个线程中“真实的”执行,完成后通过callback方法通知调用方。
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
BIO:blocking I/O,同步阻塞,面向流,客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,适用于连接数目比较小且固定的架构
NIO:non-blocking I/O,同步非阻塞,面向缓冲区线程可以等他的数据全部写入到缓冲区中形成一个数据块然后再去处理他,在这期间该线程可以去处理其他请求。
三大核心部分:channel(通道)、buffer(缓冲区)、selector(选择器)
数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中,单个线程控制Selector(选择器)用于监听多个通道事件。
比如说流IO他是一个流,你必须时刻去接着他,不然一些流就会丢失造成数据丢失,所以处理这个请求的线程就阻塞了他无法去处理别的请求,他必须时刻盯着这个请求防止数据丢失。而块IO就不一样了,线程可以等他的数据全部写入到缓冲区中形成一个数据块然后再去处理他,在这期间该线程可以去处理其他请求。
AIO:异步非阻塞,无需一个线程去轮询所有IO操作的状态改变,在相应的状态改变后,系统会通知对应的线程来处理。
序列化(Serialization)
序列化:将对象的状态信息转换为可以存储或传输的形式的过程(字节序列)。
目的
- 网络传输(分布式系统传输数据):网络只能传输二进制数据,所以要先对java对象序列化
- 对象持久化:把对象的字节序列永久保存到硬盘上形成一个文件
反序列化:把获取的字节序列恢复为原先的Java对象。
使用时,对象需要有serializable接口(其实里面啥也没有),否则不能序列化
然后使用writeObject(Object obj)方法对指定的obj对象序列化,并把得到的字节序列写到一个目标输出流中。
ObjectOutputStream objectOutputStream =
new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) );
objectOutputStream.writeObject( student );
objectOutputStream.close();
反序列化使用readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
ObjectInputStream objectInputStream =
new ObjectInputStream( new FileInputStream( new File("student.txt") ) );
Student student = (Student) objectInputStream.readObject();
objectInputStream.close();
有些隐私信息需要避免对象序列化,否则传输时会造成安全问题
可以使用static(类的状态)或transient关键字(临时数据)
死锁
定义:多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
预防死锁:一次性分配,有序分配
避免死锁:银行家算法,算推进顺序
检测死锁:资源分配表(看有没有环)
接触死锁:
剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。
单例模式
定义:在程序中多次使用同一个对象且作用相同时,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
类型:
懒汉式:懒汉式创建对象的方法是在程序使用对象前,先判断该对象是否已经实例化(判空),若已实例化直接返回该类对象。,否则则先执行实例化操作。
为了避免并发安全(创建多个对象)以及性能低效(加了synchronized锁)问题,提出了
double check(双重校验)+ Lock(加锁):
先判断对象有没有创建(一重校验),有就直接返回
没有创建的话,线程争抢锁(加锁)
抢到锁之后,再判断对象有没有创建(二重校验)
禁止指令重排(volatile)
此外,创建对象是分三步:1.为singleton分配内存空间2.初始化singleton对象3.让singleton指向分配好的空间,JVM在保证最终结果正确的情况下,为提高性能可以不按照指令顺序132
这样可能导致线程A执行了1、3步骤后线程B判断singleton不为空就获取了未初始化的single对象,报NPE异常
public class Singleton {
private static volatile Singleton singleton;
private Singleton(){};
public static Singleton getInstance() {
if (singleton == null) { // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
synchronized(Singleton.class) { // 线程A或线程B获得该锁进行初始化
if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
singleton = new Singleton();
}
}
}
return singleton;
}
}
饿汉式:饿汉式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可。
常用linux命令
1.cd:切换目录
- cd … 返回上一级目录
- cd - 返回上一次目录
2.pwd:显示当前工作路径
3.ls:查看文件与目录
- ls -l 列出文件和目录的详细资料
- ls -a 查看隐藏文件
4.cp 复制文件
- cp -i 询问
- cp -u 不同才复制
5.mv 移动文件
- mv -i 询问是否覆盖
- mv -u 比目标文件新才覆盖
- mv -f 直接覆盖
6.rm 删除文件
rm -i 询问
rm -f 强制删除
rm -r 递归删除,用来删目录以及下面文件,rmdir只能删除空目录
7.查看文件
head -n x file:正向查看文件前n行内容
tail -n x file:逆向查看文件前n行内容
tail -f 100 xxx.log:实时查看最新日志的100行
vim file:修改文件
8.chmod 修改权限
r=4读,w=2写,x=1执行
chmod -r 777 /home r表示递归执行,7(所有者)7(群组)7(其他人)
表示将home以及下面所有目录文件对三个群体开通开通三个权限
9.chown 改变所有者
chown user1 file1 改变一个文件
chowen -R user1 directory 改变该文件目录下所有文件和目录
10.解压zvf
tar –xvf file.tar //解压 tar包
tar -xzvf file.tar.gz //解压tar.gz
压缩cvf
tar -cvf xxx.tar /data : 仅打包
tar -zcvf xxx.tar /data : 打包后,以gzip方式压缩
11.查看进程
ps -ef | grep all // 查看全部进程
ps -ef | grep 进程名
kill -9 pid号 // 强制杀死进程(可能会丢失数据、没有释放内存等)
kill -15 pid号 // 正常杀死进程
12.查看端口(a:显示全部信息 )
netstat -anp | grep 端口号
13.sed 行编辑器
删除或修改操作必须-i
sed -i 's/A/B/' student.txt // 只匹配每行第一个字段修改
sed -i 's/A/B/g' student.txt //将student.txt中所有的A更改为B
sed -i "s/java/linux/g" `grep java -rl /test` //修改/test目录下全部文件的java为linux
renema bbb BBB 123* //当前目录下所有123开头的文件名中的bbb替换成BBB
使用shell脚本
#!/bin/bash
for file in `ls file*`
do
mv $file `echo $file |sed 's/\.sh/\.pdf/'` #第二种方法
done
文本查找
grep "Copyright" LICENSE.txt // 查找字符在文本中的对应情况
vim后的文本查找
:/Copyright
然后按n查找下一个
线程池
- corePoolSize:核心线程数。
核心线程数:是指线程池中长期存活的线程数。 - maximumPoolSize:最大线程数。
最大线程数:线程池允许创建的最大线程数量,当前线程数达到corePoolSize后,如果继续有任务被提交到线程池,会将任务缓存到工作队列(后面会介绍)中。如果队列也已满,则会去创建一个新线程来出来这个处理。 - keepAliveTime:空闲线程存活时间。一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁
- TimeUnit:时间单位。 空闲线程存活时间的描述单位
- BlockingQueue:线程池任务队列。被提交但尚未被执行的任务,有链表型、双向链表型、数组型、支持优先级队列型
- ThreadFactory:创建线程的工厂。通过此方法可以设置线程的优先级、线程命名规则以及线程类型(用户线程还是守护线程)等。
- RejectedExecutionHandler:拒绝策略。当线程池的任务超出线程池队列可以存储的最大值之后,执行的策略。
AbortPolicy:拒绝并抛出异常。
CallerRunsPolicy:使用当前调用的线程来执行此任务。
DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务。
DiscardPolicy:忽略并抛弃当前任务。
线程间通信
1.volatile关键字:基于共享内存
多个线程同时监听一个变量,当该变量发生变化的时候,线程能够感知并执行相应的业务。
2.wait()/notify()/notifyAll()搭配synchronized锁
wait():线程就会释放掉自己所占有的锁,释放CPU,然后进入阻塞状态,直到被notify()方法唤醒。
notify():随机唤醒一个等待该对象锁的线程,让其进入就绪队列,但自己不会立即释放锁,需要等待执行完对象锁锁住的区域后再释放锁。
notifyAll():和notify()方法差不多,只不过他是唤醒所有等待该对象锁的线程,让他们进入就绪队列,但是谁执行就看谁抢占到CPU
元组和列表区别
1.列表长度不固定,元素可以增、删、改,使用resize()更改列表容量,元组长度固定,一旦生成无法变动,但将两个元组合并成一个新元组,T = T1 + T2;
2.列表用中括号[]、元组用()
分布式和集群
分布式:把一个大业务拆分成多个子业务,每个子业务都是一套独立的系统,子业务之间相互协作最终完成整体的大业务。
集群:把处理同一个业务的系统部署多个节点 。负载均衡是相对于集群而言的。
多线程和多进程
1.进程是资源分配的最小单元,线程是CPU分配的最小单元
2.不同进程间的资源的独立的,而一个进程可以由多个线程组成,分别执行不同的任务,同一个进程内的线程共享进程的资源
3.由于进程创建、切换、销毁的代价较大,所以需要频繁…的优先使用多线程
4.由于同一进程内的线程共享进程的资源,所以强相关的处理优先使用多线程(通信方便)
MySQL数据库
sql注入
如果用户输入的数据被构造成恶意 Sql 代码,Web 应用又未对动态构造的 Sql 语句使用的参数进行审查,就会造成一些危险。
比如登录时,对于输入的账号、密码,实际的sql语句是
如果输入123’ or 1 = 1 #,这样#会把后面的注释掉
那么结果是
select * from users where username='123' or 1=1
${}是拼接符,在编译时会不加引号的拼接上去,就是为了设计sql语句产生的,比如表名、字段名
#{}是占位符,在编译时相当于,编译过后会对传递的值加上双引号
事务的ACID特性
-
原子性(Atomicity):事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
例如:
A转出200给B(A-200)
B收到200(B-200)
这两个动作就是一组事务,要么全部完成,要么全部不完成。 -
一致性(Consistency):事务前后数据的完整性必须保持一致。
如上例,在转账前A账户的钱加上B账户的钱为2000,那么在转账事务发生后账户A与账户B的钱的总和也应该为2000。 -
隔离性(isolation):排除其他事务对本次事务的影响
-
持久性(Durability)
事务处理结束后,对数据的修改就是永久的,不随着外界原因导致数据丢失。
数据库并发事务会带来哪些问题?
数据库并发会带来脏读、幻读、丢弃更改、不可重复读这四个常见问题,其中:
脏读:一个事务读取到了另一个事务正在操作但还没有提交到数据库中的数据。这种数据称之为脏数据
幻读:一次事务在多次查询时,结果集的个数不一致。多出来或者少出来的那一行叫做幻行(侧重于行数量变化)
不可重复读:不可重复读指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据出现不一致的情况。(侧重同一个数据两次不一致)。
丢弃修改:两个写事务T1 T2同时对A=0进行递增操作,结果T2覆盖T1,导致最终结果是1 而不是2,事务被覆盖。
数据库隔离级别
未提交读,一个事务提交前的操作其它事务是可见的。事务中发生了修改,即使没有提交,其他事务也是可见的,比如对于一个数A原来50修改为100,但是我还没有提交修改,另一个事务看到这个修改,而这个时候原事务发生了回滚,这时候A还是50,但是另一个事务看到的A是100.可能会导致脏读、幻读或不可重复读
提交读,一个事务提交前的操作其他事务是不可见的对于一个事务从开始直到提交之前,所做的任何修改是其他事务不可见的,举例就是对于一个数A原来是50,然后提交修改成100,这个时候另一个事务在A提交修改之前,读取的A是50,刚读取完,A就被修改成100,这个时候另一个事务再进行读取发现A就突然变成100了;可以阻止脏读,但是幻读或不可重复读仍有可能发生
重复读,在某个事务开始读取数据时,不允许其他事务进行修改操作。示例:还是小明有1000元,准备跟朋友聚餐消费这个场景,当他买单(事务开启)时,收费系统检测到他卡里有1000元,这个时候,他的女朋友不能转出金额。接下来,收费系统就可以扣款成功了,小明醉醺醺的回家,准备跪脱衣板。
可串行化读,在此级别下,事务串行执行。
数据库悲观锁和乐观锁的原理和应用场景分别有什么?
悲观锁:先获取锁,再进行业务操作,一般就是利用类似 SELECT … FOR UPDATE 这样的语句获取被操作数据的锁,锁在事务结束后自动释放。
乐观锁:先进行业务操作,只在最后实际更新数据时进行检查数据是否被更新过。
例如Compare and Swap(CAS)技术,通过where来比较实现
//查询出商品库存信息,quantity = 3
select quantity from items where id=1
//修改商品库存为2
update items set quantity=2 where id=1 and quantity = 3;
但会存在ABA问题(即几次交换后又换回以前的值了,但不代表没问题),比如说一个线程1从数据库中取出库存数3,这时候另一个线程2也从数据库中库存数3,并且线程2进行了一些操作将库存数变成了2,紧接着又将库存数变成3,这时候线程1进行CAS操作发现数据库中仍然是3,然后线程1操作成功。尽管线程1的CAS操作成功,但是不代表这个过程就是没有问题的。
解决方案:增加递增的version字段
//查询出商品信息,version = 1
select version from items where id=1
//修改商品库存为2
update items set quantity=2,version = 3 where id=1 and version = 2;
查询每个班级排名前三的学生
select a.class,a.score from student a
where
(select
count(*)
from
student
where a.class=class and a.score<score)
<3
order by
a.class, a.score
desc;
MySQL优化了解吗?说一下从哪些方面可以做到性能优化?
1.为搜索字段创建索引
2.垂直分割分表,基于数据表中的“列”进行划分。某个表字段较多,可以新建一张扩展表,将不经常用或者字段长度较大的字段拆分出去。
3.优化查询语句:
(1)避免使用 Select *,列出需要查询的字段,不需要的列会增加数据传输时间和网络开销
(2)避免索引失效
4.选择合适的引擎:具体从两者的各种区别开始引申:存聚外行锁事崩,并在自己的项目中举例哪些地方选哪个。
MyISAM适合SELECT密集型的表,而InnoDB适合INSERT和UPDATE密集型的表
删除数据前需要做什么操作,drop、truncate、delete的区别?
会对数据进行备份操作,以防万一,可以进行数据回退
1.DELETE语句执行删除的过程是每次从表中删除一行,并且同时将该行的删除操作作为事务记录在日志中保存以便进行进行回滚操作。不会释放表或索引占用的空间。(所以比如前面id0-6都被删了,那么现在insert会从7开始)
可加条件(where)、可回滚=未释放空间(rollback)
2.TRUNCATE TABLE 删除表数据,但不删除表结构,不把删除操作记录记入日志保存,删除行是不能恢复的。并且在删除的过程中不会激活与表有关的删除触发器。会释放表或索引占用的空间。(所以比如前面id0-6都被删了,那么现在insert会从0开始)
删除所有行,相当于不可加条件的delete、不可回滚=释放空间
3.drop删除表的数据以及结构、索引,并将表所占用的空间全部释放,不能回滚。
1和2、3的区别在于有没有把删除记录在日志和不会释放表或索引空间,能回滚。(假删,只是让我们看不到)
2和3的区别在于有没有删除表结构。
MyISAM和InnoDB的区别
(存、聚、锁、事、崩)
(1)数据的存储结构不同
MyISAM在磁盘上存储成三个文件它们以表的名字开头来命名:.frm文件存储表结构文件。.MYD(MYD)存储数据文件。.MYI(MYIndex)存储索引文件。
InnoDB:.frm文件同样存储为表结构文件,.ibd文件存储的是数据和索引文件。
由于MyISAM的索引和数据是分开的,所以在索引查找时,MyISAM叶子节点存储的是数据所在的地址而非数据;而Innodb叶子节点存储的是数据。
(2)InnoDB是聚集索引,使用B+Tree作为索引结构,数据文件是和(主键)索引绑在一起的(表数据文件本身就是按B+Tree组织的一个索引结构),必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。
MyISAM是非聚集索引,也是使用B+Tree作为索引结构,索引和数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
也就是说:InnoDB的B+树主键索引的叶子节点就是数据文件,辅助索引的叶子节点是主键的值;而MyISAM的B+树主键索引和辅助索引的叶子节点都是数据文件的地址指针。
(3)MyISAM不支持事务,InnoDB支持。
(4)MyISAM仅支持表级锁,而InnoDB支持表、行级锁(使用where时也会锁整张表),如果有大量增删改操作时,使用Innodb会更好。
(5)MyISAM支持全文索引,大量查操作时,使用MyISAM。
(6)MyISAM不支持外键,InnoDB支持。
(7)系统易发生崩溃,使用InnoDB。
索引
定义:数据库管理系统中一个用于排序的数据结构,用来快速查询数据库中的数据。
1)普通索引:允许空值,允许重复值
2)唯一索引:不允许重复值,允许空值
3)主键索引:不允许空值,不允许重复值
主键:表中某一个属性组(注意是组,不一定是一列)能唯一标识一条记录,该属性组就可以成为一个主键 ,指定为’PRIMARY KEY’
4)全文索引:将文档中所有的文字序列都作为检索对象,找出包含检索词汇的数据项。
1.聚集索引比非聚集索引性能高/主键索引比普通索引性能高
- 如果主键被定义,那么该主键作为聚集索引
- 如果没有定义主键,那么该表第一个唯一非空索引作为聚集索引
- 如果没有主键和唯一非空索引,那么内部生成一个隐藏主键作为聚集索引,该隐藏主键的列值会随数据插入自增
(1)二者都是用B+树,即非叶子节点只存储键和指针,叶子节点存储数据,而且叶子节点通过双向链表连接
(2)聚集索引的叶子节点存放数据,非聚集索引的叶子节点存放当前索引的值以及对应的主键索引的键值,然后根据这个键值再去主键索引中进行一次查询(称为回表查询)
2.优化回表查询——组合索引、覆盖索引
如下图所示,表的数据如右图,ID 为主键,创建的联合索引为 (A,B),注意联合索引顺序,左图是模拟的联合索引的 B+ Tree 存储结构
检索规则是(索引符合最左匹配原则,联合索引是从左到右(A左B右)的顺序来建立搜索树的):
key<5:p1
5<=key<8:p2
8<=key:p3
根据该图可以看出A是有序的,B是无序的。当A相等的时候,B才是有序的(相对有序)。
组合索引的查找过程:
B+树会先比较A来确定下一步的搜索方向(二分查找),往左还是往右,当A相同的时候再比较B(二分查找),拿到联合索引所在行的主键值后,通过主键索引找到具体行的数据。
alter table user add index 'index_A_B'(A,B);
最左前缀匹配原则(带头大哥不能死,中间兄弟不能断):
①where子句中使用最频繁的一列放在最左边
②mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配
上图3发生了索引失效
使用覆盖索引会避免回表问题:在辅助索引里面,不管是单列索引还是联合索引,如果select的数据列只用从索引中就能够取得,不必从数据区中读取,这时候使用的索引就叫做覆盖索引,这样就避免了回表。
先创建联合索引
CREATE INDEX idx_name_phoneonuser_innodb(name,phone);
这三个查询语句都用到了覆盖索引:
EXPLAIN SELECT name,phone FROM user_innodb WHERE name='青山' AND phone='13666666666';
EXPLAIN SELECT name FROMuser_innodb WHERE name='青山' AND phone='13666666666';
EXPLAIN SELECT phone FROM user_innodb WHERE name='青山' AND phone='13666666666';
3.索引条件下推(ICP,Index condition pushdown)
注意:联合索引的创建和where内的条件是相关的,这样效率才能最大化
在没有使用ICP之前
explain select * from tuser where name like '张%' and age = 10;
联合索引没有开启ICP前,存储引擎通过主键索引检索到数据,就回回表,将完整的行记录反馈给服务器,服务再根据第二个条件筛选。增加了回表次数,浪费索引字段。
Using Where代表存储引擎根据通过联合索引找到name likelike ‘张%’ 的主键id(1、4),逐一进行回表扫描,去聚簇索引找到完整的行记录,server层再对数据根据age=10进行筛选。浪费了联合索引字段age
开启ICP后
set optimizer_switch='index_condition_pushdown=on';
explain select * from tuser where name like '张%' and age = 10;
using index condition pushdown存储引擎根据(name,age)联合索引,找到,由于联合索引中包含列,所以存储引擎直接再联合索引里按照age=10过滤。按照过滤后的数据再一一进行回表扫描。
索引失效的六种情况
创建表
CREATE TABLE `student` (
`id` int NOT NULL COMMENT 'id',
`name` varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT '姓名',
`age` int DEFAULT NULL COMMENT '年龄',
`birthday` datetime DEFAULT NULL COMMENT '生日',
PRIMARY KEY (`id`),
KEY `idx_name` (`name`) USING BTREE,
KEY `idx_name_age` (`name`,`age`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
插入数据
INSERT INTO `student` VALUES (1, '张三', 18, '2021-12-23 17:12:44');
INSERT INTO `student` VALUES (2, '李四', 20, '2021-12-22 17:12:48');
1.查询条件中有or,即使有部分条件带索引也会失效
explain SELECT * FROM `student` where id =1
发现命中主键索引(const表示主键索引),但当查询语句带or后
explain SELECT * FROM `student` where id =1 or birthday = "2021-12-23"
总结:如果使用了or关键字,那么它前面和后面的字段都要加索引,不然所有的索引都会失效
2like语句中的%出现在查询条件左边时索引失效
explain select * from student where name = "张三"
非模糊查询,此时命中name索引(ref表示普通索引),当使用模糊查询后
explain select * from student where name like "%三"
发现此时type=ALL,未命中索引,全表扫描
原因:一般索引是按字母或者拼音从小到大,从左到右排序,是有顺序的,我们无法从右边开始匹配。
3.字段类型不同
如果name是string字段的,那么需要加上""
explain select * from student where name = "张三"
上面的名字name索引,下面的没有命中索引
explain select * from student where name = 2222
4. 索引列上有计算、函数
explain select * from user where id + 1=2;
explain select * from user where SUBSTR(height,1,2)=17;
5.违背最左匹配原则
建立联合索引时,查询条件中没有包含给定联合索引字段最左边的索引字段,即字段name。
explain select * from student where age =18
7.mysql估计使用全表扫描要比使用索引快,则不使用索引
int(8)还是char(>8)类型上建索引
当二者都可以成为唯一索引时,由于int一般是8个字节,而char占用的大于8个字节,B+树在简历多级索引时,为了保证最高级索引在一个盘块中(一次io可以取出),所以每个数据越大单个节点能存储的数据就越少,从而树也就越深。
三层B+树索引可以存储多少条数据
innodb中每个页的大小是16K
第一层中至少两条索引,否则就没有第二层了,有两条索引说明第一条索引一定是满的
同时每个索引指向一个页,每页最多能存16/1=16个记录
慢查询原因和优化
1.没加索引
2.索引失效
3.limit深分页问题:offset过大
select id,name,balance from account where update_time> '2020-09-19' limit 100000,10;
花费时间0.742
select id,name,balance from account where update_time> '2020-09-19' limit 0,10;
花费时间0.006
因为前者的过程是:
- 通过普通二级索引树idx_update_time,过滤update_time条件,找到满足条件的记录ID。
- 通过ID,回到主键索引树,找到满足记录的行,然后取出展示的列(回表)
- 扫描满足条件的100010行,然后扔掉前100000行,返回。
解决方法是:通过子查询优化,把条件转移到主键索引树,避免回表,即通过子查询得到对应主键的起始位置,然后主查询中通过主键做筛选条件再对满足的数据行做分页
select id,name,balance from account where id >=(select id from account where update_time > 2020-09-09 limit 100000,1) limit 10;
Git的预备知识
首先设置用户名和邮箱
git config --global user.name "xkcc" # 名称
git config --global user.email "783133716@qq.com" # 邮箱
Git本地有三个工作区域:工作目录(Working Directory)、暂存区(Stage/index)、资源库(Repository或Git Directory),加上远程的git仓库(Remote Directory)一共分为四个工作区域
如下所示
Git使用
创建仓库的两种方法:
1.初始化项目(先进到对应文件夹)
git init
2.克隆远程仓库,即远程服务器上的
// git clone [url]
git clone https://gitee.com/wild_farmer/Digital_Stopwatch.git
查看指定文件状态
git status [filename]
查看所有文件状态(新建文件会提示:git add to track,add后会提示to be committed)
git status
提交
git add . // 提交所有到缓存区
git commit -m "new file hello.txt" // 提交到本地仓库,同时消息内容是“”里面的
有的文件我不想传上去,那么可以在主目录下新建.gitignore文件(使用自动生成即可)
Gitee绑定SSH公钥,以后提交时免登录
首先进入
C:\Users\盛建华.ssh 目录
在该目录下执行(这里的rsa是一种非对称加密算法)
ssh-keygen -t rsa
生成完成,有两个新文件
打开pub表示公钥,将里面内容复制到
确定即可,现在直接从仓库clone就可以了(注意必须是开源的代码),无需账号密码
git clone https://gitee.com/shengjh1999/gitstudy.git
使用码云创建自己的仓库
Redis数据库
与MySQL区别
1.不具有原子性,因为执行时有某个语句错误,其他正确的语句仍会执行,不会发生回滚。
2.单独的隔离操作:事务执行过程中不会被其他客户端发来的命令打断。(即单线程,那么也就不存在隔离级别的问题了)
Redis连接池
1.测试teamview
2.通过ab工具进行压力测试,设置请求数和同一时刻请求数
3.并发使用乐观锁解决,每个线程用watch查看数据,如果修改时数据有变化,则不做修改,重新等待
4.连接数据库很昂贵,因为socket通信时间很长,所以采用连接池,提前建立若干连接,并为连接池的类上锁,从而每个线程通过锁获取连接,因为获取锁的时间相较于过去连接的时间很短,这样可以避免连接的超时问题
对Spring的理解
AOP(Aspect-Oriented Programming,面向切面编程)
将与业务无关,却为业务模块所调用的逻辑或责任(事务处理、日志管理、权限控制)封装起来,减少系统的重复代码,降低耦合度,一般用于解决系统交叉业务的织入。
例如某个method有100个方法,现在要为这些方法创建日志,本来可能需要在这些方法内部的前后进行相关编码,而现在可以在这些方法调用的前后进行相关编码。
为什么可以写在前后呢?因为AOP是通过动态代理,代理对象调用目标对象的同名方法。
IOC(Inversion of Control,控制反转)
Spring IOC:以前由程序员自己控制对象创建,现在将创建对象的控制权交给Spring的IOC,使用时通过DI(依赖注入)@autowired自动注入来使用对象。
比如对象A需要操作数据库,我们需要自己编码来获得一个Connection对象,有了 spring我们就只需要告诉spring,A中需要一个Connection,至于这个Connection怎么构造,何时构造,A不需要知道。在系统运行时,spring会在适当的时候制造一个Connection,并注入到A当中,这样就完成了对各个对象之间关系的控制。
逻辑题
病狗问题
第一晚没鸣枪:说明所有人都不可能看到49只好狗,因为这样的话回去一定鸣枪,说明一定有病狗,病狗数量>1,假设病狗数量为1,必然有一个人回去鸣枪,所有人都知道了病狗数量>=2(因为那两个病狗主人看到了病狗就不会鸣枪了)
第二晚上没鸣枪:假设有两只病狗(>=2),病狗主人当晚只能看到一只病狗,那必然是自己的狗也病了,所以第二晚也会鸣枪,所有人都知道病狗数量>=3
第三晚鸣枪了:=3
8球称重
有8个球,其中一个轻一点,把这些球放在天平上称几次,能找出轻的球,写出方法?
试题答案
将8个球分成3、3、2三组,在天平两边分别各放3个称量,会出现两种情况:
(1)一种是如果天平平衡,轻的球就在剩下2个当中,再把这2个球分别放在天平两边进行第二次称量,轻的就在上翘的那边;
(2)另一种是如果天平不平衡,轻的球就在上翘的那边,再把3个当中的任意2个放在天平两边进行第二次称量,如果平衡剩下的那个就是轻的球;如果不平衡,轻的球就在上翘的那边
所以2次就能找出轻的那个.
答:把这些球放在天平上称2次,就能找出轻的球