java

一文足以了解什么是 Java 中的锁

作者 | cxuan

责编 | Elle

Java 锁分类

Java 中的锁有很多,可以按照不同的功能、种类进行分类,下面是我对 Java 中一些常用锁的分类,包括一些基本的概述

从线程是否需要对资源加锁可以分为 悲观锁 和 乐观锁从资源已被锁定,线程是否阻塞可以分为 自旋锁从多个线程并发访问资源,也就是 Synchronized 可以分为 无锁、偏向锁、 轻量级锁 和 重量级锁从锁的公平性进行区分,可以分为公平锁 和 非公平锁从根据锁是否重复获取可以分为 可重入锁 和 不可重入锁从那个多个线程能否获取同一把锁分为 共享锁 和 排他锁下面我们依次对各个锁的分类进行详细阐述。

线程是否需要对资源加锁

Java 按照是否对资源加锁分为乐观锁和悲观锁,乐观锁和悲观锁并不是一种真实存在的锁,而是一种设计思想,乐观锁和悲观锁对于理解 Java 多线程和数据库来说至关重要,下面就来探讨一下这两种实现方式的区别和优缺点

悲观锁

悲观锁是一种悲观思想,它总认为最坏的情况可能会出现,它认为数据很可能会被其他人所修改,所以悲观锁在持有数据的时候总会把资源 或者 数据 锁住,这样其他线程想要请求这个资源的时候就会阻塞,直到等到悲观锁把资源释放为止。传统的关系型数据库里边就用到了很多这种锁机制,**比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。**悲观锁的实现往往依靠数据库本身的锁功能实现。

Java 中的 Synchronized 和 ReentrantLock 等独占锁(排他锁)也是一种悲观锁思想的实现,因为 Synchronzied 和 ReetrantLock 不管是否持有资源,它都会尝试去加锁,生怕自己心爱的宝贝被别人拿走。

乐观锁

乐观锁的思想与悲观锁的思想相反,它总认为资源和数据不会被别人所修改,所以读取不会上锁,但是乐观锁在进行写入操作的时候会判断当前数据是否被修改过(具体如何判断我们下面再说)。乐观锁的实现方案一般来说有两种:版本号机制 和 CAS实现 。乐观锁多适用于多读的应用类型,这样可以提高吞吐量。

在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的。

两种锁的使用场景

上面介绍了两种锁的基本概念,并提到了两种锁的适用场景,一般来说,悲观锁不仅会对写操作加锁还会对读操作加锁,一个典型的悲观锁调用:

select * from student wherename="cxuan"forupdate

这条 sql 语句从 Student 表中选取 name = "cxuan" 的记录并对其加锁,那么其他写操作再这个事务提交之前都不会对这条数据进行操作,起到了独占和排他的作用。

悲观锁因为对读写都加锁,所以它的性能比较低,对于现在互联网提倡的三高(高性能、高可用、高并发)来说,悲观锁的实现用的越来越少了,但是一般多读的情况下还是需要使用悲观锁的,因为虽然加锁的性能比较低,但是也阻止了像乐观锁一样,遇到写不一致的情况下一直重试的时间。

相对而言,乐观锁用于读多写少的情况,即很少发生冲突的场景,这样可以省去锁的开销,增加系统的吞吐量。

乐观锁的适用场景有很多,典型的比如说成本系统,柜员要对一笔金额做修改,为了保证数据的准确性和实效性,使用悲观锁锁住某个数据后,再遇到其他需要修改数据的操作,那么此操作就无法完成金额的修改,对产品来说是灾难性的一刻,使用乐观锁的版本号机制能够解决这个问题,我们下面说。

乐观锁的实现方式

乐观锁一般有两种实现方式:采用版本号机制 和 CAS(Compare-and-Swap,即比较并替换)算法实现。

版本号机制

版本号机制是在数据表中加上一个 version 字段来实现的,表示数据被修改的次数,当执行写操作并且写入成功后,version = version + 1,当线程A要更新数据时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

我们以上面的金融系统为例,来简述一下这个过程。

成本系统中有一个数据表,表中有两个字段分别是 金额 和 version,金额的属性是能够实时变化,而 version 表示的是金额每次发生变化的版本,一般的策略是,当金额发生改变时,version 采用递增的策略每次都在上一个版本号的基础上 + 1。在了解了基本情况和基本信息之后,我们来看一下这个过程:公司收到回款后,需要把这笔钱放在金库中,假如金库中存有100 元钱下面开启事务一:当男柜员执行回款写入操作前,他会先查看(读)一下金库中还有多少钱,此时读到金库中有 100 元,可以执行写操作,并把数据库中的钱更新为 120 元,提交事务,金库中的钱由 100 -> 120,version的版本号由 0 -> 1。开启事务二:女柜员收到给员工发工资的请求后,需要先执行读请求,查看金库中的钱还有多少,此时的版本号是多少,然后从金库中取出员工的工资进行发放,提交事务,成功后版本 + 1,此时版本由 1 -> 2。上面两种情况是最乐观的情况,上面的两个事务都是顺序执行的,也就是事务一和事务二互不干扰,那么事务要并行执行会如何呢?

事务一开启,男柜员先执行读操作,取出金额和版本号,执行写操作

beginupdate 表 set 金额 = 120,version = version + 1where 金额 = 100andversion = 0

此时金额改为 120,版本号为1,事务还没有提交

事务二开启,女柜员先执行读操作,取出金额和版本号,执行写操作

beginupdate 表 set 金额 = 50,version = version + 1where 金额 = 100andversion = 0

此时金额改为 50,版本号变为 1,事务未提交

现在提交事务一,金额改为 120,版本变为1,提交事务。理想情况下应该变为 金额 = 50,版本号 = 2,但是实际上事务二 的更新是建立在金额为 100 和 版本号为 0 的基础上的,所以事务二不会提交成功,应该重新读取金额和版本号,再次进行写操作。

这样,就避免了女柜员 用基于 version = 0 的旧数据修改的结果覆盖男操作员操作结果的可能。

beginupdate 表 set 金额 = 120,version = version + 1where 金额 = 100andversion = 0beginupdate 表 set 金额 = 120,version = version + 1where 金额 = 100andversion = 0beginupdate 表 set 金额 = 120,version = version + 1where 金额 = 100andversion = 0CAS 算法

省略代码,完整代码请参照 看完你就应该能明白的悲观锁和乐观锁

CAS 即 compare and swap(比较与交换),是一种有名的无锁算法。即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization

Java 从 JDK1.5 开始支持,java.util.concurrent 包里提供了很多面向并发编程的类,也提供了 CAS 算法的支持,一些以 Atomic 为开头的一些原子类都使用 CAS 作为其实现方式。使用这些类在多核 CPU 的机器上会有比较好的性能。

如果要保证它们的原子性,必须进行加锁,使用 Synchronzied 或者 ReentrantLock,我们前面介绍它们是悲观锁的实现,我们现在讨论的是乐观锁,那么用哪种方式保证它们的原子性呢?请继续往下看

CAS 中涉及三个要素:

需要读写的内存值 V进行比较的值 A拟写入的新值 B当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

我们以 java.util.concurrent 中的AtomicInteger 为例,看一下在不用锁的情况下是如何保证线程安全的

publicclassAtomicCounter{private AtomicInteger integer = new AtomicInteger();public AtomicInteger getInteger(){return integer; }publicvoidsetInteger(AtomicInteger integer){this.integer = integer; }publicvoidincrement(){ integer.incrementAndGet(); }publicvoiddecrement(){ integer.decrementAndGet(); }}publicclassAtomicProducerextendsThread{private AtomicCounter atomicCounter;publicAtomicProducer(AtomicCounter atomicCounter){this.atomicCounter = atomicCounter; }@Overridepublicvoidrun(){for(int j = 0; j < AtomicTest.LOOP; j++) { System.out.println("producer : " + atomicCounter.getInteger()); atomicCounter.increment(); } }}publicclassAtomicConsumerextendsThread{private AtomicCounter atomicCounter;publicAtomicConsumer(AtomicCounter atomicCounter){this.atomicCounter = atomicCounter; }@Overridepublicvoidrun(){for(int j = 0; j < AtomicTest.LOOP; j++) { System.out.println("consumer : " + atomicCounter.getInteger()); atomicCounter.decrement(); } }}publicclassAtomicTest{finalstaticint LOOP = 10000;publicstaticvoidmain(String[] args)throws InterruptedException { AtomicCounter counter = new AtomicCounter(); AtomicProducer producer = new AtomicProducer(counter); AtomicConsumer consumer = new AtomicConsumer(counter); producer.start(); consumer.start(); producer.join(); consumer.join(); System.out.println(counter.getInteger()); }}

经测试可得,不管循环多少次最后的结果都是0,也就是多线程并行的情况下,使用 AtomicInteger 可以保证线程安全性。incrementAndGet 和 decrementAndGet 都是原子性操作。

乐观锁的缺点

任何事情都是有利也有弊,软件行业没有完美的解决方案只有最优的解决方案,所以乐观锁也有它的弱点和缺陷:

ABA 问题

ABA 问题说的是,如果一个变量第一次读取的值是 A,准备好需要对 A 进行写操作的时候,发现值还是 A,那么这种情况下,能认为 A 的值没有被改变过吗?可以是由 A -> B -> A 的这种情况,但是 AtomicInteger 却不会这么认为,它只相信它看到的,它看到的是什么就是什么。

JDK 1.5 以后的 AtomicStampedReference类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

也可以采用CAS的一个变种DCAS来解决这个问题。DCAS,是对于每一个V增加一个引用的表示修改次数的标记符。对于每个V,如果引用修改了一次,这个计数器就加1。然后在这个变量需要update的时候,就同时检查变量的值和计数器的值。

循环开销大

我们知道乐观锁在进行写操作的时候会判断是否能够写入成功,如果写入不成功将触发等待 -> 重试机制,这种情况是一个自旋锁,简单来说就是适用于短期内获取不到,进行等待重试的锁,它不适用于长期获取不到锁的情况,另外,自旋循环对于性能开销比较大。

CAS与synchronized的使用情景

简单的来说 CAS 适用于写比较少的情况下(多读场景,冲突一般较少),synchronized 适用于写比较多的情况下(多写场景,冲突一般较多)

对于资源竞争较少(线程冲突较轻)的情况,使用 Synchronized 同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗 cpu 资源;而 CAS 基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。对于资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大,从而浪费更多的 CPU 资源,效率低于 synchronized。

资源已被锁定,线程是否阻塞

自旋锁的提出背景

由于在多处理器环境中某些资源的有限性,有时需要互斥访问(mutual exclusion),这时候就需要引入锁的概念,只有获取了锁的线程才能够对资源进行访问,由于多线程的核心是CPU的时间分片,所以同一时刻只能有一个线程获取到锁。那么就面临一个问题,那么没有获取到锁的线程应该怎么办?

通常有两种处理方式:一种是没有获取到锁的线程就一直循环等待判断该资源是否已经释放锁,这种锁叫做自旋锁,它不用将线程阻塞起来(NON-BLOCKING);还有一种处理方式就是把自己阻塞起来,等待重新调度请求,这种叫做互斥锁。

什么是自旋锁

自旋锁的定义:当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁(spinlock)。

自旋锁的原理

自旋锁的原理比较简单,如果持有锁的线程能在短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞状态,它们只需要等一等(自旋),等到持有锁的线程释放锁之后即可获取,这样就避免了用户进程和内核切换的消耗。

因为自旋锁避免了操作系统进程调度和线程切换,所以自旋锁通常适用在时间比较短的情况下。由于这个原因,操作系统的内核经常使用自旋锁。但是,如果长时间上锁的话,自旋锁会非常耗费性能,它阻止了其他线程的运行和调度。线程持有锁的时间越长,则持有该锁的线程将被 OS(Operating System) 调度程序中断的风险越大。如果发生中断情况,那么其他线程将保持旋转状态(反复尝试获取锁),而持有该锁的线程并不打算释放锁,这样导致的是结果是无限期推迟,直到持有锁的线程可以完成并释放它为止。

解决上面这种情况一个很好的方式是给自旋锁设定一个自旋时间,等时间一到立即释放自旋锁。自旋锁的目的是占着CPU资源不进行释放,等到获取锁立即进行处理。但是如何去选择自旋时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用 CPU 资源,进而会影响整体系统的性能。因此自旋的周期选的额外重要!JDK在1.6 引入了适应性自旋锁,适应性自旋锁意味着自旋时间不是固定的了,而是由前一次在同一个锁上的自旋时间以及锁拥有的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间。

自旋锁的优缺点

自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换!

但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,占着 XX 不 XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cpu 的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁。

自旋锁的实现

下面我们用Java 代码来实现一个简单的自旋锁

publicclassSpinLockTest{private AtomicBoolean available = new AtomicBoolean(false);publicvoidlock(){// 循环检测尝试获取锁while (!tryLock()){// doSomething... } }publicbooleantryLock(){// 尝试获取锁,成功返回true,失败返回falsereturn available.compareAndSet(false,true); }publicvoidunLock(){if(!available.compareAndSet(true,false)){thrownew RuntimeException("释放锁失败"); } }}

这种简单的自旋锁有一个问题:无法保证多线程竞争的公平性。对于上面的 SpinlockTest,当多个线程想要获取锁时,谁最先将available设为false谁就能最先获得锁,这可能会造成某些线程一直都未获取到锁造成线程饥饿。就像我们下课后蜂拥的跑向食堂,下班后蜂拥地挤向地铁,通常我们会采取排队的方式解决这样的问题,类似地,我们把这种锁叫排队自旋锁(QueuedSpinlock)。计算机科学家们使用了各种方式来实现排队自旋锁,如TicketLock,MCSLock,CLHLock。接下来我们分别对这几种锁做个大致的介绍。

TicketLock

在计算机科学领域中,TicketLock 是一种同步机制或锁定算法,它是一种自旋锁,它使用ticket 来控制线程执行顺序。

就像票据队列管理系统一样。面包店或者服务机构(例如银行)都会使用这种方式来为每个先到达的顾客记录其到达的顺序,而不用每次都进行排队。通常,这种地点都会有一个分配器(叫号器,挂号器等等都行),先到的人需要在这个机器上取出自己现在排队的号码,这个号码是按照自增的顺序进行的,旁边还会有一个标牌显示的是正在服务的标志,这通常是代表目前正在服务的队列号,当前的号码完成服务后,标志牌会显示下一个号码可以去服务了。

像上面系统一样,TicketLock 是基于先进先出(FIFO) 队列的机制。它增加了锁的公平性,其设计原则如下:TicketLock 中有两个 int 类型的数值,开始都是0,第一个值是队列ticket(队列票据), 第二个值是 出队(票据)。队列票据是线程在队列中的位置,而出队票据是现在持有锁的票证的队列位置。可能有点模糊不清,简单来说,就是队列票据是你取票号的位置,出队票据是你距离叫号的位置。现在应该明白一些了吧。

当叫号叫到你的时候,不能有相同的号码同时办业务,必须只有一个人可以去办,办完后,叫号机叫到下一个人,这就叫做原子性。你在办业务的时候不能被其他人所干扰,而且不可能会有两个持有相同号码的人去同时办业务。然后,下一个人看自己的号是否和叫到的号码保持一致,如果一致的话,那么就轮到你去办业务,否则只能继续等待。上面这个流程的关键点在于,每个办业务的人在办完业务之后,他必须丢弃自己的号码,叫号机才能继续叫到下面的人,如果这个人没有丢弃这个号码,那么其他人只能继续等待。下面来实现一下这个票据排队方案

publicclassTicketLock {// 队列票据(当前排队号码)private AtomicInteger queueNum = new AtomicInteger();// 出队票据(当前需等待号码)private AtomicInteger dueueNum = new AtomicInteger();// 获取锁:如果获取成功,返回当前线程的排队号publicintlock(){int currentTicketNum = dueueNum.incrementAndGet();while (currentTicketNum != queueNum.get()){// doSomething... }return currentTicketNum; }// 释放锁:传入当前排队的号码publicvoidunLock(int ticketNum){ queueNum.compareAndSet(ticketNum,ticketNum + 1); }}

每次叫号机在叫号的时候,都会判断自己是不是被叫的号,并且每个人在办完业务的时候,叫号机根据在当前号码的基础上 + 1,让队列继续往前走。

但是上面这个设计是有问题的,因为获得自己的号码之后,是可以对号码进行更改的,这就造成系统紊乱,锁不能及时释放。这时候就需要有一个能确保每个人按会着自己号码排队办业务的角色,在得知这一点之后,我们重新设计一下这个逻辑

publicclassTicketLock2 {// 队列票据(当前排队号码)private AtomicInteger queueNum = new AtomicInteger();// 出队票据(当前需等待号码)private AtomicInteger dueueNum = new AtomicInteger();private ThreadLocal<Integer> ticketLocal = new ThreadLocal<>();publicvoidlock(){int currentTicketNum = dueueNum.incrementAndGet();// 获取锁的时候,将当前线程的排队号保存起来 ticketLocal.set(currentTicketNum);while (currentTicketNum != queueNum.get()){// doSomething... } }// 释放锁:从排队缓冲池中取publicvoidunLock(){ Integer currentTicket = ticketLocal.get(); queueNum.compareAndSet(currentTicket,currentTicket + 1); }}

这次就不再需要返回值,办业务的时候,要将当前的这一个号码缓存起来,在办完业务后,需要释放缓存的这条票据。

缺点

TicketLock 虽然解决了公平性的问题,但是多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量queueNum ,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。

为了解决这个问题,MCSLock 和 CLHLock 应运而生。

CLHLock

上面说到TicketLock 是基于队列的,那么 CLHLock 就是基于链表设计的,CLH的发明人是:Craig,Landin and Hagersten,用它们各自的字母开头命名。CLH 是一种基于链表的可扩展,高性能,公平的自旋锁,申请线程只能在本地变量上自旋,它会不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。

publicclassCLHLock {publicstaticclassCLHNode{privatevolatile boolean isLocked = true; }// 尾部节点privatevolatile CLHNode tail;privatestatic final ThreadLocal<CLHNode> LOCAL = new ThreadLocal<>();privatestatic final AtomicReferenceFieldUpdater<CLHLock,CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class,CLHNode.class,"tail");publicvoidlock(){// 新建节点并将节点与当前线程保存起来 CLHNode node = new CLHNode(); LOCAL.set(node);// 将新建的节点设置为尾部节点,并返回旧的节点(原子操作),这里旧的节点实际上就是当前节点的前驱节点 CLHNode preNode = UPDATER.getAndSet(this,node);if(preNode != null){// 前驱节点不为null表示当锁被其他线程占用,通过不断轮询判断前驱节点的锁标志位等待前驱节点释放锁while (preNode.isLocked){ } preNode = null; LOCAL.set(node); }// 如果不存在前驱节点,表示该锁没有被其他线程占用,则当前线程获得锁 }publicvoidunlock() {// 获取当前线程对应的节点 CLHNode node = LOCAL.get();// 如果tail节点等于node,则将tail节点更新为null,同时将node的lock状态职位false,表示当前线程释放了锁if (!UPDATER.compareAndSet(this, node, null)) { node.isLocked = false; } node = null; }}

MCSLock

MCS Spinlock 是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。MCS 来自于其发明人名字的首字母:John Mellor-Crummey 和 Michael Scott。

publicclassMCSLock{publicstaticclassMCSNode{volatile MCSNode next;volatileboolean isLocked = true; }privatestaticfinal ThreadLocal<MCSNode> NODE = new ThreadLocal<>();// 队列@SuppressWarnings("unused")privatevolatile MCSNode queue;privatestaticfinal AtomicReferenceFieldUpdater<MCSLock,MCSNode> UPDATE = AtomicReferenceFieldUpdater.newUpdater(MCSLock.class,MCSNode.class,"queue");publicvoidlock(){// 创建节点并保存到ThreadLocal中 MCSNode currentNode = new MCSNode(); NODE.set(currentNode);// 将queue设置为当前节点,并且返回之前的节点 MCSNode preNode = UPDATE.getAndSet(this, currentNode);if (preNode != null) {// 如果之前节点不为null,表示锁已经被其他线程持有 preNode.next = currentNode;// 循环判断,直到当前节点的锁标志位为falsewhile (currentNode.isLocked) { } } }publicvoidunlock(){ MCSNode currentNode = NODE.get();// next为null表示没有正在等待获取锁的线程if (currentNode.next == null) {// 更新状态并设置queue为nullif (UPDATE.compareAndSet(this, currentNode, null)) {// 如果成功了,表示queue==currentNode,即当前节点后面没有节点了return; } else {// 如果不成功,表示queue!=currentNode,即当前节点后面多了一个节点,表示有线程在等待// 如果当前节点的后续节点为null,则需要等待其不为null(参考加锁方法)while (currentNode.next == null) { } } } else {// 如果不为null,表示有线程在等待获取锁,此时将等待线程对应的节点锁状态更新为false,同时将当前线程的后继节点设为null currentNode.next.isLocked = false; currentNode.next = null; } }}

CLHLock 和 MCSLock

都是基于链表,不同的是CLHLock是基于隐式链表,没有真正的后续节点属性,MCSLock是显示链表,有一个指向后续节点的属性。将获取锁的线程状态借助节点(node)保存,每个线程都有一份独立的节点,这样就解决了TicketLock多处理器缓存同步的问题。

多个线程并发访问资源

锁状态的分类

Java 语言专门针对 synchronized 关键字设置了四种状态,它们分别是:无锁、偏向锁、轻量级锁和重量级锁,但是在了解这些锁之前还需要先了解一下 Java 对象头和 Monitor。

Java 对象头

我们知道 synchronized 是悲观锁,在操作同步之前需要给资源加锁,这把锁就是对象头里面的,而Java 对象头又是什么呢?我们以 Hotspot 虚拟机为例,Hopspot 对象头主要包括两部分数据:Mark Word(标记字段) 和 class Pointer(类型指针)。

Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。

class Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

在32位虚拟机和64位虚拟机的 Mark Word 所占用的字节大小不一样,32位虚拟机的 Mark Word 和 class Pointer 分别占用 32bits 的字节,而 64位虚拟机的 Mark Word 和 class Pointer 占用了64bits 的字节,下面我们以 32位虚拟机为例,来看一下其 Mark Word 的字节具体是如何分配的

用中文翻译过来就是

无状态也就是无锁的时候,对象头开辟 25bit 的空间用来存储对象的 hashcode ,4bit 用于存放分代年龄,1bit 用来存放是否偏向锁的标识位,2bit 用来存放锁标识位为01偏向锁 中划分更细,还是开辟25bit 的空间,其中23bit 用来存放线程ID,2bit 用来存放 epoch,4bit 存放分代年龄,1bit 存放是否偏向锁标识, 0表示无锁,1表示偏向锁,锁的标识位还是01轻量级锁中直接开辟 30bit 的空间存放指向栈中锁记录的指针,2bit 存放锁的标志位,其标志位为00重量级锁中和轻量级锁一样,30bit 的空间用来存放指向重量级锁的指针,2bit 存放锁的标识位,为11GC标记开辟30bit 的内存空间却没有占用,2bit 空间存放锁标志位为11。其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。

关于为什么这么分配的内存,我们可以从 OpenJDK 中的markOop.hpp类中的枚举窥出端倪

来解释一下

age_bits 就是我们说的分代回收的标识,占用4字节lock_bits 是锁的标志位,占用2个字节biased_lock_bits 是是否偏向锁的标识,占用1个字节max_hash_bits 是针对无锁计算的hashcode 占用字节数量,如果是32位虚拟机,就是 32 - 4 - 2 -1 = 25 byte,如果是64 位虚拟机,64 - 4 - 2 - 1 = 57 byte,但是会有 25 字节未使用,所以64位的 hashcode 占用 31 bytehash_bits 是针对 64 位虚拟机来说,如果最大字节数大于 31,则取31,否则取真实的字节数cms_bits 我觉得应该是不是64位虚拟机就占用 0 byte,是64位就占用 1byteepoch_bits 就是 epoch 所占用的字节大小,2字节。Synchronized锁

synchronized用的锁是存在Java对象头里的。

JVM基于进入和退出 Monitor 对象来实现方法同步和代码块同步。代码块同步是使用 monitorenter 和 monitorexit 指令实现的,monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处。任何对象都有一个 monitor 与之关联,当且一个 monitor 被持有后,它将处于锁定状态。

根据虚拟机规范的要求,在执行 monitorenter 指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应地,在执行 monitorexit 指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。

Monitor

Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为重量级锁。

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁:锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级。

所以锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。JDK 1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking=false来禁用偏向锁。

锁的分类及其解释

先来个大体的流程图来感受一下这个过程,然后下面我们再分开来说

无锁

无锁状态,无锁即没有对资源进行锁定,所有的线程都可以对同一个资源进行访问,但是只有一个线程能够成功修改资源。

无锁的特点就是在循环内进行修改操作,线程会不断的尝试修改共享资源,直到能够成功修改资源并退出,在此过程中没有出现冲突的发生,这很像我们在之前文章中介绍的 CAS 实现,CAS 的原理和应用就是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。

偏向锁

HotSpot 的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,还存在锁由同一线程多次获得的情况,偏向锁就是在这种情况下出现的,它的出现是为了解决只有在一个线程执行同步时提高性能。

可以从对象头的分配中看到,偏向锁要比无锁多了线程ID 和 epoch,下面我们就来描述一下偏向锁的获取过程

偏向锁获取过程

首先线程访问同步代码块,会通过检查对象头 Mark Word 的锁标志位判断目前锁的状态,如果是 01,说明就是无锁或者偏向锁,然后再根据是否偏向锁 的标示判断是无锁还是偏向锁,如果是无锁情况下,执行下一步线程使用 CAS 操作来尝试对对象加锁,如果使用 CAS 替换 ThreadID 成功,就说明是第一次上锁,那么当前线程就会获得对象的偏向锁,此时会在对象头的 Mark Word 中记录当前线程 ID 和获取锁的时间 epoch 等信息,然后执行同步代码块。全局安全点(Safe Point):全局安全点的理解会涉及到 C 语言底层的一些知识,这里简单理解 SafePoint 是 Java 代码中的一个线程可能暂停执行的位置。

等到下一次线程在进入和退出同步代码块时就不需要进行 CAS 操作进行加锁和解锁,只需要简单判断一下对象头的 Mark Word 中是否存储着指向当前线程的线程ID,判断的标志当然是根据锁的标志位来判断的。如果用流程图来表示的话就是下面这样

关闭偏向锁

偏向锁在Java 6 和Java 7 里是默认启用的。由于偏向锁是为了在只有一个线程执行同步块时提高性能,如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

关于 epoch

偏向锁的对象头中有一个被称为 epoch 的值,它作为偏差有效性的时间戳。

轻量级锁

轻量级锁是指当前锁是偏向锁的时候,资源被另外的线程所访问,那么偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能,下面是详细的获取过程。

轻量级锁加锁过程

紧接着上一步,如果 CAS 操作替换 ThreadID 没有获取成功,执行下一步;如果使用 CAS 操作替换 ThreadID 失败(这时候就切换到另外一个线程的角度)说明该资源已被同步访问过,这时候就会执行锁的撤销操作,撤销偏向锁,然后等原持有偏向锁的线程到达全局安全点(SafePoint)时,会暂停原持有偏向锁的线程,然后会检查原持有偏向锁的状态,如果已经退出同步,就会唤醒持有偏向锁的线程,执行下一步;检查对象头中的 Mark Word 记录的是否是当前线程 ID,如果是,执行同步代码,如果不是,执行偏向锁获取流程 的第2步。如果用流程表示的话就是下面这样(已经包含偏向锁的获取)

重量级锁

重量级锁的获取流程比较复杂,小伙伴们做好准备,其实多看几遍也没那么麻烦,呵呵。

重量级锁的获取流程

1. 接着上面偏向锁的获取过程,由偏向锁升级为轻量级锁,执行下一步

2 . 会在原持有偏向锁的线程的栈中分配锁记录,将对象头中的 Mark Word 拷贝到原持有偏向锁线程的记录中,然后原持有偏向锁的线程获得轻量级锁,然后唤醒原持有偏向锁的线程,从安全点处继续执行,执行完毕后,执行下一步,当前线程执行第4步

3. 执行完毕后,开始轻量级解锁操作,解锁需要判断两个条件

如果上面两个判断条件都符合的话,就进行锁释放,如果其中一个条件不 符合,就会释放锁,并唤起等待的线程,进行新一轮的锁竞争。

拷贝在当前线程锁记录的 Mark Word 信息是否与对象头中的 Mark Word 一致。

判断对象头中的 Mark Word 中锁记录指针是否指向当前栈中记录的指针

4. 在当前线程的栈中分配锁记录,拷贝对象头中的 MarkWord 到当前线程的锁记录中,执行 CAS 加锁操作,会把对象头 Mark Word 中锁记录指针指向当前线程锁记录,如果成功,获取轻量级锁,执行同步代码,然后执行第3步,如果不成功,执行下一步

5. 当前线程没有使用 CAS 成功获取锁,就会自旋一会儿,再次尝试获取,如果在多次自旋到达上限后还没有获取到锁,那么轻量级锁就会升级为 重量级锁

如果用流程图表示是这样的

锁的公平性与非公平性

我们知道,在并发环境中,多个线程需要对同一资源进行访问,同一时刻只能有一个线程能够获取到锁并进行资源访问,那么剩下的这些线程怎么办呢?这就好比食堂排队打饭的模型,最先到达食堂的人拥有最先买饭的权利,那么剩下的人就需要在第一个人后面排队,这是理想的情况,即每个人都能够买上饭。那么现实情况是,在你排队的过程中,就有个别不老实的人想走捷径,插队打饭,如果插队的这个人后面没有人制止他这种行为,他就能够顺利买上饭,如果有人制止,他就也得去队伍后面排队。

对于正常排队的人来说,没有人插队,每个人都在等待排队打饭的机会,那么这种方式对每个人来说都是公平的,先来后到嘛。这种锁也叫做公平锁。

那么假如插队的这个人成功买上饭并且在买饭的过程不管有没有人制止他,他的这种行为对正常排队的人来说都是不公平的,这在锁的世界中也叫做非公平锁。

那么我们根据上面的描述可以得出下面的结论

公平锁表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO先进先出顺序。而非公平锁就是一种获取锁的抢占机制,是随机获得锁的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁,结果也就是不公平的了。

锁公平性的实现

在 Java 中,我们一般通过 ReetrantLock 来实现锁的公平性

我们分别通过两个例子来讲解一下锁的公平性和非公平性

锁的公平性

publicclassMyFairLockextendsThread{private ReentrantLock lock = new ReentrantLock(true);publicvoidfairLock(){try {lock.lock(); System.out.println(Thread.currentThread().getName() + "正在持有锁"); }finally { System.out.println(Thread.currentThread().getName() + "释放了锁");lock.unlock(); } }publicstaticvoidmain(String[] args) { MyFairLock myFairLock = new MyFairLock(); Runnable runnable = () -> { System.out.println(Thread.currentThread().getName() + "启动"); myFairLock.fairLock(); }; Thread[] thread = new Thread[10];for(int i = 0;i < 10;i++){ thread[i] = new Thread(runnable); }for(int i = 0;i < 10;i++){ thread[i].start(); } }}

我们创建了一个 ReetrantLock,并给构造函数传了一个 true,我们可以查看 ReetrantLock 的构造函数

publicReentrantLock(boolean fair){ sync = fair ? new FairSync() : new NonfairSync();}

根据 JavaDoc 的注释可知,如果是 true 的话,那么就会创建一个 ReentrantLock 的公平锁,然后并创建一个 FairSync ,FairSync 其实是一个 Sync 的内部类,它的主要作用是同步对象以获取公平锁。

而 Sync 是 ReentrantLock 中的内部类,Sync 继承 AbstractQueuedSynchronizer 类,AbstractQueuedSynchronizer 就是我们常说的 AQS ,它是 JUC(java.util.concurrent) 中最重要的一个类,通过它来实现独占锁和共享锁。

abstractstaticclassSyncextendsAbstractQueuedSynchronizer{...}

也就是说,我们把 fair 参数设置为 true 之后,就可以实现一个公平锁了,是这样吗?我们回到示例代码,我们可以执行一下这段代码,它的输出是顺序获取的(碍于篇幅的原因,这里就暂不贴出了),也就是说我们创建了一个公平锁

锁的非公平性

与公平性相对的就是非公平性,我们通过设置 fair 参数为 true,便实现了一个公平锁,与之相对的,我们把 fair 参数设置为 false,是不是就是非公平锁了?用事实证明一下

private ReentrantLock lock = new ReentrantLock(false);

其他代码不变,我们执行一下看看输出(部分输出)

Thread-1启动Thread-4启动Thread-1正在持有锁Thread-1释放了锁Thread-5启动Thread-6启动Thread-3启动Thread-7启动Thread-2启动

可以看到,线程的启动并没有按顺序获取,可以看出非公平锁对锁的获取是乱序的,即有一个抢占锁的过程。也就是说,我们把 fair 参数设置为 false 便实现了一个非公平锁。

ReentrantLock 基本概述

ReentrantLock 是一把可重入锁,也是一把互斥锁,它具有与 synchronized 相同的方法和监视器锁的语义,但是它比 synchronized 有更多可扩展的功能。

ReentrantLock 的可重入性是指它可以由上次成功锁定但还未解锁的线程拥有。当只有一个线程尝试加锁时,该线程调用 lock() 方法会立刻返回成功并直接获取锁。如果当前线程已经拥有这把锁,这个方法会立刻返回。可以使用 isHeldByCurrentThread 和 getHoldCount 进行检查。

这个类的构造函数接受可选择的 fairness 参数,当 fairness 设置为 true 时,在多线程争夺尝试加锁时,锁倾向于对等待时间最长的线程访问,这也是公平性的一种体现。否则,锁不能保证每个线程的访问顺序,也就是非公平锁。与使用默认设置的程序相比,使用许多线程访问的公平锁的程序可能会显示较低的总体吞吐量(即较慢;通常要慢得多)。但是获取锁并保证线程不会饥饿的次数比较小。无论如何请注意:锁的公平性不能保证线程调度的公平性。因此,使用公平锁的多线程之一可能会连续多次获得它,而其他活动线程没有进行且当前未持有该锁。这也是互斥性 的一种体现。

也要注意的 tryLock() 方法不支持公平性。如果锁是可以获取的,那么即使其他线程等待,它仍然能够返回成功。

推荐使用下面的代码来进行加锁和解锁

classMyFairLock {private final ReentrantLock lock = new ReentrantLock();publicvoidm() {lock.lock();try {// ... } finally {lock.unlock() } }}

ReentrantLock 锁通过同一线程最多支持2147483647个递归锁。尝试超过此限制会导致锁定方法引发错误。

ReentrantLock 如何实现锁公平性

我们在上面的简述中提到,ReentrantLock 是可以实现锁的公平性的,那么原理是什么呢?下面我们通过其源码来了解一下 ReentrantLock 是如何实现锁的公平性的

跟踪其源码发现,调用 Lock.lock() 方法其实是调用了 sync 的内部的方法

abstractvoidlock();

而 sync 是最基础的同步控制 Lock 的类,它有公平锁和非公平锁的实现。它继承 AbstractQueuedSynchronizer 即 使用 AQS 状态代表锁持有的数量。

lock 是抽象方法是需要被子类实现的,而继承了 AQS 的类主要有

我们可以看到,所有实现了 AQS 的类都位于 JUC 包下,主要有五类:ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch 和 ThreadPoolExecutor,其中 ReentrantLock、ReentrantReadWriteLock、Semaphore 都可以实现公平锁和非公平锁。

下面是公平锁 FairSync 的继承关系

非公平锁的NonFairSync 的继承关系

由继承图可以看到,两个类的继承关系都是相同的,我们从源码发现,公平锁和非公平锁的实现就是下面这段代码的区别(下一篇文章我们会从原理角度分析一下公平锁和非公平锁的实现)

通过上图中的源代码对比,我们可以明显的看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()。

hasQueuedPredecessors() 也是 AQS 中的方法,它主要是用来 查询是否有任何线程在等待获取锁的时间比当前线程长,也就是说每个等待线程都是在一个队列中,此方法就是判断队列中在当前线程获取锁时,是否有等待锁时间比自己还长的队列,如果当前线程之前有排队的线程,返回 true,如果当前线程位于队列的开头或队列为空,返回 false。

综上,公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况。

根据锁是否可重入进行区分

可重入锁

可重入锁又称为递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java 中 ReentrantLock 和synchronized 都是可重入锁,可重入锁的一个优点是在一定程度上可以避免死锁。

我们先来看一段代码来说明一下 synchronized 的可重入性

private synchronized voiddoSomething(){ System.out.println("doSomething..."); doSomethingElse();}private synchronized voiddoSomethingElse(){ System.out.println("doSomethingElse...");}

在上面这段代码中,我们对 doSomething() 和 doSomethingElse() 分别使用了 synchronized 进行锁定,doSomething() 方法中调用了 doSomethingElse() 方法,因为 synchronized 是可重入锁,所以同一个线程在调用 doSomething() 方法时,也能够进入 doSomethingElse() 方法中。

不可重入锁

如果 synchronized 是不可重入锁的话,那么在调用 doSomethingElse() 方法的时候,必须把 doSomething() 的锁丢掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。

也就是说,不可重入锁会造成死锁

多个线程能够共享同一把锁

独占锁和共享锁

独占多和共享锁一般对应 JDK 源码的 ReentrantLock 和 ReentrantReadWriteLock 源码来介绍独占锁和共享锁。

独占锁又叫做排他锁,是指锁在同一时刻只能被一个线程拥有,其他线程想要访问资源,就会被阻塞。JDK 中 synchronized和 JUC 中 Lock 的实现类就是互斥锁。

共享锁指的是锁能够被多个线程所拥有,如果某个线程对资源加上共享锁后,则其他线程只能对资源再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。

我们看到 ReentrantReadWriteLock 有两把锁:ReadLock 和 WriteLock,也就是一个读锁一个写锁,合在一起叫做读写锁。再进一步观察可以发现 ReadLock 和 WriteLock 是靠内部类 Sync 实现的锁。Sync 是继承于 AQS 子类的,AQS 是并发的根本,这种结构在CountDownLatch、ReentrantLock、Semaphore里面也都存在。

在 ReentrantReadWriteLock 里面,读锁和写锁的锁主体都是 Sync,但读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升。

声明:本文为作者投稿,版权归作者个人所有。

【End】

展开
收起

Java 面试如何坐等 offer?

2015 年,因为工作岗位的变动,开始负责给集团招聘一些技术人员,出于对公司的负责,也为了更好的胜任技术经理的职位,在面试的这件事上,我做了大量的“功课”,首先我研究了几乎所有大厂的面试题,还和负责招聘工作的几个朋友,详细的探讨了 Java 面试所要涉及的知识点,于是就有了今天大家看到的这 200 多道面试题。

为什么要公开这些面试题?

原因一:身边从事 Java 开发的人员越来越多,我的表弟表妹们,朋友的表弟表妹们,朋友的朋友的表弟表妹们,每次问我要相同的面试复习材料,已经让我疲于应付,索性整理出来,直接发链接给他们。

原因二:节省招聘双方彼此的时间,有些来公司面试人,无论是有几年工作经验的还是刚毕业的,就连这些最基础的面试题都搞不定,这确实让人很遗憾。常言道“一屋不扫何以扫天下”,也是同样的道理,如果连基础的概念都搞不明白,又怎么让面试官相信你能写出高质量的程序呢?与其浪费彼此的时间,还不如花点时间把自己的基础知识掌握牢固。

原因三:提高 Java 从业人员整体的能力模型,让优秀的人能“冒”出来。有了这些面试题不意味着,死记硬背之后就能进入企业工作,尤其是 BAT 等工作岗位竞争更为激烈,这些面试题只能成为面试体系中的一道“开胃菜”,从而提高了整个 Java 面试的壁垒,让愿与学的人,变的更加优秀,从而和懒惰的人拉开差距,让企业也能更轻易的甄别。

这些面试题包含哪些内容?

这份面试题包含了 19 个模块:Java 基础、容器、多线程、反射、对象拷贝、Java Web、异常、网络、设计模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、Mybatis、RabbitMQ、Kafka、Zookeeper、MySql、Redis、JVM 等。

如下图所示:

本面试题解决的痛点

稀缺性,提供真实的,覆盖面全的面试集合,包含 200+ 面试题。权威性,解决了目前市场上答案太旧甚至是错误的问题。真实性,这份面试题是企业常用的,不会偏离实际。易理解性,通俗易懂条理清晰,部分面试题包含题目解析和代码示例,让小白也能看的懂。节约时间,屏蔽无效重复信息,为真正需要面试题的人节省检索时间。

适宜阅读人群

待面试的 初/中/高级 Java 程序员查漏补缺的人想要不断完善和扩充自己 Java 技术栈的人Java 面试官限于篇幅,答案不能完全展示

扫码获取完整的 200+ Java面试题答案

面试题展示

一、Java 基础

1.JDK 和 JRE 有什么区别?

2.== 和 equals 的区别是什么?

3.两个对象的 hashCode()相同,则 equals()也一定为 true,对吗?

4.final 在 java 中有什么作用?

5.java 中的 Math.round(-1.5) 等于多少?

6.String 属于基础的数据类型吗?

7.java 中操作字符串都有哪些类?它们之间有什么区别?

8.String str="i"与 String str=new String("i")一样吗?

9.如何将字符串反转?

10.String 类的常用方法都有那些?

11.抽象类必须要有抽象方法吗?

12.普通类和抽象类有哪些区别?

13.抽象类能使用 final 修饰吗?

14.接口和抽象类有什么区别?

15.java 中 IO 流分为几种?

16.BIO、NIO、AIO 有什么区别?

17.Files的常用方法都有哪些?

二、容器

18.java 容器都有哪些?

19.Collection 和 Collections 有什么区别?

20.List、Set、Map 之间的区别是什么?

21.HashMap 和 Hashtable 有什么区别?

22.如何决定使用 HashMap 还是 TreeMap?

23.说一下 HashMap 的实现原理?

24.说一下 HashSet 的实现原理?

25.ArrayList 和 LinkedList 的区别是什么?

26.如何实现数组和 List 之间的转换?

27.ArrayList 和 Vector 的区别是什么?

28.Array 和 ArrayList 有何区别?

29.在 Queue 中 poll()和 remove()有什么区别?

30.哪些集合类是线程安全的?

31.迭代器 Iterator 是什么?

32.Iterator 怎么使用?有什么特点?

33.Iterator 和 ListIterator 有什么区别?

34.怎么确保一个集合不能被修改?

三、多线程

35.并行和并发有什么区别?

36.线程和进程的区别?

37.守护线程是什么?

38.创建线程有哪几种方式?

39.说一下 runnable 和 callable 有什么区别?

40.线程有哪些状态?

41.sleep() 和 wait() 有什么区别?

42.notify()和 notifyAll()有什么区别?

43.线程的 run()和 start()有什么区别?

44.创建线程池有哪几种方式?

45.线程池都有哪些状态?

46.线程池中 submit()和 execute()方法有什么区别?

47.在 java 程序中怎么保证多线程的运行安全?

48.多线程锁的升级原理是什么?

49.什么是死锁?

50.怎么防止死锁?

51.ThreadLocal 是什么?有哪些使用场景?

52.说一下 synchronized 底层实现原理?

53.synchronized 和 volatile 的区别是什么?

54.synchronized 和 Lock 有什么区别?

55.synchronized 和 ReentrantLock 区别是什么?

56.说一下 atomic 的原理?

四、反射

57.什么是反射?

58.什么是 java 序列化?什么情况下需要序列化?

59.动态代理是什么?有哪些应用?

60.怎么实现动态代理?

五、对象拷贝

61.为什么要使用克隆?

62.如何实现对象克隆?

63.深拷贝和浅拷贝区别是什么?

六、Java Web

64.jsp 和 servlet 有什么区别?

65.jsp 有哪些内置对象?作用分别是什么?

66.说一下 jsp 的 4 种作用域?

67.session 和 cookie 有什么区别?

68.说一下 session 的工作原理?

69.如果客户端禁止 cookie 能实现 session 还能用吗?

70.spring mvc 和 struts 的区别是什么?

71.如何避免 sql 注入?

72.什么是 XSS 攻击,如何避免?

73.什么是 CSRF 攻击,如何避免?

七、异常

74.throw 和 throws 的区别?

75.final、finally、finalize 有什么区别?

76.try-catch-finally 中哪个部分可以省略?

77.try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?

78.常见的异常类有哪些?

八、网络

79.http 响应码 301 和 302 代表的是什么?有什么区别?

80.forward 和 redirect 的区别?

81.简述 tcp 和 udp的区别?

82.tcp 为什么要三次握手,两次不行吗?为什么?

83.说一下 tcp 粘包是怎么产生的?

84.OSI 的七层模型都有哪些?

85.get 和 post 请求有哪些区别?

86.如何实现跨域?

87.说一下 JSONP 实现原理?

九、设计模式

88.说一下你熟悉的设计模式?

89.简单工厂和抽象工厂有什么区别?

十、Spring/Spring MVC

90.为什么要使用 spring?

91.解释一下什么是 aop?

92.解释一下什么是 ioc?

93.spring 有哪些主要模块?

94.spring 常用的注入方式有哪些?

95.spring 中的 bean 是线程安全的吗?

96.spring 支持几种 bean 的作用域?

97.spring 自动装配 bean 有哪些方式?

98.spring 事务实现方式有哪些?

99.说一下 spring 的事务隔离?

100.说一下 spring mvc 运行流程?

101.spring mvc 有哪些组件?

102.@RequestMapping 的作用是什么?

103.@Autowired 的作用是什么?

十一、Spring Boot/Spring Cloud

104.什么是 spring boot?

105.为什么要用 spring boot?

106.spring boot 核心配置文件是什么?

107.spring boot 配置文件有哪几种类型?它们有什么区别?

108.spring boot 有哪些方式可以实现热部署?

109.jpa 和 hibernate 有什么区别?

110.什么是 spring cloud?

111.spring cloud 断路器的作用是什么?

112.spring cloud 的核心组件有哪些?

十二、Hibernate

113.为什么要使用 hibernate?

114.什么是 ORM 框架?

115.hibernate 中如何在控制台查看打印的 sql 语句?

116.hibernate 有几种查询方式?

117.hibernate 实体类可以被定义为 final 吗?

118.在 hibernate 中使用 Integer 和 int 做映射有什么区别?

119.hibernate 是如何工作的?

120.get()和 load()的区别?

121.说一下 hibernate 的缓存机制?

122.hibernate 对象有哪些状态?

123.在 hibernate 中 getCurrentSession 和 openSession 的区别是什么?

124.hibernate 实体类必须要有无参构造函数吗?为什么?

十三、Mybatis

125.mybatis 中 #{}和 ${}的区别是什么?

126.mybatis 有几种分页方式?

127.RowBounds 是一次性查询全部结果吗?为什么?

128.mybatis 逻辑分页和物理分页的区别是什么?

129.mybatis 是否支持延迟加载?延迟加载的原理是什么?

130.说一下 mybatis 的一级缓存和二级缓存?

131.mybatis 和 hibernate 的区别有哪些?

132.mybatis 有哪些执行器(Executor)?

133.mybatis 分页插件的实现原理是什么?

134.mybatis 如何编写一个自定义插件?

十四、RabbitMQ

135.rabbitmq 的使用场景有哪些?

136.rabbitmq 有哪些重要的角色?

137.rabbitmq 有哪些重要的组件?

138.rabbitmq 中 vhost 的作用是什么?

139.rabbitmq 的消息是怎么发送的?

140.rabbitmq 怎么保证消息的稳定性?

141.rabbitmq 怎么避免消息丢失?

142.要保证消息持久化成功的条件有哪些?

143.rabbitmq 持久化有什么缺点?

144.rabbitmq 有几种广播类型?

145.rabbitmq 怎么实现延迟消息队列?

146.rabbitmq 集群有什么用?

147.rabbitmq 节点的类型有哪些?

148.rabbitmq 集群搭建需要注意哪些问题?

149.rabbitmq 每个节点是其他节点的完整拷贝吗?为什么?

150.rabbitmq 集群中唯一一个磁盘节点崩溃了会发生什么情况?

151.rabbitmq 对集群节点停止顺序有要求吗?

十五、Kafka

152.kafka 可以脱离 zookeeper 单独使用吗?为什么?

153.kafka 有几种数据保留的策略?

154.kafka 同时设置了 7 天和 10G 清除数据,到第五天的时候消息达到了 10G,这个时候 kafka 将如何处理?

155.什么情况会导致 kafka 运行变慢?

156.使用 kafka 集群需要注意什么?

十六、Zookeeper

157.zookeeper 是什么?

158.zookeeper 都有哪些功能?

159.zookeeper 有几种部署模式?

160.zookeeper 怎么保证主从节点的状态同步?

161.集群中为什么要有主节点?

162.集群中有 3 台服务器,其中一个节点宕机,这个时候 zookeeper 还可以使用吗?

163.说一下 zookeeper 的通知机制?

十七、MySql

164.数据库的三范式是什么?

165.一张自增表里面总共有 7 条数据,删除了最后 2 条数据,重启 mysql 数据库,又插入了一条数据,此时 id 是几?

166.如何获取当前数据库版本?

167.说一下 ACID 是什么?

168.char 和 varchar 的区别是什么?

169.float 和 double 的区别是什么?

170.mysql 的内连接、左连接、右连接有什么区别?

171.mysql 索引是怎么实现的?

172.怎么验证 mysql 的索引是否满足需求?

173.说一下数据库的事务隔离?

174.说一下 mysql 常用的引擎?

175.说一下 mysql 的行锁和表锁?

176.说一下乐观锁和悲观锁?

177.mysql 问题排查都有哪些手段?

178.如何做 mysql 的性能优化?

十八、Redis

179.redis 是什么?都有哪些使用场景?

180.redis 有哪些功能?

181.redis 和 memecache 有什么区别?

182.redis 为什么是单线程的?

183.什么是缓存穿透?怎么解决?

184.redis 支持的数据类型有哪些?

185.redis 支持的 java 客户端都有哪些?

186.jedis 和 redisson 有哪些区别?

187.怎么保证缓存和数据库数据的一致性?

188.redis 持久化有几种方式?

189.redis 怎么实现分布式锁?

190.redis 分布式锁有什么缺陷?

191.redis 如何做内存优化?

192.redis 淘汰策略有哪些?

193.redis 常见的性能问题有哪些?该如何解决?

十九、JVM

194.说一下 jvm 的主要组成部分?及其作用?

195.说一下 jvm 运行时数据区?

196.说一下堆栈的区别?

197.队列和栈是什么?有什么区别?

198.什么是双亲委派模型?

199.说一下类加载的执行过程?

200.怎么判断对象是否可以被回收?

201.java 中都有哪些引用类型?

202.说一下 jvm 有哪些垃圾回收算法?

203.说一下 jvm 有哪些垃圾回收器?

204.详细介绍一下 CMS 垃圾回收器?

205.新生代垃圾回收器和老生代垃圾回收器都有哪些?有什么区别?

206.简述分代垃圾回收器是怎么工作的?

207.说一下 jvm 调优的工具?

208.常用的 jvm 调优的参数都有哪些?

展开
收起

2020 年最流行的 Java 开发技术

不知不觉间,2020 年即将于十几天之后到来,作为技术圈中你,准备好迎接最新的变化了吗?在本文中,我们将以编程界最常用的编程语言 Java 为例,分享最为主流的技术与工具。

作者 | divyesh.aegis

译者 | 弯月,责编 | 屠敏

以下为译文:

Java几乎无处不在,无论在智能手机、台式机、游戏设备还是科学超级计算机上,处处都有Java的影子。全世界有数百万的Java程序员在开发基于Java的产品。

然而,如此激烈的竞争,意味着Java开发人员必须时刻保持领先地位。

为此,他们必须随时了解和洞悉Java生态系统中的最新动态。Java程序员需要不断进步。

在本文中,我们将讨论2020年Java开发人员需要掌握的Java最新趋势、工具、技术和功能。

Git

Git一直是世界上最受欢迎的Java工具之一,也是Java开发人员最杰出的工具之一。Git是一个开源工具,是一种出色的分布式版本控制解决方案。

你可以利用Git管理所有内容,无论是小项目还是大项目。此外,这个工具还有助于提高项目的速度和效率。

这个工具不仅易学,而且非常实用。Git最突出的功能之一就是提供闪电般的性能。学习和掌握Git的途径很多,最好的方法之一是参加 Udemy 或其他在线机构的课程。

持续关注OpenJDK

OpenJDK是Java SE的免费版本。它不仅包含Java社区的贡献,还包含Oracle的贡献。另外,最新版本的Java中甚至包含了几个来自OpenJDK的二进制文件。人们对于社区贡献与开源贡献的意识并没有消失。

事实证明,OpenJDK 对 Java开发人员的帮助良多。因此,该公司正在尝试升级OpenJDK。由于OpenJDK中添加了许多功能,因此开发人员必须关注新出现的功能。

进一步了解单元测试

为了职业生涯的发展,你应该专心掌握单元测试,因为这方面的技术很有价值。有很多非常适合单元测试的新框架、功能、技术和工具,可供Java开发人员使用。

还有一些用于集成测试的工具,包括 PowerMock。另外,开发人员还可以研究一下 Robot Framework,因为它可以简化自动集成测试的过程。

因此,仅仅提高编程技术还不够,你还需要注意提高单元测试的技术。学习单元测试的途径有很多,比如线上和线下的课程。但是,最好的方法是实践单元测试,并确保你的测试技术能够达到要求。

移动Java开发

预计在未来几年中,移动应用程序的开发将以飞快的速度增长。有报告称,到2020年,全球智能手机的销量有望达到16亿部。

因此,在2020年,Java开发人员应该关注与智能手机开发相关的新技巧、工具和趋势。许多 Android 程序员都比较喜欢Java。

这是因为他们相信Java的可靠性,相信Java可以帮助他们为移动平台开发高质量的产品。

有人认为Java是最适合制作现代移动应用程序的语言之一。它可以在包括 Android 在内的各种平台上运行。Android 是应用最广泛的操作系统之一,而它也是用 Java 编写的。

因此,毫无疑问,通过学习Java,你能够为 Android 操作系统开发出色的移动应用程序。另外,如果你想抓住移动应用程序技术的未来,那么就应该专心学习Java移动应用程序的开发。

Kotlin 也变得非常有价值

说起 Android 应用程序开发,Kotlin 似乎在开发人员中颇受欢迎。各个企业对学习 Kotlin 非常感兴趣,而且他们渴望利用 Kotlin 来开发优秀的基于Java的手机产品。

因此,Kotlin 和 Kotlin 开发人员的需求也在与日俱增。如果你想学习 Kotlin,那么可以选择一些线上线下的课程。Kotlin Bootcamp等教程提供了代码库。

这些教程致力于为学习者提供实践知识,以便开发人员快速掌握 Kotlin。

Java开发人员需要勤练习

提升Java技术力的最佳途径就是不断练习。练习Java项目可以帮助你轻松掌握语言本身,甚至还可以掌握新功能和工具。另外,如果你练习建立新项目,那么可以更快地掌握Java。同时,别忘了听取Java专家的建议,了解更多有关Java的信息。

Java开发人员还应该注意提高自己的沟通技巧。如果你想成为一名出众的程序员,那么沟通技巧至关重要。另外,如果你想提高自己的职业生涯,那么还需掌握其他技能,包括沟通能力。

最后,请密切关注Java领域即将发生的大事和会议,以获取Java相关的最新动态。

原文:https://hackernoon.com/java-developers-are-you-excited-for-the-new-things-in-2020-3d1i32g0

本文为 CSDN 翻译,转载请注明来源出处。

【End】

展开
收起

Java之异常的简单介绍

各位小伙伴们大家好,这次小编要介绍的是Java当中的异常,首先我们来介绍一下异常的概念,异常是指程序在执行过程中出现的非正常情况,最终导致JVM的非正常停止。

在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象,异常指的并不是语法错了,语法错了编译不通过,不会产生字节码文件程序不会运行。Java处理异常的方式是中断处理。

在异常体系中,异常的根类是java.lang.Throwable,其下有两个子类:java.lang.Error与java.lang.Exception,平常所说的异常指java.lang.Exception。异常机制其实是帮助我们找到程序中的问题。

Java中异常有两种,一种是Exception,编译期异常,进行编译Java程序出现的问题,其下有一个子类RuntimeException,运行期异常,Java程序运行过程中出现的问题。

异常就相当于一个人得了一点小感冒,把异常处理掉程序就可以继续执行。

第二种,Error是错误,这种情况下,程序就像生了一场大病,必须修改源代码,就像做手术,程序才可以继续执行。

异常的处理一,代码如下:

public class Demo05Exception {

public static void main(String args[]) throws ParseException {

//Exception:编译异常,进行编译Java程序出现问题

SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");//用来格式化日期

Date date=sdf.parse("2019-122");//把字符串日期,解析为Date日期

System.out.println(date);

}

}

throws ParseException利用虚拟机进行处理,异常会被直接抛出在控制台上:Exception in thread "main" java.text.ParseException: Unparseable date: "2019-122"。

异常处理方式二,代码如下:

public class Demo05Exception {

public static void main(String args[]) {

//Exception:编译异常,进行编译Java程序出现问题

SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd");//用来格式化日期

Date date= null;//把字符串日期,解析为Date日期

try {

date = sdf.parse("2019-122");

} catch (ParseException e) {

e.printStackTrace();

}

System.out.println(date);

System.out.println("把异常处理掉,后续程序继续执行");

}

}

用try catch处理异常代码之后,后续的程序会继续运行。

异常二,RuntimeException:运行期异常,Java程序运行过程中出现问题。

代码如下:

public class Demo05Exception {

public static void main(String args[]) {

//RuntimeException:运行期异常,Java程序运行过程中出现的问题

char[] arr={'a','b','c'};

System.out.println(arr[4]);

System.out.println("把异常处理掉,后续程序继续执行");

}

}

异常:Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 4

异常处理,代码如下:

public class Demo05Exception {

public static void main(String args[]) {

//RuntimeException:运行期异常,Java程序运行过程中出现的问题

char[] arr={'a','b','c'};

try {

//可能会出现异常的代码

System.out.println(arr[4]);

}catch(Exception e){

//异常的处理逻辑

System.out.println(e);

}

System.out.println("把异常处理掉,后续程序继续执行");

}

}

第二种异常,Error错误:

public class Demo05Exception {

public static void main(String args[]) {

int[] arr=new int[1024*1024*1024];

System.out.println("把异常处理掉,后续程序继续执行");

}

}

运行结果:Exception in thread "main" java.lang.OutOfMemoryError: Java heap space,数组长度溢出,需要对代码进行处理。

关于Java中的异常小编就先说到这里,主要有两种,一种是Exception,编译期异常,进行编译Java程序出现的问题,其下有一个子类RuntimeException,运行期异常,这些异常经过一些简单的处理之后程序是可以运行的。还有一种是报错Error,这种需要修改源代码。希望这篇文章可以帮到大家,也欢迎各位小伙伴补充和纠错。

图片来自网络,如有侵权,请联系作者删除

展开
收起

Java 开发者最困惑的四件事

掌握 Java 绝非易事,因为许多概念非常晦涩,非常复杂。

作者 | Himanshu Verma

译者 | 弯月,责编 | 屠敏

以下为译文:

大概每个人在学生时代开始就使用Java了,我们一直在学习Java,但Java中总有一些概念含混不清,不论是对初级还是高级程序员都是如此。所以,这篇文章的目的就是弄清楚这些概念。读完本文你会对这些概念有更深入的了解,还能弄清楚一切灰色的东西。在本书中,我们将讨论匿名内联类、多线程、同步和序列化。

匿名类的用法多线程同步的实现序列化

匿名类

Java匿名类很像局部类或内联类,只是没有名字。我们可以利用匿名类,同时定义并实例化一个类。只有局部类仅被使用一次时才应该这么做。匿名类不能有显式定义的构造函数。相反,每个匿名类都隐含地定义了一个匿名构造函数。

创建匿名类有两种方法:

扩展已有的类(可以是抽象类,也可以是具体类)创建接口理解代码的最好方法就是先阅读,所以我们首先来看看代码。

interfaceFootball{voidkick();}classAnnonymousClass {publicstatic Football football = new Football() { @Overridepublicvoidkick() { System.out.println("Nested Anonymous Class."); } };publicstaticvoidmain(String[] args){// anomynous class inside the method Football footballObject = new Football() { @Overridepublicvoidkick(){ System.out.println("Anonymous Class"); } }; footballObject.kick(); AnnonymousClass.football.kick(); } }

匿名类可以在类和函数代码块中创建。你也许知道,匿名类可以用接口来创建,也可以通过扩展抽象或具体的类来创建。上例中我先创建了一个接口Football,然后在类的作用域和main()方法内实现了匿名类。Football也可以是抽象类,也可以是与interface并列的顶层类。

Football可以是抽象类,请看下面的代码。

publicabstractclassFootball{ abstractvoidkick();}

匿名类不仅可以是抽象类,还可以是具体类。

// normal or concrete classpublic classFootball{ public void kick(){}}// end ofclassscope.

如果Football类没有不带参数的构造方法怎么办?我们可以在匿名类中访问类变量吗?我们需要在匿名类中重载所有方法吗?

// normal or concrete classpublicclassFootball {protectedint score;publicFootball(int score){this.score = score; }publicvoidscore(){ System.out.println("Score "+score); };publicvoidkick(){}publicstaticvoidmain(String[] args) { Football football = new Football(7) { @Overridepublicvoidscore() { System.out.println("Anonymous class inside the method "+score); } }; football.score(); }}// end of class scope.

创建匿名类时可以使用任何构造方法。注意这里也使用了构造方法的参数。匿名类可以扩展顶层类,并实现抽象类或接口。所以,访问控制的规则依然适用。我们可以访问protected变量,而改成private就不能访问了。由于上述代码中扩展了Football类,我们不需要重载所有方法。但是,如果它是个接口或抽象类,那么必须为所有未实现的方法提供实现。匿名类中不能定义静态初始化方法或成员接口。匿名类可以有静态成员变量,但它们必须是常量。 匿名类的用途:

更清晰的项目结构:通常我们在需要随时改变某个类的某些方法的实现时使用匿名类。这样做就不需要在项目中添加新的*.java文件来定义顶层类了。特别是在顶层类只被使用一次时,这种方法非常好用。UI事件监听器:在图形界面的应用程序中,匿名类最常见的用途就是创建各种事件处理器。例如,下述代码:button.setOnClickListener(new View.OnClickListener() {publicvoidonClick(View v) {// your handler code here } });

我们创建了一个匿名类,实现了setOnClickListener接口。当用户点击按钮时会触发它的onClick方法。

多线程

Java中的多线程能够同时执行多个线程。线程是轻量级的子进程,也是处理的最小单位。使用多线程的主要目的是最大化CPU的使用率。我们使用多线程而不是多进程,因为线程更轻量化,也可以共享同一个进程内的内存空间。多线程用来实现多任务。

线程的生命周期

如上图所示,线程的生命周期主要有5个状态。我们来依次解释每个状态。

New:创建线程的实例后,它会进入new状态,这是第一个状态,但线程还没有准备好运行。Runanble:调用线程类的start()方法,状态就会从new变成Runnable,意味着线程可以运行了,但实际上什么时候开始运行,取决于Java线程调度器,因为调度器可能在忙着执行其他线程。线程调度器会以FIFO(先进先出)的方式从线程池中挑选一个线程。Blocked:有很多情况会导致线程变成blocked状态,如等待I/O操作、等待网络连接等。此外,优先级较高的线程可以将当前运行的线程变成blocked状态。Waiting:线程可以调用wait()进入waiting状态。当其他线程调用notify()时,它将回到runnable状态。Terminated:start()方法退出时,线程进入terminated状态。为什么使用多线程?

使用线程可以让Java应用程序同时做多件事情,从而加快运行速度。用技术术语来说,线程可以帮你在Java程序中实现并行操作。由于现代CPU非常快,还可能包含多个核心,因此仅有一个线程就没办法使用所有的核心。

需要记住的要点

多线程可以更好地利用CPU。提高响应性,提高用户体验减少响应时间同时为多个客户端提供服务创建线程的方法主要有两种:

扩展Thread类实现Runnable接口通过扩展Thread类来创建线程

创建一个类扩展Thread类。该类应当重载Thread类中的run()方法。线程在run()方法中开始生命周期。我们创建新类的对象,然后调用start()方法开始执行线程。在Thread对象中,start()会调用run()。

publicclassMultithreadingTestextendsThread{publicvoidrun(){try{ System.out.println("Thread "+Thread.currentThread().getName()+" is now running"); }catch (Exception ex) { ex.printStackTrace(); } }publicstaticvoidmain(String[] args){for(int i=0;i<10;i++) { MultithreadingTest multithreadingTest = new MultithreadingTest(); multithreadingTest.start(); } }}

也可以通过接口创建类。

下面的代码创建了一个类,实现java.lang.Runnable接口并重载了run()方法。然后我们实例化一个Thread对象,调用该对象的start()方法。

publicclassMultithreadingTestimplementsRunnable{@Overridepublicvoidrun(){ System.out.println("Thread "+Thread.currentThread().getName()+" is now running"); //To change body of generated methods, choose Tools | Templates. }publicstaticvoidmain(String[] args){for(int i=0;i<10;i++) { Thread thread = new Thread(new MultithreadingTest()); thread.start(); } }}

Thread类与Runnable接口

扩展Thread类,就无法扩展更多的类,因为Java不允许多重继承。多重继承可以通过接口实现。所以最好是使用接口而不是Thread类。如果扩展Thread类,那么它还包含了一些方法,如yield()、interrupt()等,我们的程序可能用不到。而在Runnable接口中就没有这些排不上用场的方法。

同步

同步指的是多线程的同步。synchronized的代码块在同一时刻只能被一个线程执行。Java中的同步是个很重要的概念,因为Java是多线程语言,多个线程可以并行执行。在多线程环境中,Java对象的同步,或者说Java类的同步非常重要。

为什么要同步?

如果代码在多线程环境下执行,那么在多个线程中共享的对象之间需要同步,以避免破坏状态,或者造成任何不可预料的行为。

在深入同步的概念之前先来理解一下这个问题。

classTable{voidprintTable(int n){//method not synchronized for (int i = 1; i <= 5; i++) { System.out.print(n * i+" ");try { Thread.sleep(400); } catch (Exception e) { System.out.println(e); } } }}classMyThread1extendsThread{ Table t; MyThread1(Table t) {this.t = t; }publicvoidrun(){ t.printTable(5); }}classMyThread2extendsThread{ Table t; MyThread2(Table t) {this.t = t; }publicvoidrun(){ t.printTable(100); }}classTestSynchronization1{publicstaticvoidmain(String args[]){ Table obj = new Table();//only one object MyThread1 t1 = new MyThread1(obj); MyThread2 t2 = new MyThread2(obj); t1.start(); t2.start(); }}

运行这段代码就会注意到,输出结果非常不稳定,因为没有同步。我们来看看程序的输出。

输出:

100 5 200 10 300 15 20 400 500 25

classTable {synchronized voidprintTable(int n) {//synchronized method for (int i = 1; i <= 5; i++) { System.out.print(n * i+" ");try { Thread.sleep(400); } catch (Exception e) { System.out.println(e); } } }}classTestSynchronization3 {publicstaticvoidmain(String args[]) { final Table obj = new Table();//only one object Thread t1 = new Thread() {publicvoidrun() { obj.printTable(5); } }; Thread t2 = new Thread() {publicvoidrun() { obj.printTable(100); } }; t1.start(); t2.start(); }}

给printTable()方法加上synchronized,那么synchronized的方法在执行结束之前不会让其他线程进入。下面的输出结果就非常稳定了。

输出:

5 10 15 20 25 100 200 300 400 500

类似地,Java的类和对象也可以同步。

注意:我们并不一定需要同步整个方法。有时候最好是仅同步方法的一小部分。Java的synchronized代码段可以实现这一点。

序列化

Java中的序列化是一种机制,可以将对象的状态写入到字节流中。相反的操作叫做反序列化,将字节流转换成对象。

序列化和反序列化的过程是平台无关的,也就是说,在一个平台上序列化对象,然后可以在另一个平台上反序列化。

序列化时调用ObjectOutputStream的writeObject()方法,反序列化调用ObjectInputStream类的readObject()方法。

下图中,Java对象被转换成字节流,然后存储在各种形式的存储中,这个过程叫做序列化。图右侧,内存中的字节流转换成Java对象,这个过程叫作反序列化。

为什么要序列化

显然,创建的Java类在程序执行结束或中止后,对象就销毁了。为了避免这个问题,Java提供了序列化功能,通过它可以将对象存储起来,或者将状态进行持久化,以便稍后使用,或者在其他平台上使用。

下面的代码演示了该过程。

publicclassEmployeeimplementsSerializable{privatestaticfinallong serialVersionUID = 1L;private String serializeValueName;privatetransientint nonSerializeValueSalary;public String getSerializeValueName(){return serializeValueName; }publicvoidsetSerializeValueName(String serializeValueName){this.serializeValueName = serializeValueName; }publicintgetNonSerializeValueSalary(){return nonSerializeValueSalary; }publicvoidsetNonSerializeValueSalary(int nonSerializeValueSalary){this.nonSerializeValueSalary = nonSerializeValueSalary; }@Overridepublic String toString(){return"Employee [serializeValueName=" + serializeValueName + "]"; }}

import java.io.FileOutputStream;import java.io.IOException;import java.io.ObjectOutputStream;publicclassSerializingObject{publicstaticvoidmain(String[] args){ Employee employeeOutput = null; FileOutputStream fos = null; ObjectOutputStream oos = null; employeeOutput = new Employee(); employeeOutput.setSerializeValueName("Aman"); employeeOutput.setNonSerializeValueSalary(50000);try { fos = new FileOutputStream("Employee.ser"); oos = new ObjectOutputStream(fos); oos.writeObject(employeeOutput); System.out.println("Serialized data is saved in Employee.ser file"); oos.close(); fos.close(); } catch (IOException e) { e.printStackTrace(); } }}

输出:

Serialized data is saved in Employee.ser file.

import java.io.FileInputStream;import java.io.IOException;import java.io.ObjectInputStream;publicclassDeSerializingObject {publicstaticvoidmain(String[] args) { Employee employeeInput = null; FileInputStream fis = null; ObjectInputStream ois = null;try { fis = new FileInputStream("Employee.ser"); ois = new ObjectInputStream(fis); employeeInput = (Employee)ois.readObject(); System.out.println("Serialized data is restored from Employee.ser file"); ois.close(); fis.close(); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } System.out.println("Name of employee is : " + employeeInput.getSerializeValueName()); System.out.println("Salary of employee is : " + employeeInput.getNonSerializeValueSalary()); }}

输出:

Serialized data is restored from Employee.ser fileNameof employee is : AmanSalary of employee is : 0

需要记住的重点

如果父类实现了Serializable接口,那么子类就不需要实现了,但反过来不一定成立。只有非静态数据成员可以在序列化过程中保存下来。静态数据成员和临时数据成员不会在序列化过程中保存下来。所以,如果不想保存某个非静态数据成员,则可以将其设置为transient。反序列化过程中不会调用对象的构造函数。关联对象必须实现Serializable接口。

总结

首先我们解释了匿名类,以及用途和使用方法。其次我们讨论了Java中的多线程,线程的生命周期,以及用途。同步只允许一个线程进入同步的方法或代码块去访问资源,其他线程必须在队列中等待。序列化就是存储对象状态供以后使用的过程。原文:https://medium.com/swlh/4-things-that-java-developer-thinks-are-most-confusing-complicated-87c2598f33f0

本文为 CSDN 翻译,转载请注明来源出处。

展开
收起

Java是什么,Java能做什么?

在中国Java是一种比较流行的编程语言,现在很多公司的首选语言几乎都是Java。那么Java究竟是什么呢?

什么是Java

Java是什么

Java是一款计算机编程语言,它是一种可以编写跨平台应用软件、完全面向对象的程序设计语言。它不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。#编程语言#

在《如果编程语言是武器》一书中有这么一段描述:

C语言是M1式加兰德步枪,很老但很可靠;C++是双截棍,挥舞起来很强悍,很吸引人,但需要你多年的磨练来掌握,很多人希望改用别的武器;PHP是水管,你通常会把它的一端接到汽车的排气管,另一端插进车窗里,然后你坐进车里,开动引擎;Python是一种“v2/v3”双管枪,每次只能用一个管子发射,你永远不知道该用哪个管子发射好;Java是M240通用弹夹式自动机枪。

Java是威力最大的武器

不难看出以上威力最大的武器就算Java。确实,相对而言Java是开发效率、开发门槛、性能、跨平台这几方面平衡得最好的语言。

Java语言发展史

Java是Sun公司于1995年推出的一款高级编程语言。说到高级肯定有低级,它的低级不是大家想象的那种低级,这里的低级指的是机器语言。

机器语言

很多人都听说过,计算机的世界是由0和1组成的,这里说的0和1指的就是机器语言。早期用的机器语言大家不好理解,对我们日常开发来说难度比较高,所以说我们就推出了许多高级语言。高级语言比较接近人类的自然语言,这样的话可以降低一些我们的开发门槛。

从Java语言发展史中不难发现,前几年Java都是两年一更新,最近是半年一更新,所以说最近更新比较频繁。目前Java已经更新到了14,但是现在大家用得最多的版本还是Java 8。因为Java8版本相对之前一些版本来说改动得比较大,而Java9、Java10这些相对于Java8来说改动得比较少。而像12、13、14这些版本,用的人也不多,因为这些新版本的推出其实可能还不够稳定,而做开发的话,稳定是最重要的。Java8版本相对比较稳定,而且Java8的功能完全能满足我们日常开发需求。

Java的应用方向

Java的方向其实有很多,我们可以做网页开发,比如 Java web,我们还可以做一个app的后台服务的接口,比如网页开发我们可以通过浏览器搜索进入网页页面。

Java网页开发(Java web)

网页开发:其实很多后台服务都可以用Java实现,但我们看到的这个页面其实是一个前端的技术,比如说一些HTML、CSS还有一些JS。我们Java开发的话,你不仅仅要学Java技术,可能我们会涉及到一些前端的技术。

App后台服务接口:在这个app流行的时代,我们每天都在使用app。那么这些app的数据是从哪里来的呢?

app的数据是从后台服务来的,而后台服务则是由编程语言进行编写,如C、C++这些等等。

而Java做的则是一些逻辑判断,比如说你要登录某个app,你储存在app中的个人信息也是通过Java去调用数据库返回给前端,然后前端根据服务器返回的需求做展示。

所以说,Java其实做的是一个后台服务,也可以做一个后台服务的接口。当然现在安卓的上层应用层也是Java编程的,但Java并不是等同于安卓。安卓其实是Java的一个分支,它的SDK其实包括了Java大部分的SDK了,但它也剔除了一些对它没用的SDK。

Java大数据平台开发:这里的大数据平台开发就是Hadoop,Hadoop是一个开源的Java平台。我们现在的Java课其实都是偏向网页开发的。

今天的内容分享就到这里啦,大家记得点赞关注喔~

展开
收起

java必背综合知识点总结(基础篇)

一、JDK常用的包

java.lang: 这个是系统的基础类,比如String、Math、Integer、System和Thread, 提供常用功能。java.io: 这里面是所有输入输出有关的类,比如文件操作等java.net: 这里面是与网络有关的类,比如URL,URLConnection等。java.util : 这个是系统辅助类,特别是集合类Collection,List,Map等。java.sql: 这个是数据库操作的类,Connection, Statememt,ResultSet等

二、Get和Post的区别

1.get是从服务器上获取数据,post是向服务器传送数据,2.get传送的数据量较小,不能大于2KB。post传送的数据量较大,一般被默认为不受限制。3.get安全性非常低,post安全性较高。但是执行效率却比Post方法好。4.在进行文件上传时只能使用post而不能是get。

三、Java多态的具体体现

面向对象编程有四个特征:抽象,封装,继承,多态。多态有四种体现形式:

接口和接口的继承。类和类的继承。重载。重写。其中重载和重写为核心。重载:重载发生在同一个类中,在该类中如果存在多个同名方法,但是方法的参数类型和个数不一样,那么说明该方法被重载了。

重写:重写发生在子类继承父类的关系中,父类中的方法被子类继承,方法名,返回值类型,参数完全一样,但是方法体不一样,那么说明父类中的该方法被子类重写了。

四、StringBuffer StringBuilder String 区别

String 字符串常量 不可变 使用字符串拼接时是不同的2个空间StringBuffer 字符串变量 可变 线程安全 字符串拼接直接在字符串后追加StringBuilder 字符串变量 可变 非线程安全 字符串拼接直接在字符串后追加

1.StringBuilder执行效率高于StringBuffer高于String.2.String是一个常量,是不可变的,所以对于每一次+=赋值都会创建一个新的对象, StringBuffer和StringBuilder都是可变的,当进行字符串拼接时采用append方 法,在原来的基础上进行追加,所以性能比String要高,又因为StringBuffer 是 线程安全的而StringBuilder是线程非安全的,所以StringBuilder的效率高于 StringBuffer.3.对于大数据量的字符串的拼接,采用StringBuffer,StringBuilder.

五、Hashtable与HashMap的区别

HashMap不是线程安全的,HashTable是线程安全。HashMap允许空(null)的键和值(key),HashTable则不允许。HashMap性能优于Hashtable。

Map1.Map是一个以键值对存储的接口。Map下有两个具体的实现,分别是HashMap和HashTable.2.HashMap是线程非安全的,HashTable是线程安全的,所以HashMap的效率高于HashTable.3.HashMap允许键或值为空,而HashTable不允许键或值为空.

六、九大隐式对象

输入/输出对象: request response out作用域通信对象: session application pageContextServlet 对象: page config错误对象: exception

七、Forword(请求转发)与Redirect(重定向)

1、从数据共享上Forword是一个请求的延续,可以共享request的数据Redirect开启一个新的请求,不可以共享request的数据2、从地址栏Forword转发地址栏不发生变化Redirect转发地址栏发生变化

八、JQurey总结

jquery是一个轻量级的js框架,具有跨浏览器的特性,兼容性好,并且封装了很多工具,方便使用。常用的有: 选择器 ,dom操作 ,ajax(ajax不能跨域) ,特效,工具类

九、XML和Json的特点

Xml特点:1、有且只有一个根节点;2、数据传输的载体3、所有的标签都需要自定义4、是纯文本文件

Json(JavaScript Object Notation)特点:json分为两种格式:

json对象(就是在{}中存储键值对,键和值之间用冒号分隔,键 值 对之间用逗号分隔);

json数组(就是[]中存储多个json对象,json对象之间用逗号分隔)(两者间可以进行相互嵌套)数据传输的载体之一

区别:传输同样格式的数据,xml需要使用更多的字符进行描述,流行的是基于json的数据传输。xml的层次结构比json更清晰。

共同点:xml和json都是数据传输的载体,并且具有跨平台跨语言的特性。

十、request.getSession()、reqeust.getSession(false)和 request.getSession(true)

getSession()/getSession(true):当session存在时返回该session,否则新建一个 session并返回该对象getSession(false):当session存在时返回该session,否则返回null

十一、Page和PageContext的区别

Page是servlet对象;使用this关键字,它的作用范围是在同一页面。PageContext是作用域通信对象;通常使用setAttribute()和getAttribute()来设置和获取存放对象的值。

十二、Ajax总结

AJAX 全称: 异步JavaScript及 XML(Asynchronous JavaScript And XML)Ajax的核心是JavaScript对象XmlHttpRequest(XHR)。

Ajax的优点:提高用户体验度(UE)提高应用程序的性能进行局部刷新AJAX不是一种新的编程语言,而是一种用于创建更好更快以及交互性更强的 Web 应用程序的技术。2. 通过 AJAX,我们的 JavaScript 可使用JavaScript的XMLHttpRequest对象来直接与服务器进行通信。通过这个对象,我们的 JavaScript 可在不重载页面的情况与Web服务器交换数据,即可局部刷新。3. AJAX 在浏览器与 Web 服务器之间使用异步数据传输(HTTP 请求),这样就可使网页从服务器请求少量的信息,而不是整个页面,减轻服务器的负担,提升站点的性能。AJAX 可使因特网应用程序更小、更快,更友好,用户体验(UE)好。5. Ajax是基于标准化并被广泛支持的技术,并且不需要插件和下载小程序

十三、JSP9大隐视对象中四个作用域的大小与作用范围

四个作用域从大到小:appliaction>session>request>pageapplication:全局作用范围,整个应用程序共享.生命周期为:应用程序启动到停止。session:会话作用域,当用户首次访问时,产生一个新的会话,以后服务器就可以记 住这个会话状态。request:请求作用域,就是客户端的一次请求。page:一个JSP页面。以上作用范围使越来越小, request和page的生命周期都是短暂的,他们之间的区别就是:一个request可以包含多个page页(include,forward)。

十四、List,Set,Collection,Collections

1.List和Set都是接口,他们都继承于接口Collection,List是一个有序的可重复的集合,而Set的无序的不可重复的集合。Collection是集合的顶层接口,Collections是一个封装了众多关于集合操作的静态方法的工具类,因为构造方法是私有的,所以不能实例化。

2.List接口实现类有ArrayList,LinkedList,Vector。ArrayList和Vector是基于数组实现的,所以查询的时候速度快,而在进行增加和删除的时候速度较慢LinkedList是基于链式存储结构,所以在进行查询的时候速度较慢但在进行增加和删除的时候速度较快。又因为Vector是线程安全的,所以他和ArrayList相比而言,查询效率要低。

十五、java的基本数据类型

数据类型 大小byte(字节) 1(8位)shot(短整型) 2(16位)int(整型) 4(32位)long(长整型) 8(32位)float(浮点型) 4(32位)double(双精度) 8(64位)char(字符型) 2(16位)boolean(布尔型) 1位附加:String是基本数据类型吗?(String不是基本数据类型)String的长度是多少,有限制?(长度受内存大小的影响)

十六、冒泡排序

public class Sort {public static void sort() {Scanner input = new Scanner(System.in);int sort[] = new int[10];int temp;System.out.println("请输入10个排序的数据:");for (int i = 0; i < sort.length; i++) {sort[i] = input.nextInt();}for (int i = 0; i < sort.length - 1; i++) {for (int j = 0; j < sort.length - i - 1; j++) {if (sort[j] < sort[j + 1]) {temp = sort[j];sort[j] = sort[j + 1];sort[j + 1] = temp;}}}System.out.println("排列后的顺序为:");for(int i=0;i<sort.length;i++){System.out.print(sort[i]+" ");}}public static void main(String[] args) {sort();}}

十七、二分查找法

public class BinarySearch { /** * 二分查找算法 * * @param srcArray 有序数组 * @param key 查找元素 * @return key的数组下标,没找到返回-1 */ public static void main(String[] args) { int srcArray[] = {3,5,11,17,21,23,28,30,32,50,64,78,81,95,101}; System.out.println(binSearch(srcArray, 0, srcArray.length - 1, 81)); } // 二分查找递归实现 public static int binSearch(int srcArray[], int start, int end, int key) { int mid = (end - start) / 2 + start; if (srcArray[mid] == key) { return mid; } if (start >= end) { return -1; } else if (key > srcArray[mid]) { return binSearch(srcArray, mid + 1, end, key); } else if (key < srcArray[mid]) { return binSearch(srcArray, start, mid - 1, key); } return -1; } // 二分查找普通循环实现 public static int binSearch(int srcArray[], int key) { int mid = srcArray.length / 2; if (key == srcArray[mid]) { return mid; } int start = 0; int end = srcArray.length - 1; while (start <= end) { mid = (end - start) / 2 + start; if (key < srcArray[mid]) { end = mid - 1; } else if (key > srcArray[mid]) { start = mid + 1; } else { return mid; } } return -1; } }

十八、时间类型转换

public class DateFormat {public static void fun() {SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日");String newDate;try {newDate = sdf.format(new SimpleDateFormat("yyyyMMdd").parse("20121115"));System.out.println(newDate);} catch (ParseException e) {e.printStackTrace();}}public static void main(String args[]) {fun();}}

十九、阶乘

public class Multiply {public static int multiply(int num) {if (num < 0) {System.out.println("请输入大于0的数!");return -1;} else if (num == 0 || num == 1) {return 1;} else {return multiply(num - 1) * num;}}public static void main(String[] args) {System.out.println(multiply(10));}}

二十、UE和UI的区别

UE 是用户体验度UI 界面原型(用户界面)(相当于买房时用的模型)

设计UI的作用:1、帮助程序员工作(界面已由美工设计完成)2、提前让用户对项目有个宏观的了解,知道效果是什么样子。

二十一、osi七层模型

第一层:物理层第二层:数据链路层第三层:网络层第四层:传输层第五层:会话层第六层:表示层第七层:应用层

二十二、线程和进程的区别

1.线程(Thread)与进程(Process)进程定义的是应用程序与应用程序之间的边界,通常来说一个进程就代表一个与之对应的应用程序。不同的进程之间不能共享代码和数据空间,而同一进程的不同线程可以共享代码和数据空间。

2.一个进程可以包括若干个线程,同时创建多个线程来完成某项任务,便是多线程。3.实现线程的两种方式:继承Thread类,实现Runable接口

二十三、jvm的内存结构

java虚拟机的内存结构分为堆(heap)和栈(stack),堆里面存放是对象实例也就是new出来的对象。栈里面存放的是基本数据类型以及引用数据类型的地址。

对于所谓的常量是存储在方法区的常量池里面。

二十四、内存泄露和内存溢出

内存泄露 (memory leak),是指应用程序在申请内存后,无法释放已经申请的内存空间.一次内存泄露危害可以忽略,但如果任其发展最终会导致内存溢出(out of memory).如读取文件后流要进行及时的关闭以及对数据库连接的释放。

内存溢出(out of memory)是指应用程序在申请内存时,没有足够的内存空间供其使用。如我们在项目中对于大批量数据的导入,采用分段批量提交的方式。

二十五、单例

单例就是该类只能返回一个实例。单例所具备的特点:1.私有化的构造函数2.私有的静态的全局变量3.公有的静态的方法单例分为懒汉式、饿汉式和双层锁式饿汉式:

public class Singleton1 {private Singleton1() {};private static Singleton1 single = new Singleton1();public static Singleton1 getInstance() {return single;}}

懒汉式:

public class Singleton2 {private Singleton2() {}private static Singleton2 single=null;public tatic Singleton2 getInstance() {if (single == null) {single = new Singleton2();}return single;}}

线程安全:

public class Singleton3 {private Singleton3() {}private static Singleton3 single ;public static Singleton3 getInstance() {if(null == single){synchronized(single ){if(null == single){single = new Singleton3();}}}return single;}}

参考:

通过双重判断来保证单列设计模式在多线程中的安全性,并且它在性能方面提高了很多。

synchronized在方法上加锁 (同步锁)

synchronized在代码块内部加锁 (同步代码块)

synchronized(同步锁)

使用synchronized如何解决线程安全的问题?1.synchronized在方法上加锁2.synchronized在代码块内部加锁

1.懒汉 2.饿汉 3.双重判断

二十六、解析xml文件的几种技术

1、 解析xml的几种技术1.dom4j2.sax3.jaxb4.jdom5.dom1.dom4jdom4j是一个Java的XML API,类似于jdom,用来读写XML文件的。dom4j是一个非常优秀的Java XML API,具有性能优异、功能强大和极端易用使用的特点,同时它也是一个开放源代码的软件。2.saxSAX(simple API for XML)是一种XML解析的替代方法。相比于DOM,SAX是一种速度更快,更有效的方法。它逐行扫描文档,一边扫描一边解析。而且相比于DOM,SAX可以在解析文档的任意时刻停止解析,但任何事物都有其相反的一面,对于SAX来说就是操作复杂。3.jaxbJAXB(Java Architecture for XML Binding) 是一个业界的标准,是一项可以根据XML Schema产生Java类的技术。该过程中,JAXB也提供了将XML实例文档反向生成Java对象树的方法,并能将Java对象树的内容重新写到XML实例文档。从另一方面来讲,JAXB提供了快速而简便的方法将XML模式绑定到Java表示,从而使得Java开发者在Java应用程序中能方便地结合XML数据和处理函数。

2、dom4j 与 sax 之间的对比:【注:必须掌握!】dom4j不适合大文件的解析,因为它是一下子将文件加载到内存中,所以有可能出现内存溢出,sax是基于事件来对xml进行解析的,所以他可以解析大文件的xml也正是因为如此,所以dom4j可以对xml进行灵活的增删改查和导航,而sax没有这么强的灵活性所以sax经常是用来解析大型xml文件,而要对xml文件进行一些灵活(crud)操作就用dom4j

二十七、项目的生命周期

1.需求分析2.概要设计3.详细设计(用例图,流程图,类图)4.数据库设计(powerdesigner)5.代码开发(编写)6.单元测试(junit 白盒测试)(开发人员)svn版本管理工具(提交,更新代码,文档)7.集成测试 (黑盒测试,loadrunner(编写测试脚本)(高级测试))8.上线试运行 (用户自己体验)9.压力测试(loadrunner)10.正式上线11.维护

二十八、OSCache的判断

Object obj = CacheManager.getInstance().getObj(“oaBrandList”);//从缓存中取数据if (null == obj) {obj = brandDao.getBrandList();//如果为空再从数据库获取数据//获取之后放入缓存中CacheManager.getInstance().putObj(“oaBrandList”, obj);}return (List)obj;

二十九、经常访问的技术网站

1.csdn(详细步骤的描述)2.iteye(详细步骤的描述)3.oschina(开源中国获取java开源方面的信息技术)4.java开源大全 www.open-open.com(获取java开源方面的信息技术)5.infoq(对java,php,.net等这些语言的一些最新消息的报道)

三十、项目团队中交流的工具

飞秋(局域网) qq(局域网,外网)RTX(局域网,外网) 邮箱 (局域网,外网)

三十一、平时浏览的书籍

实战经验:*** in action(实战)*** 深入浅出*** 入门指南思想基础:大话设计模式 重构

三十二、java Exception体系结构

java 异常是程序运行过程中出现的错误。Java把异常当作对象来处理,并定义一个基类java.lang.Throwable作为所有异常的超类。在Java API中定义了许多异常类,分为两大类,错误Error和异常Exception。其中异常类Exception又分为运行时异常(RuntimeException)和非运行时异常(非runtimeException),也称之为不检查异常(Unchecked Exception)和检查异常(Checked Exception)。

1、Error与ExceptionError是程序无法处理的错误,比如OutOfMemoryError、ThreadDeath等。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。

Exception是程序本身可以处理的异常,这种异常分两大类运行时异常和非运行时异常。程序中应当尽可能去处理这些异常。

2、运行时异常和非运行时异常运行时异常: 都是RuntimeException类及其子类异常: IndexOutOfBoundsException 索引越界异常ArithmeticException:数学计算异常NullPointerException:空指针异常ArrayOutOfBoundsException:数组索引越界异常ClassNotFoundException:类文件未找到异常ClassCastException:造型异常(类型转换异常)

这些异常是不检查异常(Unchecked Exception),程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的。

非运行时异常:是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如:IOException、文件读写异常FileNotFoundException:文件未找到异常EOFException:读写文件尾异常MalformedURLException:URL格式错误异常SocketException:Socket异常SQLException:SQL数据库异常

三十三、session和cookie的区别

session是存储在服务器端,cookie是存储在客户端的,所以安全来讲session的安全性要比cookie高,然后我们获取session里的信息是通过存放在会话cookie里的sessionid获取的。又由于session是存放在服务器的内存中,所以session里的东西不断增加会造成服务器的负担,所以会把很重要的信息存储在session中,而把一些次要东西存储在客户端的cookie里,然后cookie确切的说分为两大类分为会话cookie和持久化cookie,会话cookie确切的说是,存放在客户端浏览器的内存中,所以说他的生命周期和浏览器是一致的,浏览器关了会话cookie也就消失了,然而持久化cookie是存放在客户端硬盘中,而持久化cookie的生命周期就是我们在设置cookie时候设置的那个保存时间,然后我们考虑一问题当浏览器关闭时session会不会丢失,从上面叙述分析session的信息是通过会话cookie的sessionid获取的,当浏览器关闭的时候会话cookie消失所以我们的sessionid也就消失了,但是session的信息还存在服务器端,这时我们只是查不到所谓的session但它并不是不存在。那么,session在什么情况下丢失,就是在服务器关闭的时候,或者是session过期(默认时间是30分钟),再或者调用了invalidate()的或者是我们想要session中的某一条数据消失调用session.removeAttribute()方法,然后session在什么时候被创建呢,确切的说是通过调用getsession()来创建,这就是session与cookie的区别.访问HTML页面是不会创建session,但是访问index.JSP时会创建session(JSP实际上是一个Servlet, Servlet中有getSession方法)

三十四、字节流与字符流的区别

stream结尾都是字节流,reader和writer结尾都是字符流两者的区别就是读写的时候一个是按字节读写,一个是按字符。实际使用通常差不多。在读写文件需要对内容按行处理,比如比较特定字符,处理某一行数据的时候一般会选择字符流。只是读写文件,和文件内容无关的,一般选择字节流。

三十五、final,finally,finalize 三者区别

Final是一个修饰符:当final修饰一个变量的时候,变量变成一个常量,它不能被二次赋值当final修饰的变量为静态变量(即由static修饰)时,必须在声明这个变 量的时候给它赋值当final修饰方法时,该方法不能被重写当final修饰类时,该类不能被继承Final不能修饰抽象类,因为抽象类中会有需要子类实现的抽 象方法,(抽 象类中可以有抽象方法,也可以有普通方法,当一个抽象类中没有抽象方 法时,这个抽象类也就没有了它存在的必要)Final不能修饰接口,因为接口中有需要其实现类来实现的方法Finally:Finally只能与try/catch语句结合使用,finally语句块中的语句一定会执行, 并且会在return,continue,break关键字之前执行finalize:Finalize是一个方法,属于java.lang.Object类,finalize()方法是GC (garbage collector垃圾回收)运行机制的一部分,finalize()方法是在 GC清理它所从 属的对象时被调用的

三十六、Io流的层次结构

从流的方向输入流 输出流

从流的类型上字符流 字节流

inputstream和outputstream都是抽象类

它们下面的实现包括

FileInputStream,BufferedInputStream

FileOutputStream,BufferedOutputStream

reader 和 writer

FileReader,BufferedReader,StringReader

FileWriter,BufferedWriter,StringWriter,PrintWriter

三十七、JAVA:

Java是面向对象的,跨平台的,它通过java虚拟机来进行跨平台操作,它可以进行自动垃圾回收的【c语言是通过人工进行垃圾回收】,java还会进行自动分配内存。【c语言是通过指定进行分配内存的】,只需要new一个对象,这个对象占用了多少空间,不需要我们来管,java虚拟机负责管这些,用完之后也不需要我们来释放,java虚拟机会自动释放

三十八、JavaSE JavaEE JavaME区别

是什么:Java SE=Java Standard Edition=j2se = java 标准版Java EE=Java Enterprise Edition=j2ee= java 企业版Java ME=Java Mobile Edition=j2me = java移动版特点:SE主要用于桌面程序(swing),控制台开发(main程序)。

EE企业级开发(JSP,EJB,Spring MVC,Struts,hibernate,ibatis等),用于企业级软件开发,网络开发,web开发。

ME嵌入式开发(手机,小家电,PDA)。[苹果的ios,黑莓]

三者之间的关系:Java SE(Java Platform, Standard Edition,Java标准版)就是基于JDK和JRE的。

Java SE为Java EE提供了基础。

Java EE除了基于我们这个所谓的Java SE外,还新加了企业应用所需的类库

三十九、JDK JRE JVM的区别:

Jdk【Java Development ToolKit】就是java开发工具箱, JDK是整个JAVA的核心里边包含了jre,它除了包含jre之外还包含了一些javac的工具类,把java源文件编译成class文件,java文件是用来运行这个程序的,除此之外,里边还包含了java源生的API,java.lang.integer在rt的jar包里边【可以在项目中看到】,通过rt这个jar包来调用我们的这些io流写入写出等JDK有以下三种版本:

J2SE,standard edition,标准版,是我们通常用的一个版本J2EE,enterpsise edtion,企业版,使用这种JDK开发J2EE应用程序J2ME,micro edtion,主要用于移动设备、嵌入式设备上的java应用程序Jre【Java Runtime Enviromental】是java运行时环境,那么所谓的java运行时环境,就是为了保证java程序能够运行时,所必备的一基础环境,也就是它只是保证java程序运行的,不能用来开发,而jdk才是用来开发的,所有的Java程序都要在JRE下才能运行。包括JVM和JAVA核心类库和支持文件。与JDK相比,它不包含开发工具——编译器、调试器和其它工具。

Jre里边包含jvmJvm:【Java Virtual Mechinal】因为jre是java运行时环境,java运行靠什么运行,而底层就是依赖于jvm,即java虚拟机,java虚拟机用来加载类文件,java中之所以有跨平台的作用,就是因为我们的jvm

关系:J2se是基于jdk和jre,JDK是整个JAVA的核心里边包含了jre,Jre里边包含jvm

四十、报错的状态码:

301 永久重定向302 临时重定向304 服务端 未改变403 访问无权限200 正常404 路径500 内部错误

四十一、协议以及默认的端口号

ftp 21 文件传输协议Pop3 110 它是因特网 http://baike.baidu.com/view/1706.htm电子邮件http://baike.baidu.com/view/1524.htm的第一个离线 http://baike.baidu.com/view/113466.htm协议标准Smtp 25 简单邮件传输协议http 80 超文本传输协议oracle 默认端口号1521mysql默认端口号 3306

四十二、抽象类与接口的区别

1.一个类只能进行单继承,但可以实现多个接口。

2.有抽象方法的类一定是抽象类,但是抽象类里面不一定有抽象方法;接口里面所有的方法的默认修饰符为public abstract,接口里的成员变 量默认的修饰符为 pulbic static final。

关系接口和接口 继承接口和抽象类 抽象类实现接口类和抽象类 类继承抽象类类和类 继承

四十三、修饰符的作用

修饰符的作用范围:

四十四、onready和onload的区别

1.onready比onload先执行

2.onready是在页面解析完成之后执行,而onload是在页面所有元素加载后执行

3.onload只执行最后一个而onready可以执行多个。

参考:

1.执行时间window.onload必须等到页面内包括图片的所有元素加载完毕后才能执行。$(document).ready()是DOM结构绘制完毕后就执行,不必等到加载完毕。2.编写个数不同window.onload不能同时编写多个,如果有多个window.onload方法,只会执行一个$(document).ready()可以同时编写多个,并且都可以得到执行3.简化写法window.onload没有简化写法

(document).ready(function())可以简写成

(function(){});

另外,需要注意一点,由于在$(document).ready()方法内注册的事件,只要DOM就绪就会被执行,因此可能此时元素的关联文件未下载完。例如与图片有关的html下载完毕,并且已经解析为DOM树了,但很有可能图片还没有加载完毕,所以例如图片的高度和宽度这样的属性此时不一定有效。要解决这个问题,可以使用Jquery中另一个关于页面加载的方法—load()方法。Load()方法会在元素的onload事件中绑定一个处理函数。如果处理函数绑定给window对象,则会在所有内容(包括窗口、框架、对象和图像等)加载完毕后触发,如果处理函数绑定在元素上,则会在元素的内容加载完毕后触发。Jquery代码如下:$(window).load(function(){//编写代码});等价于JavaScript中的以下代码Window.onload=function(){//编写代码}

四十五、switch默认接受的几种数据类型

Short, int, byte, char

四十六、request 跟session的区别

1.他们的生命周期不同,request对应的是一次请求,session对应的是一次会话2.request占用资源比较少,相对来说缺乏持续性,而session资源消耗比较大,所以通常使用request来保存信息

四十七、找到解决svn冲突方法

对于svn冲突,可以采用手工处理将冲突的部分进行整合,之后备份最新整合后的文件,采用覆盖更新的方式处理完冲突之后,再把最新整合后的文件进行提交。

四十八、反射的描述

通过字符串可以动态创建java对象,并且可以动态访问方法,属性等。我们在项目中的时候封装过数据库jdbc的持久层,其中就利用反射这项技术来达到通用和灵活的目的。

展开
收起

什么?Java8还没弄懂!Java 11就正式发布了!

想必有很多人对Java8都没有没有精通,但是Java11都已经发布了,Java程序猿们想必都抓破了脑袋!下面,我们就来具体的分析一下。

Java官方宣布 Java 11 在北京时间 9 月 26 日正式发布。真是“千呼万唤始出来”。然而大多程序猿却不为之感冒,对于这 “Java XS MAX”,就和安卓开发的版本一样,绝大部分的开发版本总比官方的版本慢三个版本,这是“常识”。而在官方的强烈推荐下(不支持Java8,对Java11提供长期支持),逼着Java程序猿学习新的内容。

最新发布的 JDK 11 共带来了十多项的重大更新,其中具有突破性的是:Java11实现了局部变量类型推断的扩展。类型推断是从其余源代码和键入规则中推导出的数据类型。这无疑是一次巨大的进步,对于开发者来说。并且从 Java 10 开始,可以使用关键字 var 声明局部变量,如下所示:

这也是对于开发者来说,极大提高开发效率的进步。Java11 中另一个亮点在于新 HTTP Client API 的标准化,HTTP Client API 除了实现了HTTP(1.1和2)、WebSocket。同时使用清晰易懂的 Fluent 界面,将来可能会淘汰其他 HTTP 客户端(如 Apache)的使用。

Java的启动单文件源代码程序现在可以启动尚未编译的类。在脚本语言和小程序领域。减少很多不必要的编译时间的等待。甚至,基于 Java 10 的程序中可以通过三种方式启动:“* .class文件”,“作为* .jar文件中的主类”,“作为模块中的主类”。

令人欣喜的是Java 11 不仅提供了长期支持,还将作为 Java 平台的参考实现。而且新的长期支持版本每三年发布一次。总之,Java的发展是越来越好,功能也与时俱进,符合开发的需求。但是,在实际的开发当中,却面临很多的问题,比如有很多团队在Java10上“翻了车”,不敢轻易尝试新的版本,害怕调到坑中在返回使用Java10。可见,对于企业来说,还是稳中取胜的。

在小编看来,Java11还是比较良心的,提供长期的支持,大家还是去尝试一下吧!

展开
收起

Java0基础教程——java的安装

在阅读本篇教程的时候,建议您先快速通读一下有个大概的了解后,再边阅读边用电脑跟着我们的步骤操作,来完成您的第一个Java程序。阅读完本教程后,您应该具备搭建JDK环境,并能用记事本开发第一个简单的Java程序,并对java的编译有一定的理解。本文适合对Java语言感兴趣且无任何编程语言基础的读者,最好能具备一定的电脑基础。

Java1

理论部分:

什么是JDK? JDK是 Java 语言的软件开发工具包,主要用于移动设备、嵌入式设备上的java应用程序。JDK是整个java开发的核心,它包含了JAVA的运行环境(JVM+Java系统类库)和JAVA工具。

开发Java程序的步骤是什么?

Java2

本文开发第一个Java程序的步骤是什么?安装JDK并配置JDK环境使用记事本编写第一个Java程序,HelloWord.java在Dos里面编译并运行Java程序实操部分:

下载Java开发工具包——JDK,下载地址:

Java3

如果您的操作系统是64位的,请下载:jdk-8u181-windows-x64.exe,如图所示:

Java4

如果您的系统是32位的,请下载jdk-8u181-windows-i586.exe,如图所示:

Java5

如果您觉得在官网下载时需要注册账号比较麻烦,您还可以选择通过我百度网盘分享的连接进行下载:

Java6

在安装前,最好先创建一个空文件夹,用于作为安装路径,如:D:\jdk8,而不要用系统盘如C:盘作为安装路径。下载完成以后,双击已下载的.exe文件进行安装,如图:

Java7

更改安装路径,如之前创建的安装路径:D:\jdk8

Java8

更改好路径以后点击下一步等待安装,如图:

Java9

在随后弹出的对话框中选择jre的安装路径,最好是和刚才的安装路径在同一级目录下,如都是D:\jdk8,如果提示说当前的目录不为空路径,则可以在此路径下创建一个新的空文件夹jre,则路径应为:D:\jdk8\jre,好了以后点下一步继续

Java10Java11

等待安装程序完成安装,当出现下图所示则说明安装完成,然后点击关闭按钮,结束安装。

Java12

配置环境变量: 1、先找到刚才的安装路径如:D:\jdk8,里面的内容大概如图所示,而我目前的安装路径为:D:\Program Files\jdk8,怕出错的话,可以复制你的安装路径

Java13

2、此电脑(我的电脑)->鼠标右键单击->属性,如图所示:

Java14

3、高级系统设置->环境变量…,然后打开窗口如图:

Java15

4、创建系统变量JAVA_HOME,如图所示:

Java16

5、在弹出的对话框中的变量名输入框中输入:JAVA_HOME,在变量值输入框中输入jdk的安装路径,如:D:\jdk8,图中变量值为我的安装路径,和您的未必一样,此处请填写您JDK的安装路径。

Java17

6、修改系统变量Path,如图:

Java18

7、以后安装完成以后,我们需要检查下我们的JDK是否安装好了。在开始里点运行:开始->运行或者,通过快捷键win+R,打开运行窗口,在窗口的输入框中输入cmd,如图:

Java19Java20Java21

使用记事本编写第一个Java程序HelloWord.java 1、先创建一个记事本,比如名为:HelloWord.txt,如图所示:

Java22

2、在记事本中输入如下代码,并保存,如图所示:

Java23

3、保存以后,将文件名HelloWord.txt的扩展名改为.java,则此时文件名应为:HelloWord.java,如图所示:

Java24

到此,我们编写Java程序就已经完成了,在这个过程中,有几个容易犯的错误,大家需要引起注意:

Class后面的HelloWord与文件名HelloWord.java中的HelloWord不一致。System.out.println("Hello World"); 后面的‘;’容易忘记,这个是英文半角的‘;’没有注意的话,容易打成中文的‘;’。在写代码的时候没有严格按照大小写来。括号没有成对出现。为了方便大家测试,我将我写的代码先贴出来,供大家参考。public class HelloWord {

/* 第一个Java程序

* 它将打印字符串 Hello World

*/

public static void main(String[] args) {

// TODO Auto-generated method stub

System.out.println("Hello Word");// 打印 Hello World

}

}

在Dos里面编译并运行Java 1、通过开始->运行或通过win+R打开运行窗口,并输入在打开的窗口中输入cmd,如图:

Java25

2、在打开的窗口中通过dos命令cd找到你刚才创建的文件的路径,比如:我是刚在桌面上创建的HelloWord.java,那么我的路径应为:C:\Users\逍遥\Desktop,所以我的Dos命令可以写为:cd Desktop 或者 cd C:\Users\逍遥\Desktop,如图所示

Java26Java27

3、在Dos里面运行javac HelloWord.java 注意中间有空格,如图所示,如果报错为:“错误:编码GBK不可映射的字符……”则有可能是你的记事本的编码格式是utf-8的,则可使用:javac -encoding utf-8 HelloWord.java,需要注意的是javac、-encoding、utf-8、HelloWord.java之间有空格相隔,如图所示:

Java28

4、此时再查看你源码的路径,我当前的路径为桌面,则发现多出一个名为HelloWord.class文件,这种文件我们也称之为字节码文件,如图所示:

Java29

5、再回到Dos窗口中继续运行java HelloWord,则可看到运行的结果,在Dos命令窗口中输出了“HelloWord”,这样一个字符串,至此,说明您的第一个Java程序已经编写成功。

Java30

至此,我们今天的内容就到此结束了,如果您能做出来,那么也恭喜您开启了Java之门!想成为一名优秀的软件工程师,需要坚持不懈的努力与奋斗,这个过程我希望我能陪您一起走下去!加油!未来的It精英们!

展开
收起

JAVA-二进制基础

一、二进制的概念

计算机进制有:二进制、八进制、十六进制

进制的基本特点是:每一位置上的数字必须在0-(进制-1)的范围内,也就是说二进制只能有0、1;八进制为:0-7;十六进制比较特殊为:0-9,A(10) B(11) C(12) D(13) E(14) F(15)

进制转换:

1、非十进制转换十进制:每位数字乘以进制数的权重次方,将所有位置上的结果进行求和即可。

进制的权重:一个数值,在每一位都有一个权重,权重为从右向左数,位数-1

进行转化: 举例:将二进制011转10进制为:从右向左,1*2的0次方+1*2的1一次方+0*2的2次方=3; 将八进制34转化成10进制:4*8的0次方+3*8的1次方=28

2、十进制转非十进制: 采用短除法: 用十进制数除以要转化的进制数,用本次除法的商继续进行除以要转化的进制数的除法运算,一直到商为0,保留每次除法的余数,将余数按照从后往前进行排序,即为最终转化后的数。

举例:将10进制100转化成八进制数: 十进制 100: 100/8(进制数) 商:12 余数 4 继续用上次的商12除以8:12/8 商:1 余数 4 继续用上次的商1除以8:1/8 商:0 余数 1 商为0,停止运算,将余数从后往前排序:144 得到最终转化后的八进制为144。 十进制转十六进制同理

二、位运算

优点:

(1)特定情况下,计算方便,速度快,被支持面广

(2)如果用算数方法,速度慢,逻辑复杂

2.1、按位与&

两位全为一,结果才为1

0&0=0;0&1=0;1&0=0;1&1=1;

用法:

(1)清零,如果想要将一个单元清零,即使其全部二进制位0,只要与一个各位都为零的数值相与,结果位零

(2)取一个数中指定位,找一个数,对应X要取的位,该数的对应位为1,其余为零,此数与X进行“与运算”可以得到X中的指定位

2.2、按位或|

只要有一个为1,结果就为1

0|0=0;1|0=1;0|1=1;1|1=1;

用法:常用来对一个数据的某些位置设为1,找到一个数,对应X要设置为1的位,该数的对应位为1,其余位为零,此数与X想与可使X中某些位设置为1

2.3、异或运算^

两个相应位为“异”(值不同),则改位结果为1,否则为0

0^0=0;0^1=1;1^0=1;1^1=0;

用法:

(1)使特定位翻转 找一个数,对应X要翻转的各位,该数的对应位为1,其余位为0,此数与X对应位异或即可

(2)与0相异或,保留原值

两个变量交换值

(1)借助第三个变量来实现

(2)利用加减法实现两个变量的交换

A=A+B;B=A-B;B=A-B

(3)异或运算

任意一个变量X与其自身进行异或运算,结果为0

A=A^B;B=A^B;A=A^B

2.4、取反运算~

对一个二进制数按位取反,即将0变1,1变0

~1=0;~0=1;

2.5、左移运算<<

将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)

2<<1=4

若左移时舍弃的最高位不包含1,则每左移一位,相当于该数乘以2

2.6、右移运算>>

将一个数的二进制位全部右移若干位。正数左补0,负数左补1,右边丢弃。 每右移1位,相当于除以2.

左补0还是1得看被移数是正还是负

2.7、无符号右移运算>>>

各位向右移动指定位数。右移后的左边空出位用0补。右边的位被丢弃。 与>>的不同。 >>>不管正数负数。左边只会补0; >>正数补0,负数补1

2.8、示例

原码:一个整数按照绝对值大小转换成的二进制数称为原码

反码:将二进制数按位取反,取得的新二进制数称为原二进制数的反码。

补码:反码加1称为补码

三、JDK内置的进制转换

四、JAVA中的进制

JAVA中二进制用的多吗?平时开发中“进制转换”和“位操作”用的不多,Java处理的是高层;在跨平台中用的较多,如:文件读写,数据通信(客户机(java)——0.1进制——服务器(C))。

基本类型:int型数据类型:byte(8bit -128~127)1 short(16bit)2 int(32bit)4 long(64bit)8字节float数据类型:单精度(32bit float)4字节 双精度(64bit double)boolean类型变量的取值:true false 1bitchar数据类型:Unicode字符,16位 2字节对应的类类型:Byte Short Integer Long Float Double Boolean Character

数据类型转换字节:1.大小端:

小端法:低位字节排放在内存的低地址端即该值的起始地址,高位字节排放在内存的高地址端大端法:高位字节排放在内存的低地址端即该位置的起始地址,低位字节排放在内存的高地址端2.字符串->字节数组:String s ; byte [] bs = s.getBytes();字节数组->字符串:byte[] bs = new byte[int]; String s = new String(bs); 或String s = new String(bs,encode); //encode指编码方式:gb2312, utf8

展开
收起