2014年8月1日星期五

JAVA多线程编程(一)

Thread.join(), Thread.start()

Java线程类java.lang.Thread中,有两个方法,start和join。start和直接执行run有何区别?调用join方法会导致什么后果?

start()

启动一个线程并调用run()方法;API中的注释说明:调用start()方法会导致线程开始执行;JVM会调用本线程的run()方法;结果是两个线程并行运行:当前线程和另外执行run方法的线程;
如果直接调用run()方法,则当前线程不会启动;

join()

join()方法在API的注释是:等待直到当前线程死去;这个方法可能会抛出InterruptedException,当其他任何线程打断当前线程时;
它有两个重载方法:join(long millis)和join(long millis, int nanos);
join(long millis):在millis(单位毫秒)时间内等待线程死去;第二个是再多等nanos纳秒的时间;
join可以用来使线程顺序执行,例如:
Thread t2 = new Thread(consumer);
Thread t3 = new Thread(consumer);
Thread t4 = new Thread(consumer);
try {
   t2.start();
   t2.join();
   t3.start();
   t3.join();
   t4.start();
   t4.join();
} catch (InterruptedException e) {
 e.printStackTrace();
}

Lock接口和synchronized

@since 1.5,在JDK5中添加的锁接口;它相比synchronized关键字,提供了更多的锁操作;除了可以实现和synchronized关键字一样的效果之外,Lock接口还提供了读写锁,读操作和写操作的加锁是分开的。提高了锁的效率;也可以满足类似ConcurrentHashMap这种数据结构的高性能的有条件锁;
如图,ConcurrentHashMap的锁结构,

ConcurrentHashMap通过分段锁的机制来达到提高并发处理能力的目的,每个Segment是一个重入锁(ReentrantLock,每次写数据的时候,会根据key的Hash值来使用不同的Segment加锁;

wait和sleep方法的不同

wait()方法是Object的一个方法,调用这个方法会导致当前线程处于等待的状态,直到另外一个线程调用它的notify()或notifyAll()方法,或者一个指定的超时时间已经结束;这个方法会导致当前线程将自己放在一个等待集合中,然后放弃所有在这个对象的同步锁;sleep()方法会导致线程休眠指定的时间,而休眠期间,所有关于当前线程的锁都不会被释放;

volatile关键字

volatile关键字修饰的共享变量在线程并发读取时,JAVA内存模型保证每个线程在读取的时刻的值永远是最新的数据;
解释volatile要说到JMM(JAVA内存模型);
我们看下面的代码,执行一个volatile类型的变量的new操作会导致:
0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: lock addl $0x0,(%esp);
volatile变量会比普通变量多了第二个汇编操作,这个操作在多核处理器会引起:

  • 将当前处理器缓存的数据保存到系统内存
  • 其他cpu的缓存数据失效
操作系统中,处理器为了处理数据速度更快,通常会将数据缓存到L1,L2级缓存中;但是处理器不知道何时将数据回写到主存中,当volatile类型的变量被创建时,JVM会向处理器发送一个lock指令,此时处理器会在更新数据之后,将缓存行数据回写到主存中。其他的处理器的数据还是旧的,为了实现缓存一致性,各个处理器会实现一个缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来判断自己的数据是否过期了。当处理器发现自己的缓存行数据地址被修改,当前处理器会将这个数据标记为无效,当需要操作这个数据时,处理器会强制从系统内存中重新读取这个数据;

JAVA中阻塞队列

java.util.concurrent.BlockingQueue是java中的阻塞队列接口; 他的核心方法包括:
抛出异常 特殊值 阻塞 超时
插入 add(e) offer(e) put(e) offer(e, time, unit)
移除 remove() poll() take() poll(time, unit)
检查 element() peek() 不可用 不可用


  • add()向队列写入一条数据,如果对象写入失败,则抛出异常,否则返回true
  • remove() 从队列删除数据,如果队列不包括这个对象,则抛出异常,否则返回true
  • offer()想队列写入一条数据,如果写入成功返回true,否则返回false
  • pool()从队列头获取一条数据,在指定超时时间内未能成功获取,则返回null
  • put() 向队列写入一条数据,如果队列没有空间写入,则阻塞知道写入成功为止;
  • take() 从队列头部获取一条数据,如果队列没有数据可以获取,则阻塞到有数据获取为止;

BlockingQueue特点

  • BlockingQueue可以限定容量;没有容量限制的队列,总会返回Integer.MAX_VALUE的剩余容量;
  • BlockingQueue主要用于生产者-消费者队列;关于生产者-消费者问题
  • BlockingQueue的实现是线程安全的;
  • BlockingQueue不支持使用一种类似close或shutdown的操作来指定不再添加对象;

CountdownLatch和CyclicBarriar

CountdownLatch

    可以看做是一个同步计数器;任何调用该对象的await()方法的函数都会被阻塞;调用该对象的countdown方法可以使计数器减一,这是一个原子操作,同时只有一个线程可以执行这个方法,直到计数器=0之后,await方法的阻塞会被取消;这个典型的应用场景就是需要等待众多线程工作完才能继续执行的场合,可以利用它来阻塞等待线程执行完毕;

CyclicBarriar

    正好相反,CyclicBarriar也是一个同步计数器,当计数器达到指定数目之后,会执行CyclicBarriar中自定义的run()方法,然后被await()方法阻塞的线程会被唤醒;这个典型应用场景是多个线程在执行前,要等待一个公用的方法执行完毕,可以利用CyclicBarriar计数器来完成;

总结

    上面两个计数器中的CountDownLatch. countdown()是通过CAS实现的原子性;而CyclicBarriar.await()是通过ReentrantLock实现的原子性;
    有关原子性会在下一篇文章中详细讨论;

不可变对象(immutable objects)

不可变对象:即被创建出来之后状态不可改变的对象,例如String,Integer等各种基础类型的包装类,都是不可变对象;每次对它的改变都会创建新的不可变对象;
StringBuffer是可变对象,因为每次修改StringBuffer是修改该对象本身;
图.String's Immutablity
如图,我们创建一个String类型的变量s,
String s = "abcd";
s实际上存储了一个引用地址,该地址指向heap中的一个String对象,"abcd";
s = s.concat(s, "ef");
当对String执行concat操作之后,实际上会创建一个新的String对象:"abcdef", 并且s的引用地址修改指向到新的String对象;
不可变对象在多线程中可以不使用锁机制就可以被多个线程共享;它在多线程编程中有以下特点:
  • 线程安全
  • 不需要锁同步,可以直接使用
  • 提高了性能,避免使用锁或synchronized关键字
  • 可以重复使用;
上图也显示了,不可变对象存在的问题,会产生大量垃圾,给垃圾回收带来压力;对String的concat操作会使之前创建的对象"abcd"变成没有引用的垃圾对象,会逐步被垃圾回收机制回收。

线程调度

分时操作系统

CPU将时间切割为时间片,然后将时间片分配给程序,一个程序的时间片运行结束后,下一个程序的时间片继续执行,多个程序轮流执行;由于CPU的高速处理特性,给人感觉是同时处理一样;

进程和线程

    我们使用的操作系统(Linux,Windows)都有进程的概念,进程就是我们上面所说的程序,它有自己独立的内存空间和系统资源,每个进程的内部的数据和状态都是独立的。创建并执行一个进程的开销比较大,因此线程出现了;
    线程是程序执行流的最小单元,它属于某一个进程,并能和其他线程共享该进程内部的所有资源;线程通常只有寄存器和堆栈,用于存储该线程运行的数据;它比进程更加轻量级,系统在线程间切换比进程消耗更小的资源;一个进程包含多个线程;

线程状态

图. 线程运行机制

  • 创建状态
    • 线程刚刚被创建
  • 可运行状态
    • 线程准备好,等待调度,获取CPU时间片运行前的状态
  • 运行状态
    • 获得CPU资源,处于运行状态
  • 阻塞状态
    • 调用了sleep使得线程处于阻塞状态,此时不会释放锁;
    • 调用await方法,使得线程处于等待状态,此时线程会释放锁
    • 等待获得锁
    • 被阻塞事件阻塞,例如等待数据输入等
  • 死亡状态
    • 线程运行结束

Java线程调度器

java的线程调度器负责调度线程,线程被调度器分为10个级别,1-10,默认是NORMAL_PRIORITY=5.
java的线程调度器负责按照优先级调度线程,一旦时间片有空余,会先让优先级高的线程运行,直到线程dead或sleep或wait,才会执行低优先级的线程;
当有多个线程处于可运行状态,并且优先级相同时,JVM会随机选取一个线程运行。
调度算法有两种:分时和独占式。分时是让所有线程轮流使用CPU时间片;独占则是让一个线程一直执行直到执行结束;JVM采用独占式调度算法;

没有评论:

发表评论