本篇文章通过Q&A的方式,对普通java类成员变量用public static修饰还是private static修饰进行思考。如果大家有不同的观点欢迎写在评论区,这会让我学到更多。
本文前提:
问题讨论:
在Java中,对于类成员变量,使用public static
修饰和private static
修饰都有其适用的场景,没有绝对的好坏之分。这主要取决于你的需求和设计。
1.public static
修饰:
2.private static
修饰:
总的来说,如果你希望其他类能够访问和修改这个变量,那么使用public static
修饰;如果你希望其他类不能访问和修改这个变量,那么使用private static
修饰。
在Java中,使用set
方法修改被private static
修饰的成员变量的原理主要涉及到了Java的访问修饰符和静态成员变量的特性。下面是一些关键点:
1.访问修饰符:在Java中,private
修饰符表示成员变量只能在其所在的类中被访问。这意味着其他类无法直接访问和修改这个private
成员变量。
2.静态成员变量:当一个成员变量被static
修饰时,这个成员变量就不再属于任何一个具体的对象实例,而是属于类本身。因此,你可以通过类名来直接访问这个静态成员变量,而不需要先创建类的实例。
然而,虽然private static
成员变量不能直接从类的外部访问和修改,但是可以在类内部提供一个public
的静态方法来修改这个private static
成员变量。这就是所谓的set
方法。
例如:
public class MyClass {
private static int myVariable;
public static void setMyVariable(int newValue) {
myVariable = newValue;
}
}
在这个例子中,我们提供了一个set
方法(这里是setMyVariable
)来修改myVariable
的值。虽然这个方法使用了public
修饰符,但是因为它是静态的,所以它不能从类的外部访问。它只能从类内部或者这个类的其他静态方法中访问。因此,即使你将变量设置为private static
,你仍然可以通过在类中提供一个静态的public
方法来修改它。
但是,这样做的一个潜在问题是,任何可以访问这个类的其他代码都可以通过调用这个set
方法来修改myVariable
的值,这可能会导致未预期的行为。因此,在使用这种技术时需要谨慎考虑其可能带来的影响。
如果将setMyVariable()
方法从静态改为非静态,那么该方法将不再属于类本身,而是属于类的每个实例。在这种情况下,你可以通过创建类的对象来调用这个方法,修改myVariable
的值。
以下是将setMyVariable()
方法从静态改为非静态的示例:
public class MyClass {
private static int myVariable;
public void setMyVariable(int newValue) {
myVariable = newValue;
}
}
使用非静态方法修改静态成员变量可能导致一些问题。因为静态成员变量属于类本身,而非静态方法属于类的实例。当非静态方法修改静态成员变量时,每个对象实例都会修改相同的静态变量,这可能会导致不可预测的行为。
因此,如果你想让每个对象实例都有自己的变量值副本,可以将成员变量设为非静态,同时为修改该变量提供一个静态方法。这样,每个对象实例都可以通过静态方法修改属于它自己的变量值副本。
使用非静态方法修改静态成员变量可能导致以下问题:
如果不想破坏封装性,又想在类的外部修改静态成员变量,可以考虑使用类提供的静态方法进行修改。
下面是一个可能引发线程问题的代码实例。
public class SharedResource {
private int count = 0;
public void incrementCount() {
count++;
}
public int getCount() {
return count;
}
}
public class Main {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
resource.incrementCount();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
resource.incrementCount();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Count: " + resource.getCount());
}
}
这个程序创建了两个线程,每个线程都会对 SharedResource
对象的 count
属性进行 10 万次增加操作。因为这两个线程是并行执行的,所以在执行过程中可能会出现线程安全问题,导致最终 count
的值小于 20 万(期望值)。
为了解决这个问题,可以使用 synchronized
关键字来保证在任何时候只有一个线程可以访问 incrementCount()
方法。或者使用 java.util.concurrent.atomic
包中的原子类(如 AtomicInteger
),这些类提供了线程安全的操作。
下面是一段使用非静态方法修改静态成员变量但导致线程不安全的Java代码示例:
public class SharedData {
private static int count = 0;
public void incrementCount() {
synchronized(this) {
count++;
}
}
public static int getCount() {
return count;
}
}
public class Main {
public static void main(String[] args) {
SharedData sharedData = new SharedData();
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
sharedData.incrementCount();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
sharedData.incrementCount();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Count: " + SharedData.getCount());
}
}
这段代码试图通过在 incrementCount()
方法中使用 synchronized(this)
来同步访问静态成员变量 count
,从而保证线程安全。然而,这种方法是不正确的,因为 this
锁定的只是一个对象实例,而不是静态成员变量 count
。在多个线程中对同一个对象实例的多个调用之间并不能保证同步。
要正确地同步访问静态成员变量,应该使用类作为锁,即 synchronized(SharedData.class)
。
使用synchronized
关键字以整个类作为锁,这确实是一种可能的做法,但它有一些潜在的缺点。以下是一些可能的问题:
1.没有细粒度控制:在整个类上同步意味着任何线程访问该类的任何实例时都需要获得这个锁。这可能阻止并发访问同一个类的不同实例,但对于并发访问同一实例的不同部分,这可能并不理想。
2.锁的持有时间过长:由于在整个类上持有锁,因此该锁可能会被持有的时间过长,从而减少了可用的线程并可能导致性能瓶颈。这特别在长生命周期的对象上造成问题,比如单例或者静态实例。
3.非公平锁:如果使用类锁,那么等待最长时间的线程可能并不能优先获得锁,因为类锁是非公平的。这可能会导致一些线程长时间等待,而其他线程则可以立即获得锁。
4.状态的不一致性:在整个类上同步意味着所有线程在执行任何修改共享状态的操作时都需要获得同一个锁。这可能会导致状态不一致性,特别是当线程在持有锁的时候发生异常。
5.不易于理解和调试:在整个类上同步可能会使代码的逻辑变得不清晰,因为任何改变这个类的线程都可能影响其他线程。这使得理解和调试多线程代码变得更加困难。
为了解决这些问题,通常建议在需要同步的具体方法或代码块上使用锁,而不是在整个类上使用锁。这样可以实现更细粒度的控制,减少锁的持有时间,实现公平锁,保持状态的一致性,并使代码更容易理解和调试。