这个问题我们往往都是知道,但是并没有深究其中的缘由,可能你会说这是 Java 设计者这么决定的呀,那你有没有想过为什么要这么设计呢?这个问题在我们 Java 编程的常识有了深入理解后才可以得到一定的解释。
能继续探讨这个问题的前提是你知道 Object 类下有 wait 和 notify 方法,就如同你知道 Object 类下有 equals 和 hashCode 方法一样。等待/通知机制可以实现很多功能,比如生产者-消费者模式,那么为什么不将这两个方法放到 Thread 类中呢?唤醒一个线程难道不行吗?答案显然是不行(这不是废话吗…)
为什么呢?我觉得可以从以下几个方面来谈谈。
wait 和 notify 不仅仅是普通方法或者工具,更重要的是它们是 Java 中两个线程之间的通信机制。对于语言设计者而言,如果不能使用关键字(比如 synchronized、volatile)实现线程间通信机制,同时又需要保证这个机制能对每个对象都适用,那么声明在 Object 类中是最合适的。我们也要区分同步和等待/通知机制,这两个是没有关系的,同步的意思是提供互斥并确保 Java 类的线程安全性,而 wait 和 notify 则是两个线程间的通信机制。
在 Java 中为了进入代码的临界区,线程需要锁定并等待锁定,但是它们不知道哪些线程持有锁,而是只能知道锁被某个线程持有。从这个角度说也不能将等待/通知机制放到 Thread 上,并且它们应该只需要去等到获取锁,而不是去了解哪个线程在同步块内,并请求它们释放锁定。
Java 是基于 Hoare 的监视器的思想。在 Java 中,所有对象都有一个监视器,线程在监视器上等待,为了执行等待,我们需要两个参数:一个线程和一个监视器(任何对象)。在 Java 设计中,线程不能被指定,它总是运行当前代码的线程,但是我们可以指定监视器(也就是我们称之为等待的对象)。这是一个很好的设计,因为如果我们可以让任何其它线程在所需的监视器上等待,这将导致”入侵”,导致在设计并发程序的时候遇到困难。并且在 Java 中,所有在另外一个线程的执行中侵入的操作都被弃用了(比如 stop 方法)。
这个可以从两方面角度来答,一个就是多重继承本身会产生菱形问题(又叫钻石问题),详细的可以看这里;另外一个就是可以从接口的好处来答,多重继承确实使设计复杂化并在转换、构造函数链接等过程中产生问题。假设你需要多重继承的情况并不多,简单起见,明智的决定是省略它,而接口只有方法声明而没有任何实现,如果在一个类中实现的两个接口中有同名的方法,可以在编译阶段就发现错误,因此不会产生歧义。
相同的问题还有为什么 String 设计成 final 的之类的。
字符串常量池的设计,为什么要设计 String Pool,其中的一个考虑就是 String 作为 Java 中用的最多的类型之一,需要提高它的访问速度,所以设计了字符串常量池,想象字符串池没有使字符串不可变,它根本不可能,因为在字符串池的情况下,一个字符串对象/文字,例如 “Test” 已被许多参考变量引用,因此如果其中任何一个更改了值,其他参数将自动受到影响。
字符串已被广泛用作许多 Java 类的参数,例如,为了打开网络连接,你可以将主机名和端口号作为字符串传递,你可以将数据库 URL 作为字符串传递, 以打开数据库连接,你可以通过将文件名作为参数传递给 File I/O 类来打开 Java 中的任何文件。如果 String 不是不可变的,这将导致严重的安全威胁,我的意思是有人可以访问他有权授权的任何文件,然后可以故意或意外地更改文件名并获得对该文件的访问权限。由于不变性,你无需担心这种威胁。这个原因也说明了,为什么 String 在 Java 中是 final 的,通过使 java.lang.String final,Java 设计者确保没有人覆盖 String 类的任何行为。
由于 String 是不可变的,它可以安全地共享许多线程,这对于多线程编程非常重要. 并且避免了 Java 中的同步问题,不变性也使得 String 实例在 Java 中是线程安全的,这意味着你不需要从外部同步 String 操作。关于 String 的另一个要点是由截取字符串 SubString 引起的内存泄漏,这不是与线程相关的问题,但也是需要注意的。
为什么 String 在 Java 中是不可变的另一个原因是允许 String 缓存其哈希码,Java 中的不可变 String 缓存其哈希码,并且不会在每次调用 String 的 hashcode 方法时重新计算,这使得它在 Java 中的 HashMap 中使用的 HashMap 键非常快。简而言之,因为 String 是不可变的,所以没有人可以在创建后更改其内容,这保证了 String 的 hashCode 在多次调用时是相同的。
String 不可变的绝对最重要的原因是它被类加载机制使用,因此具有深刻和基本的安全考虑。如果 String 是可变的,加载“java.io.Writer” 的请求可能已被更改为加载”mil.vogoon.DiskErasingWriter”,安全性和字符串池是使字符串不可变的主要原因。顺便说一句,上面的理由很好回答另一个 Java 面试问题: 为什么 String 在 Java 中是最终的。要想是不可变的,你必须是最终的,这样你的子类才不会破坏不变性。
当两个或多个线程在等待彼此释放所需的资源(锁定)并陷入无限等待即是死锁。它仅在多任务或多线程的情况下发生。
虽然这可以有很多答案, 但我的版本是首先我会看看代码, 如果我看到一个嵌套的同步块,或从一个同步的方法调用其他同步方法, 或试图在不同的对象上获取锁, 如果开发人员不是非常小心,就很容易造成死锁。
另一种方法是在运行应用程序时实际锁定时找到它, 尝试采取线程转储,在 Linux 中,你可以通过kill -3命令执行此操作, 这将打印应用程序日志文件中所有线程的状态, 并且你可以看到哪个线程被锁定在哪个线程对象上。你可以使用 fastthread.io 网站等工具分析该线程转储, 这些工具允许你上载线程转储并对其进行分析。
另一种方法是使用 jConsole 或 VisualVM, 它将显示哪些线程被锁定以及哪些对象被锁定。
/** * Java 程序通过强制循环等待来创建死锁。 * * */ public class DeadLockDemo { /* * 此方法请求两个锁,第一个字符串,然后整数 */ public void method1() { synchronized (String.class) { System.out.println("Aquired lock on String.class object"); synchronized (Integer.class) { System.out.println("Aquired lock on Integer.class object"); } } } /* * 此方法也请求相同的两个锁,但完全 * 相反的顺序,即首先整数,然后字符串。 * 如果一个线程持有字符串锁,则这会产生潜在的死锁 * 和其他持有整数锁,他们等待对方,永远。 */ public void method2() { synchronized (Integer.class) { System.out.println("Aquired lock on Integer.class object"); synchronized (String.class) { System.out.println("Aquired lock on String.class object"); } } } }
如果 method1() 和 method2() 都由两个或多个线程调用,则存在死锁的可能性, 因为如果线程 1 在执行 method1() 时在 Sting 对象上获取锁, 线程 2 在执行 method2() 时在 Integer 对象上获取锁, 等待彼此释放 Integer 和 String 上的锁以继续进行一步, 但这永远不会发生。
死锁产生的四个条件:
那么需要破坏死锁产生的四个条件之一就可以:
破坏互斥条件:将临界资源改为可共享使用的资源(如SPOOLing技术),例如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。缺点:可行性不高,很多时候无法破坏互斥条件。
破坏占有和等待条件:运行前分配好所有需要的资源,之后一直保持。缺点:资源利用率很低,可能会导致进程饥饿。
破坏不可抢占条件:方案一:申请的资源得不到满足时,立即释放拥有的所有资源;方案二:申请的资源被其它进程占用时,由操作系统协助剥夺(考虑优先级)。缺点:实现复杂;剥夺资源可能导致部分工作失效,反复申请和释放资源导致系统开销大;可能会导致线程饥饿。
破坏环路等待:给资源统一编号,进程只能按编号从小到大的顺序申请资源。缺点:不方便增加新设备;会导致资源浪费;用户编程麻烦。