并发编程-线程安全策略之不可变对象

Posted 爱上口袋的天空

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程-线程安全策略之不可变对象相关的知识,希望对你有一定的参考价值。

脑图

四个线程安全策略

 

线程限制

一个被线程限制的对象,由线程独占,并且只能被占有它的线程修改

共享只读

一个共享只读的对象,在没有额外同步的情况下,可以被多个线程并发访问,但是任何线程都不能修改它

线程安全对象

一个线程安全的对象或者容器,在内部通过同步机制来保证线程安全,所以其他线程无需额外的同步就可以通过公共接口随意访问它

被守护对象

被守护对象只能通过获取特定的锁来访问

 不可变对象定义

在Java中,有一种对象发布了就是安全的,被称之为不可变对象。

不可变对象可以在多线程中可以保证线程安全

不可变对象需要满足的条件

  • 对象创建以后其状态就不能修改
  • 对象所有域都是final类型
  • 对象是正确创建的(在对象创建期间,this引用没有逸出)

 如何创建不可变对象

 

  • 将类声明成final类型,使其不可以被继承
  • 将所有的成员设置成私有的,使其他的类和对象不能直接访问这些成员
  • 对变量不提供set方法
  • 将所有可变的成员声明为final,这样只能对他们赋值一次
  • 通过构造器初始化所有成员,进行深度拷贝
  • 在get方法中,不直接返回对象本身,而是克隆对象,返回对象的拷贝

提到不可变的对象就不得不说一下final关键字,该关键字可以修饰类、方法、变量:

使用final关键字定义不可变对象

final关键字可以修饰类、方法、变量

  • 修饰类:不能被继承(final类中的所有方法都会被隐式的声明为final方法)
  • 修饰方法:

1、锁定方法不被继承类修改;
2、可提升效率(private方法被隐式修饰为final方法)

  • 修饰变量:

基本数据类型变量: 初始化之后不能修改
引用类型变量: 初始化之后不能再修改其引用

  • 修饰方法参数:同修饰变量

修饰变量示例

final修饰基本数据类型及String: 初始化之后不能修改 (线程安全)

可知:编译报错,被final修饰后,基本类型和String的变量无法被修改 

final修饰引用类型变量:初始化之后不能再修改其引用,但可以修改值 (线程不安全)

package com.artisan.example.immutable;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import com.google.common.collect.Maps;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class FinalDemo {

	// 基本数据类型 int 使用final修饰 验证被final修饰的基本数据类型无法改变
	private final static int num = 1;

	// String类型 使用final修饰 验证被final修饰的基本数据类型无法改变
	private final static String name = "小工匠";

	// 引用类型 初始化之后不能再修改其引用,但是可修改其中值
	private final static Map<String, Object> map = Maps.newHashMap();

	static {
		map.put("name", "artisan");
		map.put("age", 20);
		map.put("sex", "男");
	}

	public static void main(String[] args) {

		// 被final修饰的基本数据类型和String无法改变
		// 编译报错: The final field FinalDemo.num cannot be assigned
		// num = 2;
		// 编译报错: The final field FinalDemo.name cannot be assigned
		// name = "artisan";

		// 引用对象,此引用无法指向别的对象,但可修改该对象的值
		map.put("name", "小工匠");
		log.info("name:{}", map.get("name"));
		
		
		// 验证 方法参数被final修饰的情况
		List<String> list = new ArrayList<>();
		list.add("我是小工匠");
		test2(list);
		
	}

	// final修饰传递进来的变量基本类型,不可别改变
	private void test(final int a) {
		// 不能修改
		// 编译报错: The final local variable a cannot be assigned. It must be blank and not using a compound assignment
		// a = 2;
		log.info("a:{}", a);
	}

	// final修饰方法传递进来的变量 引用对象,无法指向别的对象,但可修改该对象的值
	private static void test2(final List<String> list) {
		// 添加数据
		list.add("我是artisan");
		list.forEach(str ->{
			log.info("数据:{}",str);
		});
	}

}

需要我们注意的是,final修饰引用类型时,虽然不能将引用再指向别的对象,但可修改该对象的值。 线程不安全

使用JDK / Guava中提供的工具类创建不可变对象 

除了final可以定义不可变对象,java提供的Collections类,也可定义不可变对象。

  • JDK中的 Collections.unmodifiableXXX传入的对象一经初始化便无法修改,XXX可表示Collection、List、Set、Map等

  • 谷歌的Guava中的ImmutableXXX,XXX同样可表示Collection、List、Set、Map等

Collections.unmodifiableXXX 示例 (线程安全)

执行后结果如下:

 

由此可见,用Collections.UnmodifiableMap修饰的对象是不可修改的,如果尝试修改对象的值,在程序运行时会抛出异常。

跟下Collections.UnmodifiableMap的源码

 

继续看下UnmodifiableMap

 

主要是将一个新的集合的所有更新方法变为抛出异常 

Guava ImmutableXXX 示例 (线程安全)

package com.artisan.example.immutable;

import com.artisan.anno.ThreadSafe;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;


@ThreadSafe
public class GuavaImmutableSetDemo {

	// 使用Guava中提供的类来定义不可变对象的集合
	
	// 不可变list
	private final static ImmutableList<Integer> list = ImmutableList.of(1, 2, 3);
	// 不可变的set
	private final static ImmutableSet<Integer> set = ImmutableSet.copyOf(list);
	// 不可变的map,需要以k/v的形式传入数据,即奇数位参数为key,偶数位参数为value
	private final static ImmutableMap<String, String> map = ImmutableMap.of("k1", "v1", "k2","v2");
	// 通过builder调用链的方式构造不可变的map
	private final static ImmutableMap<String, String> map2 = ImmutableMap.<String, String>builder()
																			.put("key1", "value1")
																			.put("key2", "value2")
																			.put("key3", "value3")
																			.build();

	public static void main(String[] args) {
		
		// 修改对象内的数据就会抛出UnsupportedOperationException异常
		
		// 不能添加新的元素 ,运行将抛出 java.lang.UnsupportedOperationException
		list.add(4);

		// 不能添加新的元素 ,运行将抛出 java.lang.UnsupportedOperationException
		set.add(4);

		// 不能添加新的元素 ,运行将抛出 java.lang.UnsupportedOperationException
		map.put("k3", "v3");

		// 不能添加新的元素 ,运行将抛出 java.lang.UnsupportedOperationException
		map2.put("key4", "value4");
	}
}


 

上述代码是线程安全的,开发时如果我们的对象可以变为不可变对象,我们尽量将对象变为不可变对象,这样可以避免线程安全问题。 

以上是关于并发编程-线程安全策略之不可变对象的主要内容,如果未能解决你的问题,请参考以下文章

并发编程-线程安全策略之线程封闭

并发编程-线程安全策略之常见的线程不安全类

多线程编程-设计模式之不可变对象模式

Java多线程编程之不可变对象模式

并发编程之线程安全性

并发编程安全发布对象