网站首页 > java教程 正文
- 学习目标
- 1 内存可见性问题
- 1.1 什么是内存可见性
- 1.2 内存可见性问题的原因
- 2 volatile的作用与使用场景
- 2.1 volatile关键字介绍
- 2.2 volatile的内存语义
- 2.3 volatile的适用场景
- 2.4 volatile不能解决的问题
- 3 volatile与synchronized的区别
- 3.1 可见性与原子性
- 3.2 使用范围
- 3.3 性能开销
- 3.4 适用场景对比
- 4 volatile的内部原理
- 4.1 内存屏障(Memory Barrier)
- 4.2 happens-before关系
- 4.3 volatile的底层实现
- 5 实战案例:使用volatile实现一个简单的缓存系统
- 6 常见问题与解决方案
- 6.1 volatile不保证原子性
- 6.2 过度使用volatile
- 6.3 volatile与单例模式
- 6.4 volatile数组
- 6.5 volatile的性能考量
- 7 小结
学习目标
- 理解Java内存模型与内存可见性问题
- 掌握volatile关键字的作用与正确使用场景
- 了解volatile与synchronized的区别与联系
- 能够使用volatile实现一个简单的缓存系统
1 内存可见性问题
1.1 什么是内存可见性
在多核CPU环境下,每个处理器都有自己的高速缓存。由于处理器的运行速度远大于内存访问速度,为了提高性能,处理器会将运算需要的数据提前缓存在高速缓存中。当程序在运行过程中,会将运算所需要的数据从主内存复制到CPU的高速缓存中,而高速缓存中的数据会在某个时间点刷新到主内存。
内存可见性问题指的是:当多个线程操作共享数据时,彼此无法看到对方线程对共享变量所做的修改。
让我们通过一个简单的例子来理解这个问题:
package org.devlive.tutorial.multithreading.chapter06;
public class VisibilityProblemDemo {
// 共享变量
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
// 创建线程1,当检测到flag为true时退出循环
Thread thread1 = new Thread(() -> {
System.out.println("线程1启动");
// 当flag为false时,无限循环
while (!flag) {
// 空循环
}
System.out.println("线程1检测到flag变为true,退出循环");
});
// 创建线程2,将flag设置为true
Thread thread2 = new Thread(() -> {
try {
// 休眠1秒,确保线程1先启动
Thread.sleep(1000);
System.out.println("线程2将flag设置为true");
flag = true;
System.out.println("线程2设置完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 启动线程
thread1.start();
thread2.start();
// 等待线程执行完毕
thread1.join();
thread2.join();
}
}
在这个例子中,你可能会觉得线程2将flag设置为true后,线程1应该能够检测到并退出循环。但在某些情况下(特别是在优化编译和多核CPU的环境下),线程1可能永远无法退出循环。这就是内存可见性问题的体现。
提示:如果你运行这个程序,可能在某些机器上线程1确实能够退出循环,但在其他机器上可能会一直循环下去。这取决于JVM的实现、CPU架构以及编译器的优化策略。
1.2 内存可见性问题的原因
内存可见性问题主要由以下几个原因导致:
- CPU缓存:每个CPU都有自己的缓存,线程1对变量的修改可能只更新了线程1所在CPU的缓存,而没有及时刷新到主内存。
- 编译器优化:为了提高性能,编译器和CPU会对指令进行重排序,可能导致指令的实际执行顺序与代码编写顺序不一致。
- JVM内存模型:Java内存模型允许JVM对代码进行各种优化,其中包括将变量存储在寄存器而不是内存中,这会导致其他线程无法及时看到变量的变化。
下面是一个图示,展示了内存可见性问题是如何发生的:
线程1(CPU核心1) 线程2(CPU核心2)
+----------------+ +----------------+
| 本地缓存: | | 本地缓存: |
| flag = false | | flag = true |
+----------------+ +----------------+
↑ ↓
| |
| |
+--------------------------------------------------+
| 主内存: flag = ? |
+--------------------------------------------------+
如图所示,线程2已经在自己的本地缓存中将flag设置为true,但尚未将这个更新刷新到主内存中。同时,线程1仍然使用本地缓存中的旧值(false)。
2 volatile的作用与使用场景
2.1 volatile关键字介绍
volatile是Java提供的一种轻量级的同步机制,它能够保证变量在多线程之间的可见性,但不能保证原子性。
volatile关键字主要有两个作用:
- 保证可见性:当一个线程修改了被volatile修饰的变量后,无论这个变量是否被缓存,其他线程都能立即看到最新值。
- 禁止指令重排序:volatile关键字会在指令序列中插入内存屏障,禁止特定类型的指令重排序,从而避免由于指令重排序导致的并发问题。
让我们修改前面的例子,使用volatile关键字来解决内存可见性问题:
package org.devlive.tutorial.multithreading.chapter06;
public class VolatileVisibilityDemo {
// 使用volatile修饰共享变量
private static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
// 创建线程1,当检测到flag为true时退出循环
Thread thread1 = new Thread(() -> {
System.out.println("线程1启动");
// 当flag为false时,无限循环
while (!flag) {
// 空循环
}
System.out.println("线程1检测到flag变为true,退出循环");
});
// 创建线程2,将flag设置为true
Thread thread2 = new Thread(() -> {
try {
// 休眠1秒,确保线程1先启动
Thread.sleep(1000);
System.out.println("线程2将flag设置为true");
flag = true;
System.out.println("线程2设置完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 启动线程
thread1.start();
thread2.start();
// 等待线程执行完毕
thread1.join();
thread2.join();
}
}
在这个修改后的版本中,我们使用volatile关键字修饰flag变量。这样,当线程2修改flag的值时,线程1能够立即看到这个变更,从而退出循环。
2.2 volatile的内存语义
为了更深入地理解volatile的工作原理,我们需要了解Java内存模型(JMM)以及volatile的内存语义。
在Java内存模型中,volatile变量的写操作和读操作分别具有以下内存语义:
- volatile写:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
- volatile读:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量。
这种特性保证了,线程A对volatile变量的写入对线程B的读取可见,即线程A写入的值,能够被线程B读取到。
下面是一个图示,展示了volatile变量如何保证内存可见性:
线程1(CPU核心1) 线程2(CPU核心2)
+----------------+ +----------------+
| 本地缓存: | | 本地缓存: |
| flag = false |<---+ | flag = true |
+----------------+ | +----------------+
↑ | ↓
| | |
| | |
+--------------------------------------------------+
| 主内存: flag = true |
+--------------------------------------------------+
|
|
+--- volatile保证其他线程能看到最新值
2.3 volatile的适用场景
volatile关键字适用于以下场景:
- 状态标记:当一个变量作为状态标记时(如开关控制),通常使用volatile修饰。
- 双重检查锁定(Double-Checked Locking):在单例模式的双重检查锁定中,使用volatile可以防止由于指令重排序导致的问题。
- 独立观察:一个线程写入变量,另一个线程读取变量,两个线程之间没有其他共享变量,这种情况下可以使用volatile。
让我们看一个使用volatile实现的双重检查锁定单例模式:
package org.devlive.tutorial.multithreading.chapter06;
public class SafeSingleton {
// 使用volatile修饰instance
private static volatile SafeSingleton instance;
// 私有构造函数
private SafeSingleton() {
System.out.println("创建SafeSingleton实例");
}
// 获取实例的方法
public static SafeSingleton getInstance() {
// 第一次检查
if (instance == null) {
// 同步代码块
synchronized (SafeSingleton.class) {
// 第二次检查
if (instance == null) {
instance = new SafeSingleton();
}
}
}
return instance;
}
public static void main(String[] args) {
// 创建多个线程同时获取实例
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SafeSingleton singleton = SafeSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + " 获取到实例: " + singleton);
}).start();
}
}
}
在这个例子中,我们使用volatile修饰instance变量。这样做的目的是防止指令重排序导致的问题。因为instance = new SafeSingleton()这一行代码实际上包含三个步骤:
- 分配对象的内存空间
- 初始化对象
- 将引用指向分配的内存空间
如果不使用volatile,这三个步骤可能会被重排序,可能导致其他线程在对象还没有完全初始化时就获取到了实例,从而导致错误。
注意:虽然双重检查锁定模式是volatile的一个经典应用场景,但在实际开发中,更推荐使用静态内部类或枚举实现单例模式,因为它们更简单且线程安全。
2.4 volatile不能解决的问题
虽然volatile能够保证可见性和禁止指令重排序,但它不能保证操作的原子性。这意味着,当一个操作需要先读取值,然后修改值,最后写回值的时候,volatile不能保证这个过程是原子的。
例如,多线程环境下的计数器:
package org.devlive.tutorial.multithreading.chapter06;
public class VolatileCounterDemo {
// 使用volatile修饰计数器
private static volatile int counter = 0;
public static void main(String[] args) throws InterruptedException {
// 创建10个线程,每个线程将counter递增1000次
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter++; // 非原子操作
}
});
threads[i].start();
}
// 等待所有线程执行完毕
for (Thread thread : threads) {
thread.join();
}
// 输出结果
System.out.println("Expected: " + (10 * 1000));
System.out.println("Actual: " + counter);
}
}
在这个例子中,我们使用volatile修饰counter变量,但最终的结果很可能小于10000。这是因为counter++操作不是原子的,它包含三个步骤:读取counter的值、将值加1、将新值写回counter。在多线程环境下,这三个步骤可能会被其他线程的操作打断,导致最终结果不正确。
对于这种需要保证原子性的场景,应该使用synchronized或
java.util.concurrent.atomic包中的原子类,如AtomicInteger。
3 volatile与synchronized的区别
volatile和synchronized是Java中常用的两种同步机制,它们有着不同的特性和适用场景。
3.1 可见性与原子性
- volatile:保证可见性和禁止指令重排序,但不保证原子性。
- synchronized:保证可见性、原子性和有序性。
3.2 使用范围
- volatile:只能修饰变量。
- synchronized:可以修饰方法和代码块。
3.3 性能开销
- volatile:轻量级,性能较好。
- synchronized:重量级,会导致线程上下文切换,性能较差(虽然从Java 6开始,synchronized已经进行了许多优化)。
3.4 适用场景对比
- 如果只需要保证可见性,使用volatile即可。
- 如果需要保证原子性,应该使用synchronized或java.util.concurrent.atomic包中的原子类。
- 如果一个变量被多个线程访问,但只有一个线程修改,可以使用volatile保证可见性。
- 如果多个线程都需要修改这个变量,需要使用synchronized或其他锁机制保证原子性。
让我们通过一个例子来对比这两种同步机制:
package org.devlive.tutorial.multithreading.chapter06;
import java.util.concurrent.atomic.AtomicInteger;
public class SynchronizationComparisonDemo {
// 使用volatile修饰的计数器,不能保证原子性
private static volatile int volatileCounter = 0;
// 使用synchronized保护的计数器
private static int synchronizedCounter = 0;
// 使用AtomicInteger的计数器
private static AtomicInteger atomicCounter = new AtomicInteger(0);
// synchronized方法,确保原子性
private static synchronized void incrementSynchronizedCounter() {
synchronizedCounter++;
}
public static void main(String[] args) throws InterruptedException {
// 创建10个线程,分别递增三种计数器
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
volatileCounter++; // 非原子操作
incrementSynchronizedCounter(); // 使用synchronized保证原子性
atomicCounter.incrementAndGet(); // 使用AtomicInteger保证原子性
}
});
threads[i].start();
}
// 等待所有线程执行完毕
for (Thread thread : threads) {
thread.join();
}
// 输出结果
System.out.println("Expected: " + (10 * 1000));
System.out.println("Volatile Counter: " + volatileCounter);
System.out.println("Synchronized Counter: " + synchronizedCounter);
System.out.println("Atomic Counter: " + atomicCounter.get());
}
}
运行这个程序,你会发现volatileCounter的值很可能小于10000,而synchronizedCounter和atomicCounter的值一定是10000。这说明volatile不能保证原子性,而synchronized和AtomicInteger可以保证原子性。
4 volatile的内部原理
为了更深入地理解volatile的工作原理,我们需要了解Java内存模型以及volatile在底层的实现。
4.1 内存屏障(Memory Barrier)
volatile的底层实现主要依赖于内存屏障(Memory Barrier)指令。内存屏障是一种CPU指令,用于控制特定条件下的重排序和内存可见性。
在Java中,volatile通过插入内存屏障来实现以下功能:
- 保证可见性:当写一个volatile变量时,会在写操作后插入一个写屏障(Store Memory Barrier);当读一个volatile变量时,会在读操作前插入一个读屏障(Load Memory Barrier)。
- 禁止指令重排序:通过内存屏障,确保volatile变量的读写操作不会被重排序。
4.2 happens-before关系
Java内存模型(JMM)定义了一种happens-before关系,用来表示一个操作对另一个操作可见。如果操作A happens-before操作B,那么操作A的结果对操作B可见。
volatile变量的读写建立了happens-before关系:
- 对一个volatile变量的写操作happens-before后续对这个volatile变量的读操作。
这意味着,当线程A写入一个volatile变量,线程B随后读取这个变量,那么线程A在写入volatile变量之前的所有操作对线程B都是可见的。
4.3 volatile的底层实现
在不同的硬件架构和JVM实现中,volatile的底层实现可能有所不同。但基本原理是一致的:
- x86架构:在x86架构上,写操作自动具有释放(release)语义,读操作自动具有获取(acquire)语义,所以volatile的写操作只需要在写后插入一个写屏障,而volatile的读操作不需要插入读屏障。
- 其他架构:在其他架构(如ARM)上,可能需要在volatile写后插入写屏障,在volatile读前插入读屏障。
以下是一个简化的示意图,展示了volatile在底层的实现原理:
// 写volatile变量
store value -> volatile variable
StoreStore barrier (防止写操作重排序)
StoreLoad barrier (确保其他处理器能看到该写操作)
// 读volatile变量
LoadLoad barrier (防止读操作重排序)
load value <- volatile variable
LoadStore barrier (防止读操作与后续写操作重排序)
注意:以上是一个简化的示意图,实际实现可能因JVM版本和硬件架构而异。
5 实战案例:使用volatile实现一个简单的缓存系统
下面我们将实现一个简单的缓存系统,使用volatile确保缓存一致性。
package org.devlive.tutorial.multithreading.chapter06;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class SimpleCache {
// 使用volatile修饰缓存对象,确保可见性
private static volatile Map<String, Object> cache = new HashMap<>();
// 模拟从数据库加载数据
private static Object loadFromDB(String key) {
System.out.println("从数据库加载数据:" + key);
try {
// 模拟数据库操作的延迟
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Data for " + key;
}
// 从缓存获取数据,如果缓存中没有则从数据库加载
public static Object get(String key) {
// 从缓存中获取数据
Object value = cache.get(key);
if (value == null) {
synchronized (SimpleCache.class) {
// 双重检查,防止多个线程同时加载同一个数据
value = cache.get(key);
if (value == null) {
// 从数据库加载数据
value = loadFromDB(key);
// 更新缓存
Map<String, Object> newCache = new HashMap<>(cache);
newCache.put(key, value);
cache = newCache; // 原子更新整个缓存,确保可见性
}
}
}
return value;
}
// 更新缓存
public static void put(String key, Object value) {
synchronized (SimpleCache.class) {
Map<String, Object> newCache = new HashMap<>(cache);
newCache.put(key, value);
cache = newCache; // 原子更新整个缓存,确保可见性
}
}
// 清除缓存
public static void clear() {
synchronized (SimpleCache.class) {
cache = new HashMap<>();
}
}
public static void main(String[] args) {
// 创建多个线程同时访问缓存
for (int i = 0; i < 10; i++) {
final int index = i;
new Thread(() -> {
Object data = get("key" + (index % 5));
System.out.println(Thread.currentThread().getName() + " 获取数据:" + data);
}).start();
}
}
}
在这个实战案例中,我们使用volatile修饰缓存Map对象,并且在更新缓存时创建一个新的Map对象,而不是直接修改现有的Map。这种方式保证了缓存的一致性和可见性。
注意:虽然这个简单的缓存系统可以工作,但在实际应用中,我们通常会使用ConcurrentHashMap或专业的缓存库如Caffeine、Guava Cache等。
6 常见问题与解决方案
在使用volatile关键字时,有一些常见问题需要注意:
6.1 volatile不保证原子性
问题:volatile变量的复合操作(如i++)不是原子的,可能导致线程安全问题。
解决方案:
- 使用synchronized关键字保证原子性。
- 使用java.util.concurrent.atomic包中的原子类,如AtomicInteger。
示例:
package org.devlive.tutorial.multithreading.chapter06;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicSolutionDemo {
// 使用AtomicInteger替代volatile int
private static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
// 创建10个线程,每个线程将counter递增1000次
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
counter.incrementAndGet(); // 原子操作
}
});
threads[i].start();
}
// 等待所有线程执行完毕
for (Thread thread : threads) {
thread.join();
}
// 输出结果
System.out.println("Expected: " + (10 * 1000));
System.out.println("Actual: " + counter.get());
}
}
6.2 过度使用volatile
问题:过度使用volatile可能导致不必要的内存同步,影响性能。
解决方案:
- 只在必要的场景下使用volatile。
- 对于复杂的同步需求,考虑使用java.util.concurrent包中的工具类。
6.3 volatile与单例模式
问题:在双重检查锁定单例模式中,如果不使用volatile修饰instance变量,可能会因为指令重排序导致问题。
解决方案:
- 使用volatile修饰instance变量。
- 考虑使用静态内部类或枚举实现单例模式,这些方式更简单且线程安全。
示例(静态内部类实现单例):
package org.devlive.tutorial.multithreading.chapter06;
public class StaticInnerClassSingleton {
// 私有构造函数
private StaticInnerClassSingleton() {
System.out.println("创建StaticInnerClassSingleton实例");
}
// 静态内部类,持有单例实例
private static class SingletonHolder {
private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
// 获取实例的方法
public static StaticInnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
public static void main(String[] args) {
// 创建多个线程同时获取实例
for (int i = 0; i < 10; i++) {
new Thread(() -> {
StaticInnerClassSingleton singleton = StaticInnerClassSingleton.getInstance();
System.out.println(Thread.currentThread().getName() + " 获取到实例: " + singleton);
}).start();
}
}
}
6.4 volatile数组
问题:当使用volatile修饰数组时,只有数组引用是volatile的,数组元素不是volatile的。
解决方案:
- 使用AtomicReferenceArray。
- 对数组元素的访问加锁。
- 考虑使用CopyOnWriteArrayList等线程安全的集合类。
示例:
package org.devlive.tutorial.multithreading.chapter06;
import java.util.concurrent.atomic.AtomicIntegerArray;
public class VolatileArrayDemo {
// 使用volatile修饰数组,只有数组引用是volatile的,数组元素不是
private static volatile int[] volatileArray = new int[10];
// 使用AtomicIntegerArray保证元素的原子性
private static AtomicIntegerArray atomicArray = new AtomicIntegerArray(10);
public static void main(String[] args) throws InterruptedException {
// 创建10个线程,每个线程操作不同索引的元素
Thread[] threads = new Thread[10];
for (int i = 0; i < threads.length; i++) {
final int index = i;
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
// 对volatileArray[index]进行非原子的自增操作
volatileArray[index]++;
// 对atomicArray[index]进行原子的自增操作
atomicArray.incrementAndGet(index);
}
});
threads[i].start();
}
// 等待所有线程执行完毕
for (Thread thread : threads) {
thread.join();
}
// 输出结果
boolean volatileArrayCorrect = true;
boolean atomicArrayCorrect = true;
for (int i = 0; i < 10; i++) {
if (volatileArray[i] != 1000) {
volatileArrayCorrect = false;
}
if (atomicArray.get(i) != 1000) {
atomicArrayCorrect = false;
}
}
System.out.println("Expected value for each element: 1000");
System.out.println("Volatile Array correct: " + volatileArrayCorrect);
System.out.println("Atomic Array correct: " + atomicArrayCorrect);
}
}
6.5 volatile的性能考量
问题:虽然volatile比synchronized轻量级,但过度使用仍会影响性能。
解决方案:
- 在性能关键的代码中,谨慎使用volatile。
- 考虑使用Java 8引入的StampedLock或其他高性能同步工具。
- 进行性能测试,确定最适合的同步机制。
7 小结
在本章中,我们深入学习了Java中的volatile关键字,主要内容包括:
- 内存可见性问题:了解了多线程环境下的内存可见性问题及其产生原因。
- volatile的作用:掌握了volatile的两个主要作用:保证可见性和禁止指令重排序。
- volatile的适用场景:学习了volatile的适当使用场景,包括状态标记、双重检查锁定等。
- volatile的局限性:认识到volatile不能保证操作的原子性,对于需要原子性的操作,应该使用synchronized或原子类。
- volatile与synchronized的区别:比较了这两种同步机制的异同点,了解它们各自的适用场景。
实战案例:通过实例展示了如何使用volatile实现线程安全的开关控制和简单的缓存系统。
- 常见问题与解决方案:介绍了使用volatile时的常见问题及其解决方法。
volatile是Java并发编程中的一个重要工具,它提供了一种轻量级的同步机制。合理使用volatile可以在某些场景下避免使用重量级的synchronized关键字,从而提高程序性能。但需要注意,volatile并不适用于所有场景,特别是需要保证操作原子性的场景。
在下一章中,我们将学习ThreadLocal的使用,它是另一种重要的线程安全机制,用于实现线程隔离。
源代码地址:
https://github.com/qianmoQ/tutorial/tree/main/java-multithreading-tutorial/src/main/java/org/devlive/tutorial/multithreading/chapter06
猜你喜欢
- 2025-04-27 Java多线程编程中的锁优化技巧:锁的艺术之旅
- 2025-04-27 ECC加密算法Illegal key size错误源码详解
- 2025-04-27 基于Vue.js的http网站如何实现帐号密码加密传输?
- 2025-04-27 Java虚拟机类加载机制剖析:探秘Java世界的“魔法师”
- 2025-04-27 Springboot 配置文件、隐私数据脱敏最佳实践(原理+源码)
- 2025-04-27 Java 如何加载带密码的 PCKS8 PEM 私钥
- 2025-04-27 Java网络编程中的Socket通信优化之道
- 2025-04-27 Java 17中的新加密API:实现数据安全传输
- 2025-04-27 Java代码保护方法之四:JVMTI实现Java源码保护
- 2025-04-27 Java并发编程中的锁机制:掌控多线程世界的钥匙
你 发表评论:
欢迎- 04-27微服务部署架构设计详解(图文全面总结)
- 04-27Java微服务架构选型与对比:一场技术流派的巅峰对决
- 04-27微服务架构下Java的最佳实践
- 04-27Java微服务架构选型:优雅拆分与高效整合
- 04-27微服务架构下的Java代码拆分策略:像拼图一样构建系统
- 04-27微服务架构下的Java最佳实践
- 04-27微服务架构下Java的挑战与机遇
- 04-27微服务架构下Java事务管理的艺术
- 最近发表
- 标签列表
-
- java反编译工具 (77)
- java反射 (57)
- java接口 (61)
- java随机数 (63)
- java7下载 (59)
- java数据结构 (61)
- java 三目运算符 (65)
- java对象转map (63)
- Java继承 (69)
- java字符串替换 (60)
- 快速排序java (59)
- java并发编程 (58)
- java api文档 (60)
- centos安装java (57)
- java调用webservice接口 (61)
- java深拷贝 (61)
- 工厂模式java (59)
- java代理模式 (59)
- java.lang (57)
- java连接mysql数据库 (67)
- java重载 (68)
- java 循环语句 (66)
- java反序列化 (58)
- java时间函数 (60)
- java是值传递还是引用传递 (62)
本文暂时没有评论,来添加一个吧(●'◡'●)