Java笔记:final关键字补充、不可变对象
final关键字
1、修饰基本数据类型:基本数据类型属性或方法局部变量一经赋值后无法改变,必须赋初值且只能赋值一次,若对其多次赋值则不能通过编译。
2、修饰引用数据类型:该引用数据类型变量与最初交给它的内存空间形成绑定且不能修改,必须赋初值且只能赋值一次,不能将该引用变量指向其他对象的内存空间,但仍可以修改开始绑定的对象内部值。
3、修饰类:该类无法被继承,当类被修饰为final类时,其方法隐式指定为final方法。
4、修饰方法:该方法无法被子类重写,保护该方法父类的原有实现。类中的private方法被隐式地指定为final方法。
String类中的final关键字
1、String类被final关键字修饰,不可被继承。
2、String类中使用字符数组(char value[])存储,由final关键字修饰,属于不可变的对象。即没有机会去修改该对象,每一次的修改会产生新的对象。
String补充
字符串会创建在堆内存字符串常量池中,需要创建字符串时,先检查字符串常量池中是否含有相同字符串,若含有,则将常量池中字符串直接与引用变量绑定;若不含有,则在常量池中创建新的字符串。从而可以解释以下例子:
String s1 = “hello”;
String s2 = “hello”;
System.out.println(s1 == s2); //true
字符串s1和s2都指向字符串常量池相同的区域,双等号比较的即为两个对象的hashcode,即存放地址是否相等,相同字符串存放在常量池的相同位置,因此s1 == s2结果为真。但若出现String s3 = new String(“hello”);语句,new操作会在堆中开辟新的内存,新对象地址必不与原字符串常量池中”hello”存放地址相同,因此s1 == s3,s2 == s3均为false。
final实现不可变对象与线程安全
为实现线程安全,我们可以通过使用锁的形式:synchronized关键字保证同一时刻只有一个线程获取synchronized锁对象的monitor对象,即同一时刻只有一个线程能够修改锁对象,同步对该对象的修改操作,以串行化线程的方式保证线程安全。
使用不可变对象也能保证线程安全,其具体思路为:将一个对象数据域声明为final域,一经定义无法直接被其他操作(不管是自身线程还是其他线程)修改,若要对该对象进行修改操作时,只能舍弃原有对象,返回新的对象。这样,同一对象传入不同线程后,原本不同线程中的多份引用变量起初指向同一块内存,在每个线程对该对象进行修改操作后,每个线程中引用变量指向不同的内存区域。多个线程操作同一对象变成了多个线程操作每个线程内部自身的对象,实现了数据在不同线程的相互隔离即线程安全,但却不能实现资源的共享,线程间通信无法实现,但避免了synchronized锁排他引起的效率低的现象,代码如下:
class FinalHolder{
private final int num;
public FinalHolder(int i){
this.num = i;
}
public FinalHolder(FinalHolder finalHolder, int i){
this.num = finalHolder.num + i;
}
public FinalHolder add(int i){
return new FinalHolder(this, i);
}
public int getNum(){
return num;
}
}
测试用线程任务类如下:
class RunTask implements Runnable{
private static int num;
private final int id;
private FinalHolder finalHolder;
private CountDownLatch countDownLatch;
public RunTask(FinalHolder finalHolder, CountDownLatch countDownLatch){
num++;
this.id = num;
this.finalHolder = finalHolder;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
for (int i = 0;i<10000;i++){
System.out.println(this + " : " + finalHolder.getNum() + " " + finalHolder.hashCode());
finalHolder = finalHolder.add(1);
}
countDownLatch.countDown();
}
@Override
public String toString() {
return "Thread " + id;
}
}
启动类代码如下:
public class FinalHolderTest {
public static void main(String[] args) {
FinalHolder finalHolder = new FinalHolder(0);
CountDownLatch countDownLatch = new CountDownLatch(3);
for (int i = 0;i<3;i++){
new Thread(new RunTask(finalHolder, countDownLatch)).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(finalHolder.getNum() + " " + finalHolder.hashCode());
}
}
main方法中开启三个线程,并将创建好的FinalHolder类的实例对象传入三个线程,每个线程的任务是使该对象num自增10000次,主线程等待三个线程执行完后打印该实例对象的num值。
代码测试结果:三个线程各执行了10000次自增操作,每个线程内部num都由1自增到10000,最终打印num值为0。
通过依次打印每个线程持有的finalHolder的hashCode可以看到,三个线程未进行finalHolder.add()操作前,三个线程持有的finalHolder为同一个对象,之后三个线程则各自创建新的对象覆盖原先的对象,因此打印得到线程内部finalHolder的hashCode一直变化;而最后main方法中finalHolder引用的对象仍为一开始传入三个线程中的对象(在main方法中含有对它的引用而未被垃圾回收机制回收),因此main方法中的finalHolder.getNum()为0。
FinalHolder的数据域num不被final关键字修饰应该也能达到一样的效果,但是却给了持有该对象线程修改原始数据的机会,导致获取到的num可能不为通过正常操作获取到的值。
String类中也使用了不可变对象的原理,分离出主线程的每一个操作不会影响原始的数据,保证了线程之间对象的隔离与安全。
本篇为《Java高并发编程详解 多线程与架构设计》汪文君著 第18章笔记。