7、为什么任何查询都不要使用SELECT *?
- 多出一些不用的列,这些列可能正好不在索引的范围之内(索引的好处不多说)select * 杜绝了索引覆盖的可能性,而索引覆盖又是速度极快,效率极高,业界极为推荐的查询方式。(索引覆盖)
- 数据库需要知道 * 等于什么 = 查数据字典会增大开销(记录数据库和应用程序元数据的目录)。
- 不需要的字段会增加数据传输的时间,即使 mysql 服务器和客户端是在同一台机器上,使用的协议还是 tcp,通信也是需要额外的时间。
- 大字段,例如很长的 varchar,blob,text。准确来说,长度超过 728 字节的时候,会把超出的数据放到另外一个地方,因此读取这条记录会增加一次 io 操作。(mysql innodb)
- 影响数据库自动重写优化SQL(类似 Java 中编译 class 时的编译器自动优化) 。(Oracle)
- select * 数据库需要解析更多的 对象,字段,权限,属性相关,在 SQL 语句复杂,硬解析较多的情况下,会对数据库造成沉重的负担。
- 额外的 io,内存和 cpu 的消耗,因为多取了不必要的列。
- 用 SELECT * 需谨慎,因为一旦列的个数或顺序更改,就有可能程序执行失败。
多线程
Java实现多线程有几种方式?
有三种方式:
- 继承Thread类,并重写run方法。
- 实现Runnable接口,并重写run方法。
- 实现Callable接口,并重写run方法,并使用FutureTask包装器。
线程的生命周期
1、新建状态(New):新创建了一个线程对象。
2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
- 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
- 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)
5、死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
start()方法和run()方法的区别?
- start()方法会使得该线程开始执行,java虚拟机会去调用该线程的run()方法。
- 通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,run方法运行结束,此线程随即终止。
- run()方法只是类的一个普通方法而已,如果直接调用run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到多线程的目的。
Runnable接口和Callable接口的区别?
- Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已。
- Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
- 这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable + Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。
volatile关键字
volatile基本介绍:volatile可以看成是synchronized的一种轻量级的实现,但volatile并不能完全代替synchronized,volatile有synchronized可见性的特性,但没有synchronized原子性的特性。可见性即用volatile关键字修饰的成员变量表明该变量不存在工作线程的副本,线程每次直接都从主内存中读取,每次读取的都是最新的值,这也就保证了变量对其他线程的可见性。另外,使用volatile还能确保变量不能被重排序,保证了有序性。
当一个变量定义为volatile之后,它将具备两种特性:
- ①保证此变量对所有线程的可见性:当一条线程修改了这个变量的值,新值对于其他线程可以说是可以立即得知的。Java内存模型规定了所有的变量都存储在主内存,每条线程还有自己的工作内存,线程的工作内存保存了该线程使用到的变量在主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读取主内存中的变量。
- ②禁止指令重排序优化:
volatile boolean isOK = false; //假设以下代码在线程A执行 A.init(); isOK=true; //假设以下代码在线程B执行 while(!isOK){ sleep(); } B.init();
A线程在初始化的时候,B线程处于睡眠状态,等待A线程完成初始化的时候才能够进行自己的初始化。这里的先后关系依赖于isOK这个变量。如果没有volatile修饰isOK这个变量,那么isOK的赋值就可能出现在A.init()之前(指令重排序,Java虚拟机的一种优化措施),此时A没有初始化,而B的初始化就破坏了它们之前形成的那种依赖关系,可能就会出错。
volatile使用场景:
如果正确使用volatile的话,必须依赖下以下种条件:
- 对变量的写操作不依赖当前变量的值。
- 该变量没有包含在其他变量的不变式中。
在以下两种情况下都必须使用volatile:
- 状态的改变。
- 读多写少的情况。
什么是线程安全?
如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
线程安全的级别:
- 1)不可变:像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用。
- 2)绝对线程安全:不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet。
- 3)相对线程安全:相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。
- 4)线程非安全:ArrayList、LinkedList、HashMap等都是线程非安全的类。
sleep方法和wait方法有什么区别?
- 原理不同:sleep()方法是Thread类的静态方法,是线程用来控制自身流程的,它会使此线程暂停执行一段时间,而把执行机会让给其他线程,等到计时时间一到,此线程会自动苏醒。而wait()方法是Object类的方法,用于线程间的通信,这个方法会使当前拥有该对象锁的进程等待,直到其他线程用调用notify()或notifyAll()时才苏醒过来,开发人员也可以给它指定一个时间使其自动醒来。
- 对锁的处理机制不同:由于sleep()方法的主要作用是让线程暂停一段时间,时间一到则自动恢复,不涉及线程间的通信,因此调用sleep()方法并不会释放锁。而wait()方法则不同,当调用wait()方法后,线程会释放掉它所占用的锁,从而使线程所在对象中的其他synchronized数据可被别的线程使用。
- 使用区域不同:wait()方法必须放在同步控制方法或者同步语句块中使用,而sleep方法则可以放在任何地方使用。
- sleep()方法必须捕获异常,而wait()、notify()、notifyAll()不需要捕获异常。在sleep的过程中,有可能被其他对象调用它的interrupt(),产生InterruptedException异常。
- 由于sleep不会释放锁标志,容易导致死锁问题的发生,一般情况下,不推荐使用sleep()方法,而推荐使用wait()方法。
写一个会导致死锁的程序。
public class MyThread{ private static Object lock1 = new Object(); private static Object lock2 = new Object(); public static void main(String[] args) { new Thread(()->{ synchronized (lock1){ System.out.println("thread1 get lock1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2){ System.out.println("thread1 get lock2"); } System.out.println("thread1 end"); } }).start(); new Thread(()->{ synchronized (lock2){ System.out.println("thread2 get lock2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock1){ System.out.println("thread2 get lock1"); } System.out.println("thread2 end"); } }).start(); } }
类加载过程
1、类加载过程:加载->链接(验证+准备+解析)->初始化(使用前的准备)->使用->卸载
具体过程如下:
1)加载:首先通过一个类的全限定名来获取此类的二进制字节流;其次将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;最后在java堆中生成一个代表这个类的Class对象,作为方法区这些数据的访问入口。总的来说就是查找并加载类的二进制数据。
2)链接:
验证:确保被加载类的正确性。
准备:为类的静态变量分配内存,并将其初始化为默认值。
解析:把类中的符号引用转换为直接引用。
- 符号引用即用字符串符号的形式来表示引用,其实被引用的类、方法或者变量还没有被加载到内存中。
- 直接引用则是有具体引用地址的指针,被引用的类、方法或者变量已经被加载到内存中。
直接引用可以是:
- 直接指向目标的指针。(个人理解为:指向对象,类变量和类方法的指针)
- 相对偏移量。(指向实例的变量,方法的指针)
- 一个间接定位到对象的句柄。
为什么要使用符号引用?
符号引用要转换成直接引用才有效,这也说明直接引用的效率要比符号引用高。那为什么要用符号引用呢?这是因为类加载之前,javac会将源代码编译成.class文件,这个时候javac是不知道被编译的类中所引用的类、方法或者变量他们的引用地址在哪里,所以只能用符号引用来表示,当然,符号引用是要遵循java虚拟机规范的。
还有一种情况需要用符号引用,就例如前文举得变量的符号引用的例子,是为了逻辑清晰和代码的可读性。
3)为类的静态变量赋予正确的初始值。