530 likes | 748 Views
Java并发编程. 窦蕾. 一、并发基础. 2. 线程的状态. 3. 使用并发的优点和风险. 充分利用处理器资源,包括多处理器单但处理器 更好的响应性 安全性风险;没有被正确同步 活跃度风险;死锁,饥饿,活锁 性能风险;线程带来性能开销,如上下文切换. 4. 二.线程安全. 5. 线程安全的定义. 多个线程访问一个类的时候,不用考虑考虑线程的 调度和执行顺序,也不需要做额外的同步和其他协 调,这个类的行为仍然是正确的,这个类就是线程 安全的。 无状态的类是线程安全的. 6. 共享变量. 只要有共享变量,就会有线程安全问题,涉及到两 个问题:
E N D
Java并发编程 窦蕾
一、并发基础 2
线程的状态 3
使用并发的优点和风险 充分利用处理器资源,包括多处理器单但处理器 更好的响应性 安全性风险;没有被正确同步 活跃度风险;死锁,饥饿,活锁 性能风险;线程带来性能开销,如上下文切换 4
二.线程安全 5
线程安全的定义 多个线程访问一个类的时候,不用考虑考虑线程的 调度和执行顺序,也不需要做额外的同步和其他协 调,这个类的行为仍然是正确的,这个类就是线程 安全的。 无状态的类是线程安全的 6
共享变量 只要有共享变量,就会有线程安全问题,涉及到两 个问题: 原子性 2.可见性 7
原子性 一些非原子性操作 1)复合操作: if(p == null){ p = xxxxxx; } 2)复合运算符:++, -- 8
原子变量 JDK提供的原子变量:AtomXXXX开头的一系列类 AtomLong count = new AtomLong(); count.increaseAndGet(); // ++count 使用了原子变量,也不能保证符合操作线程安全: if(count.get() == 0){ count.incrementAndGet(); } 原子变量提供了非阻塞的操作如 CAS(compareAndSet) 9
锁 锁的作用:保证原子性,保证可见性 1.内部锁:synchronized 内部锁是互斥锁,只有一个线程可以获得 锁,其他线程必须阻塞。 2.高级锁:Lock接口的实现,可以实现一些高级特性 10
内部锁使用 使用一个对象作为锁 sychronized(obj1){ //do some thing } 使用当前对象作为锁 class Test{ synchronized void func1(){......} synchronized void func2(){......} } //没有获得锁的线程,不能调用任何一个函数 //已经获得锁的线程,请求已经获得的锁一定会成功(锁的重进入) 11
可见性 问题: 1)向对象写入一个值,其他线程是否能立即读 取到这个值 2)是否能保证每个线程看到的对象是完整的 解决共享变量的可见性问题的几种方式: 线程封闭 使用不可变对象 安全发布
可见性问题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
可见性事例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
64位操作 64位操作 非volatile的long, double的读写,会被拆成两个32位操作,不是原子的 15
保证可见性的方法 1.同步 synchronized 2.volatile 1)不会缓存在寄存器中 2)不会和其他内存操作一起重排序(修正后的) 区别: synchronized同时保持了原子性和可见性 volatile只保证了原子性 16
线程封闭 不在线程之间共享变量,以达到线程安全 ad-hoc:完全有调用者来保证 栈限制:变量范围限制在堆栈内(方法内) ThreadLocal:每个线程维护的副本 17
发布和逸出 • 发布:把共享变量暴露到作用域范围外 • 逸出:发布了不完整的对象 public class UnsafeHolder{ public Holder holder; public void init(){ holder = new Holder(); //其他线程可能看见不完整对象 } } 18
隐含的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) {........} }
初始化安全 以下方式,避免产生不完整的对象 • 静态初始化器(clinit): public static Holder holder = new Holder() • final对象: public final Set<String> = new HashSet<String>() • volatile修饰的对象 • 使用AtomicReference 如果对象是可变的,以后还是需要同步的
三.Java5的并发工具 19
任务框架和线程池 20
任务执行 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
饱和策略 • 通常不会用无限队列作为等待队列,以防止内存不够。这样就涉及到在队列满的时候,再提交任务,线程池会做出怎样的反应,即饱和策略; • 饱和策略的分类: AbortPolicy:抛出RejectException,丢弃提交的任务 CallerRunsPolicy:在调用者线程运行任务 DiscardPolicy:和AbordPolicy类似,但不抛出异常 DiscardOldPolicy:放弃最旧的等待任务 JDK中并没有阻塞提交的饱和策略,需要自己用信号量等方式实现 • 设置饱和策略: executor.setRejectedExecutionHandler( new ThreadPoolExecutor.CallerRunsPolicy());
Callable和Future • Callable和Runnable比较: 1)可携带返回结果 2)可抛出受检异常 通过Future,可以控制任务的生命周期,等待运行结 果
扩展ThreadPoolExecutor • protected afterExecute(Runnable r, Throwable t) • protected beforeExecute(Thread t, Runnable r) • protected void terminated() • 构造时可以使用自定义的ThreadFactory来创建线程
同步辅助工具 信号量Semaphore 控制访问资源的许可证数量 闭锁CountdownLatch 等待所有人都完成工作 22
同步容器 ConcurrentHashMap 采用锁分离技术的高效Map BlockingQueue 生产者-消费者模式 23
四.取消和关闭 24
程序终止 不调用System.exit()的情况下,JVM的自然退出需要满足以下条件: 所有线程,除了daemon线程之外,全部终止 daemon线程:Thread.setDaemon(true) 在Jvm退出前捕获退出信号: Runtime.addShutdownHook(Runnable); 25
几种关闭方式 自定义的关闭标识 阻塞的线程不好处理 使用interrupt()机制 可以使阻塞的线程抛出InterruptException 26
线程池的关闭 平滑的关闭,处理完所有任务退出 executor.shutdown(); executor.awaitTermination(100,TimeUnit.SECONDS); 强制关闭,取消所有任务,包括等待中的 List<Runnable> tasks = executor.shutdownNow(); executor.awaitTermination(100, TimeUnit.SECONDS); 27
活跃度风险包括 • 死锁 • 活锁:不断尝试同样操作,但无法成功 • 饥饿:其他线程永远无法获取到资源,比如CPU
死锁 • 锁顺序死锁 两个线程互相持有对方的锁引起的 ThreadA: trylock(A) trylock(B) (WAIT) unlock(B) unlock(A) ThreadB: trylock(B) trylock(A) (WAIT) unlock(A) unlock(B) • 资源死锁 两个线程互相持有对方的资源引起的死锁 • 活锁 并不存在互相持有资源的问题,而是不断尝试获取某资源,却永远不能成功
死锁的诊断和解除 • 检查死锁的工具 threaddump jconsole • 避免死锁 指定锁顺序 使用Lock做可轮询或有时间限制的锁
串行和并行 • 合理分割应用中的串行化和并行化部分是比较关键的 • 消耗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使用率
线程引入的开销 • 上下文切换 • 内存同步 • 阻塞
上下文切换带来的开销 • 当线程数大于CPU数量的时候,为保证其他线程能使用CPU,会强行换出正在执行的线程,调入新的线程。 • 频繁竞争锁,而被阻塞,会引起更多的上下文切换
内存同步带来的开销 • volatile, synchronized使用的存储关卡指令,刷新缓存,另外抑制了编译器的优化(比如重排序) • JVM的一些优化措施: 锁省略:通过逸出分析,省略没有暴露在外的对象的锁请求 锁粗化:合并相邻的锁 public String getStoogeNames() { List<String> stooges = new Vector<String>(); stooges.add("Moe"); stooges.add("Larry"); stooges.add("Curly"); return stooges.toString(); }
竞争锁 • 一旦有一个线程获取锁时被阻塞,这个锁就由非竞争锁变成了竞争锁 • 竞争锁导致:1)串行化 2)上下文切换 • 有以下两点会影响锁的竞争性: 1)持有锁的时间 2)请求锁的频率 可以通过一下方式来减少锁的竞争: 1)减少持有锁的时间,可以通过缩小锁的范围 2)减少请求锁的频率,可以通过分离锁或拆分锁
拆分锁 • 两个不相干的变量,却用了一个锁守护,例如下面: 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()用另一把锁
分离锁 • 拆分锁的扩展,例如一个数组,每一部分用不同的锁守护,避免对整个数组加锁带来的低效率 LOCK1 LOCK2 LOCK3 LOCK4 LOCK5 ConcurrentHashMap使用16个锁来守护不同的Hash值范围 缺点:增加了复杂性,某些操作需要获取全部的锁
性能调试工具 • 在并发程序中通常的目标是充分利用处理器, • 可以用一些操作系统工具来检测比如vmstat, w,top等 • 引起CPU利用率不高的原因,比如IO受限,频繁的锁竞争等 • 上图是Netbeans Profiler一个不错的性能分析工具,比JConsole要强大得多
Lock的使用 Lock lock = new ReentrantLock(); lock.lock(); try{ .... } finally{ lock.unlock();//一定要记得在finally中unlock }
ReentrantLock和内部锁的比较 • 内部锁使用起来比较简单,使用Lock需要时时记得在finally块释放锁 • 在JDK5中,ReentrantLock无法在thread dump中显示出来,但在JDK6中已经得到了支持 • ReentrantLock支持一些高级特性,比如定时,轮询,中断等等。synchronized如果遇到死锁是比较严重的。
可轮询和定时的锁 可中断的锁 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{ //获取失败,做些事情 } }
读写锁 private static ReadWriteLock lock = new ReentrantReadWriteLock(); Lock r = lock.readLock(); //读锁 Lock w = lock.writeLock();//写锁 读写锁之允许一个线程持有写锁,多个线程持有读 锁。 在写比较少,读很多,且每次读的时间较长的情况 下可以使用