Java核心技术笔记4

断言:

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方法的线程试图获得同一个锁,那么程序死锁:被挂起的线程等着被恢复,而将其 挂起的线程等待获得锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值