Java基础补缺2
1)多线程调度:
每个对象都可以调用 Object 的 wait/notify 方法来实现等待/通知机制。
public class WaitNotifyDemo {
public static void main(String[] args) {
Object lock = new Object();
new Thread(() -> {
synchronized (lock) {
System.out.println("线程1:我要等待");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1:我被唤醒了");
}
}).start();
new Thread(() -> {
synchronized (lock) {
System.out.println("线程2:我要唤醒");
lock.notify();
System.out.println("线程2:我已经唤醒了");
}
}).start();
}
}
在大多数情况下,由于线程1先被启动,它会先获得锁。以下是详细的执行序列:
- 主线程启动:
- •主线程创建了一个共享对象
lock作为监视器锁。 - •主线程启动线程1。线程1进入
Runnable状态,等待CPU调度。 - •主线程立即启动线程2。线程2也进入
Runnable状态。
- •主线程创建了一个共享对象
- 线程1获得锁并进入等待:
- •线程1获得CPU时间片,并成功获取
lock上的同步锁,进入synchronized代码块。 - •线程1打印:
"线程1:我要等待"。 - •线程1执行
lock.wait()。这个方法调用会导致以下关键操作:- •线程1释放其持有的
lock锁。 - •线程1的状态由
RUNNABLE变为WAITING,并被放入与lock对象关联的等待队列中。
- •线程1释放其持有的
- •此时,
lock的锁被释放,其他线程可以竞争它。
- •线程1获得CPU时间片,并成功获取
- 线程2获得锁并发出通知:线程1被唤醒并继续执行:
- •线程2获得CPU时间片,并成功获取到已空闲的
lock锁,进入其synchronized代码块。 - •线程2打印:
"线程2:我要唤醒"。 - •线程2执行
lock.notify()。这个方法调用会引发以下操作:- •JVM将线程1从
lock的等待队列移送到同步队列(或锁的阻塞队列)。线程1的状态由WAITING变为BLOCKED,因为它现在需要重新竞争锁才能继续执行。 - •请注意:
notify()调用本身并不会释放锁,线程2会继续持有锁并执行后续代码。
- •JVM将线程1从
- •线程2打印:
"线程2:我已经唤醒了"。 - •线程2执行到其
synchronized代码块的结束处},这时它会释放lock锁。
- •线程2获得CPU时间片,并成功获取到已空闲的
- •
lock锁被释放后,在同步队列中等待的线程1开始尝试重新获取该锁。 - •线程1成功获取锁后,从其
wait()方法处恢复执行。 - •线程1打印:
"线程1:我被唤醒了"。 - •线程1执行到其
synchronized代码块的结束处},释放锁。程序随后结束。
线程1:我要等待
线程2:我要唤醒
线程2:我已经唤醒了
线程1:我被唤醒了
潜在风险:竞态条件(Race Condition)
这段代码存在一个典型的竞态条件问题。如果线程调度器让线程2先于线程1执行,将会发生以下情况:
- 线程2先获得
lock锁。 - 线程2调用
lock.notify()。但此时,线程1尚未执行到wait()方法,即等待队列是空的。因此,这次notify()调用相当于一次空操作,没有任何线程会被唤醒。 - 线程2执行完毕,释放锁。
- 之后,线程1获得锁,执行
lock.wait()并进入等待状态。 - 由于已经没有任何其他线程会再次调用
lock.notify(),线程1将永远等待下去,程序无法正常终止。
2)Java包
Java 定义了一种名字空间,称之为包:package。一个类总是属于某个包,类名(比如Person)只是一个简写,真正的完整类名是包名.类名。
在 Java 虚拟机执行的时候,JVM 只看完整类名,因此,只要包名不同,类就不同。
要特别注意:包没有父子关系。java.util和java.util.zip是不同的包,两者没有任何继承关系。
在写import的时候,可以使用*,表示把这个包下面的所有class都导入进来(但不包括子包的class)。我们一般不推荐这种写法,因为在导入了多个包后,很难看出Arrays类属于哪个包。
还有一种import static的语法,它可以导入一个类的静态字段和静态方法。import static很少使用。
package main;
// 导入System类的所有静态字段和静态方法:
import static java.lang.System.*;
public class Main {
public static void main(String[] args) {
// 相当于调用System.out.println(…)
out.println("Hello, world!");
}
}
Java 编译器最终编译出的.class文件只使用 完整类名,因此,在代码中,当编译器遇到一个class名称时:
- 如果是完整类名,就直接根据完整类名查找这个
class; - 如果是简单类名,按下面的顺序依次查找:
- 查找当前
package是否存在这个class; - 查找
import的包是否包含这个class; - 查找
java.lang包是否包含这个class。
- 查找当前
如果按照上面的规则还无法确定类名,则编译报错。
编写 class 的时候,编译器会自动帮我们做两个 import 动作:
- 默认自动
import当前package的其他class; - 默认自动
import java.lang.*。
注意:自动导入的是java.lang包,但类似java.lang.reflect这些包仍需要手动导入。
如果有两个class名称相同,例如,mr.jun.Arrays和java.util.Arrays,那么只能import其中一个,另一个必须写完整类名。
3)可变参数
可变参数是 Java 1.5 的时候引入的功能,它允许方法使用任意多个、类型相同(is-a)的值作为参数。
public static void print(String... strs) {
for (String s : strs)
System.out.print(s);
System.out.println();
}
阿里巴巴开发手册一条规约,尽量不要使用可变参数,如果要用的话,可变参数必须要在参数列表的最后一位
当使用可变参数的时候,实际上是先创建了一个数组,该数组的大小就是可变参数的个数,然后将参数放入数组当中,再将数组传递给被调用的方法。
public static void main(String[] args) {
print(new String[]{"沉"});
print(new String[]{"沉", "默"});
print(new String[]{"沉", "默", "王"});
print(new String[]{"沉", "默", "王", "二"});
}
public static void print(String... strs) {
for (String s : strs)
System.out.print(s);
System.out.println();
}
当一个方法需要处理任意多个相同类型的对象时,就可以定义可变参数。Java 中有一个很好的例子,就是 String 类的 format() 方法
%d 表示将整数格式化为 10 进制整数,%s 表示输出字符串。
System.out.println(String.format("年纪是: %d", 18));
System.out.println(String.format("年纪是: %d 名字是: %s", 18, "沉默王二"));
要避免重载带有可变参数的方法——这样很容易让编译器陷入自我怀疑中
4)构造方法
构造方法不能是抽象的(abstract)、静态的(static)、最终的(final)、同步的(synchronized)。多个线程不会同时创建内存地址相同的同一个对象,所以用 synchronized 关键字修饰没有必要。
如果用 void 声明构造方法的话,编译时不会报错,但 Java 会把这个所谓的“构造方法”当成普通方法来处理。
构造方法的调用是隐式的,通过编译器完成。如果没有明确提供无参构造方法,编译器会提供。而普通方法任何情况下都不由编译器提供。
复制对象有几种方法
4.1)通过构造函数
public class CopyConstrutorPerson {
private String name;
private int age;
public CopyConstrutorPerson(CopyConstrutorPerson person) {
this.name = person.name;
this.age = person.age;
}
public static void main(String[] args) {
CopyConstrutorPerson p1 = new CopyConstrutorPerson("沉默王二",18);
p1.out();
CopyConstrutorPerson p2 = new CopyConstrutorPerson(p1);
p2.out();
}
}
4.2)通过Object类的clone()方法
public class ClonePerson implements Cloneable {
private String name;
private int age;
public ClonePerson(String name, int age) {
this.name = name;
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public void out() {
System.out.println("姓名 " + name + " 年龄 " + age);
}
public static void main(String[] args) throws CloneNotSupportedException {
ClonePerson p1 = new ClonePerson("沉默王二",18);
p1.out();
ClonePerson p2 = (ClonePerson) p1.clone();
p2.out();
}
}
通过 clone() 方法复制对象的时候,ClonePerson 必须先实现 Cloneable 接口的 clone() 方法,然后再调用 clone() 方法(ClonePerson p2 = (ClonePerson) p1.clone())。
5)初始代码块
public class Car {
Car() {
System.out.println("构造方法");
}
{
System.out.println("代码初始化块");
}
public static void main(String[] args) {
new Car();
}
}
等价于
public class Car {
Car() {
{
System.out.println("代码初始化块");
}
System.out.println("构造方法");
}
}
结果
代码初始化块
构造方法
- 类实例化的时候执行代码初始化块;
- 实际上,代码初始化块是放在构造方法中执行的,只不过比较靠前;
- 代码初始化块里的执行顺序是从前到后的。
在默认情况下,子类的构造方法在执行的时候会主动去调用父类的构造方法。
public class Example {
// 静态变量
public static int staticVar = 1;
// 实例变量
public int instanceVar = 2;
// 静态初始化块
static {
System.out.println("执行静态初始化块");
staticVar = 3;
}
// 实例初始化块
{
System.out.println("执行实例初始化块");
instanceVar = 4;
}
// 构造方法
public Example() {
System.out.println("执行构造方法");
}
public static void main(String[] args) {
System.out.println("执行main方法");
Example e1 = new Example();
Example e2 = new Example();
System.out.println("e1的静态变量:" + e1.staticVar);
System.out.println("e1的实例变量:" + e1.instanceVar);
System.out.println("e2的静态变量:" + e2.staticVar);
System.out.println("e2的实例变量:" + e2.instanceVar);
}
}
结果
执行静态初始化块 执行main方法 执行实例初始化块 执行构造方法 执行实例初始化块 执行构造方法 e1的静态变量:3 e1的实例变量:4 e2的静态变量:3 e2的实例变量:4
静态初始化块在类加载时执行,只会执行一次,并且优先于实例初始化块和构造方法的执行;
实例初始化块在每次创建对象时执行,在构造方法之前执行。
浙公网安备 33010602011771号