备战面试之Java语言基础

1、并发

使用并发编程可以同时进行多个任务,提升CPU的利用效率以及简化业务分割。

同时使用并发编程也会带来相应的问题,一个是安全性问题,一个是活跃性问题,还有一个是性能方面的问题。

其中,所谓安全性问题就是指,当多个线程同时访问共享数据,并且企图修改的时候,可能出现实际结果跟预期不一致的情况。解决这个问题有以下几种常用的方法:

  • 加锁:加锁就是将异步操作转变为同步操作,比如synchronized关键字,可重入锁ReentrantLock,这类锁又叫做独占锁,或者互斥锁,只有持有锁的线程才能执行程序,上面两种锁的主要区别是,使用Synchronized关键字加锁,没拿到锁的线程将会一直堵塞,而使用ReentrantLock加锁,没拿到锁的线程等待一段时间后会中断等待,转而去做别的事。
  • 非阻塞算法:加锁的优点就是使用简单,最大的缺点就是造成线程阻塞,影响性能,因此提出能够保证原子操作,又不会造成线程阻塞的算法替代加锁,CAS算法就是其中一个,CAS全程compare and swap,是一种先比较再交换的算法,CAS算法再写入内存之前,先比较内存里面的值是否等于预期的值,如果是就用新值替换旧值,否则不做更新,在并发编程中,只有第一个线程能通过比较并成功更新值,后面的线程因为内存中的值已经被修改了,与预期的值不同所以没法通过比较,然而CAS算法会遇到所谓的ABA问题,也就是一个线程把内存里的值由A更新成了B,又由B更新成了A,这时候第二个线程比较的时候发现内存里的值和预期的值也就是A一样,就会误判为还没更新而去修改内存中的值,进而引发线程安全问题,那么解决这个问题的方法是给每次修改都添加一个版本号,每次更新成功版本号加一,只有版本号也一致的时候才能判断为变量还没有更新。可以看到当多个线程使用CAS算法修改变量时,只有一个线程能成功,其他线程会返回修改失败,但是这个过程中并不会发生线程阻塞,即保证了线程安全,又提升了性能,鼓励使用。
  • 使用final关键字:加了final关键字的变量就是不可变变量,不可变变量不存在线程安全问题。
  • 使用volatile关键字:给对象加上volitail关键字,可以保证对象变化的可见性和有序性,也就是说,当对象的值改变后,其他线程可以马上获取到新值,在部分场景下可以避免线程安全问题。但是当对象的修改,基于其当前状态时,那么使用这种方式还是不能解决线程安全问题。比如变量自增,新值等于旧值加一,变化后的新值基于变量的当前状态。这种情况加volatile关键字仍然不能保证线程安全,这时候必需保证自增操作的原子性。但是由于使用了volatile关键字并没有额外开销,因此合理的使用能够大大提升性能。
  • ThreadLocal:上面几种方式,都是通过保证对共享变量操作的原子性、可见性、有序性来解决线程安全问题的,而ThreadLocal另辟蹊径,它采用为每个线程分配一个共享变量的副本来实现线程安全,既然每个线程操作的都是不同的变量,那么自然就没有线程安全问题了。

以上是解决线程安全问题常用的几种方法,如果通过加锁的方式解决线程安全问题除了影响性能之外,还有可能会引发死锁的问题,死锁问题就是当两个线程出现循环锁依赖时出现的问题,也就是说,如果存在两个线程,竞争两个对象锁,两个线程各自拿到了不同的对象锁,并企图占有另外线程手中的锁,这时候就会出现死锁,如果不加干预,程序将永远阻塞。解决死锁最好的方式就是合理安排锁顺序,避免发生死锁;使用CAS等非阻塞算法除了不会造成线程阻塞,提升性能之外,也不会发生死锁问题;使用final和volatile关键字是提升性能最有效的方式,但在有些情况下并不能解决线程安全问题;使用ThreadLocal更加简单有效,但同时增加了额外的内存开销,而且需要注意的是,为每个线程分配的副本必需通过深拷贝获得,如果副本只是对象的引用或者通过浅拷贝获得,那么实际上多个线程还是能访问同一块内存,线程安全问题仍然存在。

使用多线程模型在提升性能的同时,也会引入一些新的性能方面的问题,其中主要有以下两个方面:

  • 加锁引发的性能问题:因为加锁将异步操作变为同步,等待锁释放的线程处于阻塞状态,将大大影响性能,这种情况下常用的提升性能的手段有减小锁的粒度、使用非阻塞算法替代加锁(比如使用CAS算法替代独占锁)、使用final和volitale关键字,使用ThreadLocal替代加锁。
  • 线程切换带来的性能开销:并不是线程越多,性能就越好,因为线程的切换会引发额外的开销,比如线程的创建与销毁、线程调度、线程上下文的切换。因此需要针对具体情况决定开多少线程。针对CPU密集型的程序,CPU执行大量计算,长期处于一个比较高的占用率,这时候就不适合开辟多个线程,而针对IO密集型,也就是程序运行时间很大程度上取决与IO读取的响应时间,这时候为了避免CPU空转就要开多个线程,让CPU等待IO响应的时候能做其他事,说白了使用多线程就是让CPU尽可能多的参与到程序执行当中,如果本身CPU已经有较高的参与度的情况下,开多线程反而会带来额外开销,反之如果CPU比较空闲,那就需要给它分配额外的任务执行,也就是开多个线程。

2、谈一谈泛型

使用泛型的目的主要是为了使我们写的代码针对不同的数据类型可以重用,一个典型的应用就是泛型在容器中使用泛型,比如List,Map,这些容器使用泛型,那么容器中的元素就可以是任何非基础数据类型。

在Java中,泛型的设计采用的是类型擦除的方式实现的,也就是不管是什么泛型变量是什么数据类型,都会被转化为限定类型,如果没有指明限定类型就被转化为Object类型,而泛型类也会被转化为原始类型,比如ArrayList,其中的元素被转化为Object保存,ArrayList也会被转化为原始数据类型ArrayList。这样做的坏处是每次操作元素的时候都需要强制类型转换,而且由于基础数据类型无法强制转换为Object类型,因此需要用Integer,Double,Boolean等类型替代基础数据类型,就会不可避免地出现频繁拆装箱操作,拖慢程序执行速度。

3、谈一谈异常

由于程序运行的过程中不可避免的可能会出现一些错误,而Java语言希望在程序出现错误的时候能够通知开发人员,并且让开发人员可以在这时候自主的选择保存运行结果,继续执行其他程序或是用一种妥善的方式终止程序,因此Java引入了异常机制,使得当程序运行发生错误的时候抛出一个异常,这个异常可以被捕捉到,捕捉到异常之后就可以合理的进行接下来的行为。

在Java中,所有的异常类继承自Throwable类,它有两个直接子类,分别是Error和Exception,其中出现Error一般是Java运行时系统内部出现了错误,这时候只能退出应用程序,而Exception又分为RuntimeException和IOException,其中RuntimeException是由于程序写的有问题造成的,比如数组越界,空指针异常等,出现这种异常需要调试错误并改正,是可以避免的,因此Java语言不要求一定捕捉这种异常,另一种异常IOException可能是由于程序与其他应用实体输入输出过程中发生的异常,比如数据库连接中断抛出的SQLException,出现这种异常可能不是程序写的有问题,因此为了保证程序的健壮性,Java语言要求一定要捕获这种异常。

在Java语言中,捕获异常使用的是try/catch代码块,try代码块里面是可能抛出异常的代码,catch代码块是捕捉到异常后执行的代码,另外还可以跟随finally代码块,无论异常是否抛出异常,finally代码块里面的代码一定会被执行,一般会在finally代码块里面写关闭资源类的代码。

4、谈一谈反射

所谓反射技术,就是使我们能够在程序运行时知晓一个类的所有信息,包括所有的字段,方法,接口等,相当于利用反射机制,一个类的所有信息对我们来说就是透明的,我们可以在运行时查看,甚至动态修改它的行为。

使用反射机制+注解可以实现声明式编程,我们定义一个注解,将注解添加到某个类、方法或字段上,然后通过反射机制解析类里面的注解,根据注解类型执行操作。

使用反射机制+配置文件可以实现以配置的方式组装对象,比如Spring的xml配置文件配置bean。

反射机制然我们可以对任何一个类,可以访问它的所有属性和方法,对于任何一个对象,可以调用它的任意一个方法,这就意味着类的封装特性被破坏了,在带来了强大的自定义功能的同时也增加了风险。

5、谈一谈注解

注解是一种声明式编程方法,在我看来他也是一种封装手段,使用注解可以非常直观的看出来某个类或者某个方法,某个字段具有哪些什么样的行为或特点。比如添加了@Override注解的方法表示该方法重写了父类方法,添加了@Autowired注解表示该字段需要被自动注入等,非常直观。

注解就像是一个标签,目的是给添加注解的类、方法或字段添加行为,那么要使一个注解起作用肯定就包括定义注解,添加注解和解析注解,仅仅是定义注解和添加注解,注解并不会起作用,关键是要解析添加注解并执行操作,在这里可以通过Java的反射机制,获取到类中的注解,根据注解类型定义操作。

我曾在开发过程中使用过自定义注解,我的项目使分模块开发的,而当某个模块使用另外一个模块的时候,我在启动类上添加@Enable+模块名的注解,就可以引入其他模块的组件。具体实现就是我的每个模块是个Springboot模块,都有一个主配置类,我在每个模块里面定义一个注解,注解上添加@Import注解,引入该模块的主配置类。

6、谈一谈IO

7、谈一谈Java语言三大特性

Java是一门面向对象的编程语言,封装、继承和多态是面向对象语言的三大特性,所谓封装,就是将完成一个功能的代码集中起来,所谓继承就是一个类可以有它的派生类,派生类属于父类并且可以扩展自己的功能,所谓多态,就是指父类型的引用可以指向子类型对象。要实现一个良好的面向对象的设计,必需合理的利用这三大特性,下面我想说一下一个良好面向对象的设计需要具备哪些特点。

  • 代码复用:首先,一个良好的面向对象的设计,应该尽量的避免出现重复代码,合理的做法是将重复出现的代码封装起来,实现代码复用。

  • 高内聚:其次,一个良好的面向对象的设计还应该使每个类的职责尽量少,也就是所谓的高内聚,这样一来每个类的职责更清晰,二来能够更好的应对变化,如果职责过多,功能太复杂,那么当发生变化的时候,这个类就很容易收到影响。这就需要我们合理的封装每个类的职责。

  • 低耦合:一个良好的面向对象的设计还应该尽量减少类之间耦合,通过面向接口编程可以实现这一点,依赖方只需要关注如何使用接口,而不需要管实现这些接口的是哪个派生类,这样一来随意更换派生类,只要遵循接口,依赖方并不会收到影响。这就用到了三大特性的继承和多态机制。

  • 避免循环依赖:除此之外,一个良好的面向对象设计应该避免循环依赖的出现,当出现两个类相互依赖的时候,可以选择引入第三个类,使双方只依赖于这个类,从而解除了循环依赖。

总的来说,无论是代码复用还是减少每个类的职责,亦或是解除类于类之间的耦合、避免出现循环依赖,目的都是使我们的代码更加有弹性,能更好的应对变化,一个良好的面向对象设计应该围绕这一点展开。

举例来说,假如我们在设计软件的时候,有多个模块用到了缓存中间件,不好的做法是每个模块各自引入缓存中间件,那么如果更换中间件,每个模块都会收到影响,我认为这时候应该引入第三个模块,即缓存模块,缓存模块提供一些抽象的接口供其他模块使用,这样一来其他模块只需要关注如何使用这些接口,而缓存模块内部具体使用的哪个中间件,或者甚至可以不使用中间件,自己手动编码实现,对其他模块不会产生影响。

全部评论

相关推荐

码砖:求职岗位要突出,一眼就能看到,教育背景放到最后,学校经历没那么重要,项目要重点突出
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务