目录
十五 . 面向对象的基本要素:封装,继承,多态
15.1 封装:
15.1.1 例子:
15.2 继承
15.2.1 例子
15.3 多态
15.3.1 例子
15.3.2 小结:谈谈多态的继承的联系
十六 . synchronized 和 ReentrantLock 的区别?
16.1 典型回答
16.2 深入理解底层锁的概念
16.2.1 synchronized
16.2.2 ReentrantLock
16.2.2.1 例子:
16.2.2.1.1 公平性(Fairness)控制:
16.2.2.1.2 可中断性(Interruptibility):
16.2.2.1.3 条件变量(Condition)支持:
16.2.2.1.4 超时控制(Timeout):
十五 . 面向对象的基本要素:封装,继承,多态
封装:封装是将数据和功能包装在一个类中,通过对外提供公共接口来隐藏内部实现细节。这样可以保护数据免受外部直接访问和修改,只能通过类提供的方法进行操作,封装提供了数据的安全性和代码的可维护性。
我们可以创建一个名为"Person"的类来封装一个人的相关信息,如姓名、年龄和性别。通过定义公共方法如"setName"、"setAge"和"setGender"来设置这些属性值,而不直接暴露给外部代码。这样可以确保属性值的正确性和一致性。
public class Person {
private String name;
private int age;
private String gender;
public Person(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public static void main(String[] args) {
Person person = new Person("John", 25, "Male");
System.out.println("Person Name: " + person.getName());
System.out.println("Person Age: " + person.getAge());
System.out.println("Person Gender: " + person.getGender());
// 修改属性值
person.setName("Alice");
person.setAge(30);
person.setGender("Female");
System.out.println("Updated Person Name: " + person.getName());
System.out.println("Updated Person Age: " + person.getAge());
System.out.println("Updated Person Gender: " + person.getGender());
}
}
继承:继承是指一个类(称为子类或派生类)可以继承另一个类(称为父类或基类)的属性和方法。
子类继承了父类的特性,并且可以在此基础上进行扩展或重写。继承实现了代码的重用和扩展性。
我们可以定义一个"Animal"类作为父类,其中包含通用的属性和方法,如"eat"和"sleep"。然后可以创建子类如"Cat"和"Dog"来继承"Animal"类,并在子类中添加特定的属性和方法,如"meow"方法和"bark"方法。
// 定义父类 Animal
class Animal {
// 父类的属性
protected String name;
// 父类的方法
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println(name + " is eating.");
}
public void sleep() {
System.out.println(name + " is sleeping.");
}
}
// 定义子类 Cat
class Cat extends Animal {
// 子类的属性
private String breed;
// 子类的方法,并调用父类的构造方法
public Cat(String name, String breed) {
super(name); // 调用父类的构造方法
this.breed = breed;
}
// 子类的自定义方法
public void meow() {
System.out.println(name + " is meowing.");
}
}
// 定义子类 Dog
class Dog extends Animal {
// 子类的属性
private String breed;
// 子类的方法,并调用父类的构造方法
public Dog(String name, String breed) {
super(name); // 调用父类的构造方法
this.breed = breed;
}
// 子类的自定义方法
public void bark() {
System.out.println(name + " is barking.");
}
}
// 测试代码
public class Main {
public static void main(String[] args) {
Cat cat = new Cat("Kitty", "Persian");
cat.eat(); // 调用继承自父类的方法
cat.sleep();
cat.meow(); // 调用子类自定义的方法
Dog dog = new Dog("Buddy", "Labrador");
dog.eat(); // 调用继承自父类的方法
dog.sleep();
dog.bark(); // 调用子类自定义的方法
}
}
输出:
Kitty is eating.
Kitty is sleeping.
Kitty is meowing.
Buddy is eating.
Buddy is sleeping.
Buddy is barking.
多态:多态是指同一种操作可以作用于不同的对象,并根据对象的实际类型执行不同的行为。通过多态,可以提高代码的灵活性和可扩展性。
定义了一个抽象类 Animal
,并具有一个抽象方法 sound
。然后,定义了两个子类 Cat
和 Dog
,它们分别继承自 Animal
并实现了 sound
方法。
在测试类中,创建了一个 Animal
类型的对象数组,并分别用 Cat
和 Dog
的实例来初始化数组的元素。然后通过循环遍历数组,并调用 sound
方法,可以看到根据对象的实际类型,程序会执行不同的行为。这就是多态的体现,相同的方法名 sound
可以作用于不同的对象,并根据对象的实际类型执行不同的行为。
// 定义父类 Animal
abstract class Animal {
abstract void sound();
}
// 定义子类 Cat
class Cat extends Animal {
void sound() {
System.out.println("喵喵喵");
}
}
// 定义子类 Dog
class Dog extends Animal {
void sound() {
System.out.println("汪汪汪");
}
}
// 测试类
public class PolymorphismExample {
public static void main(String[] args) {
// 创建 Animal 对象数组
Animal[] animals = new Animal[2];
animals[0] = new Cat();
animals[1] = new Dog();
// 循环遍历数组并调用 sound 方法
for (Animal animal : animals) {
animal.sound();
}
}
}
继承是建立类之间的一种层次关系,子类可以继承父类的属性和方法,并且可以增加自己的特定实现;而多态是在继承关系中,通过父类的类型引用来指向子类的对象,并且根据对象的实际类型执行不同的行为。继承是一种静态的关系,而多态是一种动态的行为。继承和多态通常是一起使用,多态是继承的一种体现。
十六 . synchronized 和 ReentrantLock 的区别?
Java精心设计的高效并发机制,构建大规模应用的基础之一。
synchronized 是Java内建的同步机制,也被人称作Intrinsic Locking,提供互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在哪里。
Java 5 以前,synchronized 是仅有的同步手段,在代码中,synchronized可以用来修饰方法,可以在特定的代码快上,本质上synchronized方法,等同于把方法全部语句用synchronized块包起来。
ReentrantLock ,通常称为再入锁,Java提供锁的实现,语义和synchronized差不多,再入锁可以直接通过代码,直接调用Lock()方法获取,代码书写更加灵活,ReentrantLock提供了很多实用的方法,实现了很多synchronized无法做到的细节控制,可以控制fairness,也就是公平性,或者利用定义条件等,明确调用unlock()方法释放,不然就会一直持有这个锁。
synchronized和ReentrantLock的性能比较,早期synchronized再很多场景下性能相差比较大,后续进行改进后,再低竞争的场景表现可能优于ReentrantLock。
ReentrantLock
是Java提供的可重入锁实现,它具有更细粒度的控制和更多的功能。相较于synchronized
,ReentrantLock
提供了以下优势:
ReentrantLock
可以通过构造方法的参数来指定是否按照线程请求锁的顺序获取锁(公平性),以避免某些线程长时间等待锁而产生饥饿现象。ReentrantLock
提供了可中断的获取锁的方法,即线程在等待锁的过程中可以被其他线程中断,并通过捕获InterruptedException
来处理中断事件。ReentrantLock
内置了Condition
接口,可以创建多个条件变量,使线程能够在特定条件满足时等待或被唤醒,从而实现更灵活的线程协作。ReentrantLock
提供了尝试获取锁的方法,可以指定一个超时时间,在超过指定时间后如果无法获得锁,则继续执行其他操作,避免线程长时间等待。在高竞争的多线程场景下,ReentrantLock
通常表现更好,因为它提供了对锁的更细粒度的控制,并且支持更多的高级功能。
ReentrantLock提供了公平性控制的功能。通过在构造方法中指定fair
参数为true
,可以使得锁的获取按照线程请求的顺序进行,即先到先得的原则。这样可以避免某些线程一直获取不到锁而产生饥饿现象。
相比之下,synchronized关键字并没有提供公平性控制的选项,它的锁获取是非公平的。在多个线程同时竞争同一个锁时,synchronized无法保证等待时间最长的线程优先获得锁,可能会导致一些线程长时间等待锁而无法执行。
因此,如果对公平性有较高的要求,可以使用ReentrantLock来实现可重入锁,并通过设置fair
参数为true
来保证线程的公平竞争。
假设有两个线程,线程A和线程B,它们竞争一个ReentrantLock锁。
import java.util.concurrent.locks.ReentrantLock;
public class InterruptExample {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
try {
lock.lockInterruptibly(); // 可中断地获取锁
System.out.println("线程A获得了锁");
Thread.sleep(5000); // 模拟线程A执行一些操作
} catch (InterruptedException e) {
System.out.println("线程A被中断");
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
});
Thread threadB = new Thread(() -> {
try {
Thread.sleep(2000); // 等待2秒
threadA.interrupt(); // 中断线程A
} catch (InterruptedException e) {
e.printStackTrace();
}
});
threadA.start();
threadB.start();
}
}
线程A通过调用lock.lockInterruptibly()
方法来可中断地获取锁。而线程B在等待2秒后,调用threadA.interrupt()
方法来中断线程A。
如果线程A在等待锁的过程中被中断,会触发InterruptedException
异常,然后线程A可以根据自己的需求进行相应的处理。在上述代码中,线程A会打印"线程A被中断"。
这个例子展示了ReentrantLock的可中断性,通过中断一个等待锁的线程,可以让它在等待过程中响应中断并进行相应的处理。而synchronized关键字并没有提供直接的中断支持,无法中断正在等待锁的线程。
ReentrantLock内置了Condition接口,通过它可以创建多个条件变量,实现更灵活的线程协作。
Condition接口提供了以下几个方法:
await()
:使当前线程等待,并释放锁,直到被其他线程显式地唤醒或被中断。awaitUninterruptibly()
:与await()类似,但不响应中断。signal()
:唤醒一个等待该条件的线程。signalAll()
:唤醒所有等待该条件的线程。例子:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionExample {
private static ReentrantLock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
try {
lock.lock();
System.out.println("线程A开始等待");
condition.await(); // 等待条件满足
System.out.println("线程A被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
Thread threadB = new Thread(() -> {
try {
Thread.sleep(2000); // 等待2秒
lock.lock();
System.out.println("线程B发出唤醒信号");
condition.signal(); // 发出唤醒信号
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
threadA.start();
threadB.start();
}
}
线程A通过调用condition.await()
方法使自己等待条件满足,然后线程B在等待2秒后,通过调用condition.signal()
方法唤醒线程A。
使用条件变量,我们可以实现更加灵活的线程协作,线程可以根据特定的条件进行等待或唤醒。这在一些生产者消费者模型、线程池等场景中非常有用。而synchronized关键字并没有直接提供这种条件变量的支持。
,ReentrantLock提供了尝试获取锁的方法,并且可以指定一个超时时间。如果在指定时间内无法获取到锁,则可以继续执行其他操作,避免线程长时间等待。
ReentrantLock提供了以下几个尝试获取锁的方法:
tryLock()
:尝试获取锁,如果成功获取到锁则返回true
,否则立即返回false
。tryLock(long timeout, TimeUnit unit)
:尝试在指定的超时时间内获取锁,如果在指定时间内成功获取到锁则返回true
,否则在超时后返回false
。例子:
线程A调用lock.tryLock(3, TimeUnit.SECONDS)
方法,在3秒内尝试获取锁。如果在3秒内成功获取到锁,则执行相应的操作,否则在超时后输出"线程A尝试获取锁超时"。
通过使用tryLock()方法并指定超时时间,我们可以避免线程长时间等待,并在超时后执行其他操作。这在一些需要控制等待时间的场景中非常有用。
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TimeoutExample {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
try {
if (lock.tryLock(3, TimeUnit.SECONDS)) { // 在3秒内尝试获取锁
try {
System.out.println("线程A获得了锁");
Thread.sleep(5000); // 模拟线程A执行一些操作
} finally {
lock.unlock();
System.out.println("线程A释放了锁");
}
} else {
System.out.println("线程A尝试获取锁超时");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread threadB = new Thread(() -> {
try {
Thread.sleep(2000); // 等待2秒
lock.lock(); // 线程B获得锁
System.out.println("线程B获得了锁");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("线程B释放了锁");
}
});
threadA.start();
threadB.start();
}
}
tips:
TimeUnit.SECONDS
是 Java 中的一个枚举常量,它表示时间单位为秒。
lock.tryLock(3, TimeUnit.SECONDS)
表示在 3 秒内尝试获取锁。这里的 3 就是指定的时间,而 TimeUnit.SECONDS
则表示时间单位为秒。
TimeUnit.NANOSECONDS
:纳秒TimeUnit.MICROSECONDS
:微秒TimeUnit.MILLISECONDS
:毫秒TimeUnit.MINUTES
:分钟TimeUnit.HOURS
:小时TimeUnit.DAYS
:天