1 / 50

Java并发编程

Java并发编程. 窦蕾. 一、并发基础. 2. 线程的状态. 3. 使用并发的优点和风险. 充分利用处理器资源,包括多处理器单但处理器 更好的响应性 安全性风险;没有被正确同步 活跃度风险;死锁,饥饿,活锁 性能风险;线程带来性能开销,如上下文切换. 4. 二.线程安全. 5. 线程安全的定义. 多个线程访问一个类的时候,不用考虑考虑线程的 调度和执行顺序,也不需要做额外的同步和其他协 调,这个类的行为仍然是正确的,这个类就是线程 安全的。 无状态的类是线程安全的. 6. 共享变量. 只要有共享变量,就会有线程安全问题,涉及到两 个问题:

juro
Download Presentation

Java并发编程

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. Java并发编程 窦蕾

  2. 一、并发基础 2

  3. 线程的状态 3

  4. 使用并发的优点和风险 充分利用处理器资源,包括多处理器单但处理器 更好的响应性 安全性风险;没有被正确同步 活跃度风险;死锁,饥饿,活锁 性能风险;线程带来性能开销,如上下文切换 4

  5. 二.线程安全 5

  6. 线程安全的定义 多个线程访问一个类的时候,不用考虑考虑线程的 调度和执行顺序,也不需要做额外的同步和其他协 调,这个类的行为仍然是正确的,这个类就是线程 安全的。 无状态的类是线程安全的 6

  7. 共享变量 只要有共享变量,就会有线程安全问题,涉及到两 个问题: 原子性 2.可见性 7

  8. 原子性 一些非原子性操作 1)复合操作: if(p == null){ p = xxxxxx; } 2)复合运算符:++, -- 8

  9. 原子变量 JDK提供的原子变量:AtomXXXX开头的一系列类 AtomLong count = new AtomLong(); count.increaseAndGet(); // ++count 使用了原子变量,也不能保证符合操作线程安全: if(count.get() == 0){ count.incrementAndGet(); } 原子变量提供了非阻塞的操作如 CAS(compareAndSet) 9

  10. 锁的作用:保证原子性,保证可见性 1.内部锁:synchronized 内部锁是互斥锁,只有一个线程可以获得 锁,其他线程必须阻塞。 2.高级锁:Lock接口的实现,可以实现一些高级特性 10

  11. 内部锁使用 使用一个对象作为锁 sychronized(obj1){ //do some thing } 使用当前对象作为锁 class Test{ synchronized void func1(){......} synchronized void func2(){......} } //没有获得锁的线程,不能调用任何一个函数 //已经获得锁的线程,请求已经获得的锁一定会成功(锁的重进入) 11

  12. 可见性 问题: 1)向对象写入一个值,其他线程是否能立即读 取到这个值 2)是否能保证每个线程看到的对象是完整的 解决共享变量的可见性问题的几种方式: 线程封闭 使用不可变对象 安全发布

  13. 可见性问题1-未及时更新 import java.util.concurrent.TimeUnit; public class VisibilityTest { private static boolean stop; public static void main(String[] args) throws Exception{ Thread backgroudThread = new Thread(new Runnable() { public void run() { int i = 0; while(!stop) { i++; } } }); backgroudThread.start(); TimeUnit.SECONDS.sleep(1); stop = true; } } 13

  14. 可见性事例2-重排序 public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { while (!ready) Thread.yield(); System.out.println(number); //打印结果有可能为0 } } public static void main(String[] args) { new ReaderThread().start(); number = 42; ready = true; } } 14

  15. 64位操作 64位操作 非volatile的long, double的读写,会被拆成两个32位操作,不是原子的 15

  16. 保证可见性的方法 1.同步 synchronized 2.volatile 1)不会缓存在寄存器中 2)不会和其他内存操作一起重排序(修正后的) 区别: synchronized同时保持了原子性和可见性 volatile只保证了原子性 16

  17. 线程封闭 不在线程之间共享变量,以达到线程安全 ad-hoc:完全有调用者来保证 栈限制:变量范围限制在堆栈内(方法内) ThreadLocal:每个线程维护的副本 17

  18. 发布和逸出 • 发布:把共享变量暴露到作用域范围外 • 逸出:发布了不完整的对象 public class UnsafeHolder{ public Holder holder; public void init(){ holder = new Holder(); //其他线程可能看见不完整对象 } } 18

  19. 隐含的this指针逸出 public class ThisEscape { public ThisEscape(EventSource source) { //隐含的this指针被在构造函数中被暴露给了source source.registerListener(new EventListener() { public void onEvent(Event e) { doSomething(e); } }); } void doSomething(Event e) {........} }

  20. 初始化安全 以下方式,避免产生不完整的对象 • 静态初始化器(clinit): public static Holder holder = new Holder() • final对象: public final Set<String> = new HashSet<String>() • volatile修饰的对象 • 使用AtomicReference 如果对象是可变的,以后还是需要同步的

  21. 三.Java5的并发工具 19

  22. 任务框架和线程池 20

  23. 任务执行 1.创建线程池: ThreadPoolExecutor executor = new ThreadPoolExecutor( MIN_WORKER_SIZE, //最小的工作线程数 MAX_WORKER_SIZE,//最大的工作线程数 KEEP_ALIVE_TIME, //空闲线程存活时间 TimeUnit.SECONDS, //KEEP_ALIVE的时间单位 new ArrayBlockingQueue(20));//暂存提交过来的任务的队列 2.任务执行 executor.execute(new Runnable(){………}); 3.简化的方式: Executors工具类,提供了一些默认的线程池创建方法 例如Executors.newFixedThreadPool(int nThreads) 21

  24. 饱和策略 • 通常不会用无限队列作为等待队列,以防止内存不够。这样就涉及到在队列满的时候,再提交任务,线程池会做出怎样的反应,即饱和策略; • 饱和策略的分类: AbortPolicy:抛出RejectException,丢弃提交的任务 CallerRunsPolicy:在调用者线程运行任务 DiscardPolicy:和AbordPolicy类似,但不抛出异常 DiscardOldPolicy:放弃最旧的等待任务 JDK中并没有阻塞提交的饱和策略,需要自己用信号量等方式实现 • 设置饱和策略: executor.setRejectedExecutionHandler( new ThreadPoolExecutor.CallerRunsPolicy());

  25. Callable和Future • Callable和Runnable比较: 1)可携带返回结果 2)可抛出受检异常 通过Future,可以控制任务的生命周期,等待运行结 果

  26. 扩展ThreadPoolExecutor • protected afterExecute(Runnable r, Throwable t) • protected beforeExecute(Thread t, Runnable r) • protected void terminated() • 构造时可以使用自定义的ThreadFactory来创建线程

  27. 同步辅助工具 信号量Semaphore 控制访问资源的许可证数量 闭锁CountdownLatch 等待所有人都完成工作 22

  28. 同步容器 ConcurrentHashMap 采用锁分离技术的高效Map BlockingQueue 生产者-消费者模式 23

  29. 四.取消和关闭 24

  30. 程序终止 不调用System.exit()的情况下,JVM的自然退出需要满足以下条件: 所有线程,除了daemon线程之外,全部终止 daemon线程:Thread.setDaemon(true) 在Jvm退出前捕获退出信号: Runtime.addShutdownHook(Runnable); 25

  31. 几种关闭方式 自定义的关闭标识 阻塞的线程不好处理 使用interrupt()机制 可以使阻塞的线程抛出InterruptException 26

  32. 线程池的关闭 平滑的关闭,处理完所有任务退出 executor.shutdown(); executor.awaitTermination(100,TimeUnit.SECONDS); 强制关闭,取消所有任务,包括等待中的 List<Runnable> tasks = executor.shutdownNow(); executor.awaitTermination(100, TimeUnit.SECONDS); 27

  33. 五.性能和活跃度

  34. 活跃度风险包括 • 死锁 • 活锁:不断尝试同样操作,但无法成功 • 饥饿:其他线程永远无法获取到资源,比如CPU

  35. 死锁 • 锁顺序死锁 两个线程互相持有对方的锁引起的 ThreadA: trylock(A) trylock(B) (WAIT) unlock(B) unlock(A) ThreadB: trylock(B) trylock(A) (WAIT) unlock(A) unlock(B) • 资源死锁 两个线程互相持有对方的资源引起的死锁 • 活锁 并不存在互相持有资源的问题,而是不断尝试获取某资源,却永远不能成功

  36. 死锁的诊断和解除 • 检查死锁的工具 threaddump jconsole • 避免死锁 指定锁顺序 使用Lock做可轮询或有时间限制的锁

  37. 串行和并行 • 合理分割应用中的串行化和并行化部分是比较关键的 • 消耗CPU和受限IO的操作分离 • Amdahl定律:预计并发可能带来的性能提升程度 speed <= 1/(F + (1-F)/N) speed:加速的倍速 F:串行化部分所占的比率 N:CPU的个数 8颗CPU,10%左右的必须串行化的部分,最多估计的加速是: 1/(0.1 + (1-0.1)/8) = 4.7倍 47% CPU使用率

  38. 线程引入的开销 • 上下文切换 • 内存同步 • 阻塞

  39. 上下文切换带来的开销 • 当线程数大于CPU数量的时候,为保证其他线程能使用CPU,会强行换出正在执行的线程,调入新的线程。 • 频繁竞争锁,而被阻塞,会引起更多的上下文切换

  40. 内存同步带来的开销 • volatile, synchronized使用的存储关卡指令,刷新缓存,另外抑制了编译器的优化(比如重排序) • JVM的一些优化措施: 锁省略:通过逸出分析,省略没有暴露在外的对象的锁请求 锁粗化:合并相邻的锁 public String getStoogeNames() { List<String> stooges = new Vector<String>(); stooges.add("Moe"); stooges.add("Larry"); stooges.add("Curly"); return stooges.toString(); }

  41. 竞争锁 • 一旦有一个线程获取锁时被阻塞,这个锁就由非竞争锁变成了竞争锁 • 竞争锁导致:1)串行化 2)上下文切换 • 有以下两点会影响锁的竞争性: 1)持有锁的时间 2)请求锁的频率 可以通过一下方式来减少锁的竞争: 1)减少持有锁的时间,可以通过缩小锁的范围 2)减少请求锁的频率,可以通过分离锁或拆分锁

  42. 拆分锁 • 两个不相干的变量,却用了一个锁守护,例如下面: class ShareData{ private int m; private int n; public synchronized void increaseM{m++;} public synchronized int getM{return m;} public synchronized void increaseN{n++;} public synchronized int getN{return n;} } 可以把这个锁拆成两个,减少锁的竞争: increaseM(), getM()用一把锁,increaseN(),getN()用另一把锁

  43. 分离锁 • 拆分锁的扩展,例如一个数组,每一部分用不同的锁守护,避免对整个数组加锁带来的低效率 LOCK1 LOCK2 LOCK3 LOCK4 LOCK5 ConcurrentHashMap使用16个锁来守护不同的Hash值范围 缺点:增加了复杂性,某些操作需要获取全部的锁

  44. 性能调试工具 • 在并发程序中通常的目标是充分利用处理器, • 可以用一些操作系统工具来检测比如vmstat, w,top等 • 引起CPU利用率不高的原因,比如IO受限,频繁的锁竞争等 • 上图是Netbeans Profiler一个不错的性能分析工具,比JConsole要强大得多

  45. 六、显式锁

  46. Lock的使用 Lock lock = new ReentrantLock(); lock.lock(); try{ .... } finally{ lock.unlock();//一定要记得在finally中unlock }

  47. ReentrantLock和内部锁的比较 • 内部锁使用起来比较简单,使用Lock需要时时记得在finally块释放锁 • 在JDK5中,ReentrantLock无法在thread dump中显示出来,但在JDK6中已经得到了支持 • ReentrantLock支持一些高级特性,比如定时,轮询,中断等等。synchronized如果遇到死锁是比较严重的。

  48. 可轮询和定时的锁 可中断的锁 public static void syncFunction1() throws InterruptedException{ lock.lockInterruptibly(); try{ //做些事情 } finally{ lock.unlock(); } } 可以被interrupt,避免一直阻塞 while(true){ //尝试获取锁,1秒钟获取不到,返回false if(lock.tryLock(1, TimeUnit.SECONDS)){ try{ //锁获取成功,做些事情 return; } finally{ lock.unlock(); } } else{ //获取失败,做些事情 } }

  49. 读写锁 private static ReadWriteLock lock = new ReentrantReadWriteLock(); Lock r = lock.readLock(); //读锁 Lock w = lock.writeLock();//写锁 读写锁之允许一个线程持有写锁,多个线程持有读 锁。 在写比较少,读很多,且每次读的时间较长的情况 下可以使用

  50. 分享结束,感谢您的关注!

More Related