Java实践(四)---进阶笔记之二

来源:互联网 发布:旅行收纳袋 知乎 编辑:程序博客网 时间:2024/05/01 13:06

异常

当发生异常时要做到一下几点:

  1. 向用户通告错误
  2. 保存所以的工作结果
  3. 允许用户以妥善的形式退出程序

假设一个Java程序允许时出现了一个错误,可能是以下情况:

  • 文件包含了错误信息
  • 网络连接出现问题
  • 无效的数组下标
  • 没有被赋值的对象引用

异常处理的任务:

将控制权从错误产生的地方转移给能够处理这种情况的错误处理器

抛出一个封装了错误信息的对象

方法立刻返回,不返回任何值,调用方法的代码停止,异常处理机制开始搜索能够处理这种异常的处理器

Error(未检查,不可控):

  • 系统内部错误
  • 资源耗尽错误

Exception:

  • IOException IO错误 (已检查异常,需要提供异常处理器)
  • RuntimeException 代码问题 (未检查,要避免发生)

Tips:一个方法必须抛出或声明可能的已检查异常。一旦方法抛出了异常,这个方法就不可能返回到调用者,即不必为返回的默认值或错误代码担忧

自定义异常类型,派生于Exception或其子类,定义的类包含2个构造器:

  1. 默认构造器
  2. 带详细描述信息的构造器

捕获异常要进行周密的计划:

  • 如果某个异常发生时没有在任何地方进行捕获,程序终止运行,并在控制台打印输出异常信息(异常的类型,堆栈的内容)
  • 如果编写一个覆盖超类的方法,而这个超类方法又没有抛出异常,那么子类方法就必须捕获方法代码中出现的每一个已检查异常【不允许在子类的throws说明符中出现超过超类方法所列出的异常的范围】

当捕获的异常之间不存在子类关系,可以在同一个catch中捕获,此时的异常变量e为final变量:

try{    。。。}catch(A_Exception | B_Exception e){    。。。}

使用异常机制的技巧

  1. 异常处理不能代替简单的测试【捕获异常所花费的时间较多,只在异常情况下使用异常机制】
  2. 不要过分的细化异常【不要每条语句一个try】
  3. 利用异常层次结构【如,可以抛出更适合的子类或自定义的异常类】
  4. 不要压制异常【如果异常非常重要,应进行处理】
  5. 在检测错误时,苛刻要比放任更好【早抛出,晚捕获】
  6. 不要羞于传递异常【早抛出,晚捕获】

日志

日志系统管理着一个名为Logger.global的默认日志记录器
相应位置(如在main开始处)调用Logger.getGlobal().setLevel(Level.OFF)将会取消所有日志

Tips:在一个专业的应用程序中,不要将所有的日志都记录到一个全局日志记录器中,而是可以自定义日志记录器,日志记录器的父与子之间将共享某些属性(例如,记录器级别会被继承)

调试技巧:

  1. 可以用下面的方法打印或记录任意记录的值:System.out.println("x="+x);Logger/getGlobal().info("x=+x)`
  2. 在每一个类中放置一个main方法,这样可以对每个类进行单元测试【在运行应用程序时,JVM只调用启动类的main方法】
  3. 使用单元测试框架JUnit
  4. 日志代理
  5. 利用Throwable类提供的printStackThread方法可以从任何一个异常对象中获得堆栈情况,在代码的任意位置插入Thread.dumpStack()可以获得堆栈跟踪
  6. 一般来说,堆栈跟踪显示在System.err上,利用printStackThread(PrintWriters)将它发送到一个文件中
  7. 将程序中的错误信息保存在文件中非常有用
  8. 让非捕获异常的堆栈跟踪出现在System.err中并非一个理想方法
  9. 观察类加载过程,用verbose标志启动JVM
  10. 使用-xprof标志运行JVM会运行一个基本的剖析器来跟踪那些代码中经常被调用的方法

泛型程序设计

使用泛型机制编写程序代码要比杂乱地使用object变量,然后再进行强制类型转换的代码具有更好的安全性和可读性

泛型程序设计意味着编写的代码可以被很多不同类型的对象所重用

类型参数(即,类型变量放在修饰符后,返回类型前)使得程序具有更好的可读性和安全性、

泛型类可以有多个类型变量(指定方法的放好类型以及域和局部变量的类型),如public class pair<T,V>{...},其中第一个域和第二个域使用不同的类型

Tips:

  • 泛型类可以看作普通类的工厂
  • 泛型方法可以定义在普通类中,也可以定义在泛型类中
  • 调用泛型方式时,在方法名前的<>中放入具体的类型
<T extends BoudingType>

T 应该是绑定类型的子类,T和绑定类型可以是类也可以是接口,“,”分隔类型变量,“&”分隔限定类型

和继承一样,限定类型可以有多个接口只能有一个类,存在类时,必须在第一个,为了提高效率,将标签接口放在边界列表的末尾

虚拟机没有泛型对象:

  • 所有对象都是普通类,(擦除类型变量并替换为限定类型,无限定则为object)
  • 翻译泛型表达式时,将返回的Object类型强制转换为对应类型
  • 翻译泛型方法时,类型擦除和多态性发生了冲突,需要解决这个问题,要在编译器中为类生成桥方法

Java泛型转换的事实:

  1. 虚拟机中没有泛型,只有普通的类和方法
  2. 所有的类型参数都用它们的限定类型替换
  3. 桥方法的被合成类保持多态
  4. 为保持类型安全性,必要时插入强制类型转换

ArrayList<Manager>可以被替换为一个List<Manager>
ArrayList<Manager>不是一个ArrayList<Employee>

  • 带有超类限定的通配符可以向泛型对象写入
  • 带有子类限定的通配符可以向泛型对象读取

集合

在实现方法时选择不同的数据结构会导致其实现风格以及性能存在很大差异

使用接口类型存放集合的引用

编译器简单地将foreach循环译为带有迭代器的循环

Collection接口扩展了Iterable接口,对于标准类库中的任何集合都可以使用foreach循环

元素访问顺序取决于集合类型

将Java迭代器认为是位于两个元素之间,当调用next时,迭代器就越过下一个元素并返回刚越过的哪一个元素的引用

队列(先进先出)

  • 循环数组:ArrayDeque
  • 链表:LinkedList

Tips:在Java中所有的链表都是双向的

在双向链表的ListIterator中:

  1. 调用next后,remove方法删除迭代器左侧的元素
  2. 调用previous后,remove方法删除迭代器右侧的元素

有一种简单的方法可以检测到并发修改的问题:

在每一个迭代器方法的开始处检查自己改写操作的计数值是否与几何的改写操作计数值一致,如果不一致抛出一个Concurrent.ModificationException异常
并发修改检查只对结构性修改进行检测,如set不会报错,不属于结构性修改

list.listIterator(n)将返回一个迭代器,这个迭代器返回指向索引为n的元素前面的位置,即调用next与调用list.get(n)会产生同一个元素,但是获得这个迭代器的效率很低

散列码可以是任何整数,包括正整数或负整数

树集(在Java中使用红黑树完成):可以以任意顺序将元素插入带集合中, 在对集合进行遍历的时候,每个值将自动按照排序后的顺序呈现

优先级队列中的元素可以按照任意的顺序插入,却总是按照排序的顺序进行检索;无论何时调用remove方法,总会获得当前优先级队列中最小的元素

使用优先队列的典型示例是:任务调度

优先级队列的:

  1. 迭代并不是按照元素的排列顺序访问的
  2. 删除总是删除剩余元素中优先级最小的那个元素

HashMap:对键进行散列
TreeMap:用键的整体顺序对元素进行排序

Map中keySet方法返回一个实现Set接口的类对象,这个类的方法对原映射表进行操作,这种集合成为视图;视图只是包装了接口而不是实际的集合对象,所以只能访问接口中定义的对象

集合和数组之间的转换

String[] values = ...;HashSet<String> staff = new HashSet<>(Arrays.asList(values));Object[] values = staff.toArray();//返回的是对象数组,object类型String[] valuses = staff.toArray(new String[0]);

静态方法Collections.reverseOrder()返回一个比较器进行降序排列,比较器返回b.compareTo(a)

Collections.shuffle()与排序刚好相反,随机地混排类别中的元素顺序

Collections.binarySearch()实现二分查找,注意集合必须是排好序的,苟泽算法返回错误结果;只有采用随机方访问,二分查找才有意义,如果为binarySearch提供一个链表,将自动地变为线性查找

多线程

并发执行的进程数目并不是由CPU数目制约的,操作系统将CPU的时间片分配给每一个进程,给人以并行处理的感觉

进程拥有自己的一整套变量,线程共享数据(共享变量使线程之间的通信比进程之间的通信更有效,更容易)

Thread.sleep()暂停当前线程的活动,可能抛出InterruptedException异常

不要调用Thread类或Runable对象的run方法,直接调用run方法,只会执行同一个线程中的任务,而不会启动新线程,应该调用Thread.start()方法,这个方法将创建一个执行run方法的新线程

interrupt方法可以用来请求终止线程,调用该方法线程中断状态被置位,这是每一个线程都具有的boolean标志

Thread.CurrentThread获得当前线程

Thread.CurrentThread.isInterrupted()判断中断状态是否被置位,如果线程被阻塞(线程调用sleep或wait方法)无法检测中断状态,在被阻塞的线程上调用interrupt方法将会产生InterruptrdException异常或者相反,在调用interrupt方法后的线程上调用sleep方法也会产生InterruptrdException异常并清除interrupt方法设置的状态位

被中断的线程可以决定如何相应中断:

  1. 处理完异常后继续执行
  2. 将中断作为终止的请求

线程的状态

  1. 新建状态:new操作符创建一个新线程,此时线程还没有开始运行,程序还没有开始运行线程中的代码
  2. 可运行状态:一旦调用start方法,线程处于runnable状态,【此时一个线程可能在运行,也可能没有在运行】(一个正在运行的线程任然处于可运行状态,每个处理器运行一个线程,可以有多个线程并行运行)
  3. 被阻塞或等待状态:此时线程不活动,不允许任何代码且消耗最少的资源,直到线程调度器重新激活它们【这2个状态的区别在于它们是如何到达非活动状态的】

被阻塞状态:线程试图获取一个内部对象锁(与java.util.Concurrent锁有区别),而该锁被其他线程持有,则进入此状态
等待状态:当线程等待另一个线程通知调度器一个条件时,进入此状态
计时等待状态:有几个方法有个超时等待参数(Thread.Sleep、Object.wait、Thread.join、Lock.myLock、Condition.await的计时版本),调用它们会导致线程进入计时等待状态【状态保持到超时期满或接收到适当通知】

  1. 被终止状态:1、因为run方法正常退出而自然死亡 2、因为一个没有补货的异常终止了run方法而意外死亡

这里写图片描述

线程属性

线程的优先级:默认继承父类的优先级【线程的优先级依赖于系统】

  • MIN_PRIORITY (1)
  • MAX_PRIORITY (10)
  • NORM_PRIORITY (5)

setPriority方法可以修改线程优先级

setDaemon方法将线程设置为守护线程(守护线程的唯一用途为其他线程提供服务,当只剩下守护线程的时候,虚拟机会退出)

同步

竞争条件:两个或两个以上的线程共享一个数据的存取,如果两个线程存取相同的对象,并且每个对象都调用了修改该对象状态的方法,根据线程访问对象的次序产生讹误的对象

产生竞争条件的原因:对对象的操作不是原子操作

两种方法解决竞争条件:(锁对象)

  • synchronized关键字:自动提供一个锁以及相关条件(synchronized关键字对于大多数显式锁的情况很便利的)
  • ReentrantLock类(ReentrantLock保护代码块的基本结构如下)
private Lock myLock = new ReentrantLock();myLock().lock();try{    ...//临界区}finally{    myLock.unlock();//把解锁操作放在finally子句中至关重要,如果临界区的代码抛出异常,锁必须被释放,否则其他线程永远被阻塞}   

这一结构确保任何时刻只有一个线程进入临界区

Tips:

每个对象有自己的ReentrantLock对象,如果两个线程试图访问同一个对象,那么锁以串行的方式提供服务;如果两个线程访问不同的对象,每一个线程得到不同的锁对象,两个线程都不会发生阻塞

锁可以重入,线程可以重复获得已持有的锁,锁保持一个持有计数,来跟踪对lock方法的嵌套调用,线程的每一次调用lock都有调用unlock来释放锁

(被一个锁保护的代码可以调用另一个使用相同锁的方法)

条件对象

线程进入临界区,却发现在某一个条件满足之后它才能执行,要使用一个条件对象来管理那些已经得到了一个锁但是却不能做有用工作的线程

一个锁对象可以有一个或对个相关的条件对象(每个条件对象命名也可以反映它锁表达的条件的名字)

  1. 当条件对象不满足,当前线程调用该条件对象的await()方法,并放弃锁(这样另一个线程可以获得该对象锁)
  2. 等待获得锁的线程和调用await()方法的线程有本质的区别
  3. 一旦线程调用await()方法进入该条件对象的等待集,当锁可用时,该线程不能马上接触阻塞,相反它处于阻塞知道另一个线程调用同条件对象上的signalAll()方法为止【没有signalAll,则线程永远await将导致死锁】
signalAll();//重新机会因为这一条件对象而等待的所有线程(等待集中的某个线程从await调用返回,获得该锁并从阻塞的地方继续运行,运行前再次检查条件对象是否满足,signalAll只是通知)signal();//重新激活因为这一条件对象而等待的某个线程

锁和条件的关键之处:

  • 锁用来保护代码段【任何时刻只能有一个线程执行被保护的代码】
  • 锁可以管理试图进入被保护代码段的线程
  • 锁可以拥有一个或多个条件对象

每个条件对象管理那些已经进入被保护代码段但是还不能允许的线程

Java中每个对象都有一个内部对象锁,如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法【要调用该方法,线程必须获得内部的对象锁】

public synchronized void method(){    method body;}//等价于public void method(){    this.intrinsicLock.lock();    try{        method body;    }    finally{        this.intrinsicLock.unlick();    }}

内部锁和条件的局限性:

  1. 不能中断一个正在试图获得锁的线程
  2. 试图获得锁时不能设定超时
  3. 每个锁仅有单一的条件可能是不够的

在代码中应该使用哪种锁:

  1. java.util.concurrent 包中的机制,阻塞队列
  2. synchronized关键字减少编写的代码数量
  3. Lock/Cindiction结构

内部对象锁只有一个相关条件,
wait方法添加一个线程到等待集—-intrinsicLock.await();
notifyAll/notify方法接触等待线程的阻塞状态—intrinsicLock.signalAll();

将静态方法声明为synchronized也是合法的,如果调用这个方法,该方法获得相关的子类对象的内部锁,因此没有其他线程可以调用同一个类的这个或任何其他的同步静态方法

每个Java对象有一个锁,线程可以调用同步方法获得锁,还有另一种机制可以获得锁,通过进入一个同步阻塞

synchronized(obj){critical section;}

obj对象被创建仅仅是用来使用每个Java对象持有的锁

监视器

  1. 监视器是只包含私有域的类
  2. 每个监视器类的对象有一个相关的锁
  3. 使用该锁对所有方法进行加锁【即,调用obj.method(),那obj对象的锁是在调用开始时自动获得并在方法返回时自动释放】
  4. 该锁可以有任意多个相关条件【每个条件变量管理一个独立的线程集】

Java中的每一个对象有一个内部的锁和内部的条件,如果一个方法用synchronized关键字声明,那么它表现的就像一个监视器方法,通过调用wait、notifyAll、notify来访问条件变量,但是在下面三个方面Java对象和监视器不同:

  1. 域不要求必须是private
  2. 方法不要求必须是synchronized
  3. 内部锁对客户是可用的

volatile关键字为实例域的同步访问提供了一种免锁机制:如果声明一个域为volatile那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的,volatile变量不能提供原子性

java.util.concurrent.locks

提供2个锁类:

  • ReentrantLock
  • ReentrantReadWriteLock

如果很多线程从一个数据结构读取数据而很少线程修改其中的数据的话,ReentrantReadWriteLock跟有效,在这种情况下允许对读者线程共享访问是适合的,写操作依然必须是互斥操作

private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();private Lock readLock = rwl.readLock();private Lock writeLock = rwl.writeLock();public double getTotalBalance(){    readLock.lock();//读共享,写排斥    try{        ...    }    finally{        readLock.unlock();    }}public void transfer(){    writeLock.lock();//读和写都排斥    try{        ...    }    finally{        writeLock.unlock();    }}

阻塞队列

对于许多多线程问题,可以通过使用一个或多个队列以优雅且安全的方式将其形式化,使用队列可以安全地从一个线程向另一个线程传递数据,在协调多个线程之间的合作时,阻塞队列是一个有用的工具

阻塞队列

  1. 如果将队列当做线程管理工具来使用,将要用put和take方法
  2. 在一个多线程程序中,队列会在任意时候空或满,因此一定要使用offer、poll、peek方法
  3. 向队列插入null是非法的

java.util》concurrent包提供了几种阻塞队列的变种:

  • LinkedBlockingQueue:容量没有上界,但可以指定最大容量
  • LinkedBlockingDeque:上一个的双端版本
  • ArrayBlockingQueue:在构造时要指定容量,并且有一个可选的参数来指定是否要公平性
  • priorityBlockingQueue:带有优先级的队列而不是先进先出对垒,没有容量上限
  • DelayQueue:包含实现Delay接口的对象, 元素只有在延迟用完的情况下才能从DelayQueue移除

Tips:使用队列数据结构作为一种同步机制,不需要显示的线程同步

线程安全的集合

java.util.concurrent包提供:

  • 映射表:concurrentHashMap、concurrentSkipListMap
  • 有序集:concurrentSkipListSet
  • 队列:concurrentLinkedQueue

较早的线程安全集合:

  • vector:动态数组
  • Hashtable:散列表

任何集合可以通过使用同步包装器变成线程安全的(synchronization wrapper)

List<E> synchArrayList = Collections.synchronizedList(new ArrayList<E>());Map<E> synchHahMap = Collections.synchronizedMap(new HashMap<K,V>());

线程池

一个线程池中包含许多准备运行的空闲线程

  • 将Runnable对象交给线程池就会有一个线程调用run方法,当run方法退出线程不死亡,在池中为下一个请求服务
  • 使用线程池减少并发线程的数目

执行器有许多静态工厂方法用来构造线程池:

  1. newCachedThreadPool:构建一个线程池,对于每个任务,如果有空闲线程可用,立即让他执行任务,否则创建一个新线程
  2. newFixedThreadPool:构建一个具有固定大小的线程池,如果提交的任务数多于空闲线程数,那么把得不到服务的任务放置在队列中
  3. newSingleThreadExecutor:一个退化的大小为1的线程池,由一个线程执行提交的任务,一个接着一个的执行

以上三个执行器返回实现了ExecutorService接口的ThreadPoolExecutor类的对象

使用线程池:

  • 调用Executors类中的静态方法newCachedThreadPool或newFixedThreadPool
  • 调用submit提交Runnable或Callable对象
  • 如果想要取消任务或如果提交Callable对象那就要保存好返回的Future对象
  • 当不再提交任何任务时,调用shutdown

java.util.concurerrent包含了几个帮助人们管理相互合作的线程集的类,这些机制具有线程之间的功用集结点模式模式的“预置功能”