一、单项选择题 5. 有如下代码:
public static void main(String args[])
{
Threadt=DewThread()
{
public void run()
{
world();
}
};
t.run();
System.out.print("hello");
}
static void world()
{
System.out.print("world");
}
上面程序的运行结果是______。
A.helloworld B.worldhello C.A和B都有可能 D.都不输出
A B C D
B
[解析] Thread类提供了一个start()方法,这个方法的功能是让这个线程开始执行,当开始执行后,JVM将会调用这个线程的run()方法来执行这个线程的任务。如果直接调用run()方法就与调用普通的方法类似。 对于本题而言,首先调用t.run()方法,输出“world”,等调用结束后才会执行System.out.print("hello")语句,输出“hello”。所以,选项B正确。 如果把t.run()改成t.start(),在调用t.start()方法后不需要等这个线程结束,这个方法就会立即返回,然后执行语句System.out.print("Hllo"),在这种情况下,这两个输出语句的执行顺序是无法保证的,任何一个语句都有可能先执行,因此,答案就是选项C。
6. 有如下代码:
public class Test extends Thread
{
public static void main(String argv[])
{
Test b=new Test();
b.run();
}
public void start()
{
for(int i=0;i<10;i++)
{
System.out.println("Value of i="+i);
}
}
}
当编译并运行上面程序时,输出结果是______。
A.编译错误,指明run方法没有定义 B.运行错误,指明run方法没有定义 C.编译通过并输出0到9 D.编译通过但无输出
A B C D
D
[解析] 在Java语言中,可以采用以下两种方法来创建线程:继承Thread类与实现Runnable接口。其中,在使用Runnable接口时,需要建立一个Thread实例。所以,无论是通过Thread类创建线程还是通过Runnable接口创建线程,都必须建立Thread类或它的子类的实例。 Thread类提供了一个start()方法,该方法的功能是让这个线程开始执行,当这个线程开始执行后,JVM将会调用这个线程的run()方法来执行这个线程的任务。在实现多线程时,在继承了Thread类后必须实现run()方法,也就是说,线程的核心逻辑都存在于run()方法中,这个方法被start()方法调用来实现多线程的功能,如果直接调用run()方法,就与调用普通的方法类似。 对于本题而言,Test类继承了Thread类,但是没有重写Thread类的run()方法,因此,b.run()实际上调用的是Tbread类的run()方法,而Thread类的run()方法的方法体为空,故这个程序能编译通过,但是没有输出结果。所以,选项D正确。
10. 有如下代码:
public class X extends Thread implements Runable
{
public void run()
{
System.out.println("this is run()");
}
public static void main(String args[])
{
Thread t=new Thread(new X());
t.start();
}
}
程序的运行结果为______。
A.第一行会产生编译错误 B.第六行会产生编译错误 C.第六行会产生运行错误 D.程序会运行和启动
A B C D
21. Servlet处理请求的方式为______。
A.以程序的方式 B.以进程的方式 C.以线程的方式 D.以响应的方式
A B C D
C
[解析] SeiMet是采用Java语言编写的服务器端程序,运行于Web服务器的Servlet容器中,其主要功能是提供请求/响应的Web服务模式,可以生成动态的Web内容,工作原理如图所示。
Servlet工作原理
Servlet处理客户端请求通常有如下几个步骤:
1)用户通过单击一个链接来向Servlet发起请求。
2)Web服务器接收到该请求后,会把该请求提交给相应的容器来处理,当容器发现这是对Servlet发起的请求后,容器此时会创建两个对象:HttpServletResponse和HttpServletRequest。
3)容器可以根据请求消息中的URL消息找到对应的Servlet,然后针对该请求创建一个单独的线程,同时把第2)步中创建的两个对象以参数的形式传递到新创建的线程中。
4)容器调用Servlel的service()方法来完成对用户请求的响应,service()方法会调用doPost()方法或doGet()方法来完成具体的响应任务,同时把生成的动态页面返回给容器。
5)容器把响应消息组装成HTTP格式返回给客户端。此时,这个线程运行结束,同时删除第2)步创建的两个对象.HttpServletResponse和HttpServletRequest。
容器会针对每次请求创建一个新的线程进行处理,同时,会针对每次请求创建HttpServletResponse和HttpServletRequest两个对象,处理完成后,线程也就退出了。所以,选项C正确。
22. 按照MVC设计模式,JSP用于实现______。
A.Controller(控制器) B.View(视图) C.Model(模型) D.Database(数据库)
A B C D
B
[解析] 使用JSP与Servlet实现的MVC模型如图所示。
MVC模型
在这个MVC模型中,视图模块采用JSP来实现,主要负责数据的展现,视图可以从控制器上获取模型的状态,当然不是直接从控制器上获取到的,而是控制器把模型的数据放到一个视图可以访问的地方,通过这种间接的方式来访问模型的数据。
控制器使用Servlet来实现,客户端的所有请求都发送给Servlet,它接受请求,并根据请求消息把它们分发给对应的JSP页面来响应,同时根据需求生成JavaBean实例供JSP来使用。
模型采用JavaBean来实现的,这个模块实现了实际的业务逻辑。
从以上分析可知,选项B正确。
三、论述题 1. 实现多线程的方法有哪几种?
Java虚拟机(Java Virtual Machine,JVM,是运行所有Java程序的抽象计算机,是Java语言的运行环境)允许应用程序并发地运行多个线程。在Java语言中,多线程的实现一般有以下三种方法: 1)实现Runnable接口,并实现该接口的run()方法。以下是主要步骤: ①自定义类并实现Runnable接口,实现run()方法。 ②创建Thread对象,用实现Runnable接口的对象作为参数实例化该Thread对象。 ③调用Thread的start()方法。 class MyThread implements Runnable {//创建线程类 public void run() { System.out.println("Thread body"); } } public class Test { public static void main(String[]args) { MyThread thread=new MyThread(); Thread t=new Thread(thread); t.start(); //开启线程 } 2)继承Thread类,重写run方法。Thread本质上也是实现了Runnable接口的一个实例,它代表一个线程的实例,并且,启动线程的唯一方法就是通过Thread类的start()方法。start()方法是一个native(本地)方法,它将启动一个新线程,并执行run()方法(Thread中提供的run()方法是一个空方法)。这种方式通过自定义类直接extends Thread,并重写run()方法,就可以启动新线程并执行自己定义的run()方法。需要注意的是,当start()方法调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行多线程代码是由操作系统决定的。 下例给出了Thread的使用方法。 class MyThread extends Thread {//创建线程类 public void run() { System.out.println("Thread body"); //线程的方法体 } } public class Test { public static void main(String[]args) { MyThread thread=new MyThread(); thread.start(); //开启线程 } 3)实现Callable接口,重写call()方法。Callable对象实际是属于Executor框架中的功能类,Callable接口与Runnable接口类似,但是提供了比Runnable更强大的功能,主要表现为以下三点: ①Callable可以在任务结束后提供一个返回值,Runnable无法提供这个功能。 ②Callable中的call()方法可以抛出异常,而Runnable的run()方法不能抛出异常。 ③运行Callable可以拿到一个Future对象,Future对象表示异步计算的结果。它提供了检查计算是否完成的方法。由于线程属于异步计算模型,所以无法从其他线程中得到方法的返回值,在这种情况下,就可以使用Future来监视目标线程调用call()方法的情况,当调用Future的get()方法以获取结果时,当前线程就会阻塞,直到call()方法结束返回结果。 示例代码如下: importjava.util.concurrent.*; public class CallableAndFuture { //创建线程类 public static class CallableTest implements Callable<String> { public String call()throws Exception { return "Hello World!"; } } public static void main(String[]args) { ExecutorService threadPool=Executors.newSingleThreadExecutor(); //启动线程 Future<String>future=threadPool.submit(new CallableTest()); try { System.out.println("waiting thread to finish"); System.out.println(future.get());//等待线程结束,并获取返回结果 } catch(Exception e) { e.printStackTrace(); } } } 上述程序的运行结果为: waiting thread to finish Hello Worid! 在以上三种方式中,前两种方式线程执行完后都没有返回值,只有最后一种是带返回值的。当需要实现多线程时,一般推荐实现Runnable接口的方式,原因如下:首先,Thread类定义了多种方法可以被派生类使用或重写,但是只有run方法是必须被重写的,在run方法中实现这个线程的主要功能。这当然是实现Runnable接口所需的同样的方法。而且,很多Java开发人员认为,一个类仅在它们需要被加强或修改时才会被继承。因此,如果没有必要重写Thread类中的其他方法,那么通过继承Thread的实现方式与实现Runnable接口的效果相同,在这种情况下最好通过实现Runnable接口的方式来创建线程。
2. 多线程同步有几种实现方法?
当使用多线程访问同一个资源时,非常容易出现线程安全的问题(例如,当多个线程同时对一个数据进行修改时,会导致某些线程对数据的修改丢失)。因此,需要采用同步机制来解决这种问题。Java主要提供了三种实现同步机制的方法: (1)synchronized关键字 在Java语言中,每个对象都有一个对象锁与之相关联,该锁表明对象在任何时候只允许被一个线程所拥有,当一个线程调用对象的一段synchronized代码时,首先需要获取这个锁,然后去执行相应的代码,执行结束后,释放锁。 synchronized关键字主要有两种用法(synchronized方法和synchronized块),此外该关键字还可以作用于静态方法、类或某个实例,但这都对程序的效率有很大的影响。 1)synchronized方法。在方法的声明前加入synchronized关键字。例如: public synchronized void mutiThreadAccess(); 只要把多个线程访问的资源的操作放到mutiThreadAccess方法中,就能够保证这个方法在同一时刻只能被一个线程访问,从而保证了多线程访问的安全性。然而,当一个方法的方法体规模非常大时,把该方法声明为synchronized会大大影响程序的执行效率。为了提高程序的执行效率,Java语言提供了synchronized块。 2)synchronized块。可以把任意的代码段声明为synchronized,也可以指定上锁的对象,有非常高的灵活性。用法如下: synchronized (syncObject) { //访问syncObject的代码 } (2)wait与notify 当使用synchronized来修饰某个共享资源的时候,如果线程A1在执行synchronized代码,另外一个线程A2也要同时执行同一对象的同一synchronized代码时,线程A2将要等到线程A1执行完成后,才能继续执行。在这种情况下,可以使用wait方法和notify方法。 在synchronized代码被执行期间,线程可以调用对象的wait方法,释放对象锁,进入等待状态,并且可以调用notify方法或notifyAll方法通知正在等待的其他线程,notify方法仅唤醒一个线程(等待队列中的第一个线程),并允许它去获得锁,而notifyAll方法唤醒所有等待这个对象的线程,并允许它们去获得锁(并不是让所有唤醒线程都获取到锁,而是让它们去竞争)。 (3)Lock JDK5新增加了Lock接口以及它的一个实现类ReentrantLock(重入锁),Lock也可以用来实现多线程的同步,具体而言,它提供了如下的一些方法来实现多线程的同步: 1)lock()。以阻塞的方式来获取锁,也就是说,如果获取到了锁,则立即返回,如果其他线程持有锁,当前线程等待,直到获取锁后返回。 2)tryLock()。以非阻塞的方式获取锁。只是尝试性地去获取一下锁,如果获取到锁,则立即返回true,否则,立即返回false。 3)tryLock(long timeout,TimeUnit unit)。如果获取了锁,立即返回true,否则,会等待参数给定的时间单元,在等待的过程中,如果获取了锁,就返回true,如果等待超时,则返回false。 4)lockInterruptibly()。如果获取了锁,则立即返回,如果没有获取锁,则当前线程处于休眠状态,直到获得锁,或者当前线程被其他线程中断(会收到InterruptedException异常)。它与lock()方法最大的区别在于:如果lock()方法获取不到锁,则会一直处于阻塞状态,且会忽略interrupt()方法。如下例所示: import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public classTest{ public static void main(String[]args) throws InterruptedException{ final Lock lock=new ReentrantLock(); lock.lock(); Thread t1=new Thread(new Runnable(){ public void run(){ try{ lock.lockInterruptibly(); //lock.lock();编译器报错 }catch(InterruptedException e){ System.out.println("interrupted."); } } }); t1.start(); t1.interrupt(); Thread.sleep(1); } } 程序的运行结果为: interrupted. 如果把lock.lockInterruptibly()替换为lock.lock(),编译器将会提示lock.lock()catch代码块无效,因为lock.lock()不会抛出异常,由此可见,lock()方法会忽略interrupt()引发的异常。
3. 在多线程编程的时候有哪些注意事项?
多线程编程是一项非常重要的技能。如何能避免死锁,如何提高多线程并发情况下的性能是非常重要的,下面列出一些在多线程编程情况下的指导原则: 1)如果能用volatile代替synchronized,尽可能用volatile。因为被synchronized修饰的方法或代码块在同一时间只允许一个线程访问,而volatile却没有这个限制,因此使用synchronized会降低并发量。由于volatile无法保证原子操作,因此在多线程的情况下,只有对变量的操作为原子操作的情况下才可以使用volatile。 2)尽可能减少synchronized块内的代码,只把临界区的代码放到synchronized块中,尽量避免用synchronized来修饰整个方法。 3)尽可能给每个线程都定义一个线程的名字,不要使用匿名线程,这样有利于调试。 4)尽可能用concurrent容器(ConcurrentHashMap)来代替synchronized容器(Hashtable)。因为synchronized容器使用synchronized关键字通过对整个容器加锁来实现多线程安全,性能比较低。而ConcurrentHashMap采用了更加细粒度的锁,因此可以支持更高的并发量。 5)使用线程池来控制多线程的执行。
4. 一个文件中有10000个数,用Java语言实现一个多线程程序,将这10000个数输出到5个不同文件中(不要求输出到每个文件中的数量相同)。要求启动10个线程,两两一组,分为5组。每组两个线程分别将文件中的奇数和偶数输出到该组对应的一个文件中,需要偶数线程每打印10个偶数以后,就将奇数线程打印10个奇数,如此交替进行。同时需要记录输出进度,每完成1000个数就在控制台中打印当前完成数量,并在所有线程结束后,在控制台输出“Done”。
本题考查的是对多线程编程的理解。为了便于理解,首先用随机函数随机生成10000个数放到文件中,以供测试使用。一次把这10000条记录读到内存中,平均分配给5组线程并行处理,因此,本题的难点是如何控制打印偶数的线程和打印奇数的线程轮流运行。 本题通过Java提供的Condition来实现线程的同步。Condition是在Java 1.5中才出现的,它用来替代传统的Object类的wait()、notify()方法,以实现线程间的协作,相比使用Object类的wait()、nofify()方法,使用Condition的await()、signal()这种方式实现线程间协作,更加安全和高效。它主要有如下特点: 1)Condition最常用的方法为await()和signal(),其中,await()对应Object类的wait()方法,signal()对应Object类的notify()方法。 2)Condition依赖于Lock接口,生成一个Condition的代码为lock.newCondition()。 3)调用Condition的await()和signal()方法必须在lock保护之内。 对于本题而言,定义两个Condition(oddLock和evenLock),首先打印奇数的线程开始运行,通过调用evenLock.await()来等待打印偶数的线程执行。接着打印偶数的线程开始运行,当输出10个偶数或者没有偶数输出后,调用evenLock.signal()来通知打印奇数的线程开始运行,然后调用oddLock.wait方法来等待打印奇数的线程运行完成。通过这种方法来控制奇数线程与偶数线程的运行顺序,实现代码如下: import java.io.*; import java.util.Random; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class Test { private static final int count=10000; private static final int threadGruopCount=5; private static final String inputFile="testInput.txt"; public static void generateTestFile()throws IOException { //用随机数生成10000个测试数据放到文件中 PrintWriter pw=new PrintWriter(new FileWriter(new File(inputFile)),true); Random random=new Random(); for(inti=0;i<count;i++) { pw.write(Math.abs(random.nextInt())%count+","); } pw.flush(); pw.close(); } public static void main(String[]args) { try { generateTestFile(); BufferedReader reader=new BufferedReader(new FileReader(inputFile)); String str=reader.readLine(); reader.close(); String[]strs=str.split(","); int index=0; //为了简单,每个文件输出数字的个数相同 int countForEachFile=count/threadGmopCount; for(int i=0;i<threadGmopCount;i++) { int records[]=new int[countForEachFile]; for(int j=0;j<countForEachFile;j++) { records[j]=Integer.parseInt(strs[index]); index++; } PrintGroup group=new PrintGroup(records,i); group.startPrint(); } } catch(Exception e) { e.printStackTrace(); } } } class PrintGroup{ //这个线程组输出数字的个数 private static volatile int count=0; private Lock lock=new ReentrantLock(); private Condition oddLock=lock.newCondition(); private Condition evenLock=lock.newCondition(); //这个线程组需要输出的数字数组 private int records[]; //这个线程组需要把数字输出到同一个文件,因此,共享一个writer //由于任意时刻只会有一个线程写文件,因此,不需要同步 private PrintWriter writer; //记录输出奇数所在的数组下标 private volatile int oddIndex=0; //记录输出偶数所在的数组下标 private volatile im evenlndex=0; //输出奇数的线程 private OddPrintThread oddPrintThread; //输出偶数的线程 private EvenPrintThread evenPrintThread; private volatile boolean first=true; private int[] result=new int[2000]; private int index=0; public PrintGroup(int[] records, int id) throws Exception { this.records=records; this.writer=new PrintWriter(new FileWriter(new File("output" + id + ".txt")), true); } public void startPrint() { oddPrintThread=new OddPrintThread(); evenPrintThread=new EvenPrintThread(); oddPrintThread.start(); evenPrintThread.start(); } private class OddPrintThread extends Thread { @Override public void run() { while (true) { try{ lock.lock(); if(first)//第一次运行时,需要等待打印偶数的线程先执行 { first=false; evenLock.await(); } for (int i=0; i<10;) if(oddIndex>=records.length&& evenIndex>=records.length) { writer.flush(); writer.close(); return; } //如果所有事物奇数都打印完了,则不打印奇数,让打印偶数的线程有 //运行机会 if (oddlndex>=records.length ) { break; } if(records[oddIndex]%2==1) { i++; writer.print(records[oddlndex] + " "); result[index++]=records[oddlndex]; writer.flush(); addCount(); } oddIndex++; } //打印完10个奇数后,通知打印偶数的线程开始运行 oddLock.signal(); //接着等待打印偶数的线程结束 evenLock.await(); } catch(Exception e) { e.printStackTrace{}; } finally { oddLock.signal(); lock.unlock(); } } } } private class EvenPrintThread extends Thread{ @Override public void rum() { while(true) { try{ //等待打印奇数的线程先运行。如果这个线程先运行调用evenLock.signal(); //然后打印奇数线程才开始运行,打印奇数线程会通过调用evenLock.await(); //进入休眠状态,此时打印奇数线程将永远不会被唤醒 while(first){ Thread.sleep(1); } lock.lock(); for(int i=0;i<10;) { if(oddIndex>=records.length&&evenIndex>=records.length) { String s=""; for(int k=0;k<2000;k++) { s+=(result[k]+" "); } writer.flush(); return; } if(evenlndex>=records.length) { break; } if(records[evenIndex]%2==0) { i++; writer.print(records[everdndex]+" "); result[index++]=records[evenIndex]; writer.flush(); addCount(); } evenIndex++; } evenLock.signal(); oddLock.await(); } catch(Exception e) { e.printStackTrace(); } finally { evenLock.signal(); lock.unlock(); } } } } private synchronized static void addCount() { count++; if(count%1000==0) { System.out.println("已完成: "+count); if(count==10000) { System.out.println("Done"); } } } } 程序的运行结果为: 己完成:1000 己完成:2000 已完成:3000 已完成:4000 已完成:5000 已完成:6000 己完成:7000 已完成:8000 已完成:9000 已完成:10000 Done
5. Java语言中有几种方法可以终止线程运行?stop()和suspend()方法为什么不推荐使用?
在Java语言中,可以使用stop方法与suspend方法来终止线程的执行。当使用Thread.stop()方法来终止线程时,它会释放已经锁定的所有的监视资源。如果当前任何一个受这些监视资源保护的对象处于一个不一致的状态,其他的线程将会看到这个不一致的状态,这可能会导致程序执行的不确定性,并且这种问题很难被定位。suspend方法的使用容易引起死锁。由于调用suspend方法不会释放锁,这就会导致一个问题:如果使用一个suspend挂起一个有锁的线程,那么在锁恢复之前将不会被释放。如果调用suspend方法的线程试图取得相同的锁,程序就会发生死锁。例如,线程A已经获取到了互斥资源M的锁,然后调用suspend方法挂起了A的执行,如果没有线程唤醒线程A且线程B也去访问互斥资源,此时线程B就会出现冻结无法执行下去了,也可以理解为出现了死锁。鉴于以上两种方法的不安全性,Java语言已经不建议使用以上两种方法来终止线程。 那么,如何才能终止线程呢?一般建议采用的方法是让线程自行结束,进入Dead(死亡)状态。一个线程要进入Dead状态,就是执行完run方法,也就是说,如果想要停止一个线程的执行,就要提供某种方式让线程能够自动结束run方法的执行。在实现的时候,可以通过设置一个flag标志来控制循环是否执行,通过这种方法来让线程离开run方法,从而终止线程。下例给出了结束线程的方法。 public class MyThread implements Runnable { private volaile Boolean flag; public void stop() { flag=false; } public void run() { while(flag) ;//do something } } 上例中,通过调用MyThread的stop方法虽然能够终止线程,但同样也存在问题:当线程处于非运行状态时(当sleep方法被调用或当wait方法被调用或当被I/O阻塞),上面介绍的方法就不可用了。此时可以使用interrupt方法来打破阻塞的情况,当interrupt被调用的时候,会抛出InterruptedException异常,可以通过在run方法中捕获这个异常来让线程安全退出,具体实现方式如下: public class Mylbread { public static void main(String[]args) { Thread thread=new Thread(new Runnable() { public void run() { System.out.println("thread go to sleep"); try { //用休眠来模拟线程被阻塞 Tbread.sleep(5000); System.out.println("thread finish"); } catch(InterruptedException e) { System.out.println("thread is imerupted!"); } } }); thread.start(); thread.intermpt(); } 程序的运行结果为: thread go to sleep thread is interupted! 如果程序因为I/O而停滞,进入非运行状态,基本上要等到I/O完成才能离开这个状态,在这种情况下,无法使用interrupt来使程序离开run方法。需要使用一个替代的方法,其基本思路也是触发一个异常,而这个异常与所使用的I/O相关,例如,如果使用readLine方法(readLine方法是BufferedReader中一个非常常用的方法,使用它可以从一段输入流中一行一行地读数据,行的区分用“\r”“\n”或者“\r\n”)在等待网络上的一个信息,此时线程处于阻塞状态。让程序离开run的方法就是使用close方法来关闭流,在这种情况下会引发IOException异常,run方法可以通过捕获这个异常来安全结束线程。
6. 下面这段代码在一些特定的情况下有问题,请指出并改正。
import java.util.List;
import java.util.ArrayList;
public class MyStack
{
private List<String>stack=new ArrayList<String>();
public synchronized void push(String value)
{
synchronized(this)
{
stack.add(value);
notify();
}
}
public synchronized String pop() throws InterruptedException
{
synchronized(this)
{
if(stack.size()<=0)
{
wait();
}
return stack.remove(stack.size()-1);
}
}
}
以上这段代码在大部分情况下都能正常运行,但在下面的场景中会有问题: 在多线程访问这个栈的时候,如果有三个线程按照如下的顺序访问,问题就会暴露。 1)线程1先执行pop操作,此时,由于list的大小为0,因此,会调用wait释放等待锁。 2)线程2执行push操作,往队列里放了一个元素,这个线程会调用notify来唤醒等待的线程。 3)就在此时恰好另外一个线程3也执行pop操作,那么线程1和线程3的执行顺序是无法保证的。如果恰好线程3先执行pop操作,执行完成后,线程2被唤醒,此时线程2会执行returnstack.remove(stack.size()-1)操作,由于此时队列已经为空,stack.size()的返回值为0,所以,程序会抛出java.lang.ArrayIndexOutOfBoundsException异常。 以上问题的解决方法也很简单,即在pop操作调用remove方法前再进行一次判断,判断列表里是否还有元素,实现代码如下: public synchronized String pop()throws IntermptedException { synchronized(this) { if(stack.size()<=0) { wait(); } if(stack.size()<=0) return null; else return stack.remove(stack.size()-1); }
7. 在int i=0;i=i++;语句中,i=i++是线程安全的吗?如果不安全,请说明上面操作在JVM中的执行过程,为什么不安全?说出JDK中哪个类能达到以上程序的效果,并且是线程安全而且高效的,简述其原理。
语句i=i++不是线程安全的。 本题中,语句i=i++的执行过程如下:先把i的值取出来放到栈顶,可以理解为引入了一个第三方变量k,此时,k的值为i,然后执行自增操作,于是i的值变为1,最后执行赋值操作i=k(自增前的值),因此,执行结束后,i的值还是0。从上面的分析可知,i=i++语句的执行过程由多个操作组成,它不是原子操作,因此,它不是线程安全的。 在Java语言中,++i和i++操作并不是线程安全的,在使用的时候,不可避免地会用到synchronized关键字。而AtomicInteger是一个提供原子操作的Integer的类,它提供了线程安全且高效的原子操作,是线程安全的,其底层的原理是利用处理器的CAS(Compare And Swap,比较与交换,一种有名的无锁算法)操作来检测栈中的值是否被其他线程改变,如果被改变,则CAS操作失败。这种实现方法在CPU指令级别实现了原子操作,因此,它比使用synchronized来实现同步效率更高。 CAS操作过程都包含三个运算符:内存地址V、期望值A和新值B。当操作的时候,如果地址V上存放的值等于期望值A,则将地址V上的值赋为新值B,否则,不做任何操作,但是要返回原值是多少。这就要求保证比较和设(置)值这两个动作是原子性操作。系统主要利用JNI(Java Native Interface,Java本地接口)来保证这个原子操作,它利用CPU硬件支持来完成,使用硬件提供swap和test_and_set指令,单CPU下同一指令的多个指令周期不可中断,SMP(Symmetric Multi-Processing,对称多处理结构)中通过锁总线支持这两个指令的原子性。
8. 请简要介绍对线程池的理解。
在Java语言中,可以通过new Thread的方法来创建一个新的线程执行任务,但是线程的创建是非常耗时的,而且创建出来的新的线程都是各自运行、缺乏统一的管理,这样做的后果是可能导致创建过多的线程从而过度消耗系统的资源,最终导致性能急剧下降,线程池的引入就是为了解决这些问题。 当使用线程池控制线程数量时,其他线程排队等候,当一个任务执行完毕后,再从队列中取最前面的任务开始执行。如果队列中没有等待进程,那么线程池中的这一资源会处于等待状态。当一个新任务需要运行时,如果线程池中有等待的工作线程,就可以开始运行了,否则,进入等待队列。 一方面,线程池中的线程可以被所有工作线程重复利用,一个线程可以用来执行多个任务,这样就减少了线程创建的次数;另一方面,它也可以限制线程的个数,从而不会导致创建过多的线程进而导致性能下降。当需要执行任务的个数大于线程池中线程的个数时,线程池会把这些任务放到队列中,一旦有任务运行结束,就会有空闲的线程,此时线程池就会从队列里取出任务继续执行。 目前Java语言主要提供了如下4个线程池的实现类: 1)newSingleThreadExecutor:创建一个单线程的线程池,它只会用唯一的工作线程来执行任务,也就是相当于单线程串行执行所有任务,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。使用方法如下: import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; class MyThread extends Thread{ public void run(){ System.out.println(Thread.currentThread().getId()+"run"); } } public class TestSingleThreadExecutor{ public static void main(String[]args){ ExecutorService pool=Executors.newSingleThreadExecutor(); //将线程放入池中进行执行 pool.execute(new MyThread()); pool.execute(new MyThread()); pool.execute(new MyThread()); pool.execute(new MyThread()); //关闭线程池 pool.shutdown(); } } 程序的运行结果为: 15 run 15 run 15 run 15 run 2)newFixedThreadPool:创建一个定长线程池,可控制线程的最大并发数,超出的线程会在队列中等待。使用这个线程池的时候,必须根据实际情况估算出线程的数量。 示例代码如下: import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; class MyThread extends Thread{ public void run(){ System.out.println(Thread.currentThread().getId()+"run"); } } public class TestNewFixedThreadPool{ public static void main(String[]args){ ExecutorService pool=Executors.newFixedThreadPool(2); //将线程放入池中进行执行 pool.execute(new MyThread()); pool.execute(new MyThread()); pool.execute(new MyThread()); pool.execute(new MyThread()); //关闭线程池 pool.shutdown(); } } 程序的运行结果为: 15 run 15 run 15 run 17 run 3)newCachedThreadPooh创建一个可缓存线程池,如果线程池的长度超过处理需要,可灵活回收空闲线程,如果不可回收,则新建线程。此线程池不会对线程池的大小做限制,线程池的大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。使用这种方式需要在代码运行的过程中通过控制并发任务的数量来控制线程的数量。示例代码如下: import java.util.concurrent ExecutorService; import java.util.concurrent.Executors; class MyThread extends Thread{ public void run(){ System.out.println(Thread.currentThread().getId()+"run"); } } public class TestNewCachedThreadPool{ public static void main(String[]args){ ExecutorService pool=Executors.newCachedThreadPool(); //将线程放入池中进行执行 pool.execute(new MyThread()); pool.execute(new MyThread()); pool.execute(new MyThread()); pool.execute(new MyThread()); //关闭线程池 pool.shutdown(); } } 程序的运行结果为: 15 run 17 run 19 run 21 run 4)newScheduledThreadPool:创建一个定长线程池。此线程池支持定时以及周期性执行任务的需求。 示例代码如下: import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; class MyThread extends Thread{ public voidrun(){ System.out.println(Thread.currentThread().getId()+"timestamp:"+System.currentTlmeMillis()); } } public class TestScheduledThreadPoolExecutor{ public static void main(String[]args){ ScheduledThreadPoolExecutor exec=new ScheduledThreadPoolExecutor(2); //每隔一段时间执行一次 exec.scheduleAtFixedRate(new MyThread(),0,3000,TimeUnit.MILLISECONDS); exec.scheduleAtFixedRate(new MyThread0,0,2000,TimeUnit.MILLISECONDS); } } 程序的运行结果为: 15 timestamp:1443421326105 17 timestamp:1443421326105 15 timestamp:1443421328105 17 timestamp:1443421329105
9. 请写出一段死锁的代码。
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。 下面给出一段死锁代码的例子。 class ShareObject1{} class ShareObject2{) class Thread 1 extends Thread{ @Override publicvoid run(){ synchronized(ShareObject1.class){ System.out.println("线程1获取到ShareObject1锁"); try{ Thread.sleep(100); }catch(InterruptedException e){ e.printStackTrace(); } synchronized(ShareObject2.class){ System.out.println("线程1获取到ShareObject2锁"); } } } } class Thread2 extends Thread{ public void run(){ synchronized(ShareObject2.class){ try{ Thread.sleep(100); }catch(InterruptedException e){ e.printStackTrace(); } System.out.println("线程2获取到ShareObject2锁"); synchronized(ShareObject1.class){ System.out.println("线程2获取到ShareObject1锁"); } } } } public class Test{ public static void main(String[]args){ new Thread1().start(); new Thread2().start(); } } 在上述代码中,线程Thread1首先获取到Shareobject1的锁,然后再去尝试获取ShareObject2的锁;此时线程Thread2已经获取到ShareObject2的锁,继续尝试去获取ShareObject1的锁。这样两个线程都需要得到对方已经占有的资源后才能继续运行,因此,会导致死锁。造成死锁的主要原因是两个线程请求资源的顺序不合理,如果这两个线程采用同样的顺序获取,那么就不会出现死锁,例如把Thread2改为如下的代码就不会死锁了: class Thread2 extends Thread { public void run(){ synchronized(ShareObject1.class){ System.out.println("线程2获取到ShareObject1锁"); try{ Thread.sleep(100); }catch(InterruptedException e){ e.printStackTrace(); } synchronized(ShareObject2.class){ System.out.println("线程2获取到ShareObject2锁"); } } } }
10. 用Java语言写一段访问Oracle数据库的程序,并实现数据查询。
示例代码如下: import java.sql.*; public class Test{ public Connection getConnection(){ Connection conn=null; String driver="oracle.jdbc.driver.OracleDriver"; String url=""; String name="user"; String psw="password"; try{ Class.forName(driver); conn=DriverManager.getConnection(url,name,psw); }catch(ClassNotFoundException e){ e.printStackTrace(); }catch(SQLException e){ e.printStackTrace(); } return conn; } public void selectFromOracle(){ Connection conn=null; PreparedStatement pstat=null; ResultSet rs=null; try{ conn=getConnection(); String sql="select name,score from Student where name=?"; pstat=conn.prepareStatement(sql); pstat.setString(1,"James"); rs=pstat.executeQuery(); while(rs.next()){ System.out.println(rs.getString("name")+1","+rs.getInt("score"));; } }catch(SQLException e){ e.printStackTrace(); }finally{ if(rs!=null) try{ rs.close(); }catch(SQLException e){ e.printStackTrace(); } if(pstat!=null) try{ pstat.close(); )catch(SQLException e){ e.printStackTrace(); } if(conn!=null) try{ conn.close(); }catch(SQLException e){ e.printStackTrace(); } } } }
11. JDBC事务隔离级别有几种?
5种。
[解析] 为了解决与“多个线程请求相同数据”相关的问题,事务之间通常会用锁相互隔离开。现今,大多数主流的数据库支持不同类型的锁。因此,JDBC API支持不同类型的事务,它们由Connection对象指派或确定。在JDBC中,定义了以下5种事务隔离级别: 1)TRANSACTION NONE JDB:不支持事务。 2)TRANSACTION READ UNCOMMITTED:未提交读。说明在提交前一个事务可以看到另一个事务的变化。这样读“脏”数据、不可重复读和“虚读”都是允许的。 3)TRANSACTION READ COMMITTED:已提交读。说明读取未提交的数据是不允许的。这个级别仍然允许不可重复读和“虚读”产生。 4)TRANSACTION REPEATABLE READ:可重复读。说明事务保证能够再次读取相同的数据而不会失败,但“虚读”仍然会出现。 5)TRANSACTION SERIALIZABLE:可序列化。它是最高的事务级别,它防止读“脏”数据、不可重复读和“虚读”。 备注:读“脏”数据:一个事务读取了另一个事务尚未提交的数据,例如,当事务A与事务B并发执行时,当事务A更新后,事务B查询读取到事务A尚未提交的数据,此时事务A回滚,则事务B读到的数据是无效的“脏”数据。不可重复读:一个事务的操作导致另一个事务前后两次读取到不同的数据,例如,当事务A与事务B并发执行时,当事务B查询读取数据后,事务A更新操作更改事务B查询到的数据,此时事务B再次读取该数据,发现前后两次的数据不一样。“虚读”:一个事务的操作导致另一个事务前后两次查询的结果数据量不同。例如,当事务A与事务B并发执行时,当事务B查询读取数据后,事务A新增或删除了一条满足事务A的查询条件的记录,此时,事务B再次查询,发现查询到前次不存在的记录,或者前次的某个记录不见了。以银行存款为例,A存款100元未提交,这时银行做报表进行统计查询帐户为200元,然后A提交了,这时银行再统计发现帐户为300元,无法判断到底以哪个为准?
12. Statement与PreparedStatement的区别是什么?
Statement用于执行不带参数的简单SQL语句,每次执行SQL语句时,数据库都要编译该SQL语句。以下是一个最简单的SQL语句: Statement stmt=conn.getStatement(); stmt.executeUpdate("insert into client values('aa','aaaa')"); 而PreparedStatement表示的是预编译的SQL语句的对象,用于执行带参数的预编译SQL语句。CallableStatement提供了用来调用数据库中存储过程的接口,如果有输出参数要注册,则说明是输出参数。下面给出一个使用PreparedStatement的例子。 import java.sql.*; public class Test{ public static void main(String[]args)throws Exception{ String user="user1"; String password="pwd1"; String url="jdbc:mysq1://localhost:3306/Test"; String driver="com.mysq1.jdbc.Driver"; Connection con=null; PreparedStatement strut=null; ResultSet rs=null; try{ Class.forName(driver); con=DriverManager getConnection(url,user,password); stmt=con.prepareStatement("select*from Employee where id=?"); stmt.setInt(1,1); rs=stmt.executeQuery(); while(rs.next()){ System.out.println(rs.getInt(1)+" "+rs.getString(2)+" "+rs.getInt(3)); } } catch(SQLException e1){ e1.printStackTrace(); }finally{ try{ if(rs!=null) rs.close(); if(stmt!=null) stmt.close(); if(con!=null) con.close(); } catch(SQLException e){ System.out.println(e.getMessage()); } } } } 程序的运行结果为: 1 Jamesl 25 虽然Statement对象与PreparedStatement对象能够实现相同的功能,但相比之下,PreparedStatement具有以下优点: (1)效率更高 在使用PreparedStatement对象执行SQL命令时,命令会被数据库编译与解析,并放到命令缓冲区。然后,每当执行同一个PreparedStatement对象时,由于在缓冲区中可以发现预编译的命令,虽然它会被再解析一次,但不会被再次编译,是可以重复使用的,能够有效提升系统性能。所以,如果要执行插入、更新、删除等操作,最好使用PreparedStatement。鉴于此,PreparedStatement适用于存在大量用户的企业级应用软件中。 (2)代码可读性和可维护性更好 例如,以下两种方法分别使用Statement与PreparedStatement来执行SQL语句: 方法1: stmt.executeUpdate("insert into t(coll,col2)values("+varl+","'+var2+"")");//stmt为Statement的一个对象 方法2: //con是Connectiond得到一个对象 PreparedStatement perstmt=con.prepareStatement("insert into tb_name(coll,col2)values(?,?)"); perstmt.setString(1,var1); perstmt.setString(2,var2); 显然,方法2具有更好的可读性。 (3)安全性更好 使用PreparedStatement能够预防SQL注入攻击。所谓SQL注入,指的是把用户输入的数据拼接到SQL语句后面作为SQL语句的一部分执行,例如,在代码中使用下面的SQL语句:sql="select top 1*fromuser where name=‘”+name+“’and password=‘”+password+“”’来验证用户名和密码是否正确,其中,name和password是用户输入的内容,当用户输入用户名aa,密码bb’or’a’=’a,那么拼接出来的SQt。语句就为select top 1*from user where name=‘aa’and password=‘bb’or‘a’=‘a’,只要user表中有数据,这条SQL语句就会有返回结果。这就达到了SQL注入的目的,而使用PreparedStatement就可以避免这种情况的发生。
13. JDBC开发需要注意的问题有哪些?
1)尽可能使用PreparedStatement。因为它有更高的执行效率、更好的安全性(能防止SQL注入),也有更好的可读性。当然,在使用PreparedStatement的时候,只有使用占位符(?)才能实现高效率及高安全性。 2)尽可能关闭自动提交模式。在获取与数据库的连接Connection的对象后,尽可能关闭事务的自动提交模式,自动提交模式的运行机制如下:每当执行一条SQL语句时,它就马上提交(事务的提交操作也是一个耗时的操作),因此,一条SQL语句可以被看成是一个事务。如果关闭自动提交模式,改为采用显式调用commit方法来提交,可以在执行完多条SQL语句后再调用commit进行提交,从而提高执行效率。 3)尽可能使用数据库连接池。因为连接池是一个非常重要的资源,创建与数据库的连接也是一个耗时的操作,使用数据库连接池能够节约不必要的建立数据库连接的时间,同时还能对数据库连接资源进行很好的管理。 4)当Statement、PreparedStatement、ResultSet等对象使用完成后,应及时调用close方法释放资源。 5)尽可能采用批处理的方式执行SQL语句。JDBC提供了addBatch方法把SQL语句加入到批处理中,然后调用executeBatch方法执行所有的SQL语句,这种方法减少了JDBC与数据库之间的交互次数,因此,具有更好的性能。 6)在调用ResultSet的get方法时,尽可能使用列名而不是列索引,当使用列名后,即使select语句中列的顺序有所调整,也不需要对代码进行修改。在调用getXXX方法时,根据实际的类型选用对应的getXXX方法从而避免类型转换。 7)尽可能使用标准的SQL语句,这样有利于代码在不同的数据库之间进行移植。