4.java并发编程之不可变

58 阅读8分钟

不可变对象

不可变对象,实际是另一种避免竞争的方式。

如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改啊!

public class Immutable{
 private int value = 0;
 public Immutable(int value){
 	this.value = value;
 }
 public int getValue(){
 	return this.value;
 }
 
 public Immutable add(int v){
 	return new Immutable(this.value + v);
 } 
}

☆建立一个不可变类-final 的使用

类添加final修饰符,保证类不被继承。

保证所有成员变量私有,并加上final修饰

不提供改变成员变量的方法,包括setter

通过构造器初始化所有成员,进行深拷贝

getter方法中,不返回对象本身,返回克隆对象

不可变类中所有属性都是 final的

属性用 final 修饰保证了该属性是只读的,不能修改

类用 final 修饰保证了该类中的方法不能被覆盖,防止子类无意间破坏不可变性

内部状态不可改变:保护性拷贝

String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的

有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安 全的呢?

但有同学会说,使用字符串时,也有一些跟修改相关的方法啊,比如 substring 等,那么下面就看一看这些方法是如何实现的,就以 substring 为例:

public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

发现其内部是调用 String 的构造方法创建了一个新字符串,再进入这个构造看看,是否对 final char[] value 做出了修改:
public String(char value[], int offset, int count) {
    if (offset < 0) {
    	throw new StringIndexOutOfBoundsException(offset);
    }
    if (count <= 0) {
    	throw new StringIndexOutOfBoundsException(count);
    }
    if (offset <= value.length) {
   	 	this.value = "".value;
    	return;
    }
}
if (offset > value.length - count) {
	throw new StringIndexOutOfBoundsException(offset + count);
}
	this.value = Arrays.copyOfRange(value, offset, offset+count);
}
结果发现也没有,构造新字符串对象时,会生成新的 char[] value,对内容进行复制 。这种通过创建副本对象来避免共享的手段称之为【保护性拷贝(defensive copy)】

不可变类线程安全性

String、Integer 等都是不可变类,所谓的不可变类是指这个类的实例一旦创建完成后,就不能改变其成员变量值。因为其内部的状态不可以改变,因此它们的方法都是线程安全的。有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?查看源码发现有以下几个特点:

1. String类被final修饰,不可继承
2. string内部所有成员都设置为私有变量
3. 不存在value的setter
4. 并将value和offset设置为final。
5. 当传入可变数组value[]时,进行copy而不是直接将value[]复制给内部变量。
6. 获取value时不是直接返回对象引用,而是返回对象的copy.

string对象在内存创建后就不可改变,不可变对象的创建一般满足以下5个原则:

1、 不要提供任何会修改类状态的方法,包括setter;避免通过其他接口改变成员变量的值,破坏不可变特性。

2、类添加final修饰符,保证类不被继承如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖父类的方法并且继承类可以改变成员变量值,那么一旦子类以父类的形式出现时,不能保证当前类是否可变。

3、保证所有成员变量必须私有,并且加上final修饰, 一个类的private方法会隐式被指定为final方法。这种方式保证成员变量不可改变。但只做到这一步还不够,因为如果是对象成员变量有可能再外部改变其值。所以第5点弥补这个不足。

4、如果类具有指向可变对象的域,则必须确保该类的使用者无法获得指向这些对象的引用。如构造器传入的对象直接赋值给成员变量,还是可以通过对传入对象的修改进而导致改变内部变量的值。
public final class ImmutableDemo {
    private final int[] myArray; 
    public ImmutableDemo(int[] array) { 
        this.myArray = array; // wrong 
    } 
}
这种方式不能保证不可变性,myArray和array指向同一块内存地址,用户可以在ImmutableDemo之外通过修改array对象的值来改变myArray内部的值。

5、保护性拷贝

为了保证内部的值不被修改,可以采用深度copy来创建一个新内存保存传入的值。正确做法:

public final class MyImmutableDemo {  
    private final int[] myArray;  
    public MyImmutableDemo(int[] array) {  
        this.myArray = array.clone();   
    }   
}

这种做法也是防止对象外泄,防止通过getter获得内部可变成员对象后对成员变量直接操作,导致成员变量发生改变。在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝 这种做法也是防止对象外泄,防止通过getter获得内部可变成员对象后对成员变量直接操作,导致成员变量发生改变。

首先,关于类的成员变量(也就是属性),如果它们都不是不可变类,那么它们应该是私有的、final的。通过私有的封装来让属性外部无法修改,而final的作用是让属性初始化后就不能再改变(第3点);

如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖父类的方法并且继承类可以改变成员变量值,那么一旦子类以父类的形式出现时,不能保证当前类是否可变。(第2点);

其次,关于类的方法,我们不能提供任何修改属性是方法(第1点),比如常见的setXXX方法就不能再出现了。

如果类成员变量本身不是不可变的,那么需要注意两点:

1、在初始化(比如构造器里)给该成员变量赋值时,应该取的是外部对象引用的克隆(第5点);

2、如果不可变类提供给外部用于获取该成员变量的方法时,应该使用该成员变量的克隆,而非直接返回成员变量本身。这两点的目的都是避免外部通过对象引用修改不可变类的内部成员变量(第5点)。

具体参考:

www.cnblogs.com/wuxun1997/p…

www.cnblogs.com/jaylon/p/57…

☆String对象的不可变性的优点

从上一节分析,String数据不可变类,那设置这样的特性有什么好处呢?我总结为以下几点:

1.字符串常量池的需要。

字符串常量池可以将一些字符常量放在常量池中重复使用,避免每次都重新创建相同的对象、节省存储空间。但如果字符串是可变的,此时相同内容的String还指向常量池的同一个内存空间,当某个变量改变了该内存的值时,其他遍历的值也会发生改变。所以不符合常量池设计的初衷。

2.线程安全考虑。

同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。

3.类加载器用String,不可变保证正确加载。

譬如你想加载java.sql.Connection类,而这个值被改成了hacked.Connection,那么会对你的数据库造成不可知的破坏。

4.支持hash映射和缓存。

因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

String对象的不可变性的缺点

如果有对String对象值改变的需求,那么会创建大量的String对象。

String对象的是否真的不可变

虽然String对象将value设置为final,并且还通过各种机制保证其成员变量不可改变。但是还是可以通过反射机制的手段改变其值。例如:

//创建字符串"Hello World", 并赋给引用s

  String s = "Hello World"; 

  System.out.println("s = " + s); //Hello World
//获取String类中的value字段

  Field valueFieldOfString = String.class.getDeclaredField("value");

  //改变value属性的访问权限

  valueFieldOfString.setAccessible(true);

  //获取s对象上的value属性的值

  char[] value = (char[]) valueFieldOfString.get(s);

  //改变value所引用的数组中的第5个字符

  value[5] = '_';

  System.out.println("s = " + s);  //Hello_World

打印结果为:

s = Hello World

s = Hello_World

发现String的值已经发生了改变。也就是说,通过反射是可以修改所谓的“不可变”对象的

总结

不可变类是实例创建后就不可以改变成员遍历的值。这种特性使得不可变类提供了线程安全的特性但同时也带来了对象创建的开销,每更改一个属性都是重新创建一个新的对象。

JDK内部也提供了很多不可变类如Integer、Double、String等。String的不可变特性主要为了满足常量池、线程安全、类加载的需求。合理使用不可变类可以带来极大的好处。

实例安全分析

public class MyServlet extends HttpServlet {
    // 是否安全? 不安全
    Map<String,Object> map = new HashMap<>();
    // 是否安全? 安全
    String S1 = "...";
    // 是否安全? 安全
    final String S2 = "...";
    // 是否安全? 不安全
    Date D1 = new Date();
    // 是否安全? 不安全 年月日属性仍然是可变的 
    final Date D2 = new Date();
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
    // 使用上述变量
    }
} 
public class MyServlet extends HttpServlet {
// 是否安全?不安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
	userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 记录调用次数,count是成员变量,共享可能会被并发修改
    private int count = 0;
    public void update() {
        // ...
        count++;
    }
}
@Aspect
@Component
public class MyAspect {
    // 是否安全?//不安全 默认是单例,此处应该是多例或者使用ThreadLocal或者环绕通知
    private long start = 0L;
    @Before("execution(* *(..))")
    public void before() {
    	start = System.nanoTime();
    }
    @After("execution(* *(..))")
    public void after() {
    	long end = System.nanoTime();
    	System.out.println("cost time:" + (end-start));
    }
}


public class MyServlet extends HttpServlet {
// 是否安全 安全 每次都是创建新的service
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全 安全 每次都是创建新的dao
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
public void update() {
String sql = "update user set password = ? where username = ?";
// 是否安全 没有成员变量 线程安全
try (Connection conn = DriverManager.getConnection("","","")){
// ...
} catch (Exception e) {
// ...
}
}
}

public class MyServlet extends HttpServlet {
// 安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 安全 由于UserService只有一个 所以Dao也只有一个
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}

public class UserDaoImpl implements UserDao {
// 不安全 有可能线程1创建了链接还没用 线程2把链接关闭了
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}


public class MyServlet extends HttpServlet {
// 安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
public void update() {
UserDao userDao = new UserDaoImpl();
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
//安全
private Connection = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
conn.close();
}
}


public abstract class Test {
public void bar() {
// 不安全
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
foo(sdf);//子类可能重写foo方法 对sdf做出改变 假如改为yyyy/MM/dd HH:mm:ss
 }
public abstract foo(SimpleDateFormat sdf);
public static void main(String[] args) {
new Test().bar();
}
}



public void foo(SimpleDateFormat sdf) {
String dateStr = "1999-10-11 00:00:00";
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}

其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法

private static Integer i = 0;
public static void main(String[] args) throws InterruptedException {
List<Thread> list = new ArrayList<>();
for (int j = 0; j < 2; j++) {
Thread thread = new Thread(() -> {
for (int k = 0; k < 5000; k++) {
synchronized (i) {
i++;
}
}
}, "" + j);
list.add(thread);
}
list.stream().forEach(t -> t.start());
list.stream().forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
log.debug("{}", i);
}