Java核心技术笔记4

mac2022-06-30  150

断言:

if (x < 0) throw new II 1 egalArgumentException(“x < 0”); 但是这段代码会一直保留在程序中,即使测试完毕也不会自动地删除。如果在程序中含 有大量的这种检查,程序运行起来会相当慢。 断言机制允许在测试期间向代码中插入一些检査语句。当代码发布时,这些插入的检测 语句将会被自动地移走。 Java语言引入了关键字asserto这个关键字有两种形式: assert 条件; 和 assert条件:表达式; 这两种形式都会对条件进行检测,如果结果为false,则抛出一个AssertionError异常。 在第二种形式中,表达式将被传入AssertionError的构造器,并转换成一个消息字符串。 在默认情况下,断言被禁用。可以在运行程序时用-enableassertions或-ea选项启用: java -enableassertions MyApp 需要注意的是,在启用或禁用断言时不必重新编译程序。启用或禁用断言是类加载器 (class loader)的功能。当断言被禁用时,类加载器将跳过断言代码,因此,不会降低程序运行的速度。

在Java语言中,给出了 3种处理系统错误的机制: ・抛出一个异常 ・日志 •使用断言 什么时候应该选择使用断言呢?请记住下面几点: ・断言失败是致命的、不可恢复的错误。 ・断言检查只用于开发和测阶段(这种做法有时候被戏称为“在靠近海岸时穿上救生衣, 但在海中央时就把救生衣抛掉吧”)。

泛型:

一个泛型类(generic class)就是具有一个或多个类型变量的类。本章使用一个简单的 Pair类作为例子。对于这个类来说,我们只关注泛型,而不会为数据存储的细节烦恼。下面 是Pair类的代码: public cl ass Pai r ( private T first; private T second; public Pair() ( first = null; second = null; } public Pair(T first, T second) { this.first = first; this.second = second; } public T getFirst() ( return first; } public T getSecondQ ( return second; } public void setFirst(T newValue) { first = newValue; } public void setSecond(T newValue) ( second = newValue; } } 换句话说,泛型类可看作普通类的工厂。

还可以定义一个带有类型参数的简单方法。 class A r ray Al g { public static T getMiddle(T… a) ( return a [a. length / 2]; ) } 这个方法是在普通类中定义的,而不是在泛型类中定义的。然而,这是一个泛型方法, 可以从尖括号和类型变量看出这一点。注意,类型变量放在修饰符(这里是public static)的 后面,返回类型的前面。 泛型方法可以定义在普通类中,也可以定义在泛型类中。 当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型: String middle = ArrayAlg.getMiddle(“John”, “Q.”, “Public”);

类型擦除:

无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型 的名字就是删去类型参数后的泛型类型名。擦除(erased)类型变量,并替换为限定类型(无 限定的变量用Object)。 例如,Pair的原始类型如下所示: public class Pair { private Object fi rst; private Object second; public Pair (Object first, Object second) ( this.fi rst = fi rst; this.second = second; } public Object getFirst() { return first; } public Object getSecondO { return second; } public void setFirst(Object newValue) ( first = newValue; } public void setSecond(Object newValue) { second = newValue; } } 因为T是一个无限定的变量,所以直接用Object替换。

原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换。例如, 类Pair中的类型变量没有显式的限定,因此,原始类型用Object替换T。假定声明了一 个不同的类型。 public class Interval<T extends Comparable & Serializable〉implements Serializable ( private T lower; private T upper; public Interval(T first, T second) ( if (first.compareTo(second) <= 0) ( lower = first; upper = second; } else { lower = second;叩per = first; } } } 原始类型Interval如下所示: public cl ass Interval implements Seri alizable { private Conparable lower; private Coiparable 叩per; public Interval (Comparable first, Comparable second) ( . . . } }

当程序调用泛型方法时,如果擦除返回类型,编译器插入强制类型转换。例如,下面这 个语句序列 Pair buddies =…; Employee buddy = buddies,getFirstO; 擦除getFirst的返回类型后将返回Object类型。编译器自动插入Employee的强制类型转换。 也就是说,编译器把这个方法调用翻译为两条虚拟机指令: •对原始方法Pair.getFirst的调用。 •将返回的Object类型强制转换为Employee类型。

总之,需要记住有关Java泛型转换的事实: ・虚拟机中没有泛型,只有普通的类和方法。 ・所有的类型参数都用它们的限定类型替换。 ・桥方法被合成来保持多态。 •为保持类型安全性,必要时插入强制类型转换。

并发:

•void interrupt() 向线程发送中断请求。线程的中断状态将被设置为true。如果目前该线程被一个sleep 调用阻塞,那么,InterruptedException异常被抛出。

线程可以有如下6种状态: ・New (新创建) •Runnable (可运行) •Blocked (被阻塞) •Waiting (等待) ・Timed waiting (计时等待) ・Terminated (被终止) 下一节对每一种状态进行解释。 要确定一个线程的当前状态,可调用getState方法。 14.3.1新创建线程 当用new操作符创建一个新线程时,如new Thread(r),该线程还没有开始运行。这意味 着它的状态是new。当一个线程处于新创建状态时,程序还没有开始运行线程中的代码。在 线程运行之前还有一些基础工作要做。 14.3.2可运行线程 一旦调用start方法,线程处于runnable状态。一个可运行的线程可能正在运行也可能没 有运行,这取决于操作系统给线程提供运行的时间。(Java的规范说明没有将它作为一个单独 状态。一个正在运行中的线程仍然处于可运行状态。) 一旦一个线程开始运行,它不必始终保持运行。事实上,运行中的线程被中断,目的是 为了让其他线程获得运行机会。线程调度的细节依赖于操作系统提供的服务。抢占式调度系 统给每一个可运行线程一个时间片来执行任务。当时间片用完,操作系统剥夺该线程的运行 权,并给另一个线程运行机会(见图14-4 )o当选择下一个线程时,操作系统考虑线程的优 先级——更多的内容见第14.4.1节。 现在所有的桌面以及服务器操作系统都使用抢占式调度。但是,像手机这样的小型设备 可能使用协作式调度。在这样的设备中,一个线程只有在调用yield方法、或者被阻塞或等 待时,线程才失去控制权。 在具有多个处理器的机器上,每一个处理器运行一个线程,可以有多个线程并行运行。 当然,如果线程的数目多于处理器的数目,调度器依然釆用时间片机制。 记住,在任何给定时刻,一个可运行的线程可能正在运行也可能没有运行(这就是为什么将这个状态称为可运行而不是运行)。

14.4.1线程优先级 在Java程序设计语言中,每一个线程有一个优先级。默认情况下,一羊线程继承它的父 线程的优先级。可以用setPriority方法提高或降低任何一个线程的优先级。可以将优先级设 置为在MIN PRIORITY (在Thread类中定义为1 )与MAX PRIORITY (定义为10 )之间的 任何值。NORM PRIORITY被定义为5。 每当线程调度器有机会选择新线程时,它首先选择具有较高优先级的线程。但是,线程 优先级是高度依赖于系统的。当虚拟机依赖于宿主机平台的线程实现机制时,Java线程的优 先级被映射到宿主机平台的优先级上,优先级个数也许更多,也许更少。 •static void yield() 导致当前执行线程处于让步状态。如果有其他的可运行线程具有至少与此线程同样高 的优先级,那么这些线程接下来会被调度。注意,这是一个静态方法。

14.4.2守护线程 可以通过调用 t.setDaemon(true); 将线程转换为守护线程(daemon thread)o这样一个线程没有什么神奇。守护线程的唯一用途 是为其他线程提供服务。计时线程就是一个例子,它定时地发送“计时器嘀嗒”信号给其他 线程或清空过时的高速缓存项的线程。当只剩下守护线程时,虚拟机就退出了,由于如果只 剩下守护线程,就没必要继续运行程序了。 •void setDaemon(boolean i sDaemon) 标识该线程为守护线程或用户线程。这一方法必须在线程启动之前调用。

14.5.2竞争条件详解 上一节中运行了一个程序,其中有几个线程更新银行账户余额。一段时间之后,错误不 知不觉地出现了,总额要么增加,要么变少。当两个线程试图同时更新同一个账户的时候, 这个问题就出现了。假定两个线程同时执行指令 accounts[to] += amount; 问题在于这不是原子操作该指令可能被处理如下: 1)将accounts[to]加载到寄存器。 2)增加 amounto 3)将结果写回accounts[to]o 现在,假定第1个线程执行步骤1和2,然后,它被剥夺了运行权。假定第2个线程被 唤醒并修改了 accounts数组中的同一项。然后,第1个线程被唤醒并完成其第3步。 这样,这一动作擦去了第二个线程所做的更新。于是,总金额不再正确。

synchronized:

从1.0版开始,Java 中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁 将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。 换句话说, public synchronized void method0 ( method body } 等价于 public void methodQ { this.intrinsiclock.lockQ; try { method body } finally { this.intrinsidock.uniock(); } } 例如,可以简单地声明Bank类的transfer方法为synchronized,而不是使用一个显式的锁。

14.5.6同步阻塞 正如刚刚讨论的,每一个Java对象有一个锁。线程可以通过调用同步方法获得锁。还有 另一种机制可以获得锁,通过进入一个同步阻塞。当线程进入如下形式的阻塞: synchronized (obj) // this is the syntax for a synchronized block ( critical section } 于是它获得obj的锁。

volatile:

volatile关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为volatile, 那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。 private boolean done; public synchronized boolean isDoneO ( return done; } public synchronized void setDoneO { done = true; } 或许使用内部锁不是个好主意。如果另一个线程已经对该对象加锁,isDone和setDone 方法可能阻塞。如果注意到这个方面,一个线程可以为这一变量使用独立的Lock。但是,这 也会带来许多麻烦。 在这种情况下,将域声明为volatile是合理的: private volatile boolean done; public boolean isDoneO { return done; } public void setDoneO ( done = true; } 。警告:Volatile变量不能提供原子性。例如,方法 public void flipDone() { done = idone; } // not atomic 不能确保翻转域中的值。不能保证读取、翻转和写入不被中断。

14.5.9 final 变量 上一节已经了解到,除非使用锁或volatile修饰符,否则无法从多个线程安全地读取一 个域。 还有一种情况可以安全地访问一个共享域,即这个域声明为final时。考虑以下声明: final Map<String, Double> accounts = new HashMap<>(); 其他线程会在构造函数完成构造之后才看到这个accounts变量。 如果不使用final,就不能保证其他线程看到的是accounts更新后的值,它们可能都只是 看到null,而不是新构造的HashMapo 当然,对这个映射表的操作并不是线程安全的。如果多个线程在读写这个映射表,仍然 需要进行同步。

我说说我的理解,threadlocal 的线程安全是片面的,他可以解决一定程度的并发问题,但是不能处理多个线程处理一个共享数据的问题,比如还是取钱,多人操作一个账户, 每个人只能处理自己的数据,如果A花了账户20,还剩80,B花了50,还剩50,实际上总账户应该只剩30,但是无法交互,每个人各自复制的账户还剩80,50,也就是各自玩各自的,一定程度的安全,适合场景并不全能,我可以这么理解吗

14.5.15 为什么弃用stop和suspend方法 初始的Java版本定义了一个stop方法用来终止一个线程,以及一个suspend方法用来阻 塞一个线程直至另一个线程调用resumeo stop和suspend方法有一些共同点:都试图控制一 个给定线程的行为。 stop、suspend和resume方法已经弃用。stop方法天生就不安全,经验证明suspend方法 会经常导致死锁。在本节,将看到这些方法的问题所在,以及怎样避免这些问题的出现。 首先来看看stop方法,该方法终止所有未结束的方法,包括run方法。当线程被终止,立 即释放被它锁住的所有对象的锁。这会导致对象处于不一致的状态。例如,假定TransferThread 在从一个账户向另一个账户转账的过程中被终止,钱款已经转出,却没有转入目标账户,现在银 行对象就被破坏了。因为锁已经被释放,这种破坏会被其他尚未停止的线程观察到。 接下来,看看suspend方法有什么问题。与stop不同,suspend不会破坏对象。但是, 如果用suspend挂起一个持有一个锁的线程,那么,该锁在恢复之前是不可用的。如果调用 suspend方法的线程试图获得同一个锁,那么程序死锁:被挂起的线程等着被恢复,而将其 挂起的线程等待获得锁。

最新回复(0)