Java 内存模型规定了线程与主内存之间的交互规则,具体体现在以下几个方面:
这种设计可以提高访问速度,但也会带来一个问题——可见性问题。
因为每个线程对变量的操作都发生在自己的工作内存中,当多个线程操作同一个变量时,工作内存和主内存之间的更新不同步,就会导致一个线程对变量的修改对其他线程不可见。
例如,假设有一个变量 flag
,被线程 A 和线程 B 同时使用:
flag
初始值为 true
,存储在主内存中。flag
的值加载到自己的工作内存中。flag
的值加载到自己的工作内存中。flag
的值修改为 false
并写回到主内存中。flag
值,而不会意识到主内存中的值已经被修改为 false
。结果就是,线程 B 继续操作的是一个过期的值,导致了可见性问题。
volatile
如何解决可见性问题在 Java 中,volatile
关键字是一种轻量级的同步机制,用于修饰变量。volatile
可以确保被修饰的变量在所有线程中都是可见的,具体来说有以下两方面保证:
读操作从主内存中加载最新的值:当一个变量被声明为 volatile
时,JMM 会确保每次线程读取这个变量时,都是从主内存中直接读取最新的值,而不会使用工作内存中的缓存值。
写操作立即同步到主内存:同样,当一个线程对 volatile
变量进行写操作时,JMM 会强制将这个更新后的值立即刷新回主内存,使得其他线程可以立即看到最新的变化。
以下是一个简单的代码示例,说明 volatile
如何确保线程之间的可见性:
public class VolatileExample {
private volatile boolean flag = true;
public void updateFlag() {
flag = false; // 这里修改flag值会立即同步到主内存
}
public void checkFlag() {
while (flag) {
// 这里每次读取flag的值,都是从主内存读取最新值
}
}
}
在这个例子中:
flag
被 volatile
修饰后,线程执行 updateFlag()
方法将 flag
设置为 false
时,值会立即写回主内存。checkFlag()
方法,每次读取 flag
值时,都会从主内存中获取最新的值,而不会使用线程缓存。volatile
的局限性 虽然 volatile
能解决可见性问题,但它并不能保证操作的原子性。只能用在简单的赋值操作中举例来说,count++
这样的操作包含了多个步骤(读取 count
值、增加 1 并写回),volatile
并不能确保多个线程同时执行该操作时不会产生冲突。
volatile
和 synchronized
实现单例模式synchronized
关键字
synchronized
是一种重量级的同步机制,可以修饰方法或代码块,用于控制线程访问的顺序。synchronized
可以确保同一时刻只有一个线程执行同步代码块或方法,保证了代码的原子性和可见性。进入 synchronized
块的线程会自动获取锁,执行完毕后会释放锁。轻量级和重量级同步的区别
在多线程编程中,轻量级和重量级同步主要指同步机制对系统资源的占用程度,以及对性能的影响。
1. 轻量级(volatile
属于轻量级同步)
volatile
被称为轻量级同步,原因是它仅仅确保了变量的可见性,但并不保证原子性。它没有像 synchronized
那样的锁机制,不会阻塞线程,因此不会引起线程上下文切换。使用 volatile
不需要进入同步块,也就没有额外的资源消耗和性能开销。
优点:
局限性:
count++
)。2. 重量级(synchronized
属于重量级同步)
synchronized
是重量级同步,因为它涉及锁机制。当一个线程进入 synchronized
块或方法时,其他线程无法同时进入该块,这会导致线程的阻塞和等待。锁的获取和释放会带来额外的系统开销,比如线程的上下文切换(切换线程时操作系统保存和恢复线程的状态),因此性能相对较低,属于重量级操作。
优点:
局限性:
为什么选择轻量或重量
轻量和重量的选择取决于程序对同步的需求和性能的权衡:
volatile
更合适。synchronized
更可靠,尽管它会带来更多性能开销。双重检查锁定模式(Double-Checked Locking)
双重检查锁定是一种延迟初始化的懒汉式单例模式,它利用 volatile
和 synchronized
确保线程安全,同时避免了每次获取实例时都进入同步块的性能开销。
public class Singleton {
// 使用 volatile 关键字,确保 instance 对所有线程的可见性
private static volatile Singleton instance = null;
// 私有构造函数,防止外部创建实例
private Singleton() {}
// 提供一个全局访问点
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) { // 同步代码块
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
第一个 if (instance == null):
getInstance()
方法。if (instance == null)
的判断,认为 instance
为 null
(因为最开始 instance
确实是 null
)。synchronized (Singleton.class):
synchronized
块。其他线程会在同步块外等待,直到锁被释放。synchronized
块的线程会创建 Singleton
实例。第二个 if (instance == null):
synchronized
块中再次检查 instance == null
。因为这是第一次创建实例的线程,所以 instance
仍然为 null
,于是这个线程创建实例。synchronized
块。此时 instance
已经被创建,所以这些线程在 synchronized
块内的第二次检查 if (instance == null)
会发现 instance
不再是 null
,因此不会再次创建实例。 双重检查的原因是性能优化。在大多数情况下,单例对象已经被创建,我们不需要进入 synchronized
块,从而减少了同步的开销。
代理模式是一种设计模式,它让一个对象(代理对象)代替另一个对象去处理请求。我们用代理对象来控制对实际对象的访问,这样我们可以在访问实际对象前后添加一些额外功能,比如控制权限、记录日志、延迟加载资源等。
想象一下,当你要进入一个大型活动会场时,会有一个安保人员检查你的门票。这时,安保人员就是一个代理,他控制了你对会场的“访问”。通过代理人员的检查,确保只有合法的客人才能进入会场。
适用场景
代理模式适用于以下几种情况:
代理模式通常包含以下三个角色:
接口或抽象类(Subject):定义了实际对象和代理对象共同的接口。这样代理对象和实际对象可以被同样的方式调用。
实际对象(RealSubject):这是被代理的对象,它包含了业务逻辑,比如文件读取、数据库连接等功能。
代理对象(Proxy):这是负责“代理”访问的对象,它持有对实际对象的引用,并实现了与实际对象相同的接口。
让我们来通过一个例子一步步地解释代理模式。
假设我们有一个图片类 RealImage
,它需要从磁盘加载图片的操作,但这个操作可能比较耗时。因此,我们可以创建一个代理类 ImageProxy
,用来在需要时才实际加载图片。
定义图片接口
首先,我们定义一个 Image
接口,这样代理类和实际图片类都可以实现这个接口,并保持相同的操作方法(在这里是 display()
方法):
public interface Image {
void display();
}
实现实际图片类(RealSubject)
接下来,我们创建 RealImage
类,它负责从磁盘加载图片。加载图片的操作可能很耗时,我们可以用 System.out.println
模拟这种加载的延迟效果。
public class RealImage implements Image {
private String fileName;
public RealImage(String fileName) {
this.fileName = fileName;
loadFromDisk(); // 模拟加载图片的耗时操作
}
private void loadFromDisk() {
System.out.println("Loading " + fileName);
}
public void display() {
System.out.println("Displaying " + fileName);
}
}
创建代理类(Proxy)
然后,我们创建 ImageProxy
类,它是 Image
接口的代理实现。代理类持有实际图片对象的引用(RealImage
),并在需要时才去创建和加载它。通过代理,我们可以延迟 RealImage
的初始化,直到第一次调用 display()
才加载图片。
public class ImageProxy implements Image {
private RealImage realImage; // 持有实际图片对象的引用
private String fileName;
public ImageProxy(String fileName) {
this.fileName = fileName;
}
public void display() {
if (realImage == null) { // 仅在需要时才加载实际图片
realImage = new RealImage(fileName);
}
realImage.display();
}
}
测试代理模式的效果
在客户端代码中,我们通过代理类 ImageProxy
来访问图片对象。这样,我们可以在第一次调用 display()
方法时才实际加载图片,避免了每次创建图片时都加载的开销。
public class ProxyPatternDemo {
public static void main(String[] args) {
Image image = new ImageProxy("test_image.jpg");
// 第一次调用 display,实际图片会被加载
image.display();
// 第二次调用 display,使用已经加载的图片
image.display();
}
}
执行结果
Loading test_image.jpg
Displaying test_image.jpg
Displaying test_image.jpg
解析
延迟加载:代理类 ImageProxy
通过 realImage == null
的检查,仅在第一次调用 display()
方法时才去创建 RealImage
,从而实现了延迟加载。
访问控制:用户通过 ImageProxy
访问 RealImage
,从而将实际图片的加载过程隔离出来,用户不必直接创建和加载图片对象,而是通过代理类来控制加载行为。
代理模式在不修改实际对象的情况下,控制了对实际对象的访问,还可以增加额外的操作,例如延迟加载和访问权限验证等。代理模式非常适用于需要访问控制或延迟初始化的场景。
适配器模式是一种设计模式,它将一个类的接口转换为客户端期望的接口。简单来说,适配器模式解决了接口不兼容的问题,使得原本无法直接使用的类能够配合工作。
举个简单的例子:假如你的手机充电器插头是USB-C型,但你的插座是三孔的,这时你就需要一个适配器,它能够把USB-C型插头转换成符合三孔插座的插头,让你可以正常充电。
适用场景
适配器模式适用于以下情况:
适配器模式一般包含以下几部分:
实现步骤
我们来通过一个具体示例逐步理解适配器模式的实现。
假设我们有一个音频播放器 AudioPlayer
,它只能播放 MP3 格式的音频文件。现在,我们需要扩展播放器,让它可以播放其他格式的音频文件(如 VLC 和 MP4 格式)。
1. 目标接口 (MediaPlayer
)
AudioPlayer
类需要实现 MediaPlayer
接口,该接口定义了播放器的基本方法。
public interface MediaPlayer {
void play(String audioType, String fileName); // 播放音频文件
}
2. 被适配接口 (AdvancedMediaPlayer
)
我们将定义一个 AdvancedMediaPlayer
接口,用来支持播放 MP4 和 VLC 格式的文件。这个接口包含两个方法:一个用于播放 MP4 文件,另一个用于播放 VLC 文件。
public interface AdvancedMediaPlayer {
void playVlc(String fileName); // 播放 VLC 文件
void playMp4(String fileName); // 播放 MP4 文件
}
3. 被适配的类 (VlcPlayer
和 Mp4Player
)
然后我们实现 AdvancedMediaPlayer
接口的具体类。VlcPlayer
用于播放 VLC 格式的文件,Mp4Player
用于播放 MP4 格式的文件。
public class VlcPlayer implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
System.out.println("Playing VLC file. Name: " + fileName);
}
@Override
public void playMp4(String fileName) {
// 不实现
}
}
public class Mp4Player implements AdvancedMediaPlayer {
@Override
public void playVlc(String fileName) {
// 不实现
}
@Override
public void playMp4(String fileName) {
System.out.println("Playing MP4 file. Name: " + fileName);
}
}
4. 适配器类 (MediaAdapter
)
为了使 AudioPlayer
能够播放 MP4 和 VLC 格式的文件,我们创建一个适配器类 MediaAdapter
,该类将 MediaPlayer
接口的 play()
方法与 AdvancedMediaPlayer
的方法连接起来。
public class MediaAdapter implements MediaPlayer {
AdvancedMediaPlayer advancedMusicPlayer;
public MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer = new VlcPlayer(); // 支持 VLC 格式
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedMusicPlayer = new Mp4Player(); // 支持 MP4 格式
}
}
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer.playVlc(fileName);
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedMusicPlayer.playMp4(fileName);
}
}
}
5. 音频播放器类 (AudioPlayer
)
AudioPlayer
类实现了 MediaPlayer
接口,并且在播放 MP3 文件时直接处理,如果是其他格式,则通过 MediaAdapter
来适配。
public class AudioPlayer implements MediaPlayer {
MediaAdapter mediaAdapter;
@Override
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Playing MP3 file. Name: " + fileName);
}
else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
mediaAdapter = new MediaAdapter(audioType); // 使用适配器
mediaAdapter.play(audioType, fileName);
}
else {
System.out.println("Invalid media. " + audioType + " format not supported");
}
}
}
6. 测试类 (AdapterPatternDemo
)
最终,我们可以创建一个测试类来验证我们的 AudioPlayer
是否能够成功支持 MP3、MP4 和 VLC 格式的文件。
public class AdapterPatternDemo {
public static void main(String[] args) {
AudioPlayer audioPlayer = new AudioPlayer();
audioPlayer.play("mp3", "beyond the horizon.mp3"); // MP3 文件
audioPlayer.play("mp4", "alone.mp4"); // MP4 文件
audioPlayer.play("vlc", "far far away.vlc"); // VLC 文件
audioPlayer.play("avi", "mind me.avi"); // 不支持的格式
}
}
执行结果
Playing MP3 file. Name: beyond the horizon.mp3
Playing MP4 file. Name: alone.mp4
Playing VLC file. Name: far far away.vlc
Invalid media. avi format not supported
总结
MediaPlayer
):为 AudioPlayer
类提供统一的播放方法。AdvancedMediaPlayer
):定义了播放 MP4 和 VLC 文件的方法。MediaAdapter
):实现了 MediaPlayer
接口,并通过适配的方式调用 AdvancedMediaPlayer
的方法,支持 MP4 和 VLC 文件格式。AudioPlayer
):实现了 MediaPlayer
接口,并根据文件类型选择是否通过 MediaAdapter
来播放 MP4 或 VLC 文件。 通过适配器模式,AudioPlayer
类能够扩展支持其他音频格式,而不需要改变现有的代码结构。
因篇幅问题不能全部显示,请点此查看更多更全内容