- JUC概况 以下是Java JUC包的主体结构:
? Atomic : AtomicInteger ? Locks : Lock, Condition, ReadWriteLock ? Collections : Queue, ConcurrentMap ? Executer : Future, Callable, Executor ? Tools : CountDownLatch, CyclicBarrier, Semaphore
-
原子操作 多个线程执行一个操作时,其中任何一个线程要么完全执行完此操作,要么没有执行此操作的任何步骤,那么这个操作就是原子的。出现原因: synchronized的代价比较高。 传统锁的问题 我们先来看一个例子:计数器(Counter),采用Java里比较方便的锁机制synchronized关键字,初步的代码如下: [java] view plain copy
-
class Counter {
- private int value;
- public synchronized int getValue() {
- return value;
- }
- public synchronized int increment() {
- return ++value;
- }
- public synchronized int decrement() {
- return --value;
- }
- }
其实像这样的锁机制,满足基本的需求是没有问题的了,但是有的时候我们的需求并非这么简单,我们需要更有效,更加灵活的机制,synchronized关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,直到该线程释放锁,这里会有些问题:首先,如果被阻塞的线程优先级很高很重要怎么办?其次,如果获得锁的线程一直不释放锁怎么办?(这种情况是非常糟糕的)。还有一种情况,如果有大量的线程来竞争资源,那CPU将会花费大量的时间和资源来处理这些竞争(事实上CPU的主要工作并非这些),同时,还有可能出现一些例如死锁之类的情况,最后,其实锁机制是一种比较粗糙,粒度比较大的机制,相对于像计数器这样的需求有点儿过于笨重,因此,对于这种需求我们期待一种更合适、更高效的线程安全机制。 Atomic类 在JDK5.0之前,想要实现无锁无等待的算法是不可能的,除非用本地库,自从有了Atomic变量类后,这成为可能。下面这张图是java.util.concurrent.atomic包下的类结构。 ? 标量类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference ? 数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray ? 更新器类:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater ? 复合变量类:AtomicMarkableReference,AtomicStampedReference 第一组AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference这四种基本类型用来处理布尔,整数,长整数,对象四种数据,其内部实现不是简单的使用synchronized,而是一个更为高效的方式CAS (compare and swap) + volatile和native方法,从而避免了synchronized的高开销,执行效率大为提升。我们来看个例子,与我们平时i++所对应的原子操作为:getAndIncrement()
原因:value是一个volatile变量,在内存中可见,任何线程都不允许对其进行拷贝,因此JVM可以保证任何时刻任何线程总能拿到该变量的最新值。此处value的值,可以在AtomicInteger类初始化的时候传入,也可以留空,留空则自动赋值为0。 import java.util.concurrent.atomic.AtomicInteger; public class Atomic { public static void main(String[] args) {
AtomicInteger ai = new AtomicInteger();
System.out.println(ai);
ai.getAndIncrement(); System.out.println(ai);
}
} 2.Lock类 lock: 在java.util.concurrent包内。共有三个实现: ? ReentrantLock ? ReentrantReadWriteLock.ReadLock ? ReentrantReadWriteLock.WriteLock 主要目的是和synchronized一样, 两者都是为了解决同步问题,处理资源争端而产生的技术。功能类似但有一些区别。 区别如下:
- lock更灵活,可以自由定义多把锁的枷锁解锁顺序(synchronized要按照先加的后解顺序)
- 提供多种加锁方案,lock 阻塞式, trylock 无阻塞式, lockInterruptily 可打断式, 还有trylock的带超时时间版本。
- 本质上和监视器锁(即synchronized是一样的)
- 能力越大,责任越大,必须控制好加锁和解锁,否则会导致灾难。
- 和Condition类的结合。
- 性能更高,对比如下图:
synchronized和Lock性能对比 ReentrantLock 可重入的意义在于持有锁的线程可以继续持有,并且要释放对等的次数后才真正释放该锁。 使用方法是: 1.先new一个实例 static ReentrantLock r=new ReentrantLock(); 2.加锁 r.lock()或r.lockInterruptibly(); 此处也是个不同,后者可被打断。当a线程lock后,b线程阻塞,此时如果是lockInterruptibly,那么在调用b.interrupt()之后,b线程退出阻塞,并放弃对资源的争抢,进入catch块。(如果使用后者,必须throw interruptable exception 或catch) 3.释放锁 r.unlock() 必须做!何为必须做呢,要放在finally里面。以防止异常跳出了正常流程,导致灾难。这里补充一个小知识点,finally是可以信任的:经过测试,哪怕是发生了OutofMemoryError,finally块中的语句执行也能够得到保证。 ReentrantReadWriteLock 可重入读写锁(读写锁的一个实现) ReentrantReadWriteLock lock = new ReentrantReadWriteLock() ReadLock r = lock.readLock(); WriteLock w = lock.writeLock(); 两者都有lock,unlock方法。写写,写读互斥;读读不互斥。可以实现并发读的高效线程安全代码 synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢? 在上面一篇文章中,我们了解到如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况: 1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有; 2)线程执行发生异常,此时JVM会让线程自动释放锁。 那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能干巴巴地等待,试想一下,这多么影响程序执行效率。 因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。 再举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。 但是采用synchronized关键字来实现同步的话,就会导致一个问题: 如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。 因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。 另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的。 总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点: 1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问; 2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly() 首先lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。 由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。通常使用Lock来进行同步的话,是以下面这种形式去使用的:
tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。 tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。 由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出因此InterruptedException。lockInterruptibly()一般的使用形式如下:
java5 Condition用法--实现线程间的通信 Condition的功能类似在传统线程技术中的Object.wait()和Object.natify()的功能,传统线程技术实现的互斥只能一个线程单独干,不能说这个线程干完了通知另一个线程来干,Condition就是解决这个问题的,实现线程间的通信。比如CPU让小弟做事,小弟说我先歇着并通知大哥,大哥就开始做事。 Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用,为每个对象提供多个等待 set(wait-set)。其中,Lock 替代了 synchronized 方法和语句的使用,Condition 替代了 Object 监视器方法的使用。 Condition实例实质上被绑定到一个锁上。要为特定 Lock 实例获得 Condition 实例,请使用其 newCondition() 方法。 在java5中,一个锁可以有多个条件,每个条件上可以有多个线程等待,通过调用await()方法,可以让线程在该条件下等待。当调用signalAll()方法,又可以唤醒该条件下的等待的线程。 package locks; import java.util.Random; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class AppOfficial {
/**
* BoundedBuffer 是一个定长100的集合,当集合中没有元素时,take方法需要等待,直到有元素时才返回元素
* 当其中的元素数达到最大值时,要等待直到元素被take之后才执行put的操作
* @author yukaizhao
*
*/
static class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
System .out.println("put wait lock");
lock.lock();
System.out.println("put get lock");
try {
while (count == items.length) {
System.out.println("buffer full, please wait");
notFull.await();
}
items[putptr] = x;
if (++putptr == items.length)
putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
System.out.println("take wait lock");
lock.lock();
System.out.println("take get lock");
try {
while (count == 0) {
System.out.println("no elements, please wait");
notEmpty.await();
}
Object x = items[takeptr];
if (++takeptr == items.length)
takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
final BoundedBuffer boundedBuffer = new BoundedBuffer();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1 run");
for (int i=0;i<1000;i++) {
try {
System.out.println("putting..");
boundedBuffer.put(Integer.valueOf(i));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}) ;
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<1000;i++) {
try {
Object val = boundedBuffer.take();
System.out.println(val);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}) ;
t1.start();
t2.start();
}
} 这个示例中BoundedBuffer是一个固定长度的集合,这个在其put操作时,如果发现长度已经达到最大长度,那么会等待notFull信号,如果得到notFull信号会像集合中添加元素,并发出notEmpty的信号,而在其take方法中如果发现集合长度为空,那么会等待notEmpty的信号,同时如果拿到一个元素,那么会发出notFull的信号 2.ReentrantLock \ ReadWriteLock \ ReentrantReadWriteLock ReentrantLock,意思是“可重入锁”,关于可重入锁的概念在下一节讲述。ReentrantLock是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。下面通过一些实例看具体看一下如何使用ReentrantLock。
ReadWriteLock
ReentrantReadWriteLock
.Lock和synchronized的选择 总结来说,Lock和synchronized有以下几点不同: 1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现; 2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁; 3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断; 4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。 5)Lock可以提高多个线程进行读操作的效率。 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。 6) synchronized就不是可中断锁,而Lock是可中断锁。 如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。 7) 公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。 非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。 在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。 而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。在ReentrantLock中定义了2个静态内部类,一个是NotFairSync,一个是FairSync,分别用来实现非公平锁和公平锁。 我们可以在创建ReentrantLock对象时,通过以下方式来设置锁的公平性: ReentrantLock lock = new ReentrantLock(true);即可表示公平锁 7 Collections : Queue, ConcurrentMap 阻塞队列 (BlockingQueue)是Java util.concurrent包下重要的数据结构,BlockingQueue提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类的实现都是基于BlockingQueue实现的。 JDK7提供了7个阻塞队列。分别是 ? ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。 ? LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。 ? PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。 ? DelayQueue:一个使用优先级队列实现的无界阻塞队列。 ? SynchronousQueue:一个不存储元素的阻塞队列。 ? LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。 ? LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
? ArrayBlockingQueue是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。我们可以使用以下代码创建一个公平的阻塞队列: ? LinkedBlockingQueue是一个用链表实现的有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。 ? PriorityBlockingQueue是一个支持优先级的无界队列。默认情况下元素采取自然顺序排列,也可以通过比较器comparator来指定元素的排序规则。元素按照升序排列。 ? DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。我们可以将DelayQueue运用在以下应用场景:
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock;
class Producer implements Runnable {
BlockingQueue
public Producer(BlockingQueue
this.queue = queue;
}
@Override
public void run() {
try {
String temp = "A Product, 生产线程:"
+ Thread.currentThread().getName();
System.out.println("I have made a product:"
+ Thread.currentThread().getName());
queue.put(temp);//如果队列是满的话,会阻塞当前线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Consumer implements Runnable{
BlockingQueue
public Consumer(BlockingQueue queue)
{
this.queue = queue;
}
@Override
public void run() {
try {
String temp = queue.take();//如果队列为空,会阻塞当前线程
System.out.println(temp);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Person { private int foodNum = 0;
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private final int MAX_NUM = 5;
public void produce()
{
lock.lock();
try
{
while (foodNum == MAX_NUM)
{
System.out.println("box is full,size = " + foodNum);
condition.await();
}
foodNum++;
System.out.println("produce success foodNum = " + foodNum);
condition.signalAll();
}
catch(InterruptedException e)
{
e.printStackTrace();
} finally
{
lock.unlock();
}
}
public void consume()
{
lock.lock();
try
{
while (foodNum == 0)
{
System.out.println("box is empty,size = " + foodNum);
condition.await();
}
foodNum--;
System.out.println("consume success foodNum = " + foodNum);
condition.signalAll();
}
catch(InterruptedException e)
{
e.printStackTrace();
} finally
{
lock.unlock();
}
}
} class Producer1 implements Runnable { private Person person;
public Producer1(Person person)
{
this.person = person;
}
@Override
public void run()
{
for (int i = 0; i < 10; i++)
{
person.produce();
}
}
}
class Consumer1 implements Runnable {
private Person person;
public Consumer1(Person person)
{
this.person = person;
}
@Override
public void run()
{
for (int i = 0; i < 10; i++)
{
person.consume();
}
}
} public class Test3 {
public static void main(String[] args) {
//使用阻塞队列的实现 BlockingQueue
Consumer consumer = new Consumer(queue);
Producer producer = new Producer(queue);
for (int i = 0; i < 5; i++) {
new Thread(producer, "Producer" + (i + 1)).start();
new Thread(consumer, "Consumer" + (i + 1)).start();
}
//使用lock实现的阻塞
Person person = new Person();
new Thread(new Consumer1(person), "消费者一").start();
new Thread(new Consumer1(person), "消费者二").start();
new Thread(new Consumer1(person), "消费者三").start();
new Thread(new Producer1(person), "生产者一").start();
new Thread(new Producer1(person), "生产者一").start();
new Thread(new Producer1(person), "生产者一").start();
}
}
ConcurrentHashMap 是一个并发散列映射表的实现,它允许完全并发的读取,并且支持给定数量的并发更新。相比于 HashTable 和用同步包装器包装的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 拥有更高的并发性。在 HashTable 和由同步包装器包装的 HashMap 中,使用一个全局的锁来同步不同线程间的并发访问。同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。 在使用锁来协调多线程间并发访问的模式下,减小对锁的竞争可以有效提高并发性。有两种方式可以减小对锁的竞争:
- 减小请求 同一个锁的 频率。
- 减少持有锁的 时间。 ConcurrentHashMap 的高并发性主要来自于三个方面:
- 用分离锁实现多个线程间的更深层次的共享访问。
- 用 HashEntery 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。
- 通过对同一个 Volatile 变量的写 / 读访问,协调不同线程间读 / 写操作的内存可见性。 使用分离锁,减小了请求 同一个锁的频率。 通过 HashEntery 对象的不变性及对同一个 Volatile 变量的读 / 写来协调内存可见性,使得 读操作大多数时候不需要加锁就能成功获取到需要的值。由于散列映射表在实际应用中大多数操作都是成功的 读操作,所以 2 和 3 既可以减少请求同一个锁的频率,也可以有效减少持有锁的时间。 通过减小请求同一个锁的频率和尽量减少持有锁的时间 ,使得 ConcurrentHashMap 的并发性相对于 HashTable 和用同步包装器包装的 HashMap有了质的提高。 ThreadLocal
import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future;
public class Test { public static void main(String[] args) { ExecutorService executor = Executors.newCachedThreadPool(); Task task = new Task(); Future
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
System.out.println("主线程在执行任务");
try {
System.out.println("task运行结果"+result.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("所有任务执行完毕");
}
} class Task implements Callable
final CountDownLatch latch = new CountDownLatch(2);
new Thread(){
public void run() {
try {
System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
Thread.sleep(3000);
System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
}.start();
new Thread(){
public void run() {
try {
System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");
Thread.sleep(3000);
System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");
latch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
}.start();
try {
System.out.println("等待2个子线程执行完毕...");
latch.await();
System.out.println("2个子线程已经执行完毕");
System.out.println("继续执行主线程");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} 执行结果:
线程Thread-0正在执行 线程Thread-1正在执行 等待2个子线程执行完毕... 线程Thread-0执行完毕 线程Thread-1执行完毕 2个子线程已经执行完毕 继续执行主线程
二.CyclicBarrier用法 字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。 CyclicBarrier类位于java.util.concurrent包下,CyclicBarrier提供2个构造器: 1 2 3 4 5 public CyclicBarrier(int parties, Runnable barrierAction) { }
public CyclicBarrier(int parties) { } 参数parties指让多少个线程或者任务等待至barrier状态;参数barrierAction为当这些线程都达到barrier状态时会执行的内容。 然后CyclicBarrier中最重要的方法就是await方法,它有2个重载版本: 1 2 public int await() throws InterruptedException, BrokenBarrierException { }; public int await(long timeout, TimeUnit unit)throws InterruptedException,BrokenBarrierException,TimeoutException { }; 第一个版本比较常用,用来挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务; 第二个版本是让这些线程等待至一定的时间,如果还有线程没有到达barrier状态就直接让到达barrier的线程执行后续任务。 下面举几个例子就明白了: 假若有若干个线程都要进行写数据操作,并且只有所有线程都完成写数据操作之后,这些线程才能继续做后面的事情,此时就可以利用CyclicBarrier了: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class Test { public static void main(String[] args) { int N = 4; CyclicBarrier barrier = new CyclicBarrier(N); for(int i=0;i } 执行结果: 线程Thread-0正在写入数据... 线程Thread-3正在写入数据... 线程Thread-2正在写入数据... 线程Thread-1正在写入数据... 线程Thread-2写入数据完毕,等待其他线程写入完毕 线程Thread-0写入数据完毕,等待其他线程写入完毕 线程Thread-3写入数据完毕,等待其他线程写入完毕 线程Thread-1写入数据完毕,等待其他线程写入完毕 所有线程写入完毕,继续处理其他任务... 所有线程写入完毕,继续处理其他任务... 所有线程写入完毕,继续处理其他任务... 所有线程写入完毕,继续处理其他任务... (3)Semaphore翻译成字面意思为 信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。 假若一个工厂有5台机器,但是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。那么我们就可以通过Semaphore来实现: Semaphore来实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class Test { public static void main(String[] args) { int N = 8; //工人数 Semaphore semaphore = new Semaphore(5); //机器数目 for(int i=0;i } 1、 A.join,在API中的解释是,堵塞当前线程B,直到A执行完毕并死掉,再执行B。 用一个小例子来说明吧 static class ThreadA extends Thread { @Override public void run() { // TODO Auto-generated method stub super.run(); for (int i = 0; i < 10; i++) { System.out.println("ThreadA" + i); } } } static class ThreadB extends Thread { ThreadA a; } public static void main(String[] args) { ThreadA a = new ThreadA(); ThreadB b = new ThreadB(a); b.start(); a.start(); } 执行结果: ThreadB start ThreadA0 ThreadA1 ThreadA2 ThreadA3 ThreadA4 ThreadA5 ThreadA6 ThreadA7 ThreadA8 ThreadA9 ThreadB end 首先b线程执行,a线程join后,直接执行完a,然后才执行b,证实上述说法。 2、A.yield,A让出位置,给B执行,B执行结束A再执行。跟join意思正好相反! static class ThreadA extends Thread { @Override public void run() { // TODO Auto-generated method stub super.run(); for (int i = 0; i < 10; i++) { System.out.println("ThreadA " + i); } } } static class ThreadB extends Thread { ThreadA a; } public static void main(String[] args) { ThreadA a = new ThreadA(); ThreadB b = new ThreadB(a); b.start(); a.start(); } 执行结果: ThreadB start ThreadA 0 ThreadB 0 ThreadA 1 ThreadB 1 ThreadA 2 ThreadB 2 ThreadB 3 ThreadB 4 ThreadB 5 ThreadB 6 ThreadB 7 ThreadB 8 ThreadB 9 ThreadB end ThreadA 3 ThreadA 4 ThreadA 5 ThreadA 6 ThreadA 7 ThreadA 8 ThreadA 9 首先B执行,然后A执行;在B的循环中,i=2时,A执行yield;接着B执行完,才轮到A执行。 JVM预定义的三种类型类加载器: ? 启动(Bootstrap)类加载器:是用本地代码实现的类装入器,它负责将 系统收到了我们发出的指令,启动了一个Java虚拟机进程,这个进程首先从classpath中找到AppMain.class文件,读取这个文件中的二进制数据,然后把Appmain类的类信息存放到运行时数据区的方法区中。这一过程称为AppMain类的加载过程。 接着,Java虚拟机定位到方法区中AppMain类的Main()方法的字节码,开始执行它的指令。这个main()方法的第一条语句就是: Sample test1=new Sample("测试1"); 语句很简单啦,就是让java虚拟机创建一个Sample实例,并且呢,使引用变量test1引用这个实例。貌似小case一桩哦,就让我们来跟踪一下Java虚拟机,看看它究竟是怎么来执行这个任务的: 1、 Java虚拟机一看,不就是建立一个Sample实例吗,简单,于是就直奔方法区而去,先找到Sample类的类型信息再说。结果呢,嘿嘿,没找到@@,这会儿的方法区里还没有Sample类呢。可Java虚拟机也不是一根筋的笨蛋,于是,它发扬“自己动手,丰衣足食”的作风,立马加载了Sample类,把Sample类的类型信息存放在方法区里。 2、 好啦,资料找到了,下面就开始干活啦。Java虚拟机做的第一件事情就是在堆区中为一个新的Sample实例分配内存, 这个Sample实例持有着指向方法区的Sample类的类型信息的引用。这里所说的引用,实际上指的是Sample类的类型信息在方法区中的内存地址,其实,就是有点类似于C语言里的指针啦~~,而这个地址呢,就存放了在Sample实例的数据区里。 3、 在JAVA虚拟机进程中,每个线程都会拥有一个方法调用栈,用来跟踪线程运行中一系列的方法调用过程,栈中的每一个元素就被称为栈帧,每当线程调用一个方法的时候就会向方法栈压入一个新帧。这里的帧用来存储方法的参数、局部变量和运算过程中的临时数据。OK,原理讲完了,就让我们来继续我们的跟踪行动!位于“=”前的Test1是一个在main()方法中定义的变量,可见,它是一个局部变量,因此,它被会添加到了执行main()方法的主线程的JAVA方法调用栈中。而“=”将把这个test1变量指向堆区中的Sample实例,也就是说,它持有指向Sample实例的引用。 OK,到这里为止呢,JAVA虚拟机就完成了这个简单语句的执行任务。参考我们的行动向导图,我们终于初步摸清了JAVA虚拟机的一点点底细了,COOL! 接下来,JAVA虚拟机将继续执行后续指令,在堆区里继续创建另一个Sample实例,然后依次执行它们的printName()方法。当JAVA虚拟机执行test1.printName()方法时,JAVA虚拟机根据局部变量test1持有的引用,定位到堆区中的Sample实例,再根据Sample实例持有的引用,定位到方法去中Sample类的类型信息,从而获得printName()方法的字节码,接着执行printName()方法包含的指令 HashMap 默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。 们可以看到在HashMap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表,这样就大大优化了查询的效率。 对于任意给定的对象,只要它的 hashCode() 返回值相同,那么程序调用 hash(int h) 方法所计算得到的 hash 码值总是相同的。我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,在HashMap中是这样做的:调用 indexFor(int h, int length) 方法来计算该对象应该保存在 table 数组的哪个索引处。indexFor(int h, int length) 方法的代码如下: Java代码 } 这个方法非常巧妙,它通过 h & (table.length -1) 来得到该对象的保存位,而HashMap底层数组的长度总是 2 的n 次方,这是HashMap在速度上的优化。在 HashMap 构造器中有如下代码: Java代码 capacity <<= 1; 8 & (16-1): 0100 & 1111 = 0100 9 & (16-1): 0101 & 1111 = 0101 从上面的例子中可以看出:当它们和15-1(1110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链 表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hash值会与15-1(1110)进行“与”,那么 最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!而当数组长度为16时,即为2的n次方时,2n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。 所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。 根据上面 put 方法的源代码可以看出,当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry的 value,但key不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部——具体说明继续看 addEntry() 方法的说明。 2) 读取: Java代码 } 有了上面存储时的hash算法作为基础,理解起来这段代码就很容易了。从上面的源代码中可以看出:从HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。 3) 归纳起来简单地说,HashMap 在底层将 key-value 当成一个整体进行处理,这个整体就是一个 Entry 对象。HashMap 底层采用一个 Entry[] 数组来保存所有的 key-value 对,当需要存储一个 Entry 对象时,会根据hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Entry时,也会根据hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Entry。 HashMap的resize(rehash): 当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。 那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过160.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。 HashMap的性能参数: HashMap 包含如下几个构造器: HashMap():构建一个初始容量为 16,负载因子为 0.75 的 HashMap。 HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap。 HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。 HashMap的基础构造器HashMap(int initialCapacity, float loadFactor)带有两个参数,它们是初始容量initialCapacity和加载因子loadFactor。 initialCapacity:HashMap的最大容量,即为底层数组的长度。 loadFactor:负载因子loadFactor定义为:散列表的实际元素数目(n)/ 散列表的容量(m)。 负载因子衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。 HashMap的实现中,通过threshold字段来判断HashMap的最大容量: Java代码 Java代码 resize(2 * table.length); Fail-Fast机制: 我们知道java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。 这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。 Java代码 } 在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map: 注意到modCount声明为volatile,保证线程之间修改的可见性。 Java代码 1、hashmap的数据结构 要知道hashmap是什么,首先要搞清楚它的数据结构,在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,hashmap也不例外。Hashmap实际上是一个数组和链表的结合体(在数据结构中,一般称之为“链表散列“),请看下图(横排表示数组,纵排表示数组元素【实际上是一个链表】)。 从图中我们可以看到一个hashmap就是一个数组结构,当新建一个hashmap的时候,就会初始化一个数组。我们来看看java代码: Java代码 Java代码 2、hash算法 我们可以看到在hashmap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过hashmap的数据结构是数组和链表的结合,所以我们当然希望这个hashmap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。 所以我们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,能不能找一种更快速,消耗更小的方式那?java中时这样做的, Java代码 首先算得key得hashcode值,然后跟数组的长度-1做一次“与”运算(&)。看上去很简单,其实比较有玄机。比如数组的长度是2的4次方,那么hashcode就会和2的4次方-1做“与”运算。很多人都有这个疑问,为什么hashmap的数组初始化大小都是2的次方大小时,hashmap的效率最高,我以2的4次方举例,来解释一下为什么数组大小为2的幂时hashmap访问的性能最高。 所以,在存储大容量数据的时候,最好预先指定hashmap的size为2的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的2次幂来初始化的,代码如下(HashMap的构造方法中): Java代码 总结: 本文主要描述了HashMap的结构,和hashmap中hash函数的实现,以及该实现的特性,同时描述了hashmap中resize带来性能消耗的根本原因,以及将普通的域模型对象作为key的基本要求。尤其是hash函数的实现,可以说是整个HashMap的精髓所在,只有真正理解了这个hash函数,才可以说对HashMap有了一定的理解。 3、hashmap的resize 当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。 那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过160.75=12的时候,就把数组的大小扩展为216=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。比如说,我们有1000个元素new HashMap(1000), 但是理论上来讲new HashMap(1024)更合适,不过上面annegu已经说过,即使是1000,hashmap也自动会将其设置为1024。 但是new HashMap(1024)还不是更合适的,因为0.751000 < 1000, 也就是说为了让0.75 * size > 1000, 我们必须这样new HashMap(2048)才最合适,既考虑了&的问题,也避免了resize的问题。 4、key的hashcode与equals方法改写 在第一部分hashmap的数据结构中,annegu就写了get方法的过程:首先计算key的hashcode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。所以,hashcode与equals方法对于找到对应元素是两个关键方法。 Hashmap的key可以是任何类型的对象,例如User这种对象,为了保证两个具有相同属性的user的hashcode相同,我们就需要改写hashcode方法,比方把hashcode值的计算与User对象的id关联起来,那么只要user对象拥有相同id,那么他们的hashcode也能保持一致了,这样就可以找到在hashmap数组中的位置了。如果这个位置上有多个元素,还需要用key的equals方法在对应位置的链表中找到需要的元素,所以只改写了hashcode方法是不够的,equals方法也是需要改写滴~当然啦,按正常思维逻辑,equals方法一般都会根据实际的业务内容来定义,例如根据user对象的id来判断两个user是否相等。 在改写equals方法的时候,需要满足以下三点: (1) 自反性:就是说a.equals(a)必须为true。 (2) 对称性:就是说a.equals(b)=true的话,b.equals(a)也必须为true。 (3) 传递性:就是说a.equals(b)=true,并且b.equals(c)=true的话,a.equals(c)也必须为true。 通过改写key对象的equals和hashcode方法,我们可以将任意的业务对象作为map的key(前提是你确实有这样的需要)。 总结: 本文主要描述了HashMap的结构,和hashmap中hash函数的实现,以及该实现的特性,同时描述了hashmap中resize带来性能消耗的根本原因,以及将普通的域模型对象作为key的基本要求。尤其是hash函数的实现,可以说是整个HashMap的精髓所在,只有真正理解了这个hash函数,才可以说对HashMap有了一定的理解。 Minor GC 清理年轻代 Major GC 是清理永久代。 Full GC 是清理整个堆空间—包括年轻代和永久代。 1、minor gc是新生代Copying算法: 将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。 2、full gc的老年代,采取的Mark-Compact, 先需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。 实现安全的单例模式: public class Singleton { private static Singleton instance = null; private Singleton() { } public static synchronized Singleton getInstance() { if(instance == null) { instance = new Singleton(); } return instance; } } Selector类是NIO的核心类,Selector能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。 与Selector有关的一个关键类是SelectionKey,一个SelectionKey表示一个到达的事件,这2个类构成了服务端处理业务的关键逻辑。 OutputStream outputStream = new FileOutputStream("file-new.xml"); int bytesWritten = 0; int byteCount = 0; byte[] bytes = new byte[1024]; while ((byteCount = inputStream.read(bytes)) != -1) { outputStream.write(bytes, bytesWritten, byteCount); bytesWritten += byteCount; } inputStream.close(); outputStream.close(); 字符流 reader writer File file = new File ("hello.txt"); FileInputStream in=new FileInputStream (file); InputStreamReader inReader=new InputStreamReader (in,"UTF-8"); BufferedReader bufReader=new BufferedReader(inReader); 我对于以上的情况总结如下:Integer和Int ①无论如何,Integer与new Integer不会相等。不会经历拆箱过程,i3的引用指向堆,而i4指向专门存放他的内存(常量池),他们的内存地址不一样,所以为false ②两个都是非new出来的Integer,如果数在-128到127之间,则是true,否则为false java在编译Integer i2 = 128的时候,被翻译成-> Integer i2 = Integer.valueOf(128);而valueOf()函数会对-128到127之间的数进行缓存 ③两个都是new出来的,都为false ④int和integer(无论new否)比,都为true,因为会把Integer自动拆箱为int再去比 .引用的基本概念 1.1、强引用 当我们使用new 这个关键字创建对象时被创建的对象就是强引用,如Object object = new Object() 这个Object()就是一个强引用了,如果一个对象具有强引用。垃圾回收器就不会去回收有强引用的对象。如当jvm内存不足时,具备强引用的对象,虚拟机宁可会报内存空间不足的异常来终止程序,也不会靠垃圾回收器去回收该对象来解决内存。 1.2、软引用 如果一个对象具备软引用,如果内存空间足够,那么垃圾回收器就不会回收它,如果内存空间不足了,就会回收该对象。当然没有被回收之前,该对象依然可以被程序调用。 1.3、弱引用 如果一个对象只具有弱引用,只要垃圾回收器在自己的内存空间中线程检测到了,就会立即被回收,对应内存也会被释放掉。相比软引用弱引用的生命周期要比软引用短很多。不过,如果垃圾回收器是一个优先级很低的线程,也不一定会很快就会释放掉软引用的内存。 1.4、虚引用 如果一个对象只具有虚引用,那么它就和没有任何引用一样,随时会被jvm当作垃圾进行回收。 Object有哪些公用方法? ? 方法equals测试的是两个对象是否相等 ? 方法clone进行对象拷贝 ? 方法getClass返回和当前对象相关的Class对象 ? 方法notify,notifyall,wait都是用来对给定对象进行线程同步的 线程间的状态转换: 通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。 synchronized 方法控制对类成员变量的访问: 每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态,从而有效避免了类成员变量的访问冲突。 对象锁(synchronized修饰方法或代码块) 当一个对象中有synchronized method或synchronized block的时候调用此对象的同步方法或进入其同步区域时,就必须先获得对象锁。如果此对象的对象锁已被其他调用者占用,则需要等待此锁被释放。(方法锁也是对象锁) java的所有对象都含有1个互斥锁,这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁,那么当前线程会等待;synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的1个好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。 对象锁的两种形式: public class Test { // 对象锁:形式1(方法锁) public synchronized void Method1() { System.out.println(“我是对象锁也是方法锁”); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } // 对象锁:形式2(代码块形式) public void Method2() { synchronized (this) { System.out.println("我是对象锁"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } } 类锁(synchronized 修饰静态的方法或代码块) 由于一个class不论被实例化多少次,其中的静态方法和静态变量在内存中都只有一份。所以,一旦一个静态的方法被申明为synchronized。此类所有的实例化对象在调用此方法,共用同一把锁,我们称之为类锁。 对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步。 类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。 java类可能会有很多个对象,但是只有1个Class对象,也就是说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个java对象,只不过有点特殊而已。由于每个java对象都有1个互斥锁,而类的静态方法是需要Class对象。所以所谓的类锁,不过是Class对象的锁而已。获取类的Class对象有好几种,最简单的就是[类名.class]的方式。 public class Test { // 类锁:形式1 public static synchronized void Method1() { System.out.println("我是类锁一号"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } // 类锁:形式2 public void Method2() { synchronized (Test.class) { System.out.println("我是类锁二号"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } } 栈溢出的原因 一)、是否有递归调用 二)、是否有大量循环或死循环 三)、全局变量是否过多 四)、 数组、List、map数据是否过大 Simpledateformat线性不安全 有三种方法可以解决以上问题。 1)每次使用时,都创建一个新的simpledateformat实例。如果使用不是很频繁时,可以使用此方法,这样可以降低创建新对象的开销。 2)使用同步: public class dateutil{ private simpledateformat sdf = new simpledateformat("yyyymmdd"); private date parse(string datestr) throws parseexception{ synchronized(sdf){ return sdf.parse(datestr); } } private string format(date date){ synchronized(sdf){ return sdf.format(datestr); } } } 不过,当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要block,这样的操作也会一定程度上影响性能。 个人最推荐的是第三种方法,那就是借助threadlocal对象每个线程只创建一个实例。 public class dateutil { private static final string date_format = "yyyymmdd"; @suppresswarnings("rawtypes") private static threadlocal threadlocal = new threadlocal() { protected synchronized object initialvalue() { return new simpledateformat(date_format); } }; public static dateformat getdateformat() { return (dateformat) threadlocal.get(); } public static date parse(string textdate) throws parseexception { return getdateformat().parse(textdate); } } 一致性哈希算法是分布式系统中常用的算法。比如,一个分布式的存储系统,要将数据存储到具体的节点上,如果采用普通的hash方法,将数据映射到具体的节点上,如key%N,key是数据的key,N是机器节点数,如果有一个机器加入或退出这个集群,则所有的数据映射都无效了,如果是持久化存储则要做数据迁移,如果是分布式缓存,则其他缓存就失效了。 因此,引入了一致性哈希算法: 1、平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。 2、单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。 3、分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。 4、负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同 的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。 JVM 调优 —— GC 长时间停顿问题及解决方法 两个原因: 在 CMS 启动过程中,新生代提升速度过快,老年代收集速度赶不上新生代提升速度 在 CMS 启动过程中,老年代碎片化严重,无法容纳新生代提升上来的大对象 如果频率太快的话,说明空间不足,首先可以尝试调大新生代空间和晋升阈值 如果频率太快或者 Full GC 后空间释放不多的话,说明空间不足,首先可以尝试调大老年代空间 Runnable和Callable的区别是, (1)Callable规定的方法是call(),Runnable规定的方法是run(). (2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值得 (3)call方法可以抛出异常,run方法不可以 (4)运行Callable任务可以拿到一个Future对象,Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get 方法来获取结果,如果线程没有执行完,Future.get()方法可能会阻塞当前线程的执行;如果线程出现异常,Future.get()会throws InterruptedException或者ExecutionException;如果线程已经取消,会跑出CancellationException。取消由cancel 方法来执行。isDone确定任务是正常完成还是被取消了。一旦计算完成,就不能再取消计算。如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明Future> 形式类型、并返回 null 作为底层任务的结果。Future接口的定义如下: java调用存储过程 --------------------------------------------------------------------数据库: 建立索引: 在SQL语言中,建立聚簇索引使用CREATE INDEX语句,格式为:CREATE CLUSTER INDEX index_name ON table_name(column_name1,column_name2,...); 唯一索引 唯一索引是不允许其中任何两行具有相同索引值的索引。 当现有数据中存在重复的键值时,大多数数据库不允许将新创建的唯一索引与表一起保存。数据库还可能防止添加将在表中创建重复键值的新数据。例如,如果在employee表中职员的姓(lname)上创建了唯一索引,则任何两个员工都不能同姓。 主键索引 数据库表经常有一列或列组合,其值唯一标识表中的每一行。该列称为表的主键。 在数据库关系图中为表定义主键将自动创建主键索引,主键索引是唯一索引的特定类型。该索引要求主键中的每个值都唯一。当在查询中使用主键索引时,它还允许对数据的快速访问。 聚集索引 在聚集索引中,表中行的物理顺序与键值的逻辑(索引)顺序相同。一个表只能包含一个聚集索引。 如果某索引不是聚集索引,则表中行的物理顺序与键值的逻辑顺序不匹配。与非聚集索引相比,聚集索引通常提供更快的数据访问速度。 Read Uncommitted: 直译就是"读未提交",意思就是即使一个更新语句没有提交,但是别 的事务可以读到这个改变.这是很不安全的. Read Committed: 直译就是"读提交",意思就是语句提交以后即执行了COMMIT以后 别的事务就能读到这个改变. Repeatable Read: 直译就是"可以重复读",这是说在同一个事务里面先后执行同一个 查询语句的时候,得到的结果是一样的. Serializable: 直译就是"序列化",意思是说这个事务执行的时候不允许别的事务 并发执行. Innodb引擎 Innodb引擎提供了对数据库ACID事务的支持,并且实现了SQL标准的四种隔离级别。该引擎还提供了行级锁和外键约束,它的设计目标是处理大容量数据库系统,它本身其实就是基于MySQL后台的完整数据库系统,MySQL运行时Innodb会在内存中建立缓冲池,用于缓冲数据和索引。但是该引擎不支持FULLTEXT类型的索引,而且它没有保存表的行数,当SELECT COUNT() FROM TABLE时需要扫描全表。当需要使用数据库事务时,该引擎当然是首选。由于锁的粒度更小,写操作不会锁定全表,所以在并发较高时,使用Innodb引擎会提升效率。但是使用行级锁也不是绝对的,如果在执行一个SQL语句时MySQL不能确定要扫描的范围,InnoDB表同样会锁全表。 MyIASM引擎 MyIASM是MySQL默认的引擎,但是它没有提供对数据库事务的支持,也不支持行级锁和外键,因此当INSERT(插入)或UPDATE(更新)数据时即写操作需要锁定整个表,效率便会低一些。不过和Innodb不同,MyIASM中存储了表的行数,于是SELECT COUNT() FROM TABLE时只需要直接读取已经保存好的值而不需要进行全表扫描。如果表的读操作远远多于写操作且不需要数据库事务的支持,那么MyIASM也是很好的选择。 主要区别: 1、MyIASM是非事务安全的,而InnoDB是事务安全的 2、MyIASM锁的粒度是表级的,而InnoDB支持行级锁 3、MyIASM支持全文类型索引,而InnoDB不支持全文索引 4、MyIASM相对简单,效率上要优于InnoDB,小型应用可以考虑使用MyIASM 5、MyIASM表保存成文件形式,跨平台使用更加方便 应用场景: 1、MyIASM管理非事务表,提供高速存储和检索以及全文搜索能力,如果再应用中执行大量select操作,应该选择MyIASM 2、InnoDB用于事务处理,具有ACID事务支持等特性,如果在应用中执行大量insert和update操作,应该选择InnoDB 数据库是一个多用户使用的共享资源。当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况。若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。 加锁是实现数据库并发控制的一个非常重要的技术。当事务在对某个数据对象进行操作前,先向系统发出请求,对其加锁。加锁后事务就对该数据对象有了一定的控制,在该事务释放锁之前,其他的事务不能对此数据对象进行更新操作。 (2)锁的分类: 共享(S)锁:多个事务可封锁一个共享页;任何事务都不能修改该页; 通常是该页被读取完毕,S锁立即被释放。 排它(X)锁:仅允许一个事务封锁此页;其他任何事务必须等到X锁被释放才能对该页进行访问;X锁一直到事务结束才能被释放。 更新(U)锁:更新锁在修改操作的初始化阶段用来锁定可能要被修改的资源,这样可以避免使用共享锁造成的死锁现象。因为使用共享锁时,修改数据的操作分为两步,首先获得一个共享锁,读取数据,然后将共享锁升级为排它锁,然后再执行修改操作。这样如果同时有两个或多个事务同时对一个事务申请了共享锁,在修改数据的时候,这些事务都要将共享锁升级为排它锁。这时,这些事务都不会释放共享锁而是一直等待对方释放,这样就造成了死锁。如果一个数据在修改前直接申请更新锁,在数据修改的时候再升级为排它锁,就可以避免死锁。 (3)锁的粒度: 在sql server2000中锁是具有粒度的,即可以对不同的资源加锁。锁定在较小的粒度的资源(例如行)上可以增加系统的并发量但需要较大的系统开销,从而也会影响系统的性能,因为锁定的粒度较小则操作可能产生的锁的数量会增加;锁定在较大的粒度(例如表)就并发而言是相当昂贵的,因为锁定整个表限制了其它事务对表中任意部分进行访问,但要求的开销较低,因为需要维护的锁较少,所以在这里是一种互相制约的关系。 Sql server2000中锁定的粒度包括 行、页、扩展盘区、表、库等资源。 Mysql B+树 一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。 计算机网络:HTTP请求包括三部分:请求行(Request Line),头部(Headers)和数据体(Body)。其中, (1)请求行由请求方法(method),请求网址Request-URI和协议 (Protocol)构成, (2)请求头包括多个属性,数据体则可以被认为是附加在请求之后的文本或二进制文件。 HTTP 报文是面向文本的,报文中的每一个字段都是一些 ASCII 码串,各个字段的长度是不确定的。HTTP 有两类报文:请求报文和响应报文。 请求报文是从客户端向服务器发送的报文,响应报文是从服务器到客户端的报文。下面分别介绍请求报文和响应报文的具体格式。 HTTP 请求报文的由请求行、请求头部行、空行和请求数据四部分构成,具体格式如下所示: (请求行) 方法名 + 空格 +URL+ 空格 + 版本 + 回车换行( \r\n ) (请求头部行 1 )关键字 + “:” + 空格 + 值 + 回车换行( \r\n ) (请求头部行 N )关键字 + “:” + 空格 + 值 + 回车换行( \r\n ) (空行)回车换行( \r\n ) (请求数据) …… ( 1 )请求行 请求行由请求方法字段、 URL 字段和 HTTP 协议版本字段 3 个字段组成,它们用空格分隔。最后由回车和换行表示请求行结束。例如: GET www.sdu.edu.cn HTTP/1.1 回车换行 ( \r\n ) 其中“方法”字段表示该请求报文希望服务器做什么,请求报文的类型就是由所采用的方法决定的。 HTTP请求报文的主要方法包括: GET 、 POST 、 HEAD 、 PUT 、 DELETE 、 OPTIONS 、 TRACE 、CONNECT 等。最常见的方法有 GET 和 HEAD 。 GET 是最常见的一种请求方式,当客户端要从服务器中读取文档时,当点击网页上的链接或者通过在浏览器的地址栏输入网址来浏览网页,使用的都是 GET 方式。 GET 方法要求服务器将 URL 定位的资源放在响应报文的数据部分,回送给客户端。 GET 方式不适合传送私密数据和大量数据。 HEAD 的功能与 GET 相似,只是服务器端接收到 HEAD 请求后只返回响应头,而不会发送响应内容。当我们只需要查看某个页面的状态的时候,使用 HEAD 是非常高效的,因为在传输的过程中省去了页面内容。 ( 2 )请求头部行( header ) 请求头部行包括若干行,每行由关键字及其值构成的,关键字和值用英文冒号 “:” 分隔,每一行都由回车换行表示结束。请求头部通知服务器有关于客户端请求的信息,典型的请求头部关键字有: User-Agent :产生请求的浏览器类型。 Accept :客户端可识别的内容类型列表。 Accept-Language :客户端可识别的语言类型 Host :请求的主机名。 Connection :告知服务器发送完文档后释放连接还是保持连接。 ( 3 )空行 最后一个请求头部之后是一个空行,发送回车符和换行符,通知服务器以下不再有请求头部了。 ( 4 )请求数据 GET 方法中没有请求数据的内容, POST 方法使用请求数据,用于客户端向服务器端填写表单等操作。 比如浏览器使用 GET 方法访问山东大学主页中的“学校简介”文档( URL 为www.sdu.edu.cn/2010/xxjj.htm ),则其 HTTP 请求报文可以为: GET /2010/xxjj.html HTTP/1.1 \r\n Host: www.sdu.edu.cn\r\n User-Agent : Mozilla/5.0 Accept-Language:cn /\r\n HTTP 响应也由四个部分组成,分别是:状态行、消息头部、空行和响应正文。其具体格式如下: (状态行)版本 + 空格 + 状态码 + 空格 + 短语 + 回车换行 (消息头部 1 )关键字 + “:” + 空格 + 值 + 回车换行 (消息头部 N )关键字 + “:” + 空格 + 值 + 回车换行 (空行)回车换行( \r\n ) (响应正文) …… 在响应报文的状态行中,版本字的表示服务器 HTTP 协议的版本,状态码字的表示服务器发回的响应状态代码;短语字段表示状态代码的文本描述。状态码由三位十进制数字组成,第一个数字定义了响应的类别,有五种可能取值( 1-5 ),每种状态码的含义如下: 1xx :指示信息。表示请求已接收,继续处理。 2xx :成功。表示请求已被成功接收、理解、接受。 3xx :重定向。要完成请求必须进行更进一步的操作。 4xx :客户端错误。请求有语法错误或请求无法实现。 5xx :服务器端错误。服务器未能实现合法的请求。 常见状态码及状态描述的说明如下: 200 OK :客户端请求成功。 400 Bad Request :客户端请求有语法错误,不能被服务器所理解。 401 Unauthorized :请求未经授权。 403 Forbidden :服务器收到请求,但是拒绝提供服务。 404 Not Found :请求资源不存在,比如输入了错误的 URL 。 500 Internal Server Error :服务器发生不可预期的错误。 503 Server Unavailable :服务器当前不能处理客户端的请求,一段时间后可能恢复正常。 消息头部与请求头部的格式相似,也是包含若干行,每行由关键字及其值构成,常用的关键字包括: Date: 表示返回消息的时间。 Content-Type: 表示返回消息的内容类型。 Content-Length: 返回内容的长度(字节数)。 Server :使用的服务器软件及其版本号。 同样,最后一个消息头部之后是一个空行,发送回车符和换行符,通知客户端以下不再有消息头部了。 响应正文部分是服务器端根据客户端的请求发回的具体文档内容,以 HTML 语言表示 区别: 1,HTTP/1.0协议使用非持久连接,即在非持久连接下,一个tcp连接只传输一个Web对象,; 2,HTTP/1.1默认使用持久连接(然而,HTTP/1.1协议的客户机和服务器可以配置成使用非持久连接)。 TCP拥塞控制 首先了解几个概念,为下面的叙述做铺垫 ? 拥塞窗口(cwnd):TCP拥塞控制中的主要参数,表示发送端下一次最多可以发送的数据分包的个数,是来自发送端的流量控制。 ? 接收端窗口(rwnd):又称通知窗口(Advertise Window),接受端目前每次所能接收的数据分组的最大个数,是来自接收端的流量控制。 ? 慢开始门限(ssthresh):当拥塞窗口增长到慢开始门限时,启动拥塞避免算法(后面会具体阐述)。 ? 拥塞控制常用算法:慢开始、拥塞避免、快重传、快恢复。 算法:两个队列实现栈 操作系统: 所谓死锁:是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力协助下,永远分配不到必需的资源而无法继续运行,这就产生了一种特殊现象死锁。 虽然进程在运行过程中,可能发生死锁,但死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件。 1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。 2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。 3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。 4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。 在系统中已经出现死锁后,应该及时检测到死锁的发生,并采取适当的措施来解除死锁。目前处理死锁的方法可归结为以下四种: jconsole就会给我们检测出该线程中造成死锁的线程,点击选中即可查看详情 知道子进程自父进程继承什么或未继承什么将有助于我们。下面这个名单会因为 不同Unix的实现而发生变化,所以或许准确性有了水份。请注意子进程得到的是 这些东西的 拷贝,不是它们本身。 由子进程自父进程继承到: ? 进程的资格(真实(real)/有效(effective)/已保存(saved) 用户号(UIDs)和组号(GIDs)) ? 环境(environment) ? 堆栈 ? 内存 ? 打开文件的描述符(注意对应的文件的位置由父子进程共享, 这会引起含糊情况) ? 执行时关闭(close-on-exec) 标志 (译者注:close-on-exec标志可通过fnctl()对文件描 述符设置,POSIX.1要求所有目录流都必须在exec函数调用时关闭。更详细说明, 参见《UNIX环境高级编程》 W. R. Stevens, 1993, 尤晋元等译(以下简称《高级编程》), 3.13节和8.9节) ? 信号(signal)控制设定 ? nice值 (译者注:nice值由nice函数设定,该值表示进程的优先级, 数值越小,优先级越高) ? 进程调度类别(scheduler class) (译者注:进程调度类别指进程在系统中被调度时所属的类别,不同类别有不同优先级,根据进程调度类别和nice值,进程调度程序可计算出每个进程的全局优先级(Global process prority),优先级高的进程优先执行) ? 进程组号 ? 对话期ID(Session ID) (译者注:译文取自《高级编程》,指:进程所属的对话期 (session)ID, 一个对话期包括一个或多个进程组, 更详细说明参见《高级编程》 9.5节) ? 当前工作目录 ? 根目录 (译者注:根目录不一定是“/”,它可由chroot函数改变) ? 文件方式创建屏蔽字(file mode creation mask (umask)) (译者注:译文取自《高级编程》,指:创建新文件的缺省屏蔽字) ? 资源限制 ? 控制终端 子进程所独有: ? 进程号 ? 不同的父进程号(译者注: 即子进程的父进程号与父进程的父进程号不同, 父进程号可由getppid函数得到) ? 自己的文件描述符和目录流的拷贝(译者注: 目录流由opendir函数创建,因其为顺序读取,顾称“目录流”) ? 子进程不继承父进程的进程,正文(text), 数据和其它锁定内存(memory locks) (译者注:锁定内存指被锁定的虚拟内存页,锁定后, 不允许内核将其在必要时换出(page out), 详细说明参见《The GNU C Library Reference Manual》 2.2版, 1999, 3.4.2节) ? 在tms结构中的系统时间(译者注:tms结构可由times函数获得, 它保存四个数据用于记录进程使用中央处理器 (CPU:Central Processing Unit)的时间,包括:用户时间,系统时间, 用户各子进程合计时间,系统各子进程合计时间) ? 资源使用(resource utilizations)设定为0 ? 阻塞信号集初始化为空集(译者注:原文此处不明确, 译文根据fork函数手册页稍做修改) ? 不继承由timer_create函数创建的计时器 算法: Linux 常考的算法题汇总: (1) 找出第K大的数 import java.util.Collections; public class Solution { } public int findKthLargest(int[] nums, int k) { PriorityQueue } 两个队列实现一个栈 import java.util.*; public class MyStack { private Queue } 两个栈实现队列 import java.util.*; public class MyQueue { } 求解和的组合 import java.util.*; public class Solution1 { /////////允许元素出现重复 public List } 链表: Given linked list: 1->2->3->4->5, and n = 2. After removing the second node from the end, the linked list becomes 1->2->3->5. public class Solution { public ListNode removeNthFromEnd(ListNode head, int n) { ListNode fast=head; ListNode slow=head; for(int i=0;i } 链表的反转 public class Solution { public ListNode reverseList(ListNode head) { ListNode pRevrese=head; ListNode pNode=head; ListNode pPre=null; while(pNode!=null) { } 链表是否有环 public class Solution { public boolean hasCycle(ListNode head) { ListNode point1=head; ListNode point2=head; while(point2!=null&&point2.next!=null) { point1=point1.next; point2=point2.next.next; if(point1==point2) { return true; public class Solution { public ListNode getIntersectionNode(ListNode headA, ListNode headB) { if(headA == null || headB ==null) return null; ListNode tailA=headA; ListNode tailB=headB; int len1=1; int len2=1; while(tailA.next!=null) if(len1-len2>0) { int dif=len1-len2; while(dif!=0) { t1=t1.next; dif--; } else { int dif=len2-len1; while(dif!=0) { t2=t2.next; dif--; } while(t1!=t2) { t1=t1.next; t2=t2.next; } return t1; } 字符串反转 public class Solution { public String reverseString(String s) { StringBuffer ss=new StringBuffer(); String ant=ss.append(s).reverse().toString(); return ant; } Given s = "the sky is blue", return "blue is sky the". public class Solution { public String reverseWords(String s) { StringBuffer ss=new StringBuffer(); StringBuffer temp=new StringBuffer(); String ant=ss.append(s).reverse().toString(); } 最长回文字串 public String longestPalindrome(String s) { if (s.isEmpty()) { return null; } } // Given a center, either one letter or two letter, // Find longest palindrome public String helper(String s, int begin, int end) { while (begin >= 0 && end <= s.length() - 1 && s.charAt(begin) == s.charAt(end)) { begin--; end++; } return s.substring(begin + 1, end); } } 动态规划 [ [0,0,0], [0,1,0], [0,0,0] ] public class Solution { public int uniquePathsWithObstacles(int[][] A) { int m=A.length;//表示一行 int n=A[0].length;//表示一列 int [][] res=new int[m][n]; for(int i=0; i } 最长递增子序列 public int maxSubArray(int[] nums) { int len=nums.length; int local=nums[0]; int gloal=nums[0]; for(int i=1;i DFS public class Solution { public int numIslands(char[][] grid) { int count = 0; for(int i=0; i } Spring pplicationContext applicationContext=new ClassPathXmlApplicationContext("applicationContext.xml"); listener是监听哪个事件(ServletContext创建事件) //树 iven n = 3, there are a total of 5 unique BST's. 1 3 3 2 1 \ / / / \ \ 3 2 1 1 3 2 / / \ \ 2 1 2 3 public class Solution { public int numTrees(int n) { } //对称树 public class Solution { public boolean isSymmetric(TreeNode root) { } //层次遍历二叉树shu public class Solution { public List } ////////////////////变种 public class Solution { public List } //构建树 public class Solution { public TreeNode buildTree(int[] preorder, int[] inorder) { } //排序 public class QuickSort { public int quicksort(int [] res,int left,int right) { int point=res[left]; while(left } public class TreeSort { public void shiftdown(int [] res,int i,int n) { int left=2i+1; int right=2i+2; int min=i; if(left } //统计次数 import java.util.HashMap; import java.util.Iterator; import java.util.Map.Entry; import java.util.Scanner; public class TEST { } Map map = new HashMap(); Iterator iter = map.entrySet().iterator(); while (iter.hasNext()) { Map.Entry entry = (Map.Entry) iter.next(); Object key = entry.getKey(); Object val = entry.getValue(); SpringMVC工作流程描述 尚未解决的问题 2016-10-6 nginx 负载均衡 一致hash Netty NIO 字符串组合 项目总结: 爬虫: 1.Downloader Downloader负责从互联网上下载页面,以便后续处理。WebMagic默认使用了Apache HttpClient作为下载工具。 2.PageProcessor PageProcessor负责解析页面,抽取有用信息,以及发现新的链接。WebMagic使用Jsoup作为HTML解析工具,并基于其开发了解析XPath的工具Xsoup。 在这四个组件中,PageProcessor对于每个站点每个页面都不一样,是需要使用者定制的部分。 3.Scheduler Scheduler负责管理待抓取的URL,以及一些去重的工作。WebMagic默认提供了JDK的内存队列来管理URL,并用集合来进行去重。也支持使用Redis进行分布式管理。 除非项目有一些特殊的分布式需求,否则无需自己定制Scheduler。 4.Pipeline Pipeline负责抽取结果的处理,包括计算、持久化到文件、数据库等。WebMagic默认提供了“输出到控制台”和“保存到文件”两种结果处理方案。 Pipeline定义了结果保存的方式,如果你要保存到指定数据库,则需要编写对应的Pipeline。对于一类需求一般只需编写一个Pipeline。 随着AJAX技术不断的普及,以及现在AngularJS这种Single-page application框架的出现,现在js渲染出的页面越来越多。对于爬虫来说,这种页面是比较讨厌的:仅仅提取HTML内容,往往无法拿到有效的信息。那么如何处理这种页面呢?总的来说有两种做法: MyBatis是目前非常流行的ORM框架,它的功能很强大,然而其实现却比较简单、优雅。本文主要讲述MyBatis的架构设计思路,并且讨论MyBatis的几个核心部件,然后结合一个select查询实例,深入代码,来探究MyBatis的实现。 一、MyBatis的框架设计 注:上图很大程度上参考了iteye 上的chenjc_it 所写的博文原理分析之二:框架整体设计 中的MyBatis架构体图,chenjc_it总结的非常好,赞一个! 1.接口层---和数据库交互的方式 MyBatis和数据库的交互有两种方式: a.使用传统的MyBatis提供的API; b. 使用Mapper接口 1.1.使用传统的MyBatis提供的API 这是传统的传递Statement Id 和查询参数给 SqlSession 对象,使用 SqlSession对象完成和数据库的交互;MyBatis 提供了非常方便和简单的API,供用户实现对数据库的增删改查数据操作,以及对数据库连接信息和MyBatis 自身配置信息的维护操作。 1.2. 使用Mapper接口 MyBatis 将配置文件中的每一个 根据MyBatis 的配置规范配置好后,通过SqlSession.getMapper(XXXMapper.class) 方法,MyBatis 会根据相应的接口声明的方法信息,通过动态代理机制生成一个Mapper 实例,我们使用Mapper 接口的某一个方法时,MyBatis 会根据这个方法的方法名和参数类型,确定Statement Id,底层还是通过SqlSession.select("statementId",parameterObject);或者SqlSession.update("statementId",parameterObject); 等等来实现对数据库的操作,(至于这里的动态机制是怎样实现的,我将准备专门一片文章来讨论,敬请关注~) MyBatis 引用Mapper 接口这种调用方式,纯粹是为了满足面向接口编程的需要。(其实还有一个原因是在于,面向接口的编程,使得用户在接口上可以使用注解来配置SQL语句,这样就可以脱离XML配置文件,实现“0配置”)。 2.数据处理层 数据处理层可以说是MyBatis 的核心,从大的方面上讲,它要完成三个功能: a. 通过传入参数构建动态SQL语句; b. SQL语句的执行以及封装查询结果集成List 4 引导层 引导层是配置和启动MyBatis 配置信息的方式。MyBatis 提供两种方式来引导MyBatis :基于XML配置文件的方式和基于Java API 的方式,读者可以参考我的另一片博文:Java Persistence with MyBatis 3(中文版) 第二章 引导MyBatis 二、MyBatis的主要构件及其相互关系 从MyBatis代码实现的角度来看,MyBatis的主要的核心部件有以下几个: ? SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能 ? Executor MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护 ? StatementHandler 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。 ? ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所需要的参数, ? ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合; ? TypeHandler 负责java数据类型和jdbc数据类型之间的映射和转换 ? MappedStatement MappedStatement维护了一条 三、从MyBatis一次select 查询语句来分析MyBatis的架构设计 一、数据准备(非常熟悉和应用过MyBatis 的读者可以迅速浏览此节即可) [sql] view plain copy print? values (107, 'Diana', 'Lorentz', 'DLORENTZ', 4200.00); [html] view plain copy print? 创建eclipse 或者myeclipse 的maven项目,maven配置如下: [html] view plain copy print? 二、SqlSession 的工作过程分析: MyBatis在初始化的时候,会将MyBatis的配置信息全部加载到内存中,使用org.apache.ibatis.session.Configuration实例来维护。使用者可以使用sqlSession.getConfiguration()方法来获取。MyBatis的配置文件中配置信息的组织格式和内存中对象的组织格式几乎完全对应的。上述例子中的 [html] view plain copy print? 3.MyBatis执行器Executor根据SqlSession传递的参数执行query()方法(由于代码过长,读者只需阅读我注释的地方即可): [java] view plain copy print? [java] view plain copy print? 上述的Executor.query()方法几经转折,最后会创建一个StatementHandler对象,然后将必要的参数传递给StatementHandler,使用StatementHandler来完成对数据库的查询,最终返回List结果集。 从上面的代码中我们可以看出,Executor的功能和作用是: (1、根据传递的参数,完成SQL语句的动态解析,生成BoundSql对象,供StatementHandler使用; (2、为查询创建缓存,以提高性能(具体它的缓存机制不是本文的重点,我会单独拿出来跟大家探讨,感兴趣的读者可以关注我的其他博文); (3、创建JDBC的Statement连接对象,传递给StatementHandler对象,返回List查询结果。 } 以上我们可以总结StatementHandler对象主要完成两个工作: (1. 对于JDBC的PreparedStatement类型的对象,创建的过程中,我们使用的是SQL语句字符串会包含 若干个? 占位符,我们其后再对占位符进行设值。 StatementHandler通过parameterize(statement)方法对Statement进行设值; @Override
public void run() {
System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");
try {
Thread.sleep(5000); //以睡眠来模拟写入数据操作
System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
}catch(BrokenBarrierException e){
e.printStackTrace();
}
System.out.println("所有线程写入完毕,继续处理其他任务...");
}
}
static class Worker extends Thread{
private int num;
private Semaphore semaphore;
public Worker(int num,Semaphore semaphore){
this.num = num;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println("工人"+this.num+"占用一个机器在生产...");
Thread.sleep(2000);
System.out.println("工人"+this.num+"释放出机器");
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public ThreadB(ThreadA a) {
// TODO Auto-generated constructor stub
this.a = a;
}
@Override
public void run() {
// TODO Auto-generated method stub
super.run();
System.out.println("ThreadB start");
try {
a.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("ThreadB end");
}
public ThreadB(ThreadA a) {
// TODO Auto-generated constructor stub
this.a = a;
}
@Override
public void run() {
// TODO Auto-generated method stub
super.run();
System.out.println("ThreadB start");
try {
for (int i = 0; i < 10; i++) {
if(i==2){
a.yield();
}
System.out.println("ThreadB " + i);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("ThreadB end");
}
CAS 指的是现代 CPU 广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。这个指令会对内存中的共享数据做原子的读写操作。简单介绍一下这个指令的操作过程:首先,CPU 会将内存中将要被更改的数据与期望的值做比较。然后,当这两个值相等时,CPU 才会将内存中的数值替换为新的值。否则便不做操作。最后,CPU 会将旧的数值返回。这一系列的操作是原子的。它们虽然看似复杂,但却是 Java 5 并发机制优于原有锁机制的根本。简单来说,CAS 的含义是“我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做修改,并告诉我原来的值是多少”。(这段描述引自《Java并发编程实践》)
简单的来说,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V。这是一种乐观锁的思路,它相信在它修改之前,没有其它线程去修改它;而Synchronized是一种悲观锁,它认为在它修改之前,一定会有其它线程去修改它,悲观锁效率很低。下面来看一下AtomicInteger是如何利用CAS实现原子性操作的。
//运行时, jvm 把appmain的信息都放入方法区 { public static void main(String[] args) //main 方法本身放入方法区。 { Sample test1 = new Sample( " 测试1 " ); //test1是引用,所以放到栈区里, Sample是自定义对象应该放到堆里面 Sample test2 = new Sample( " 测试2 " ); test1.printName(); test2.printName(); } } Sample.java public class Sample //运行时, jvm 把appmain的信息都放入方法区 { / 范例名称 */ private name; //new Sample实例后, name 引用放入栈区里, name 对象放入堆里 / 构造方法 / public Sample(String name) { this .name = name; } /** 输出 / public void printName() //print方法本身放入 方法区里。 { System.out.println(name); } } OK,让我们开始行动吧,出发指令就是:“java AppMain”,包包里带好我们的行动向导图,Let’s GO!
这段代码保证初始化时HashMap的容量总是2的n次方,即底层数组的长度总是为2的n次方。 当length总是 2 的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。 这看上去很简单,其实比较有玄机的,我们举个例子来说明: 假设数组长度分别为15和16,优化后的hash码分别为8和9,那么&运算后的结果如下: h & (table.length-1) hash table.length-1 8 & (15-1): 0100 & 1110 = 01009 & (15-1): 0101 & 1110 = 0100
结合负载因子的定义公式可知,threshold就是在此loadFactor和capacity对应下允许的最大元素数目,超过这个数目就重新resize,以降低实际的负载因子。默认的的负载因子0.75是对空间和时间效率的一个平衡选择。当容量超出此最大容量时, resize后的HashMap容量是容量的两倍:
Hashmap是一种非常常用的、应用广泛的数据类型,最近研究到相关的内容,就正好复习一下。网上关于hashmap的文章很多,但到底是自己学习的总结,就发出来跟大家一起分享,一起讨论。
上面的Entry就是数组中的元素,它持有一个指向下一个元素的引用,这就构成了链表。 当我们往hashmap中put元素的时候,先根据key的hash值得到这个元素在数组中的位置(即下标),然后就可以把这个元素放到对应的位置中了。如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。从hashmap中get元素时,首先计算key的hashcode,找到数组中对应位置的某一元素,然后通过key的equals方法在对应位置的链表中找到需要的元素。从这里我们可以想象得到,如果每个位置上的链表只有一个元素,那么hashmap的get效率将是最高的,但是理想总是美好的,现实总是有困难需要我们去克服,哈哈~
看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!
所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
说到这里,我们再回头看一下hashmap中默认的数组大小是多少,查看源代码可以得知是16,为什么是16,而不是15,也不是20呢,看到上面annegu的解释之后我们就清楚了吧,显然是因为16是2的整数次幂的原因,在小数据量的情况下16比15和20更能减少key之间的碰撞,而加快查询的效率。
}
B+Tree:非叶子节点只存key,大大滴减少了非叶子节点的大小,那么每个节点就可以存放更多的记录,树更矮了,I/O操作更少了。所以B+Tree拥有更好的性能。
? 不继承异步输入和输出
more和cat more more命令,功能类似 cat ,cat命令是整个文件的内容从上到下显示在屏幕上。 more会以一页一页的显示方便使用者逐页阅读,而最基本的指令就是按空白键(space)就往下一页显示,按 b 键就会往回(back)一页显示,而且还有搜寻字串的功能 。more命令从前向后读取文件,因此在启动时就加载整个文件。 Linux进程间通信的方式?套接字,fifo,消息队列,共享内存(最快),信号,信号量public int findKthLargest(int[] nums, int k) {
if (k < 1 || nums == null || nums.length < k) {
return 0; }
return findKthLargest(nums, 0, nums.length - 1, k);
}
public int findKthLargest(int[] nums, int start, int end, int k) {
// 中枢值
int pivot = nums[start];
int lo = start;
int hi = end;
while (lo < hi) {
// 将小于中枢值的数移动到数组左边
while (lo < hi && nums[hi] >= pivot) {
hi--;
}
nums[lo] = nums[hi];
// 将大于中枢值的数移动到数组右边
while (lo < hi && nums[lo] <= pivot) {
lo++;
}
nums[hi] = nums[lo];
}
nums[lo] = pivot;
// 如果已经找到了
if (end - lo + 1 == k) {
return pivot;
}
// 第k大的数在lo位置的右边
else if (end - lo + 1 > k){
return findKthLargest(nums, lo + 1, end, k);
}
// 第k大的数在lo位置的左边
else {
// k-(end-lo+1)
// (end-lo+1):表示从lo位置开始到end位置的元素个数,就是舍掉右半部分
// 原来的第k大变成k-(end-lo+1)大
return findKthLargest(nums, start, lo - 1, k - (end - lo + 1));
}
}
if(q.size()>k){
q.poll();
}
}
return q.peek();
private Queue
public void push(int x) { q1.add(x); }
//入栈:直接入队列q1即可
//出栈:把q1的除最后一个元素外全部转移到队q2中,然后把刚才剩下q1中的那个元素出队列。之后把q2中的全部元素转移回q1中
public void pop()
{
if(q1.size()==1)
{
q1.poll();
}
else
{
while(q1.size()>1)
{
q2.offer(q1.poll());
}
q1.poll();
while(!q2.isEmpty())
{
q1.offer(q2.poll());
}
}
}
public int top()
{
while(q1.size()>1)
{
q2.offer(q1.poll());
}
int x=q1.poll();
q2.add(x);
while(!q2.isEmpty())
{
q1.offer(q2.poll());
}
return x;
}
//s1是入栈的,s2是出栈的。保证所有元素都在一个栈里面
//入队列时:如果s1为空,把s2中所有的元素倒出压到s1中;否则直接压入s1
//出队列时:如果s2不为空,把s2中的栈顶元素直接弹出;否则,把s1的所有元素全部弹出压入s2中,再弹出s2的栈顶元素
private Stack
> combinationSum(int[] candidates, int target) { List
> res = new ArrayList<>(); List
return res; } public void findSum(int[] candidates, int target, int sum, int level,List> res){ if(sum == target) { res.add(new ArrayList<>(temp)); return; } else if(sum > target) { return; } else { for(int i=level;i
///////////////////////////////////不允许元素有重复
public List
> combinationSum2(int[] candidates, int target) {
List
> res = new ArrayList<>();
List
> res){
if(sum == target) {
res.add(new ArrayList<>(temp));
return;
} else if(sum > target) {
return;
} else {
for(int i=level;i
}
//if remove the first node
if(fast == null){
head = head.next;
return head;
}
if(slow.next!=null)
slow.next=slow.next.next;
return slow;
}
ListNode pNext=pNode.next;
if(pNext==null)
{
pRevrese=pNode;
}
pNode.next=pPre;
pPre=pNode;
pNode=pNext;
}
return pRevrese;
}
} } return false;
} } A: a1 → a2 ↘ c1 → c2 → c3 ↗
B: b1 → b2 → b3 {
tailA=tailA.next;
len1++;
}
while(tailB.next!=null)
{
tailB=tailB.next;
len2++;
}
if( tailA!=tailB )
{
return null;
}
ListNode t1=headA;
ListNode t2=headB;
}
}
}
}
String[] res =ant.trim().split(" ");
for(int i=0;i
if (s.length() == 1) {
return s;
}
String longest = s.substring(0, 1);
for (int i = 0; i < s.length(); i++) {
// get longest palindrome with center of i
String tmp = helper(s, i, i);
if (tmp.length() > longest.length()) {
longest = tmp;
}
// get longest palindrome with center of i, i+1
tmp = helper(s, i, i + 1);
if (tmp.length() > longest.length()) {
longest = tmp;
}
}
return longest;
res[i][0] = 1;
}
for(int i=0; i
res[0][i] = 1;
}
for(int i=1;i if(A[i][j]==0)
{
res[i][j]=res[i-1][j]+res[i][j-1];
}
}
}
return res[m-1][n-1] ;
}
return gloal;
}
11110 11010 11000 00000 Answer: 1private void search(char[][] grid, int x, int y) {
if(x<0 || x>=grid.length || y<0 || y>=grid[0].length || grid[x][y]!='1') return;
grid[x][y] = '0';
search(grid, x-1, y);
search(grid, x+1, y);
search(grid, x, y-1);
search(grid, x, y+1);
}
int[] f = new int[n+1];
f[0]=1;
f[1]=1;
for(int i=2;i<=n;i++)
{
for(int k=1;k<=i;k++)
{
f[i]+=f[k-1]*f[i-k];
}
}
return f[n];
}
if (root==null )
{
return true;
}
else{
return check(root.left,root.right);
}
}
public boolean check( TreeNode left,TreeNode right)
{
if (left==null && right==null) return true;
if (left!=null && right==null) return false;
if (left==null && right!=null) return false;
return left.val == right.val && check(left.left, right.right)&& check(left.right, right.left);
}
> levelOrder(TreeNode root) { List
> ret = new ArrayList
>(); if (root == null) { return ret; } //队列是先进先出 Queue
while (!q.isEmpty()) {
int size = q.size();
List
> levelOrder(TreeNode root) { List
> ret = new ArrayList
>(); if (root == null) { return ret; } //队列是先进先出 Queue
while (!q.isEmpty()) {
int size = q.size();
List
return bulidTree1( preorder, 0, preorder.length-1, inorder, 0, inorder.length-1);
}
public TreeNode bulidTree1(int[] preorder,int pstart,int pend, int[] inorder, int istart,int iend)
{
if(pstart>pend||istart> iend)
{
return null;
}
int temp=preorder[pstart];
int index=0;
for(int i=istart;i<=iend;i++)
{
if(inorder[i]==temp)
{
index=i;
}
}
int len=index-istart;
TreeNode root=new TreeNode(temp);
root.left=bulidTree1( preorder, pstart+1, pstart+len, inorder, istart, index-1);
root.right=bulidTree1( preorder, pstart+len+1, pend, inorder, index+1, iend);
return root;
}
right--;
res[left]=res[right];
while(left
}
}
public void HeapBuild(int[] res,int n)
{
int mid=n/2-1;
for(int i=mid;i>=0;i--)
{
shiftdown(res,i,n);
}
}
public void HeapSort(int[] res,int n)
{
HeapBuild( res, n);
for(int i=n-1;i>0;i--)
{
int temp=res[0];
res[0]=res[i];
res[i]=temp;
shiftdown(res,0,i);
}
}
public static void main(String[] args) {
TreeSort s=new TreeSort();
int [] res=new int[] {1,4 ,2,5 ,3 ,6,9,7,8};
int n=res.length;
s.HeapSort(res, n);
for(int i=0;i
public static void main(String[] args) {
HashMap
}
1. 用户向服务器发送请求,请求被Spring 前端控制Servelt DispatcherServlet捕获;
2. DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI)。然后根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain对象的形式返回;
3. DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter。(附注:如果成功获得HandlerAdapter后,此时将开始执行拦截器的preHandler(...)方法)
4. 提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)。 在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作:
HttpMessageConveter: 将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息
数据转换:对请求消息进行数据转换。如String转换成Integer、Double等
数据根式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等
数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中
5. Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象;
6. 根据返回的ModelAndView,选择一个适合的ViewResolver(必须是已经注册到Spring容器中的ViewResolver)返回给DispatcherServlet ;
7. ViewResolver 结合Model和View,来渲染视图
上述使用MyBatis 的方法,是创建一个和数据库打交道的SqlSession对象,然后根据Statement Id 和参数来操作数据库,这种方式固然很简单和实用,但是它不符合面向对象语言的概念和面向接口编程的编程习惯。由于面向接口的编程是面向对象的大趋势,MyBatis 为了适应这一趋势,增加了第二种使用MyBatis 支持接口(Interface)调用方式。
3.2. 连接池管理机制 由于创建一个数据库连接所占用的资源比较大, 对于数据吞吐量大和访问量非常大的应用而言,连接池的设计就显得非常重要,对于连接池管理机制我已经在我的博文《深入理解mybatis原理》 Mybatis数据源与连接池 中有非常详细的讨论,感兴趣的读者可以点击查看。 3.3. 缓存机制 为了提高数据利用率和减小服务器和数据库的压力,MyBatis 会对于一些查询提供会话级别的数据缓存,会将对某一次查询,放置到SqlSession中,在允许的时间间隔内,对于完全相同的查询,MyBatis 会直接将缓存结果返回给用户,而不用再到数据库中查找。(至于具体的MyBatis缓存机制,我将准备专门一篇文章来讨论,敬请关注~)
事务管理机制对于ORM框架而言是不可缺少的一部分,事务管理机制的质量也是考量一个ORM框架是否优秀的一个标准,对于数据管理机制我已经在我的博文《深入理解mybatis原理》 MyBatis事务管理机制 中有非常详细的讨论,感兴趣的读者可以点击查看。
1. 准备数据库数据,创建EMPLOYEES表,插入数据:
MyBatis封装了对数据库的访问,把对数据库的会话和事务控制放到了SqlSession对象中。
上述的"com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary",是配置在EmployeesMapper.xml 的Statement ID,params 是传递的查询参数。 让我们来看一下sqlSession.selectList()方法的定义: [java] view plain copy print?
加载到内存中会生成一个对应的MappedStatement对象,然后会以key="com.louis.mybatis.dao.EmployeesMapper.selectByMinSalary" ,value为MappedStatement对象的形式维护到Configuration的一个Map中。当以后需要使用的时候,只需要通过Id值来获取就可以了。 从上述的代码中我们可以看到SqlSession的职能是: SqlSession根据Statement ID, 在mybatis配置对象Configuration中获取到对应的MappedStatement对象,然后调用mybatis执行器来执行具体的操作。
(2.StatementHandler通过List