实现Runnable接口,继承thread类,使用线程池
1.进程
定义:进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
进程的三种基本状态:
1.就绪状态:除CPU外已分配所有资源,等待获得处理机执行2.执行状态:获得处理机,程序正在执行3.阻塞状态:因等待而无法执行,放弃处理机,处于等待状态。(等待I/O口完成,申请缓冲区不满足,等待信号等)
2.线程:
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
3.进程和线程的区别:
4.隶属关系:线程属于进程,进程退出时结束所有线程,线程占用资源少于进程。
多个执行:
一般来说因为同一进程下的线程间是共享内存资源,因此共享内存也成为理所应当的线程通信的方式。共享内存常用的数据结构有LRU和FIFO。
具体对JAVA来说,有如下的工具可以实现线程通信:
A.Wait/notify机制(JAVA特有);
方法wait()的作用是使当前执行代码的线程进行等待,wait()方法只能在同步方法中或同步块中调用,wait()方法执行后,当前线程释放锁,线程与其他线程竞争重新获取锁。方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其他线程,对其发出通知notify,并使进入就绪态,等待获取锁。如果有多个线程等待,则有线程规划器随机挑选出一个呈wait状态的线程。在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块中。
B.共享内存(下面是JAVA实现):
如果每个线程执行的代码相同,可以使用同一个Runnable对象,这个Runnable对象中有那个共享数据,例如,卖票系统就可以这么做。
如果每个线程执行的代码不同,这时候需要用不同的Runnable对象,例如,设计4个线程。其中两个线程每次对j增加1,另外两个线程对j每次减1,银行存取款
6.进程的通讯方式(本质上是不同进程的线程间通信的方式):5种
数据只能在一个方向上流动用于具有亲缘关系的进程之间的通信特殊的文件,存在于内存
可以在无关的进程之间交换数据文件形式存在于文件系统
write_fifo的作用类似于客户端,可以打开多个客户端向一个服务器发送请求信息,read_fifo类似于服务器,它适时监控着FIFO的读端,当有数据时,读出并进行处理,但是有一个关键的问题是,每一个客户端必须预先知道服务器提供的FIFO接口
消息的链表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。(消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。)
一个计数器,实现进程间的互斥与同步,而不是用于存储进程间通信数据。常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。主要作为进程间以及同一进程内不同线程之间的同步手段
两个或多个进程共享一个给定的存储区共享内存是最快的一种IPC,因为进程是直接对内存进行存取。因为多个进程可以同时操作,所以需要进行同步。信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问
1.管道:速度慢,容量有限,只有父子进程能通讯。2.FIFO文件(即命名管道):任何进程间都能通讯,但速度慢。3.消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题;信号传递信息较管道多。4.信号量:不能传递复杂消息,只能用来同步。5.共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存。
线程的资源有不少,但应该包含CPU资源和锁资源这两类。
wait():让出CPU资源和锁资源。
1.这两个方法来自不同的类,wait是Object类中的方法,sleep是Thread类中的方法。
2.sleep方法没有释放锁(但释放了CPU),而wait方法释放了锁(也释放了CPU)。
3.wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。
4.sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常。
Java中的线程的生命周期大体可分为5种状态。
1.新建(NEW):新创建了一个线程对象。
5.死亡态(DEAD):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生
死锁:两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。
注意这里产生死锁的可以是资源竞争,也有可能是彼此的通信等待。
资源竞争:进程A,B必须同时持有资源1和2才能执行。某一时刻进程A持有资源1并尝试抢占资源2,进程B持有资源2并尝试抢占资源1。这时就造成了死锁。
通信等待:进程A需要收到B的信号才能继续往下执行(包括向其他进程发信号),进程B需要收到进程A的信号才能继续往下执行。这个时候进程A和B就会因为相互等待对方的信号而死锁。
死锁产生的四个必要条件
避免死锁的方法
首先互斥条件一般是无法避免,否则也不会出线多线程的问题了。因此可以从其他三个条件出发,打破其中之一就能避免死锁。
1.破坏“请求和保持”条件
当某个线程申请两个及以上的资源时,让它要么能一次性申请成功所有资源,要么就不申请任何资源(释放申请的部分资源)。
2.破坏“不可抢占”条件
允许进程进行资源抢占。如果线程在持有部分资源的时候这部分资源发生了抢占,则允许这部分资源被抢占。更好的方法是允许操作系统抢占资源,让优先级大的线程可以抢占其他线程占有但未使用的资源。
3.破坏“循环等待”条件
a.将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序提出。
当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。
如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。
如果一个线程(比如线程3)需要一些锁,那么它必须按照确定的顺序获取锁。它只有获得了从顺序上排在前面的锁之后,才能获取后面的锁。
例如,线程2和线程3只有在获取了锁A之后才能尝试获取锁C(获取锁A是获取锁C的必要条件)。因为线程1已经拥有了锁A,所以线程2和3需要一直等到锁A被释放。然后在它们尝试对B或C加锁之前,必须成功地对A加了锁。
b.为线程设置加锁时限
4.死锁检测及处理
死锁检测的方法
检测到死锁了怎么办:
2.一个更好的方案是随机选取若干个线程而不是全部线程,让这些线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。
当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。在临界区中使用适当的同步就可以避免竞态条件。
继承Thread,实现Runnable,实现Callable(使用线程池,ExecutorService)
该类的几个核心的参数有:
corePoolSize:表示常驻核心线程数。如果等于0,则任务执行完之后,没有任何请求进入时销毁线程池的线程;如果大于0,即使本地任务执行完毕,核心线程也不会被销毁。这个值的设置非常关键,设置过大会浪费资源,设置过小会导致线程频繁地创建或销毁。(初始化的时候线程池中并不是直接创建corePoolSize个线程,而是根据需要来创建。只不过在事务完成后向线程池交还线程的时候,如果线程池中线程的数量<=coolPoolSize,则空闲的线程不会被消除,而是常驻在线程池中等待被取用。)
maximumPoolSize:表示线程池能够容纳同时执行的最大线程数。必须大于或等于1。如果待执行的线程数大于此值,需要借助workQueue参数的帮助,缓存在队列中。如果maximumPoolSize与corePoolSize相等,即是固定大小线程池。
workQueue:表示缓存队列。当请求的线程数大于corePoolSize时,线程进入BlockingQee阻塞队列。后续示例代码中使用的LinkedBlockingQueue是单向链表,使用锁来控制入队和出队的原子性,两个锁分别控制元素的添加和获取,是一个生产消费模型队列。
threadFactory表示线程工厂。它用来生产一组相同任务的线程。线程池的命名是通过给这个ctoy增加组名前级来实现的。在虚拟机栈分析时,就可以知道线程任务是由哪个线程工厂产生的。
handler:表示执行拒绝策路的对象。当workQueue参数的任务缓有区到达上限后,并且活动线程数大于maximumPoolSize的时候,线程池通过该策略处理请求,这是一种简单的限流保护。
handler:表示当拒绝处理任务时的策略,有以下四种取值:
1、如果线程池中的线程数量少于corePoolSize,即使线程池中有空闲线程,也会创建一个新的线程来执行新添加的任务;
2、如果线程池中的线程数量大于等于corePoolSize,但缓冲队列workQueue未满,则将新添加的任务放到workQueue中,按照FIFO的原则依次等待执行(线程池中有线程空闲出来后依次将缓冲队列中的任务交付给空闲的线程执行);
3、如果线程池中的线程数量大于等于corePoolSize,且缓冲队列workQueue已满,但线程池中的线程数量小于maximumPoolSize,则会创建新的线程来处理被添加的任务;
4、如果线程池中的线程数量等于了maximumPoolSize,触发拒绝策略。
总结起来,也即是说,当有新的任务要处理时,先看线程池中的线程数量是否大于corePoolSize,再看缓冲队列workQueue是否满,最后看线程池中的线程数量是否大于maximumPoolSize。
1、直接提交。缓冲队列采用SynchronousQueue,它将任务直接交给线程处理而不保持它们。如果不存在可用于立即运行任务的线程(即线程池中的线程都在工作),则试图把任务加入缓冲队列将会失败,因此会构造一个新的线程来处理新添加的任务,并将其加入到线程池中。直接提交通常要求无界maximumPoolSizes(Integer.MAX_VALUE)以避免拒绝新提交的任务。newCachedThreadPool采用的便是这种策略。
2、无界队列。使用无界队列(典型的便是采用预定义容量的LinkedBlockingQueue,理论上是该缓冲队列可以对无限多的任务排队)将导致在所有corePoolSize线程都工作的情况下将新任务加入到缓冲队列中。这样,创建的线程就不会超过corePoolSize,也因此,maximumPoolSize的值也就无效了。当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列。newFixedThreadPool采用的便是这种策略。
3、有界队列。当使用有限的maximumPoolSizes时,有界队列(一般缓冲队列使用ArrayBlockingQueue,并制定队列的最大长度)有助于防止资源耗尽,但是可能较难调整和控制,队列大小和最大池大小需要相互折衷,需要设定合理的参数。
publicinterfaceRunnable{publicabstractvoidrun();}run()方法返回值为void类型,所以在执行完任务之后无法返回任何结果
publicinterfaceCallable
3.如何使用Callable
第一个方法:submit提交一个实现Callable接口的任务,并且返回封装了异步计算结果的Future。
第二个方法:submit提交一个实现Runnable接口的任务,并且指定了在调用Future的get方法时返回的result对象。
第三个方法:submit提交一个实现Runnable接口的任务,并且返回封装了异步计算结果的Future。
因此我们只要创建好我们的线程对象(实现Callable接口或者Runnable接口),然后通过上面3个方法提交给线程池去执行即可。
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。
Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,get方法会阻塞直到任务返回结果。
Future接口定义了下面5种方法:
也就是说Future提供了三种功能:
1)判断任务是否完成;
2)能够中断任务;
3)能够获取任务执行结果。
因为Future只是一个接口,所以是无法直接用来创建对象使用的,FutureTask是Future接口的一个唯一实现类。
Lock是JUC包的顶层接口,它的实现逻辑并未用到synchronized,而是利用了volatile的可见性和CAS
ReentrantLock对于Lock接口的实现主要依赖了Sync,而Sync继承了AbstractQueuedSynchronizer(AQS),它是JUC包实现同步的基础工具。在AQS中,定义了一个volatileintstate变量作为共享资源,如果线程获取资源失败,则进入同步FIFO队列中等待;如果成功获取资源就执行临界区代码。执行完释放资源时,会通知同步队列中的等待线程来获取资源后出队并执行。
AQS是抽象类,内置自旋锁实现的同步队列,封装入队和出队的操作,提供独占、共享、中断等特性的方法。AQS的子类可以定义不同的资源实现不同性质的方法。
当Semphore的permits定义为1时,就是互斥锁,当permits>1就是共享锁。总之,ReentrantLock,CountDownLatch,CyclicBarrier,Semaphore等工具都是基于AQS的不同配置来实现的。
锁也可以确保变量的可见性,但是实现方式和volatile略有不同。线程在得到锁时读入副本,释放时写回内存。volatile解决的是多线程共享变量的可见性问题,类似于synchronized,但不具备synchronized的互斥性。
因为所有的操作同需要同步给内存,因此volatile一定使线程的执行速度变慢
volatile变量与锁的区别:
由于volatile仅仅保证对对单个volatile变量的读、写具有原子性(复合操作不保证原子性如volatiei,i++),而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。在功能上,锁比volatile更强大,在可伸缩性上和执行性能上,volatile更有优势。
信号量同步是指在不同的线程之间,通过传递同步信号量来协调线程执行的先后次序。
1、CountDownLatchend=newCountDownLatch(N);//构造对象时候需要传入参数N
2、end.await()能够阻塞线程直到调用N次end.countDown()方法才释放线程
3、end.countDown()可以在多个线程中调用计算调用次数是所有线程调用次数的总和
还有其他同步方式,如CyclicBarrier是基于同步到达某个点的信号量触发机制。CyclicBarrier从命名上即可知道它是一个可以循环使用(Cyclic)的屏障式(Barrier)多线程协作方式。采用这种方式进行刚才的安检服务,就是3个人同时进去,只有3个人都完成安检,才会放下一批进来。这是一种非常低效的安检方式。但在某种场景下就是非常正确的方式,假设在机场排队打车时,现场工作人员统一指挥,每次放3辆车进来,坐满后开走,再放下一批车和人进来。通过CyclicBarrier的reset)来释放线程资源。
ThreadLocal是用来维护线程中的变量不被其他线程干扰而出现的一个结构,内部包含一个ThreadLocalMap类,该类为Thread类的一个局部变量,该Map存储的key为ThreadLocal对象所在线程对象,value为我们要存储的对象,这样一来,在不同线程中,持有的其实都是当前线程的变量副本,与其他线程完全隔离,以此来保证线程执行过程中不受其他线程的影响。利用ThreadLocal可以实现让所有的线程持有初始值相同的一个变量副本,但在初始化以后每个线程都只能操作自己的那个变量副本,不同线程之间的变量副本互不干扰。
ThreadLocal中的键值对中的键是一个弱引用,那么在内存回收的时候,这个键很可能会被回收掉,然后键没了,就无法找到value的值,造成了内存泄漏;
我们看下set方法的实现:
可以看到,先通过Thread.currentThread()方法获取到了当前线程,然后如果取不到map对象,就会创建,下面看下create方法
很简单的方法,就是新建一个ThreadLocal的Map,这个map以ThreadLocal自身为key,以我们要设值的对象为value,创建出来map之后,将对象赋值到线程的局部变量去。看到这里,就知道ThreadLocal主要目的就是将变量设值到当前的线程上,以此来保证线程安全。那么下面看下get方法:
可以看出来,get方法就是拿的当前线程的局部变量threadLocals,然后从中取出map中存储的对象,这样每个线程中获取的一定是自己线程中存储的对象了。由上述分析可知,使用ThreadLocal是可以保证线程安全的。ThreadLocal实际上是为解决多线程程序的并发问题提供了一种新的思路(区别与sychronized关键字)。ThreadLocal这个类提供线程本地的变量。这些变量与一般正常的变量不同,它们在每个线程中都是独立的。ThreadLocal实例最典型的运用就是在类的私有静态变量中定义,并与线程关联。
对象在堆上创建之后所持有的用基实是一种变量类型,引用之间可以通过赋值构成一条引用链。从GCRoots开始遍历,判断引用是否可达。引用的可达性是判断能否被垃圾回收的基本条件。JMM会根据此自动管理内存的分配与回收,不需要开发工程师干预。但在某些场景下,即使引用可达,也希望能够根据语义的强弱进行有选择的回收,以保证系统的正常运行。根据引用类型语义的强弱来决定垃圾回收的阶段,我们可以把引用分为强引用、软引用、弱引用和虚引用四类。后三类引用,本质上是可以让开发工程师通过代码方式来决定对象的垃圾回收时机。
首先的明白Java中锁的机制synchronized
在修饰代码块的时候需要一个reference对象作为锁的对象.在修饰方法的时候默认是当前对象作为锁的对象.在修饰类时候默认是当前类的Class对象作为锁的对象.
方法锁(synchronized修饰方法时)
对象锁(synchronized修饰方法或代码块)
当一个对象中有synchronizedmethod或synchronizedblock的时候调用此对象的同步方法或进入其同步区域时,就必须先获得对象锁。如果此对象的对象锁已被其他调用者占用,则需要等待此锁被释放。(方法锁也是对象锁)
java的所有对象都含有1个互斥锁,这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁,那么当前线程会等待;synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的1个好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。
类锁(synchronized修饰静态的方法或代码块)
由于一个class不论被实例化多少次,其中的静态方法和静态变量在内存中都只有一份。所以,一旦一个静态的方法被申明为synchronized。此类所有的实例化对象在调用此方法,共用同一把锁,我们称之为类锁。
对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步。
总结:
1.类锁是对静态方法使用synchronized关键字后,无论是多线程访问单个对象还是多个对象的sychronized块,都是同步的。
2.对象锁是实例方法使用synchronized关键字后,如果是多个线程访问同个对象的sychronized块,才是同步的,但是访问不同对象的话就是不同步的。
3.类锁和对象锁是两种不同的锁,可以同时使用,但是注意类锁不要嵌套使用,这样子容易发生死锁。
类锁和对象锁区别
同步是对同一把锁而言的,同步这个概念是在多个线程争夺同一把锁的时候才能实现的,如果多个线程争夺不同的锁,那多个线程是不能同步的
synchronize修饰方法的锁对象只能是this当前对象
synchronize修饰代码块可以修改锁对象(可以是this对象,也可以自行指定)
使用this对象锁的好处:
这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁,那么当前线程会等待;synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的1个好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。
坏处:
锁的粒度大,代码效率降低;
synchronized的缺陷:当某个线程进入同步方法获得对象锁,那么其他线程访问这个对象的其余的同步方法时,也必须等待或者阻塞,这对高并发的系统是致命的,这很容易导致系统的崩溃。如果某个线程在同步方法里面发生了死循环,那么它就永远不会释放这个对象锁,那么其他线程就要永远的等待。这是一个致命的问题。
如果这时用synchronized来修饰代码块:synchronized(so){so.testsy();},那么这个方法加锁的对象是so这个对象,跟执行这行代码的对象没有关系,当一个线程执行这个方法时,这对其他同步方法时没有影响的,因为他们持有的锁都完全不一样。
Java1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。锁是按顺序膨胀的,且锁的膨胀是不可逆的。
偏向锁:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程A访问加了同步锁的代码块时,会在对象头中存储当前线程的id,后续这个线程进入和退出这段加了同步锁的代码块时,不需要再次加锁和释放锁。
轻量级锁:在偏向锁情况下,如果线程B也访问了同步代码块,比较对象头的线程id不一样,会升级为轻量级锁,并且通过自旋的方式来获取轻量级锁。
重量级锁:如果线程A和线程B同时访问同步代码块,则轻量级锁会升级为重量级锁,线程A获取到重量级锁的情况下,线程B只能入队等待,进入BLOCK状态。
悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
CAS:
CompareandSwap(CAS)
CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS是一种非阻塞式的同步方式。
CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。(在CAS的一些特殊情况下将仅返回CAS是否成功,而不提取当前值。)CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查+数据更新的原理是一样的。
两种锁的使用场景从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
重入锁是可重复获得资源的锁,已经获得锁的线程可以对当前的资源重入加锁而不会引起阻塞;不可重入锁是不可重复获得资源的锁,当已经获得锁的线程对当前资源再次加锁时,会把自己阻塞。这样做的好处是可以防止死锁。
可重入性:
从名字上理解,ReenTrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入的,两者关于这个的区别不大。同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
分为四个层次:应用层、传输层、网络互连层和主机到网络层。常见的协议如下:
基于UDP的常见的还有DNS、、RIP(路由选择协议)、DHCP动态主机设置协议
在TCP/IP参考模型中,去掉了OSI参考模型中的会话层和表示层(这两层的功能被合并到应用层实现)。同时将OSI参考模型中的数据链路层和物理层合并为主机到网络层。
物理层(PhysicalLayer)数据链路层(DatalinkLayer)网络层(NetworkLayer)传输层(TransportLayer)会话层(SessionLayer)表示层(PresentationLayer)应用层(ApplicationLayer)
TCP会话通过三次握手来初始化。三次握手的目标是使数据段的发送和接收同步。同时也向其他主机表明其一次可接收的数据量(窗口大小),并建立逻辑连接。这三次握手的过程可以简述如下:
●源主机发送一个同步标志位(SYN)置1的TCP数据段,此段中同时包含一个初始值随机的客户端序列号,ClientISN;
●目标主机发回确认数据段,此段中的同步标志位(SYN)同样被置1,且确认标志位(ACK)也置1,同时将ClientISN+1,此外,此段中还包含一个初始值随机的服务端序列号ServerISN;
●源主机收到目标主机确认数据段后,再回送一个数据段,包含ACK=1,serverISN+1;
Source--->Destination:SYN=1,Seq=ClientISN
Destination--->Source:SYN=1,ACK=1,ClientISN+1,Seq=ServerISN
Source--->Destination:ACK=1,ServerISN+1
至此为止,TCP会话的三次握手完成。接下来,源主机和目标主机可以互相收发数据。整个过程可用下图表示。
起初A和B处于ESTABLISHED状态
A发出连接释放报文段(Fin=1,Seq=u;)并处于FIN-WAIT-1状态,A不再有数据发送,但仍然可以接受数据;
B发出确认报文段(ACK=1,Seq=v,ack=u+1)且进入CLOSE-WAIT状态,并继续发送剩下的数据,A收到确认后,进入FIN-WAIT-2状态,等待B的连接释放报文段;
B没有要向A发出的数据,B发出连接释放报文段(Fin=1,ACK=1,Seq=w,ack=u+1)且进入LAST-ACK状态;
A发出确认报文段(ACK=1,Seq=w,ack=u+1)且进入TIME-WAIT状态,B收到确认报文段后进入CLOSED状态;
A(FIN-WAIT-1)--->B(ESTABLISHED):Fin=1,Seq=u;
B(CLOSE-WAIT)--->A:ACK=1,Seq=v,ack=u+1(继续发送剩余数据);
--->A(FIN-WAIT-2):A收到上一步数据,A等待B的Fin;
B(LAST-ACK)--->A:Fin=1,ACK=1,Seq=w,ack=u+1;
A(TIME-WAIT)--->B:收到上一步数据,发送ACK=1,Seq=w,ack=u+1;
--->B(CLOSED):收到上一步数据,ClOSED.
A(ClOSED):等待2MSL,进入CLOSED.
TCP的连接的拆除需要发送四个包,因此称为四次挥手(four-wayhandshake)。客户端或服务器均可主动发起挥手动作,在socket编程中,任何一方执行close()操作即可产生挥手操作。
两个原因:信息对等和防止超时
信息对等:假设A对B发起连接请求,在第二次握手时,A机器能够确认自己的发报和收报能力正常并且B的发报收报能力正常,而B机器只能确定自己的收报能力和对方的发报能力正常,不能确定对方的收报能力和自己的发报能力是否正常,这些只能通过第三次握手确认;
防止超时为了防止已失效的连接请求报文段突然又传送到了服务端,浪费服务端资源。
TCP是全双工模式,当client发出FIN报文段时,只是表示client已经没有数据要发送了,client告诉server,它的数据已经全部发送完毕了;但是,这个时候client还是可以接受来server的数据;当server返回ACK报文段时,表示它已经知道client没有数据发送了,但是server还是可以发送数据到client的;当server也发送了FIN报文段时,这个时候就表示server也没有数据要发送了,就会告诉client,我也没有数据要发送了,如果收到client确认报文段,之后彼此就会愉快的中断这次TCP连接。
MSL最长报文段寿命MaximumSegmentLifetime,MSL=2
两个理由:
1)保证A发送的最后一个ACK报文段能够到达B。
2)防止“已失效的连接请求报文段”出现在本连接中。
TCP的特点是连序控点流:有连接、有序、流量控制、点对点、字节流
面向字节流和面向报文:
用UDP传输100个字节的数据:
面向数据报如果发送端调用一次sendto,发送100个字节,那么接收端也必须调用对应的一次recvfrom,接收100个字节;而不能循环调用10次recvfrom,每次接收10个字节;
面向字节流由于缓冲区的存在,TCP程序的读和写不需要一一匹配,例如:写100个字节数据时,可以调用一次write写100个字节,也可以调用100次write,每次写一个字节;读100个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次read100个字节,也可以一次read一个字节,重复100次;
应用层交给UDP多长的报文,UDP原样发送,既不会拆分,也不会合并;TCP有一个缓冲,当应用程序传送的数据块太长,TCP就可以把它划分短一些再传送。如果应用程序一次只发送一个字节,TCP也可以等待积累有足够多的字节后再构成报文段发送出去。
以下应用一般或必须用udp实现
UDP它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。传输层无法保证数据的可靠传输,只能通过应用层来实现了。实现的方式可以参照tcp可靠性传输的方式,只是实现不在传输层,实现转移到了应用层。
实现确认机制、重传机制、窗口确认机制。
必须实现如下功能:
发送:包的分片、包确认、包的重发
接收:包的调序、包的序号确认
目前有如下开源程序利用udp实现了可靠的数据传输。分别为RUDP、RTP、UDT。
基于UDP的数据传输协议(UDP-basedDataTransferProtocol,简称UDT)是一种互联网数据传输协议
根据HTTP标准,HTTP请求可以使用多种请求方法。
HTTP1.0定义了三种请求方法:GET,POST和HEAD方法。
HTTP1.1新增了五种请求方法:OPTIONS,PUT,DELETE,TRACE和CONNECT方法。
“标准答案”:
浏览器行为:
GET和POST本质上没有区别!
GET和POST是HTTP协议中的两种发送请求的方法。
HTTP是是基于TCP/IP的关于数据如何在万维网中如何通信的协议。
HTTP的底层是TCP/IP。所以GET和POST的底层也是TCP/IP,也就是说,GET/POST都是TCP链接。GET和POST能做的事情是一样一样的。你要给GET加上requestbody,给POST带上url参数,技术上是完全行的通的。
GET和POST还有一个重大区别,简单的说:
GET产生一个TCP数据包;POST产生两个TCP数据包。
而对于POST,浏览器先发送header,服务器响应100continue,浏览器再发送data,服务器响应200ok(返回数据)。
但并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。
尽管HTTPS并非绝对安全,掌握根证书的机构、掌握加密算法的组织同样可以进行中间人形式的攻击,但HTTPS仍是现行架构下最安全的解决方案,主要有以下几个好处:
(1)使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器;
(3)HTTPS是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本。
HTTPS的缺点(效率低)
虽然说HTTPS有很大的优势,但其相对来说,还是存在不足之处的:
(2)HTTPS连接缓存不如HTTP高效,会增加数据开销和功耗,甚至已有的安全措施也会因此而受到影响;
cookie和session的区别:
①存在的位置:
cookie存在于客户端,临时文件夹中;session存在于服务器的内存中,一个session域对象为一个用户浏览器服务
②安全性cookie是以明文的方式存放在客户端的,安全性低,可以通过一个加密算法进行加密后存放;session存放于服务器的内存中,所以安全性好
③网络传输量cookie会传递消息给服务器;session本身存放于服务器,不会有传送流量
④生命周期(以20分钟为例)cookie的生命周期是累计的,从创建时,就开始计时,20分钟后,cookie生命周期结束;session的生命周期是间隔的,从创建时,开始计时如在20分钟,没有访问session,那么session生命周期被销毁。但是,如果在20分钟内(如在第19分钟时)访问过session,那么,将重新计算session的生命周期。关机会造成session生命周期的结束,但是对cookie没有影响
cookie保存在客户端;
可以设置生命时长;
cookie有个属性是路径,但该路径指的是服务端路径;
cookie的形式是一个键值对,如uid:lxj;
session一般配合cookie来保存会话信息,但在不允许cookie的情况下,也可以使用url重写的形式传递信息
此部分知识不完全,需要进一步理解
防御方案:采用认证方式连接
100——客户必须继续发出请求
101——客户要求服务器根据请求转换HTTP协议版本
200("OK")一般来说,这是客户端希望看到的响应代码。它表示服务器成功执行了客户端所请求的动作
3XX系列响应代码表明:客户端需要做些额外工作才能得到所需要的资源
301redirect:301代表永久性转移(PermanentlyMoved)
302redirect:302代表暂时性转移(TemporarilyMoved)
404——没有发现文件、查询或URl
这些响应代码表明服务器端出现错误。一般来说,这些代码意味着服务器处于不能执行客户端请求的状态,此时客户端应稍后重试。
500("InternalServerError")
①是请求方法,GET和POST是最常见的HTTP方法,除此以外还包括DELETE、HEAD、OPTIONS、PUT、TRACE。②为请求对应的URL地址,它和报文头的Host属性组成完整的请求URL,③是协议名称及版本号。④是HTTP的报文头,报文头包含若干个属性,格式为“属性名:属性值”,服务端据此获取客户端的信息。⑤是报文体,它将一个页面表单中的组件值通过param1=value1¶m2=value2的键值对形式编码成一个格式化串,它承载多个请求参数的数据。不但报文体可以传递请求参数,请求URL也可以通过类似于“/chapter15/user.htmlparam1=value1¶m2=value2”的方式传递请求参数。
Accept:客户端接受什么类型的响应,常见的如Accept:text/plain,Accept属性的值可以为一个或多个MIME类型的值。
Cookie:客户端的Cookie就是通过这个报文头属性传给服务端
Referer表示这个请求是从哪个URL过来的
Accept-Language:接受的语言类型
User-Agent:通过何种代理(浏览器)访问
Content-type,Content-Length等
Connection:
Connection:keep-alive当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接
Connection:close代表一个Request完成后,客户端和服务器之间用于传输HTTP数据的TCP连接会关闭,当客户端再次发送Request,需要重新建立TCP连接。
响应状态码和请求报文相比,响应报文多了一个“响应状态码”,它以“清晰明确”的语言告诉客户端本次请求的处理结果。HTTP的响应状态码由5段组成:
查本地缓存--查hosts文件--查ISP服务商的DNS缓存--从根域名开始递归搜索返回
浏览器利用ip直接网站主机通信,浏览器发出TCP连接请求,主机返回TCP应答报文,浏览器收到应答报文发现ACK标志位为1,表示连接请求确认,浏览器返回TCP()确认报文,主机收到确认报文,三次握手,TCP连接建立完成。
返回状态码200OK,表示服务器可以响应请求,返回报文,由于在报头中Content-type为“text/html”,浏览器以HTML形式呈现,而不是下载文件。但是对于大型网站存在多个主机站点,往往不会直接返回请求页面,而是重定向。返回的状态码就不是200OK,而是301,302以3开头的重定向吗。浏览器在获取了重定向响应后,在响应报文中Location项找到重定向地址,浏览器重新第一步访问即可。
文件处理
监控CPU-u
监控内存-r
网络工具
其他
以杀掉tomcat为例子
ps-ef|greptomcat|grep-v|awk-F'{print$2}'|xargskill-9
等同于
kill-9`ps-ef|grep'tomcat'|awk'{print$2}'`
xargs接收管道前面传过来的字符;
awk'{print$2}'的意思是选取并输出第二列的数据
在使用kill-9前,应该先使用kill-15,给目标进程一个清理善后工作的机会。如果没有,可能会留下一些不完整的文件或状态,从而影响服务的再次启动。
首先:cat-ntest.log|grep"keyword"得到关键日志的行号
然后,得到"keyword"关键字所在的行号是102行.此时如果想查看这个关键字前10行和后10行的日志:
cat-ntest.log|tail-n+92|head-n20
tail-ftest.log查看日志的尾部,并刷新显示日志变动。此方法适合在调试程序的时候查看日志,日志变动会实时刷新显示到终端。
待补充
线程是调度的基本单位,进程是资源分配的基本单位。。
守护进程是生存期长的一种进程。它们独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。他们常常在系统引导装入时启动,在系统关闭时终止。linux系统有很多守护进程,大多数服务器都是用守护进程实现的。linux上的守护进程类似于windows上的服务。
父进程在调用fork接口之后和子进程已经可以独立开,之后父进程和子进程就以未知的顺序向下执行(异步过程)。所以父进程和子进程都有可能先执行完。当父进程先结束,子进程此时就会变成孤儿进程,不过这种情况问题不大,孤儿进程会自动向上被init进程收养,init进程完成对状态收集工作。而且这种过继的方式也是守护进程能够实现的因素。如果子进程先结束,父进程并未调用wait或者waitpid获取进程状态信息,那么子进程描述符就会一直保存在系统中,这种进程称为僵尸进程。
fetch和merge和pull的区别pull相当于gitfetch和gitmerge,即更新远程仓库的代码到本地仓库,然后将内容合并到当前分支。gitfetch:相当于是从远程获取最新版本到本地,不会自动mergegitmerge:将内容合并到当前分支gitpull:相当于是从远程获取最新版本并merge到本地
常用命令gitshow#显示某次提交的内容gitshow$idgitadd
而如果超过了从–128到127之间的值,被装箱后的Integer对象并不会被重用,即相当于每次装箱时都新建一个Integer对象;以上的现象是由于使用了自动装箱所引起的,如果你没有使用自动装箱,而是跟一般类一样,用new来进行实例化,就会每次new就都一个新的对象。
面向过程
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。缺点:没有面向对象易维护、易复用、易扩展
面向对象
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护缺点:性能比面向过程低
根加载器、扩展类加载器、系统类加载器
第三层是Application加载器,主要加载用户定义的CLASSPATH路径下的类。
低层次的当前类加载器,不能覆盖更高层次类加载器已经加载的类。如果低层次的类加载器想加载一个未知类,要非常礼貌地向上逐级询问:“请问,这个类已经加载了吗?”被询问的高层次类加载器会自问两个问题:第一,我是否已加载过此类?第二,如果没有,是否可以加载此类?只有当所有高层次类加载器在两个问题上的答案均为“否”时,才可以让当前类加载器加载这个未知类。如图4-6所示,左侧绿色箭头向上逐级询问是否已加载此类,直至BootstrapClassLoader,然后向下逐级尝试是否能够加载此类,如果都加载不了,则通知发起加载请求的当前类加载器,准予加载。
在JVM中,类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化5个阶段。而解析阶段即是虚拟机将常量池内的符号引用替换为直接引用的过程。
验证:确保类加载的正确性。
准备:为类的静态变量分配内存,将其初始化为默认值。
解析:把类中的符号引用转化为直接引用。
3.初始化:为类的静态变量赋予正确的初始值,上述的准备阶段为静态变量赋予的是虚拟机默认的初始值,此处赋予的才是程序编写者为变量分配的真正的初始值
现在java程序的执行就可以分为
类成员初始化顺序总结:先静态,后父类普通构造,再子类普通构造,同级看书写顺序
第一种:通过Object类的getClass方法
Classcla=foo.getClass();
第二种:通过对象实例方法获取对象
Classcla=foo.class;
第三种:通过Class.forName方式
Classcla=Class.forName("xx.xx.Foo");
hashCode方法的主要作用是为了配合基于散列的集合一起正常运行,这样的散列集合包括HashSet、HashMap以及HashTable。当向集合中插入对象时,如何判别在集合中是否已经存在该对象了?(注意:集合中不允许重复的元素存在)也许大多数人都会想到调用equals方法来逐个进行比较,这个方法确实可行。但是如果集合中已经存在一万条数据或者更多的数据,如果采用equals方法去逐一比较,效率必然是一个问题。此时hashCode方法的作用就体现出来了,当集合要添加新的对象时,先调用这个对象的hashCode方法,得到对应的hashcode值,实际上在HashMap的具体实现中会用一个table保存已经存进去的对象的hashcode值,如果table中没有该hashcode值,它就可以直接存进去,不用再进行任何比较了;如果存在该hashcode值,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址,所以这里存在一个冲突解决的问题,这样一来实际调用equals方法的次数就大大降低了。
在每个类中,在重写equals方法的时侯,一定要重写hashcode方法。如果不这样做,就违反了hashCode的通用约定,这会阻止它在HashMap和HashSet这样的集合中正常工作。
数组加链表的形式
在JAVA8中,改进为,当链表长度达到8及以上,转为红黑树来实现;红黑树的数据数量将为6的时候又会自动转为链表。
(因为长度为6的链表平均查询次数是3,查找树的话,3次查询能查到8个数据)
第一步,我们查看的是头节点Entry6,Entry6的Key是banana,显然不是我们要找的结果。第二步,我们查看的是Next节点Entry1,Entry1的Key是apple,正是我们要找的结果。之所以把Entry放在头节点,是因为HashMap的发明者认为,后插入的Entry被查找的可能性更大。
初始长度为16,且每次自动扩容或者手动初始化的时候必须是2的幂。
对于新插入的数据或者待读取的数据,HashMap将Key的哈希值对数组长度取模,结果作为该Entry在数组中的index。在计算机中,取模的代价远高于位操作的代价,因此HashMap要求数组的长度必须为2的N次方。此时将Key的哈希值对2^N-1进行与运算,其效果即与取模等效。HashMap并不要求用户在指定HashMap容量时必须传入一个2的N次方的整数,而是会通过Integer.highestOneBit算出比指定整数大的最小的2^N值。
如何进行位运算呢?有如下的公式(Length是HashMap的长度):之前说过,从Key映射到HashMap数组的对应位置,会用到一个Hash函数:index=Hash(“apple”)如何实现一个尽量均匀分布的Hash函数呢?我们通过利用Key的HashCode值来做某种运算。index=HashCode(Key)&(Length-1)下面我们以值为“book”的Key来演示整个过程:
HashMap的size为什么必须是2的幂。这是因为2的幂用二进制表示时所有位都为1,例如16-1=15的二进制就是1111B。我们说了Hash算法是为了让hash的分布变得均匀。其实我们可以把1111看成四个通道,表示跟1111做&运算后分布是均匀的。假如默认长度取10,二进制表示为1010,这样就相当于有两个通道是关闭的,所以计算出来的索引重复的几率比较大。
HashMap的扩容条件:
(size>=threshold)&&(null!=table[bucketIndex])即达到阈值,并且当前需要存放对象的slot上已经有值。
这个方法的基本思想是:当发生地址冲突时,按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。这个过程可用下式描述:Hi(key)=(H(key)+di)modm(i=1,2,……,k(k≤m–1))其中:H(key)为关键字key的直接哈希地址,m为哈希表的长度,di为每次再探测时的地址增量。
增量d可以有不同的取法,并根据其取法有不同的称呼:(1)di=1,2,3,……线性探测再散列;(2)di=1^2,-1^2,2^2,-2^2,k^2,-k^2……二次探测再散列;(3)di=伪随机序列伪随机再散列;
放在冲突Entry最开始的地方,因为哈希表在冲突时采用的是头插法,之所以把Entry放在头节点,是因为HashMap的发明者认为,后插入的Entry被查找的可能性更大。
并发容器,hashtable和concurrenthashmap的区别
主要有死链和高并发下的数据用丢失两个问题
主要发生在多线程情况下rezise时迁移数据的transfer()方法,put(),get()方法。多个线程同时扩容时,hashmap会可能产生环,造成死循环。
新增对象丢失的原因一般有:
1.数据丢失场景:在表扩容时,当前线程迁移数据的过程中,其他线程新增的元素有可能落在迁移线程已经遍历过的哈希槽上;在遍历完成之后,table数组引用指向了newTable,这时刚才新增的元素就会失去引用,被GC回收。
2.数据覆盖场景:
或者多个线程同时执行resize,也会造成table的覆盖问题。
Hashtable在JDK1.0引入,以全互斥方式处理并发问题,性能极差;
HashMap在JDK1.2引入,非线程安全,在并发写的情形下,容易出现死链的问题;
ConcurrentHashMap在JDK5中引入,是线程安全的哈希式集合,JDK1.8之前采用分段锁的设计理念,相当于Hashtable和HashMap的折衷版。分段锁由内部类Segment实现,继承于ReentrantLock,用它来管辖辖区每个HashEntry。JDK11对JDK7做了如下优化:
待补充:
快排
索引
LinkedHashMap是有序的HashMap
map=newLinkedHashMap
Java中有5种创建对象的方式,下面给出它们的例子还有它们的字节码
把对象转换为字节序列的过程称为对象的序列化。把字节序列恢复为对象的过程称为对象的反序列化。对象的序列化主要有两种用途:1)把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;2)在网络上传送对象的字节序列。
String最慢的原因:
String为字符串常量,String对象一旦创建之后该对象是不可更改的,Java中对String对象进行的更改操作实际上是一个不断创建新的对象并且将旧的对象回收的一个过程,所以执行速度很慢。而StringBuilder和StringBuffer均为字符串变量,对变量进行操作就是直接对该对象进行更改,而不进行创建和回收的操作,所以速度要比String快很多。
如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全。
String:适用于少量的字符串操作的情况
StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况
StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况
通常情况下,在java中通过以下步骤实现不可变
1.字符串常量池的需要
字符串常量池(Stringpool,Stringinternpool,String保留池)是Java堆内存中一个特殊的存储区域,当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。假若字符串对象允许改变,那么将会导致各种逻辑错误,比如改变一个对象会影响到另一个独立对象.严格来说,这种常量池的思想,是一种优化手段。
2.允许String对象缓存HashCode
Java中String对象的哈希码被频繁地使用,比如在hashMap等容器中。字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存.这也是一种性能优化手段,意味着不必每次都去计算新的哈希码.
3.安全性
String被许多的Java类(库)用来当做参数,例如网络连接地址URL,文件路径path,还有反射机制所需要的String参数等,假若String不是固定不变的,将会引起各种安全隐患。
总体来说,String不可变的原因包括设计考虑,效率优化问题,以及安全性这三大方面。
为什么String被设计为不可变?
Java把异常当作对象来处理,并定义一个基类java.lang.Throwable作为所有异常的超类。Throwable下又分为Error和Exception。
Error:Error类对象由Java虚拟机生成并抛出,大多数错误与代码编写者所执行的操作无关。
最常见的erro是当JVM不再有继续执行操作所需的内存资源时,将出现OutOfMemoryError。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止;
单例模式:(懒汉模式、饥汉模式)1、构造方法私有化,除了自己类中能创建外其他地方都不能创建
2、在自己的类中创建一个单实例(饱汉模式是一出来就创建创建单实例,而饥汉模式需要的时候才创建)
3、提供一个方法获取该实例对象(创建时需要进行方法同步)
饿汉模式
饿汉模式就是立即加载,在方法调用前,实例就已经被创建了,所以是线程安全的。
懒汉就是延迟化加载,当需要使用的时候才进行实例化。
线程不安全方式:
工厂模式:SpringIOC就是使用了工厂模式.对象的创建交给一个工厂去创建。在工厂方法模式中,核心的工厂类不再负责所有的产品的创建,而是将具体创建的工作交给子类去做。
抽象工厂模式:抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体的情况下,创建多个产品族中的产品对象。
代理模式:SpringAOP就是使用的动态代理。
观察者模式:在对象之间定义了一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象会收到通知并自动更新。
装饰器模式:在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责,在Java源码中典型的装饰器模式就是javaI/O,其实装饰者模式也有其缺点,就是产生了太多的装饰者小类,有类爆炸的趋势。
1首先它也是一个二叉树,故满足递归定义;
2需满足左子树值<=根值<=右子树,BST的中序遍历必定是严格递增的。
3.任意节点的左右子树也分别是二叉查找树.
4.没有键值相等的节点.
5.对于某些情况,二叉查找树会退化成一个有n个节点的线性链
1.是带有平衡条件的二叉查找树,一般是用平衡因子差值判断是否平衡并通过旋转来实现平衡,
2.左右子树树高不超过1,和红黑树相比,它是严格的平衡二叉树,平衡条件必须满足(所有节点的左右子树高度差不超过1).
3.不管我们是执行插入还是删除操作,只要不满足上面的条件,就要通过旋转来保持平衡,而旋转是非常耗时的,由此我们可以知道AVL树适合用于插入删除次数比较少,但查找多的情况。
1)每个结点要么是红的,要么是黑的。(没有其他颜色)
2)根结点是黑的。
3)每个叶结点(叶结点即指树尾端NIL(NothingIntheLeaf)指针或NULL结点)是黑的。(叶子节点即为树尾端的NIL指针,或者说NULL节点。)
4)一条路径上不能出现相邻的两个红色节点
5)对于任一结点而言,其到叶结点(树尾端NIL指针)的每一条路径都包含相同数目的黑结点。(最长路径是最小路径的2倍)
6)最多三次旋转,可以达到平衡
通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍.它是一种弱平衡二叉树(由于是弱平衡,可以推出,相同的节点情况下,AVL树的高度低于红黑树),相对于要求严格的AVL树来说,它的旋转次数变少,所以对于搜索,插入,删除操作多的情况下,我们就用红黑树.
在插入时,红黑树和AVL树都能在至多2次旋转内恢复平衡;在删除时,红黑树由于只追求大致上的平衡,能在至多3次旋转内恢复平衡,而追求绝对平衡的AVL树至多需要O(logN)次旋转。
B-tree是一种平衡多路搜索树(并不一定是二叉的)B-tree树即B树
B树,一般用于外存储索引结构。系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的。二叉树、红黑树[复杂度O(h)]导致树高度非常高(平衡二叉树一个节点只能有左子树和右子树),逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,IO次数多查找慢,效率低。
特点:
(1)根节点的左子树和右子树的深度最多相差1.(确保了不会出现上图右边的极端现象)
(2)是一个平衡多路搜索树,单个节点能放多个子节点
(3)所有结点都有存储关键字(数据);
(4)不太稳定,可能走不到叶子节点
B+树是B-树的变体,也是一种平衡多路搜索树,非叶子节点只做索引,所有数据都保存在叶子节点中。
和B树区别
1:根节点和分支节点中不保存数据,只用于索引。数据和索引分离
2:所有数据都保存在叶子节点中
B+的搜索与B-树也基本相同,区别是B+树只有达到叶子结点才命中(B-树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找;
B+Tree内节点去掉了data域,只做索引,减少了io次数,因此可以拥有更大的出度,拥有更好的性能。只利用索引快速定位数据索引范围,先定位索引再通过索引高效快速定位数据。
B树:有序数组+平衡多叉树;B+树:有序数组链表+平衡多叉树;
B+树的关键字全部存放在叶子节点中,非叶子节点用来做索引,而叶子节点中有一个指针指向一下个叶子节点。做这个优化的目的是为了提高区间访问的性能。而正是这个特性决定了B+树更适合用来存储外部数据。
B树必须用中序遍历的方法按序扫库,而B+树直接从叶子结点挨个扫一遍就完了,B+树支持范围查找非常方便,而B树不支持。这是数据库选用B+树的最主要原因。比如要查5-10之间的,B+树一次找到到5这个标记,再一次找到10这个标记,然后串起来就行了,B树就非常麻烦。
举个例子来对比。B树:
比如说,我们要查找关键字范围在3到7的关键字,在找到第一个符合条件的数字3后,访问完第一个关键字所在的块后,得遍历这个B树,获取下一个块,直到遇到一个不符合条件的关键字。遍历的过程是比较复杂的。
B+树:
相比之下,B+树的基于范围的查询简洁很多。由于叶子节点有指向下一个叶子节点的指针,因此从块1到块2的访问,通过块1指向块2的指针即可。从块2到块3也是通过一个指针即可。
数据库索引采用B+树的主要原因是B树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题。正是为了解决这个问题,B+树应运而生。B+树只要遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作(或者说效率太低)。
B+树由于非叶子节点也有数据,这样在做范围查找的时候有可能某个范围的数据散落在叶子节点和非叶子节点(各层),这样就需要多次扫描B树才能找全数据,会产生多次的磁盘IO;而B+树所有的数据都在叶子节点,且叶子节点的数据前后用指针相连,只要通过索引找到范围数据在叶子节点中的起始或结束位置,就可以顺着叶子节点链表把这个范围内的数据全都读出来,避免了再次扫描,从而提高范围查找的效率。
冒泡排序、插入排序、归并排序和基数排序是稳定的排序算法。
希尔排序,也称递减增量排序算法,是插入排序的一种更高效的改进版本。但希尔排序是非稳定排序算法。希尔排序的基本思想是:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
树的深度优先遍历和广度优先遍历
搜索方法有哪些,稳定性怎么样
Redis支持五种数据类型:
替换对象分为allkeys和volatile,
替换方式分为LRU、random和ttl
总的策略=2*3=6
LRU(Leastrecentlyused,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
腾讯一面后端问了LRU的算法原理,自己怎么实现LRU
redis提供两种方式进行持久化:
AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
备份、全量复制时使用RDB;需要实时备份的时候采用AOF
RDB的优点:
RDB的缺点:·
AOF:通过追加写命令到文件实现持久化,通过appendfsync参数可以控制实时/秒级持久化。因为需要不断追加写命令,所以AOF文件体积逐渐变大,
需要定期执行重写操作来降低文件体积。Redis执行AOF恢复数据远远慢于加载RDB的方式。
1.缓存功能
Redis作为缓存层,MySQL作为存储层,绝大部分请求的热点数据都从Redis中获取。由于Redis具有支撑高并发的特性,所以缓存通常能起到加速读写和降低后端压力的作用。
2.限速
3.消息队列
Redis的lpush+brpop命令组合即可实现阻塞队列,生产者客户端使用lrpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式的“抢”列表尾部的元素,多个客户端保证了消费的负载均衡和高可用性。
4.用户标签系统
集合类型比较典型的使用场景是标签(tag)。例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。可以通过redisset找出集合的交集并集等
5.排行榜系统
数据库事务必须具备ACID特性,ACID是Atomic原子性,Consistency一致性,Isolation隔离性,Durability持久性
SQL等执行过程分为两类
在两表连接查询中,驱动表只需要访问一次,被驱动表可能被访问多次。
一般情况下,我们都把只涉及单表的过滤条件放到WHERE子句中,把涉及两表的过滤条件都放到ON子句中,我们也一般把放到ON子句中的过滤条件也称之为连接条件
左连接和右连接是左外连接和右外连接简称,只有外连接才分左右,且外连接一定会分左右。
可以据此来分辨内连接和外连接:
如果join语句前面有left或者right,则一定是外连接,此时on的语义与where的语义不同;
如果join语句前面没有left或者right,则一定是内连接,此时on的语义与where相同。
标识内连接和外连接的关键字inner|cross和outer都是可以省略的。
事务指的是逻辑上的一组操作,这组操作要么全部发生,要么全部失败。
举例:张三和李四进行转账的操作
张三向转账李四1000元张三余额-1000元李四余额+1000元
不应该出现的是在转账过程中由于一些意外,使张三的余额减去了1000元,而李四并没有收到这笔钱。使用事务来进行管理。必须一起成功或者一起失败
1、原子性:事务包含的所有数据库操作要么全部成功,要不全部失败回滚
2、一致性:一个事务执行之前和执行之后都必须处于一致性状态。拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
3、隔离性:一个事务未提交的业务结果是否对于其它事务可见。级别一般有:read_uncommit,read_commit,read_repeatable,串行化访问。
4、持久性:一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
1、脏数据所指的就是未提交的数据。。也就是说,一个事务正在对一条记录做修改,在这个事务完成并提交之前,这条数据是处于待定状态的(可能提交也可能回滚),这时,第二个事务来读取这条没有提交的数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。一个事务中访问到了另外一个事务未提交的数据,这种现象被称为脏读。
2、不可重复读(Non-RepeatableReads):一个事务先后读取同一条记录,而事务在两次读取之间该数据被其它事务所修改,则两次读取的数据不同,我们称之为不可重复读。所谓不可重复读是指在一个事务内根据同一个条件对行记录进行多次查询,但是搜出来的结果却不一致。发生不可重复读的原因是在多次搜索期间查询条件覆盖的数据被其他事务修改了。
4、所谓幻读是指同一个事务内多次查询返回的结果集不一样(比如增加了或者减少了行记录)。比如同一个事务A内第一次查询时候有n条记录,但是第二次同等条件下查询却又n+1条记录,这就好像产生了幻觉,为啥两次结果不一样那。其实和不可重复读一样,发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据。不同在于不可重复读是同一个记录的数据内容被修改了,幻读是数据行记录变多了或者少了。
数据库事务的隔离级别有4个,由低到高依次为Readuncommitted、Readcommitted、Repeatableread、Serializable,这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题。
√:可能出现×:不会出现
脏读
不可重复读
幻读
Readuncommitted
√
Readcommitted
×
Repeatableread
Serializable
当隔离级别设置为Readuncommitted时,就可能出现脏读;
当隔离级别设置为Readcommitted时,避免了脏读,但是可能会造成不可重复读。大多数数据库的默认级别就是Readcommitted,比如SqlServer,Oracle。
当隔离级别设置为Repeatableread时,可以避免不可重复读。
Mysql的默认隔离级别就是Repeatableread。
Serializable是最高的事务隔离级别,同时代价也花费最高,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻读。
1.未提交读隔离级别总是读取最新的数据行,无需使用MVCC;
2.提交读和可重复读这两种隔离级别,MySQL的InnoDB存储引擎用多版本并发控制(Multi-VersionConcurrencyControl,MVCC)实现;
3.可串行化隔离级别需要对所有读取的行都加锁。
MVCC通过版本号、记录隐藏的两个列(创建版本号、删除版本号)和Undo日志(通过回滚指针将所有的快照连接成版本链)
MVCC在每行记录后面都保存着两个隐藏的列,用来存储两个版本号:
每次对记录进行改动,都会记录一条undo日志。MVCC使用到的快照存储在Undo日志中,该日志通过回滚指针把一个数据行(Record)的所有快照连接起来。
为了建立冗余较小、结构合理的数据库,设计数据库时必须遵循一定的规则。在关系型数据库中这种规则就称为范式。
1.第一范式(确保每列保持原子性)
第一范式是最基本的范式。如果数据库表中的所有字段值都是不可分解的原子值,就说明该数据库表满足了第一范式。
第一范式的合理遵循需要根据系统的实际需求来定。比如某些数据库系统中需要用到“地址”这个属性,本来直接将“地址”属性设计成一个数据库表的字段就行。但是如果系统经常会访问“地址”属性中的“城市”部分,那么就非要将“地址”这个属性重新拆分为省份、城市、详细地址等多个部分进行存储,这样在对地址中某一部分操作的时候将非常方便。
比如在设计一个订单数据表的时候,可以将客户编号作为一个外键和订单表建立相应的关系。而不可以在订单表中添加关于客户其它信息(比如姓名、所属公司等)的字段。
索引是关系型数据库中给数据库表中一列或多列的值排序后的存储结构,聚集索引以及非聚集索引用的是B+树索引。
MySQL索引类型有:唯一索引,主键(聚集)索引,非聚集索引,全文索引
聚集(clustered)索引,也叫聚簇索引,MySQL里主键就是聚集索引。
定义:数据行的物理顺序与列值(一般是主键的那一列)的逻辑顺序相同,一个表中只能拥有一个聚集索引。查询方面,聚集索引的速度往往会更占优势。
数据行的物理顺序与列值的顺序相同,如果我们查询id比较靠后的数据,那么这行数据的地址在磁盘中的物理地址也会比较靠后。而且由于物理排列方式与聚集索引的顺序相同,所以也就只能建立一个聚集索引了。
非聚集(unclustered)索引。
定义:该索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同,一个表中可以拥有多个非聚集索引。
非聚集索引叶节点仍然是索引节点,只是有一个指针指向对应的数据块,此如果使用非聚集索引查询,而查询列中包含了其他该索引没有覆盖的列,那么他还要进行第二次的查询,查询节点上对应的数据行的数据。
分类:
1、大多数情况是正常的,只是偶尔会出现很慢的情况。
2、在数据量不变的情况下,这条SQL语句一直以来都执行的很慢。
偶发性:
a.数据库在刷新脏页。例如redolog写满了需要同步到磁盘。
b.拿不到锁。要执行的这条语句涉及到的表,别人在用,并且加锁了,我们拿不到锁,只能慢慢等待别人释放锁。要判断是否真的在等待锁,我们可以用showprocesslist这个命令来查看当前的状态
经常性:
a.没用到索引
b.索引失效(可以用explain看一下这条语句,查看possible-key字段看一下可能用到的索引,key字段查看实际用到的索引)
c.数据库索引选择错误。MySQL在执行一条语句的时候会通过分析器估计各种查询计划的查询代价,然后在其中选择代价最小的执行。如果分析器估计走索引的代价比较大,可能就放弃索引而走全表扫描。但分析器的代价估计有可能是不准确的(涉及到一个索引区分度的概念,这个区分度是用采样来统计的,采样会有偏差)。解决方法,可以强制走索引forceindex(a);可以强制数据库重新统计索引区分度:analyzetablet.
一条查询语句在经过MySQL查询优化器的各种基于成本和规则的优化会后生成一个所谓的执行计划。
explain就是用来查看一条查询语句的执行计划的。
查询计划由如下几个关键的字段:
id:在一个大的查询语句中每个SELECT关键字都对应一个唯一的id
table:该查询语句涉及的表名
type:针对单表访问的方法(有const[主键或者唯一二级索引列与常数进行等值匹配]、ref[普通的二级索引列与常量进行等值匹配]、all[全表扫描]、index[可以使用索引覆盖,但需要扫描全部的索引记录]等)。
possible_key:该查询可能用到的索引。
key:该查询实际用到的索引。
rows:执行该查询计划需要扫描的行数。(如果查询优化器决定使用全表扫描的方式对某个表执行查询时,执行计划的rows列就代表预计需要扫描的行数,如果使用索引来执行查询时,执行计划的rows列就代表预计扫描的索引记录行数)
在EXPLAIN单词和真正的查询语句中间加上FORMAT=JSON。
这样我们就可以得到一个json格式的执行计划,里边儿包含该计划花费的成本
联合索引本质:
是对多个列建立一个索引,称为联合索引。当创建(a,b,c)联合索引时,相当于创建了(a)单列索引,(a,b)联合索引以及(a,b,c)联合索引。所以只有查询条件满足a或者(a,b)或者(a,b,c)的时候,才会用上联合索引。这就是所谓的最左匹配原则。意思是以最左边的为起点任何连续的索引都能匹配上。
单列索引只是对一个字段建立索引。
1.如果条件中有or,假如or连接的两个查询条件字段中有一个没有索引的话,引擎会放弃索引而产生全表扫描。
2.对于多列索引,不是使用的第一部分(第一个),则不会使用索引
3.like查询是以%开头(以%结尾的情况可以使用)
4.查询时,采用isnull条件时,不能利用到索引,只能全表扫描
5.如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
6.如果mysql估计使用全表扫描要比使用索引快,则不使用索引
7.查询条件使用函数在索引列上,或者对索引列进行运算,运算包括(+,-,*,/,!等)错误的例子:select*fromtestwhereid-1=9;正确的例子:select*fromtestwhereid=10;
8.使用IN关键字进行范围查找,有可能不走索引(IN的范围里面有超过200个单点区间的时候会放弃索引,因为这个时候优化器对这200个单点区间作成本估计的成本很高。MySQL5.7.3之前这个单点区间的上限是10,5.7.3之后上限是200)
a.对查询进行优化,应尽量避免全表扫描,首先应考虑在where及orderby涉及的列上建立索引;
b.应尽量避免在where子句中对字段进行null值判断,否则将导致引擎放弃使用索引而进行全表扫描;
c.索引并不是越多越好,索引固然可以提高相应的select的效率,但同时也降低了insert及update的效率,因为insert或update时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。
d.应尽量避免在where子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描
一个,主键是聚集索引,数据行的物理顺序与列值(一般是主键的那一列)的逻辑顺序相同
一是主索引的区别,InnoDB的数据文件本身就是索引文件。而MyISAM的索引和数据是分开的。
二是辅助索引的区别:InnoDB的辅助索引data域存储相应记录主键的值而不是地址。而MyISAM的辅助索引和主索引没有多大区别。
简单的SQL语句(更新)
1.一定要显式定义主键2.采用与业务无关的单独列3.采用自增列4.数据类型采用int,并尽可能小,能用tinyint就不用int,能用int就不用bigint5.将主键放在表的第一列
这样设计的原因:1.在innodb引擎中只能有一个聚集索引,我们知道,聚集索引的叶子节点上直接存有行数据,所以聚集索引列尽量不要更改,而innodb表在有主键时会自动将主键设为聚集索引,如果不显式定义主键,会选第一个没有null值的唯一索引作为聚集索引,唯一索引涉及到的列内容难免被修改引发存储碎片且可能不是递增关系,存取效率低,所以最好显式定义主键且采用与业务无关的列以避免修改;如果这个条件也不符合,就会自动添加一个不可见不可引用的6byte大小的rowid作为聚集索引
2.需采用自增列来使数据顺序插入,新增数据顺序插入到当前索引的后面,符合叶子节点的分裂顺序,性能较高;若不用自增列,数据的插入近似于随机,插入时需要插入到现在索引页的某个中间位置,需要移动数据,造成大量的数据碎片,索引结构松散,性能很差3.在主键插入时,会判断是否有重复值,所以尽量采用较小的数据类型,以减小比对长度提高性能,且可以减小存储需求,磁盘占用小,进而减少磁盘IO和内存占用;而且主键存储占用小,普通索引的占用也相应较小,减少占用,减少IO,且存储索引的页中能包含较多的数据,减少页的分裂,提高效率
InnoDB,MyISAM和Memory,默认InnoDB,支持事务;memory完全在内存中,除了大小限制还有断电丢失;
一致性哈希算法
nosql不需要满足sql关系数据库数据一致性等复杂特性,非关系型一般是缓存数据库,数据加载到内存中自然更快。redis是单线程执行的,任务都放到队列中。
Oracle、MicrosoftAccess、MySQL
MongoDb、redis、HBase
非关系型数据库中,我们查询一条数据,结果出来一个数组;关系型数据库中,查询一条数据结果是一个对象。
数据库
类型
特性
优点
缺点
关系型数据库
SQLite、Oracle、mysql
1、关系型数据库,是指采用了关系模型来组织
数据的数据库;
2、关系型数据库的最大特点就是事务的一致性;
3、简单来说,关系模型指的就是二维表格模型,
而一个关系型数据库就是由二维表及其之间的联系所组成的一个数据组织。
1、容易理解:二维表结构是非常贴近逻辑世界一个概念,关系模型相对网状、层次等其他模型来说更容易理解;
2、使用方便:通用的SQL语言使得操作关系型数据库非常方便;
3、易于维护:丰富的完整性(实体完整性、参照完整性和用户定义的完整性)大大减低了数据冗余和数据不一致的概率;
4、支持SQL,可用于复杂的查询。
1、为了维护一致性所付出的巨大代价就是其读写性能比较差;
2、固定的表结构;
3、高并发读写需求;
4、海量数据的高效率读写;
非关系型数据库
1、使用键值对存储数据;
2、分布式;
3、一般不支持ACID特性;
4、非关系型数据库严格上不是一种数据库,应该是一种数据结构化存储方法的集合。
1、无需经过sql层的解析,读写性能很高;
2、基于键值对,数据没有耦合性,容易扩展;
3、存储数据的格式:nosql的存储格式是key,value形式、文档形式、图片形式等等,文档形式、图片形式等等,而关系型数据库则只支持基础类型。
1、不提供sql支持,学习和使用成本较高;
2、无事务处理,附加功能bi和报表等支持也不好;
注1:数据库事务必须具备ACID特性,ACID是Atomic原子性,Consistency一致性,Isolation隔离性,Durability持久性。
注2:数据的持久存储,尤其是海量数据的持久存储,还是需要一种关系数据库。
由一个独立的统一入口来收敛流量(接收请求),再由做二次分发的过程就是「负载均衡」。
根据实现技术不同,可分为DNS负载均衡,HTTP负载均衡,IP负载均衡,反向代理负载均衡、链路层负载均衡等。
HTTP重定向负载均衡
当用户向服务器发起请求时,请求首先被集群调度者截获;调度者根据某种分配策略,选择一台服务器,并将选中的服务器的IP地址封装在HTTP响应消息头部的Location字段中,并将响应消息的状态码设为302,最后将这个响应消息返回给浏览器。
当浏览器收到响应消息后,解析Location字段,并向该URL发起请求,然后指定的服务器处理该用户的请求,最后将结果返回给用户。
缺点:
调度服务器只在客户端第一次向网站发起请求的时候起作用。当调度服务器向浏览器返回响应信息后,客户端此后的操作都基于新的URL进行的(也就是后端服务器),此后浏览器就不会与调度服务器产生关系。
DNS负载均衡
当用户向我们的域名发起请求时,DNS服务器会自动地根据我们事先设定好的调度策略选一个合适的IP返回给用户,用户再向该IP发起请求。它的作用与HTTP重定向类似。问题是DNS会缓存IP,如果某一IP不可用(如机器故障)会导致部分用户无法正常访问,此时可以用动态DNS解决。
反向代理负载均衡
用户发来的请求都首先要经过反向代理服务器,服务器根据用户的请求要么直接将结果返回给用户,要么将请求交给后端服务器处理,再返回给用户。
优点:
缺点:
1.解决并发压力,提高应用处理性能(增加吞吐量,加强网络处理能力);
2.提供故障转移,实现高可用;
3.通过添加或减少服务器数量,提供网站伸缩性(扩展性);
4.安全防护;(负载均衡设备上做一些过滤,黑白名单等处理)
1.轮询
2.加权轮询
3.最少连接次数
4.最快响应
5.源地址Hash法
(value&(value-1))==0;
找出一个大数组里面前K个最大数,如1亿个数字中找出最大或最小的前100个数字(考虑判重和内存空间限制)?
如果不判重,可以将前100个数字做成最大堆或最小堆的数据结构(找最大的Top100用最小堆,找最小的Top100用最大堆),然后依次遍历所有数字,符合条件时替换根节点后并重新构建堆。堆的缺点是允许有重复数字!!!
第一步:Query统计(统计出每个Query出现的次数)Query统计有以下两个个方法
1、直接排序法
首先我们最先想到的的算法就是排序了,首先对这个日志里面的所有Query都进行排序,然后再遍历排好序的Query,统计每个Query出现的次数了。
但是题目中有明确要求,那就是内存不能超过1G,一千万条记录,每条记录是255Byte,很显然要占据2.375G内存,这个条件就不满足要求了。
那么,我们的算法就有了:
第二步:找出Top10(找出出现次数最多的10个)
算法二:部分排序题目要求是求出Top10,因此我们没有必要对所有的Query都进行排序,我们只需要维护一个10个大小的数组,初始化放入10个Query,按照每个Query的统计次数由大到小排序,然后遍历这300万条记录,每读一条记录就和数组最后一个Query对比,如果小于这个Query,那么继续遍历,否则,将数组中最后一条数据淘汰(还是要放在合适的位置,保持有序),加入当前的Query。最后当所有的数据都遍历完毕之后,那么这个数组中的10个Query便是我们要找的Top10了。
分析一下,在算法二中,每次比较完成之后,需要的操作复杂度都是K,因为要把元素插入到一个线性表之中,而且采用的是顺序比较。这里我们注意一下,该数组是有序的,一次我们每次查找的时候可以采用二分的方法查找,这样操作的复杂度就降到了logK,可是,随之而来的问题就是数据移动,因为移动数据次数增多了。不过,这个算法还是比算法二有了改进。
基于以上的分析,我们想想,有没有一种既能快速查找,又能快速移动元素的数据结构呢?
10亿个数中找出最大的10000个数(topK问题)
针对topK类问题,通常比较好的方案是分治+Trie树/hash+小顶堆(就是上面提到的最小堆),即先将数据集按照Hash方法分解成多个小数据集,然后使用Trie树活着Hash统计每个小数据集中的query词频,之后用小顶堆求出每个数据集中出现频率最高的前K个数,最后在所有topK中求出最终的topK。---------------------
优化的方法:可以把所有10亿个数据分组存放,比如分别放在1000个文件中。这样处理就可以分别在每个文件的10^6个数据中找出最大的10000个数,合并到一起在再找出最终的结果。
可参考Map-Reduce的思想
核心是“分治”、“归并”和哈希,第一次遍历将关键词散列到不同的文件中(散列算法是性能关键,哈希函数的性能直接影响散列的结果,尽量避免“数据倾斜”),同一个关键词一定会散列到同一个文件,理想情况是所有关键词均匀散列到不同的文件中(即分治思想,将大文件分解为小问题)。
读取每个文件并记录各关键词的次数,做个排序,从每个文件中排序出前100的关键词;
取第一个文件的记录结果,和第二个文件做“合并”,即200个结果中排序出前100个关键词,然后依次合并第三个、第四个。。。。第N个文件(将子结果合并为总结果)
主流的JVM是Oracle的HotSpotJVM,采用解释和编译混合执行的模式,JIT技术采用分层编译,极大的提高了Java的执行速度。
三种执行模式:
1.解释执行
2.JIT编译执行
3.JIT编译与解释混合执行
程序计数器(ProgramCounterRegister):线程私有的,记录当前线程的行号指示器,为线程的切换提供保障
虚拟机栈(JVMStacks)//本地方法栈:线程私有的,主要存放局部变量表,操作数栈,动态链接和方法出口等;
堆区(Heap):堆是所有线程共享的,主要用来存储对象。其中,堆可分为:年轻代和老年代两块区域。使用NewRatio参数来设定比例。对于年轻代,一个Eden区和两个Suvivor区,使用参数SuvivorRatio来设定大小。
元数据区(Metaspace):JDK8才有,保存JDK8之前永久代中的类的元信息
本地方法栈(NativeMethodStacks)
永久代和方法区的区别
HotSpot使用永久代来实现方法区。永久代是HotSpot对方法区的实现,方法区是Java虚拟机规范中的定义,是一种规范。而永久代是一种实现,一个是标准一个是实现。在Java虚拟机(JVM)内部,class文件中包括类的版本、字段、方法、接口等描述信息,还有运行时常量池,用于存放编译器生成的各种字面量和符号引用。在过去(自定义类加载器还不是很常见的时候),类大多是”static”的,很少被卸载或收集,因此被称为“永久的(Permanent)”
对于Java8,HotSpots取消了永久代,那么是不是也就没有方法区了呢?当然不是,方法区是一个规范,规范没变,它就一直在。那么取代永久代的就是元空间。
永久代和元空间的区别
1.存储位置不同,永久代物理上是堆的一部分,和新生代,老年代地址是连续的,而元空间属于本地内存;
2.存储内容不同,元空间存储类的元信息,静态变量和常量池等并入堆中。相当于永久代的数据被分到了堆和元空间中。
1.字符串存在永久代中,容易出现性能问题和内存溢出。
2.类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。永久代的垃圾收集是和老年代(oldgeneration)捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。
3.永久代会为GC带来不必要的复杂度,并且回收效率偏低
什么时候会发生堆溢出什么时候会发生栈溢出?
1.栈溢出
栈是线程私有的,他的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来存储局部变量表,操作数栈,动态链接,方法出口灯信息。局部变量表又包含基本数据类型,对象引用类型(局部变量表编译器完成,运行期间不会变化)
栈溢出就是方法执行是创建的栈帧超过了栈的深度。那么最有可能的就是方法递归调用产生栈溢出。
2.堆溢出
heapspace表示堆空间,堆中主要存储的是对象。如果不断的new对象则会导致堆中的空间溢出。
3.永久代溢出(OutOfMemoryError:PermGenspace)
永久代物理上是堆的一部分,和新生代,老年代地址是连续的,永久代溢出的表现就是堆溢出。(永久代的GC是和老年代(oldgeneration)捆绑在一起的,因此无论谁满了,都会触发永久代和老年代的垃圾收集。)
由于JDK7、8移除永久带,所以只有JDK1.6及以下会出现永久带溢出的现象
堆区储存着几乎所有的实例对象,堆由垃圾收集器自动回收,堆区由各子线程共享使用,可以通过-Xms设置最小堆容量,和-Xmx来设置最大堆容量。
堆分成两大块:新生代和老年代。
对象产生之初在新生代,步入暮年时进入老年代,但是老年代也接纳在新生代无法容纳的超大对象。
新生代=1个Eden区+2个Survior区。绝大部分对象在Eden区生成,当Eden区装填满的时候,会触发YoungGarbageCollection,即YGC。垃圾回收的时候,在Eden区实现清除策略,没有被引用的对象则直接回收。依然存活的对象会被移送到Suvivor区。Suvivor区分为S0和S1两块内存空间,送到哪块空间呢?每次YGC的时候,它们将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态。如果YGC要移送的对象大于Survivor区容量的上限,则直接移交给老年代。如果老年代也无法放下,则会触发FullGarbageCollection,FGC。如果依然无法放下,则抛出OOM。
假如一些没有进取心的对象以为可以一直在新生代的Survivor区交换来交换去,那就错了。每个对象都有一个计数器,每次YGC都会加1。-XXiMaxTenuringThreshold参数能配置计数器的值到达某个阈值的时候,对象从新生代晋升至老年代。如果该参数配置为1,那么从新生代的Eden区直接移至老年代。默认值是15,可以在Survivor区交换14次之后,晋升至老年代。
栈(Stack)是一个先进后出的数据结构。JVM中的虚拟机栈是描述Java方法执行的内在区域,它是线程私有的。栈中的元素用于支持虚拟机进行方法调用,每个方法从开始调用到执行完成的过程,就是栈帧从入栈到出栈的过程。在活动线程中,只有位于栈顶的帧才是有效的,称为当前栈帧。正在执行的方法称为当前方法,栈帧是方法运行的基本结构。在执行引擎运行时,所有指令都只能针对当前栈施进行操作。而StackOverflowError表示请求的栈溢出,导致内存耗尽,通常出现在递归方法中。
虚拟机栈通过压栈和出栈的方式,对每个方法对应的活动栈帧进行运算处理,方法正常执行结束,肯定会跳转到另一个栈帧上。在执行的过程中,如果出现异常,会进行常回溯,返回地址通过异常处理表确定。栈帧在整个JVM体系中的地位颇高,包括局部变量表、操作栈、动态连接、方法返回地址等。(1)局部变量表局部变量表是存放方法参数和局部变量的区域。
(2)操作栈操作栈是一个初始状态为空的桶式结构栈。在方法执行过程中,会有各种指令往栈中写入和提取信息。
(3)动态连接
每个栈帧中包含一个在常量池中对当前方法的引用,目的是支持方法调用过程的动态连接。
(4)方法返回地址
方法执行遇到退出情况,将返回至方法当前被调用的位置
本地方法栈在JVM内存布局中,也是线程对象私有的,但虚拟机“主内”,本地方法栈“主外”。这个“内外”是针对JVM来说的,线程调用本地方法时,会进入一个不再受JVM约束的世界。本地方法栈可以通过JNI(JavaNativeInterface)来访问虚拟机运行时的数据区,甚至可以调用寄存器,具有和JVM相同的能力和权限。最著名的本地方法应该是System.currentTimeMillis(),JNI使Java深度使用操作系统的特性功能,复用非Java代码。
为了判断对象是否存活,JVM引入了GCRoots。如果一个对象与GCRoots之间没有直接或间接的引用关系,比如某个失去任何引用的对象,或者两个互相环岛状循环引用的对象等,判决这些对象“死缓”,是可以被回收的。可以作为GCRoots对象可以是:类静态属性中引用的对象、常量引用的对象、虚拟机栈中引用的对象、本地方法栈中引用的对象等。
是实现垃圾回收算法并应用在JVM环境中的内存管理模块。当前实现的垃圾回收器有数十种,常见的有Serial、CMS、G1三种。
举例一个典型的分析内存泄漏的过程:
1.使用Heap查看当前堆大小为23.00M
2.添加一个页后堆大小变为23.40M
3.将添加的一个页删除,堆大小为23.40M
4.多次操作,结果仍相似,说明添加/删除页存在内存泄漏(也应注意排除其它因素的影响)
5.Dump出操作前后的hprof文件(1.hprof,2.hprof),用mat打开,并得到histgram结果
6.使用HomePage字段过滤histgram结果,并列出该类的对象实例列表,看到两个表中的对象集合大小不同,操作后比操作前多出一个HomePage,说明确实存在泄漏
7.将两个列表进行对比,找出多出的一个对象,用查找GCRoot的方法找出是谁串起了这条引用线路,定位结束
PS:
·很多时候堆增大是Bitmap引起的,Bitmap在Histogram中的类型是byte[],对比两个Histogram中的byte[]对象就可以找出哪些Bitmap有差异
·多使用排序功能,对找出差异很有用
2内存泄漏的原因分析
总结出来只有一条:存在无效的引用!良好的模块设计以及合理使用设计模式有助于解决此问题。
参考:
1.内存溢出原因:
1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据;2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;3.代码中存在死循环或循环产生过多重复的对象实体;4.使用的第三方软件中的BUG;5.启动参数内存值设定的过小
2.内存溢出的原因及解决方法:
对代码分析找出可能发生内存溢出的位置,可能出现的几种情况:
1、检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
2、检查代码中是否有死循环或递归调用。
3、检查是否有大循环重复产生新对象实体。
4、检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。
以文字搜模型:
基于Lucene文本搜索引擎,查找最匹配的;
以图片搜模型:
计算图片特征,对图片特征计算HashCode,搜索的时候匹配HashCode;
以模型搜模型:
计算模型的特征得到n维特征矩阵,对特征矩阵计算HashCode,搜索的时候匹配HashCode;
对外网数据去重:
一开始直接使用条件逐个字段比较判断是否重复;
后来对关键字段连接建立联合哈希值保存,用这个哈希值去重;
后来想到其实外网的url是唯一的,直接对url建立哈希值来去重;后来设想直接用url哈希之后作为主键保存,建立聚集索引;
有效性检测比较简单:
使用java.net下的类来实现,主要用到了URL和HttpURLConnection:
刚开始使用openStream()方法,这样使用倒是可以,但是速度慢;
最后使用了getResponseCode()方法,可以得到请求的响应状态,该方法返回一个int分别是200and404如无法从响应中识别任何代码则返回-1,如果对该url发起的5次请求都没有应答则认为链接失效;
Lucene,Solr
MySQL,
Redis,
Java多线程
去重的过程经历了多次迭代:
刚开始直接对记录逐个字段比较判断是否重复——>然后对关键字段建立HashCode作为标识,对比该Hash字段——>再是对外网URL建立HashCode对比;
可以对URL使用布隆过滤器做去重;(位图+多个哈希函数)
使用缓存数据库来提高并发访问;(缓存穿透(查询一个数据库一定不存在的数据),缓存击穿(一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存),缓存雪崩(缓存集中过期失效))
使用Elesticsearch来替代Solr(Elasticsearch是分布式的,不需要其他组件,分发是实时的;solr需要结合依赖其他分布式组件来实现分布式);
阿里:
一面
Snowflake,timestamp,机器id等
冯诺伊曼体系:
shell命令的执行体系:
信息熵:
程序运行中的栈式结构
TCP/IP,TCP传输层加端口号,IP网络层加ip地址;路由器主要工作在IP网络层
各层常见的协议有哪些
同步与阻塞
并行与并发
java线程的本质、内核线程与用户进程,线程调度,并行级别
CPU与内存与硬盘
内存分配管理,段业式jemalloc
二面:
java程序的运行原理:
多重继承会带来哪些问题
正向代理与反向代理
反爬机制,爬虫模拟浏览器行为
cglib方法拦截
servlet的本质
TCP长连接心跳包websocket
Netty百万级长连接优化
DSL解析到AST
代码规范,包命名规范
项目用到了数据库,谈谈对事物的理解
假设你要做一个银行app,有可能碰到多个人同时向一个账户打钱的情况,有可能碰到什么问题,如何解决(还有可能出现重复提交的问题,保证服务的幂等性)