您的位置:首页 > 职场人生

java程序员面试笔试题(五)

2016-10-20 10:54 232 查看
1.java解析字符串表达式-逆波兰表达式

问题来由:

读入一个字符串形式的四则运算表达式,输出对应的计算结果。如读入的是“6 * ( 5 + ( 2 + 3) * 8 + 3)”,那么解析后的输出结果应为288。

思路:

一般的计算过程是这样的,首先计算优先级最高的小括号里面的内容,即“( 5 + ( 2 + 3) * 8 + 3)”,
将“2 + 3”的计算结果并存为A,接着用计算“A*8”,并存为B
计算“5+B+3”,结果存为C
最后计算“6*C”,计算完毕
我们可以将这种操作顺序书写如下:

6 5 2 3 + 8 * + 3 + *

这个记法就是逆波兰(reverse Polish)表达式,其求值过程恰好就是上面所描述的过程。逆波兰表达式又叫做后缀(postfix)表达式。在通常的表达式中,运算符总是置于与之相关的两个运算对象之间,所以,这种表示法也称为中缀表示。波兰逻辑学家 J.Lukasiewicz于1929年提出了另一种表示表达式的方法。按此方法,每一运算符都置于其运算对象之后,故称为后缀表示。

后缀表达式的计算

计算后缀表达式最简单的方法就是使用一个栈:

当读到一个数字时就将它压入栈中
读到一个运算符时,就从栈中弹出两个数字,并将该运算符作用于这两个数字,然后将计算结果再压入栈中
下面演示下“6 5 2 3 + 8 * + 3 + *”的处理过程

首先读到 6 5 2 3 四个数字,没啥说的,直接打入栈中(6 5 2 3)
读到运算符“+”,从栈中弹出两个数字“3”、“2”,计算 3+2 = 5,将计算结果压入栈中;(6 5 5)
读到8,压入栈(6 5 5 8)
读到“*”,从栈中弹出两个数字“8”、“5”,计算 8 * 5 = 40,将计算结果压入栈中;(6 5 40)
读到“+”,从栈中弹出两个数字“40”、“5”,计算 40 + 5 = 45,将计算结果压入栈中;(6 45)
读到3,压入栈(6 45 3)
读到“+”,从栈中弹出两个数字“3”、“45”,计算 3 + 45 = 48,将计算结果压入栈中;(6 48)
读到“*”,从栈中弹出两个数字“48”、“6”,计算 48 * 6 = 288,计算完毕!!!

计算一个后缀表达式话费的时间是O(N),该算法的计算非常简单,同时不需要知道任何的计算优先级。

那么现在的问题就是如何将一个正常的表达式转换为后缀表达式?

真正的难点在于如何将一个正常的字符串表达式(中缀表达式)变成一个后缀表达式。如将6 * ( 5 + ( 2 + 3) * 8 + 3)变为6 5 2 3 + 8 * + 3 + *

逆波兰表达式,它的语法规定,表达式必须以逆波兰表达式的方式给出。逆波兰表达式又叫做后缀表达式。这个知识点在数据结构和编译原理这两门课程中都有介绍,下面是一些例子:

正常的中缀表示逆波兰表达式
a+ba,b,+
a+(b-c)a,b,c,-,+
a+(b-c)*da,b,c,-,d,*,+
a+d*(b-c)a,d,b,c,-,*,+
首先约定表达式中运算符的优先级,从大到小依次为:()、* 和 /、+ 和 -。暂时只考虑四则运算。

顺序读取字符串表达式,规则:

读到的是操作数,直接输出;
读到的是操作符(+-*/)(记为read),将其与栈顶的操作符(记为top)进行优先级比较:read>top,read入栈,继续读下一个;read≤top,top出栈,并输出到list中,read继续和新的top比较;top为空,read直接入栈;若top是“(”,read直接入栈,因为“(”优先级最高;
括号的处理:读到左括号“(”,直接将其压入栈中,并且除非遇到右括号“)”,“(”是不会弹出的;读到右括号“)”,将“(”之上的元素全部依次输出,并弹出“(”但不输出;
准备:一个栈stack暂存运算符,一个list存放输出,仍以6 * ( 5 + ( 2 + 3) * 8 + 3)为例。

读到“6”,直接输出【list:6;stack:】
读到“*”,与栈顶top比较,top为空,“*”入栈【list:6;stack:*】
读到“(”,直接入栈【list:6;stack:*,(】
读到“5”,直接输出【list:6,5;stack:*,(】
读到“+”,与栈顶top比较,“+”<“(”,入栈【list:6,5;stack:*,(,+】
读到“(”,直接入栈【list:6,5;stack:*,(,+,(】
读到“2”,直接输出【list:6,5,2,;stack:*,(,+,(】
读到“+”,与栈顶“(”比较,“+”<“(”,入栈【list:6,5,2,;stack:*,(,+,(,+】
读到“3”,直接输出【list:6,5,2,3,;stack:*,(,+,(,+】
读到“)”,输出“(”之上的元素【list:6,5,2,3,+;stack:*,(,+,】
读到“*”,与栈顶的“+”比较, “*”>“+”,“*”入栈【list:6,5,2,3,+;stack:*,(,+,*,】
读到“8”,直接输出【list:6,5,2,3,+,8,;stack:*,(,+,*,】
读到“+”,与栈顶的“*”比较,“+”<“*”,“*”出栈并输出【list:6,5,2,3,+,8,*;stack:*,(,+,】;
“+”,与栈顶的“+”比较,“+”=“+”,“+”出栈并输出【list:6,5,2,3,+,8,*,+;stack:*,(,】;
“+”,与栈顶的“(”比较,“+”<“(”,“+”入栈【list:6,5,2,3,+,8,*,+;stack:*,(,+】;
读到“3”,直接输出,【list:6,5,2,3,+,8,*,+,3;stack:*,(,+】
读到“)”,输出“(”之上的元素,【list:6,5,2,3,+,8,*,+,3,+;stack:*,】
输出栈中所有剩下的,【list:6,5,2,3,+,8,*,+,3,+,*;stack:,】
最终的后缀表达式就是:6 5 2 3 + 8 * + 3 + *

2.实现多线程的两种方法:Thread和Runable

使用Runnable接口

实际工作中,几乎所有的多线程应用都用实现Runnable这种方式。
Runnable适合多个相同程序代码的线程去处理同一资源的情况。把虚拟CPU(线程)同程序的代码、数据有效的分离,较好的体现了面向对象的设计思想。

避免由于Java的单继承特性带来的局限性。也就是如果新建的类要继承其他类的话,因为JAVA中不支持多继承,就只能实现java.lang.Runnable接口。

有利于程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。  

继承Thread类

不能再继承他类了。

编写简单,可以直接操纵线程,无需使用Thread.currentThread()。

请查询API 获得currentThread方法的作用说明。

使用 Runnable 接口来实现多线程使得我们能够在一个类中包容所有的代码,有利于封装,它的缺点在于,我们只能使用一套代码,若想创建多个线程并使各个线程执行不同的代码,则仍必须额外创建类,如果这样的话,在大多数情况下也许还不如直接用多个类分别继承 Thread 来得紧凑。

3.线程同步的方法:sychronized,lock,reentrantlock等

在并发量比较小的情况下,使用synchronized是个不错的选择,但是在并发量比较高的情况下,其性能下降很严重,此时ReentrantLock是个不错的方案。

1、ReentrantLock 拥有Synchronized相同的并发性和内存语义,此外还多了 锁投票,定时锁等候和中断锁等候

     线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定,

     如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断

     如果 使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情

 

    ReentrantLock获取锁定与三种方式:

    a)  lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁

    b) tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;

    c)tryLock(long timeout,TimeUnit unit),   如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;

    d) lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断

2、synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中

3、在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;

5.0的多线程任务包对于同步的性能方面有了很大的改进,在原有synchronized关键字的基础上,又增加了ReentrantLock,以及各种Atomic类。了解其性能的优劣程度,有助与我们在特定的情形下做出正确的选择。 

总体的结论先摆出来:  

synchronized: 

在资源竞争不是很激烈的情况下,偶尔会有同步的情形下,synchronized是很合适的。原因在于,编译程序通常会尽可能的进行优化synchronize,另外可读性非常好,不管用没用过5.0多线程包的程序员都能理解。 

ReentrantLock: 

ReentrantLock提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。在资源竞争不激烈的情形下,性能稍微比synchronized差点点。但是当同步非常激烈的时候,synchronized的性能一下子能下降好几十倍。而ReentrantLock确还能维持常态。 

Atomic: 

和上面的类似,不激烈情况下,性能比synchronized略逊,而激烈的时候,也能维持常态。激烈的时候,Atomic的性能会优于ReentrantLock一倍左右。但是其有一个缺点,就是只能同步一个值,一段代码中只能出现一个Atomic的变量,多于一个同步无效。因为他不能在多个Atomic之间同步。 

所以,我们写同步的时候,优先考虑synchronized,如果有特殊需要,再进一步优化。ReentrantLock和Atomic如果用的不好,不仅不能提高性能,还可能带来灾难。

4.锁的等级:方法锁,对象锁,类锁

首先介绍一下对象锁(也叫方法锁)与类锁有那些不同。下文中使用对象锁称呼代替方法锁。

  对于对象锁,是针对一个对象的,它只在该对象的某个内存位置声明一个标志位标识该对象是否拥有锁,所以它只会锁住当前的对象。一般一个对象锁是对一个非静态成员变量进行syncronized修饰,或者对一个非静态方法进行syncronized修饰。对于对象锁,不同对象访问同一个被syncronized修饰的方法的时候不会阻塞住。

类锁是锁住整个类的,当有多个线程来声明这个类的对象的时候将会被阻塞,直到拥有这个类锁的对象被销毁或者主动释放了类锁。这个时候在被阻塞住的线程被挑选出一个占有该类锁,声明该类的对象。其他线程继续被阻塞住。

无论是类锁还是对象锁,父类和子类之间是否阻塞没有直接关系。当对一个父类加了类锁,子类是不会受到影响的,相反也是如此。因为synchronized关键字并不是方法签名的一部分,它是对方法进行修饰的。当子类覆写父类中的同步方法或是接口中声明的同步方法的时候,synchronized修饰符是不会被自动继承的,所以相应的阻塞问题不会出现。

注意:这里的阻塞问题是指的按照正常情况下应该阻塞,而因为synchronized是父类与子类之间不可传递导致不会阻塞。那正常情况下阻塞是什么那,下面会详细介绍。但是,当一个子类没有覆盖父类的方法的时候,这时候通过子类访问方法则会产生阻塞。

插入一句:构造方法不可能是真正同步的(尽管可以在构造方法中使用同步块)。下面截图给出了如何声明一个对象锁和如何声明一个类锁:

1 void myMethod(){
2     synchronized(this){
3         //code
4     }
5 }
6
7 /*is equvilant to*/
8 void synchronized myMethod(){
9     //code
10 }

当同一个对象在线程1中访问一个方法,在线程2中再去访问另外一个加锁方法,则同样也会被阻塞.

对于类锁,则会把整个类锁住,也就说只能有一个对象拥有当前类的锁。当一个对象拥有了类锁之后,另外一个对象还想竞争锁的话则会被阻塞。两个对象A,B,如果A正在访问一个被类锁修饰的方法function,那么B则不能访问。因为类锁只能在同一时刻被一个对象拥有。相对于对象锁,则是不同。还是A,B两个对象,如果A正在访问对象锁修饰的function,那么这个时候B也可以同时访问。

对于对象锁,当一个对象拥有锁之后,访问一个加了对象锁的方法,而该方法中又调用了该类中其他加了对象锁的方法,那么这个时候是不会阻塞住的。这是java通过可重入锁机制实现的。可重入锁指的是当一个对象拥有对象锁之后,可以重复获取该锁。因为synchronized块是可重入的,所以当你访问一个对象锁的方法的时候,在该方法中继续访问其他对象锁方法是不会被阻塞的。

5.写出生产者和消费者模式

在实际的软件开发过程中,经常会碰到如下场景:某个模块负责产生数据,这些数据由另一个模块来负责处理(此处 的模块是广义的,可以是类、函数、线程、进程等)。产生数据的模块,就形象地称为生产者; 而处理数据的模块,就称为消费者

  单单抽象出生产者和消费者,还够不上是生产者/消费者模式。该模式还需 要有一个缓冲区处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据。

优 点

  可能有同学会问了:这个缓冲区有什么用捏?为什么不让生产者直接调用消费者的某个函数,直接把数据传递过去?搞出这么一 个缓冲区作甚?

  其实这里面是大有讲究的,大概有如下一些好处。

  ◇解耦

  假设生产者和消费者分别是两个类。如果让生产者 直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者的代码发生变化,可能会影响到生产者。而如果两者都依赖于某 个缓冲区,两者之间不直接依赖,耦合也就相应降低了。

     ◇支 持并发(concurrency)

  生产者直接调用消费者的某个方法,还有另一个弊端。由于函数调用是同步的(或者叫阻塞的),在消费者的方法 没有返回之前,生产者只好一直等在那边。万一消费者处理数据很慢,生产者就会白白糟蹋大好时光。

  使用了生产者/消费者模式之后,生产者和消费 者可以是两个独立的并发主体(常见并发类型有进程和线程两种,后面会讲两种并发类型下的应用)。生产者把制造出来的数据往缓冲区一丢,就可以再去生 产下一个数据。基本上不用依赖消费者的处理速度。

  其实当初这个模式,主要就是用来处理并发问题的。

      ◇支持忙闲不均

  缓冲区还有另一个好处。如果制造数据的速度时快时慢,缓冲区的好处就体现出来了。当数据制造快的时候,消费者来不及处理,未 处理的数据可以暂时存在缓冲区中。等生产者的制造速度慢下来,消费者再慢慢处理掉。

6.ThreadLocal的设计理念和作用

java中的ThreadLocal类允许我们创建只能被同一个线程读写的变量。因此,如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码,它们也无法访问到对方的ThreadLocal变量。

如何创建ThreadLocal变量

以下代码展示了如何创建一个ThreadLocal变量:

                    private   ThreadLocal    myThreadLocal=new ThreadLocal();

我们可以看到,通过这段代码实例化了一个ThreadLocal对象。我们只需要实例化对象一次,并且也不需要知道它是被哪个线程实例化。虽然所有的线程都能访问到这个ThreadLocal实例,但是每个线程却只能访问到自己通过调用ThreadLocal的set()方法设置的值。即使是两个不同的线程在同一个ThreadLocal对象上设置了不同的值,他们仍无法访问到对方的值。

  
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: