1.TreeMap和TreeSet在排序时如何比较元素?Collections工具类中的sort()方法如何比较元素?
TreeSet要求存放的对象所属的类必须实现Compareable接口,该接口提供了比较元素的compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap要求存放的键值对映射的键必须实现Compareable接口从而根据键对元素进行排序。
Collections工具类的sort方法有两种重载的形式,第一种要求传入的待排序容器中存放的对象比较实现Compareable接口以实现元素的比较;第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator接口的子类型(需要重写compare方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java中对函数式编程的支持)。
例子1:
package com.test;
public class Student implements Comparable<Student> {
private String name;
private int age;
public Student(String name, int age){
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student [name="+name+",age="+age+"]";
}
@Override
public int compareTo(Student o) {
return this.age - o.age;
}
}
package com.test;
import java.util.Set;
import java.util.TreeSet;
public class Test01 {
public static void main(String[] args) {
Set<Student> set = new TreeSet<>();
set.add(new Student("Hao LUO",33));
set.add(new Student("XJ WANG",32));
set.add(new Student("Bruce LEE",60));
set.add(new Student("Bob YANG",22));
for(Student stu : set){
System.out.println(stu);
}
}
}
例子2:
package com.test.practice1;
public class Student {
private String name;
private int age;
public Student(String name, int age){
this.name = name;
this.age = age;
}
public String getName(){
return name;
}
public int getAge(){
return age;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
}
package com.test.practice1;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class Test02 {
public static void main(String[] args) {
List<Student> list = new ArrayList<>();
list.add(new Student("Hao LUO", 33));
list.add(new Student("XJ WANG", 32));
list.add(new Student("Bruce LEE", 60));
list.add(new Student("Bob YANG", 22));
// 通过sort方法的第二个参数传入一个Comparator接口对象
// 相当于是传入一个比较对象大小的算法到sort方法中
// 由于Java中没有函数指针、仿函数、委托这样的概念
// 因此要将一个算法传入一个方法中唯一的选择就是通过接口回调
Collections.sort(list, new Comparator<Student> (){
public int compare(Student o1, Student o2) {
return o1.getName().compareTo(o2.getName());
}
});
for(Student stu : list){
System.out.println(stu);
}
}
}
2.Thread类的sleep()方法和对象的wait()方法都可以让线程暂停执行,它们有什么区别?
sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依旧保持,因此休眠时间结束后会自动恢复(线程回到就绪状态,请参考后面题中的线程状态转换图)。wait()是Object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。
3.线程的sleep()方法和yield()方法有什么区别?
(1)sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
(2)线程执行sleep()方法后阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
(3)sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
(4)sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。
4.当一个线程进入一个对象的synchronized方法A之后,其他线程是否可进入此对象的synchronized方法B?
不能。其他线程只能访问该对象的非同步方法,同步方法则不能进入。因为非静态方法上的synchronized修饰符要求执行方法时要获得对象的锁,如果已经进入A方法说明对象锁已经被取走,那么试图进入B方法的线程就只能在等锁池(注意不是等待池哦)中等待对象的锁。
5.请说出与线程同步以及线程调度相关的方法。
- wait():使一个线程处于等待(阻塞)状态,并且释放所持有对象的锁;
- sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
- notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
- notifyAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让他们竞争,只有获得锁的线程才能进入就绪状态;
下面的例子演示了100个线程同时向一个银行账户中存入1元钱,在没有使用同步机制和使用同步机制情况下的执行情况。
银行账户类:
package com.test;
public class Account {
private double balance;
public void deposit(double money){
double newBalance = balance + money;
try{
Thread.sleep(10);
}
catch(InterruptedException e){
e.printStackTrace();
}
balance = newBalance;
}
public double getBalance(){
return balance;
}
}
存钱线程类:
package com.test;
public class AddMoneyThread implements Runnable {
private Account account;
private double money;
public AddMoneyThread(Account account, double money){
this.account = account;
this.money = money;
}
public void run(){
account.deposit(money);
}
}
测试类:
package com.test;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test03 {
public static void main(String[] args) {
Account account = new Account();
ExecutorService service = Executors.newFixedThreadPool(100);
for(int i = 1; i<=100; i++){
service.execute(new AddMoneyThread(account, 1));
}
service.shutdown();
while(!service.isTerminated()){}
System.out.println("账户余额:"+account.getBalance());
}
}
在没有同步的情况下,执行结果通常是显示账户余额在10元以下,出现这种状况的原因是,当一个线程A试图存入1元的时候,另外一个线程B也能够进入存款的方法中,线程B读取到的账户余额仍然是线程A存入1元钱之前的账户余额,因此也是在原来的余额0上面做了加1的操作,同理线程C也会做类似的事情,所以最后100个线程执行结束时,本来期望账户余额为100元,但实际得到的通常在10元以下。解决这个问题的方法就是同步,当一个线程对银行账户存钱时,需要将次账户锁定,待其操作完成后,才允许其他的线程进行操作,代码有如下几种调整方案:
在银行账户的存款(deposit)方法上同步(synchronized)关键字:
package com.test;
public class Account {
private double balance;
public void deposit(double money){
double newBalance = balance + money;
try{
Thread.sleep(10);
}
catch(InterruptedException e){
e.printStackTrace();
}
balance = newBalance;
}
public double getBalance(){
return balance;
}
}
在线程调用存款方法时对银行账户进行同步
package com.test;
public class AddMoneyThread implements Runnable {
private Account account;
private double money;
public AddMoneyThread(Account account, double money){
this.account = account;
this.money = money;
}
public void run(){
synchronized(account){
account.deposit(money);
}
}
}
通过Java 5显示的锁机制,为每个银行账户创建一个锁对象,在存款操作进行加锁和解锁的操作
package com.test.practice2;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Account {
private Lock accountLock = new ReentrantLock();
private double balance;
public void deposit(double money){
accountLock.lock();
try{
double newBalance = balance + money;
try{
Thread.sleep(10);
}
catch(InterruptedException e){
e.printStackTrace();
}
balance = newBalance;
}
finally{
accountLock.unlock();
}
}
public double getBalance(){
return balance;
}
}
package com.test.practice2;
public class AddMoneyThread implements Runnable {
private Account account;
private double money;
public AddMoneyThread(Account account, double money){
this.account = account;
this.money = money;
}
public void run(){
account.deposit(money);
}
}
package com.test.practice2;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test01 {
public static void main(String[] args) {
Account account = new Account();
ExecutorService service = Executors.newFixedThreadPool(100);
for(int i = 1; i<=100; i++){
service.execute(new AddMoneyThread(account, 1));
}
service.shutdown();
while(!service.isTerminated()){}
System.out.println("账户余额:"+account.getBalance());
}
}
6.编写多线程程序有几种实现方式?
Java5以前实现多线程有两种实现方法:一种是继承Thread类;另一种是实现Runnable接口。两种方式都要通过重写run()方法来定义线程的行为,推荐使用后者,因为Java中的继承是单继承,一个类有一个父类,如果继承了Thread类就无法在继承其他类了,显然使用Runnable接口更为灵活。
7.synchronized关键字的用法?
synchronized关键字可以将对象或方法标记为同步,以实现对对象和方法的互斥访问,可以使用synchronized(对象){…}定义同步代码块,或者在声明方法时将synchronized作为方法的修饰符。在第5题的例子中已经展示了synchronized关键字的用法。
8.举例说明同步和异步。
如果系统中存在临界资源(资源数量少于竞争资源的线程数量的资源),例如正在写的数据以后可能被另一个线程读到,或者正在读的数据已经被另一个线程写过了,那么这些数据就必须进行同步存取(数据库操作中的排他锁就是最好的例子)。当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。事实上,所谓的同步就是指阻塞式操作,而异步就是非阻塞式操作。
9.启动一个线程是调用run()方法还是start()方法?
启动一个线程是调用start()方法,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行,这并不意味着线程就会立即运行。run()方法是线程启动后要进行回调(callback)的方法。
10.什么是线程池(thread pool)?
在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其他更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是“池化资源”技术产生的原因。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
Java5+中的Executor接口定义一个执行线程的工具。它的子类型即线程池接口是ExecutorService。要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,因此在工具类Executors里面提供了一些静态工厂方法,生成一些常用的线程池,如下所示:
- newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。此线程支持定时以及周期性执行任务的需求。
- newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
- newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
- newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。