为避免在并发环境下的线程不安全问题,可以将对象确保为不可变对象,或者也可以采用线程封闭技术。
在 Java 语言中,对各种操作共享数据按照安全强度可以分为5类:不可变、绝对线程安全、相对线程安全、线程兼容、线程对立。
不可变的对象一定是线程安全的,无论是对象的方法实现还是方法的调用者,都不需要再采取任何的线程安全保障。
如果共享数据是一个基本数据类型,在定义时用final 修饰就是不可变的
。
如果是对象
,就需要保证对象的行为不会对其状态产生任何影响(如 String 对象,调用它的方法都不会影响它原来的值。)保证不受影响的一种方式就是将对象中带有状态的变量声明为 final
。如:String 、Integer、Long、Double、BigInteger、BigDecimal等。
public class ImmutableExample1 {
private final static Integer a = 1;
private final static String b = "2";
private final static Map<Integer, Integer> map = Maps.newHashMap();
static {
map.put(1, 2);
map.put(3, 4);
map.put(5, 6);
}
public static void main(String[] args) {
// a = 2; 编译不通过
// b = "3"; 编译不通过
// map = Maps.newHashMap(); 编译不通过
map.put(1, 3); //这里是可以更改的
log.info("{}", map.get(1));
}
//用final 修饰 传参时,传参也不可修改
private void test(final int a) {
// a = 1;
}
}
public class ImmutableExample2 {
private static Map<Integer, Integer> map = Maps.newHashMap();
static {
map.put(1, 2);
map.put(3, 4);
map.put(5, 6);
// 这里map 就重新指向了 一个只读 map对象
map = Collections.unmodifiableMap(map);
}
public static void main(String[] args) {
//下面这行代码编译时不会报错,但是运行时会报错
map.put(1, 3);
log.info("{}", map.get(1));
}
}
线程封闭通俗讲就是将对象封装到一个线程里,使得这个对象即时本身不是线程安全的,也不会出现线程安全问题。
栈封闭是我们编程当中遇到的最多的线程封闭。什么是栈封闭呢?简单的说就是局部变量(方法中的本地变量)。多个线程访问一个方法,此方法中的局部变量都会被拷贝一分儿到线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。所以能用局部变量就别用全局的变量,全局变量容易引起并发问题。
注:上面的将局部变量都会被拷贝一部分到线程栈中,涉及到Java 内存模型中的 主内存与工作内存
public class Snippet {
public int loadTheArk(Collection<Animal> candidates) {
SortedSet<Animal> animals;
int numPairs = 0;
Animal candidate = null;
// animals被封闭在方法中,不要使它们逸出!
animals = new TreeSet<Animal>(new SpeciesGenderComparator());
animals.addAll(candidates);
for (Animal a : animals) {
if (candidate == null || !candidate.isPotentialMate(a))
candidate = a;
else {
ark.load(new AnimalPair(candidate, a));
++numPairs;
candidate = null;
}
}
return numPairs;
}
}
使用ThreadLocal是实现线程封闭的最好方法。ThreadLocal内部维护了一个Map,Map的key是每个线程的名称,而Map的值就是我们要封闭的对象。每个线程中的对象都对应着Map中一个值,也就是ThreadLocal利用Map实现了对象的线程封闭。
Java并发编程–理解ThreadLocal
package chapter04;
import java.util.concurrent.TimeUnit;
public class Profiler {
// 第一次get()方法调用时会进行初始化(如果set方法没有调用),每个线程会调用一次
private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>() {
protected Long initialValue() {
return System.currentTimeMillis();
}
public static final void begin() {
//通过 set()方法设置MAP的值
TIME_THREADLOCAL.set(System.currentTimeMillis());
}
public static final long end() {
//get() 方法获取原先设置的值
return System.currentTimeMillis() - TIME_THREADLOCAL.get();
}
public static void main(String[] args) throws Exception {
Profiler.begin();
TimeUnit.SECONDS.sleep(1);
System.out.println("Cost: " + Profiler.end() + " mills");
}
}
Profiler可以被复用在方法调用耗时统计的功能上,在方法的入口前执行begin()方法,在方法调用后执行end()方法,好处是两个方法的调用不用在一个方法或者类中,比如在AOP(面向方面编程)中,可以在方法调用前的切入点执行begin()方法,而在方法调用后的切入点执行end()方法,这样依旧可以获得方法的执行耗时。
如果一个类的对象同时被多个线程访问,如果不做特殊的同步或并发处理,很容易表现出线程不安全的现象,比如抛出异常、逻辑处理错误等,这种类我们就称为线程不安全的类
探秘Java中的String、StringBuilder以及StringBuffer
1、如果要操作少量的数据用String
2、单线程操作字符串缓冲区下操作大量数据StringBuilder
3、多线程操作字符串缓冲区下操作大量数据StringBuffer 。StringBuffer的方法使用了synchronized关键字修饰。
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
以下内容引自慕课网实战·高并发探索(八):线程不安全类、同步容器
像ArrayList,HashSet,HashMap 等Collection类均是线程不安全的,我们以ArrayList举例分析一下源码:
//对象数组:ArrayList的底层数据结构
private transient Object[] elementData;
//elementData中已存放的元素的个数
private int size;
//默认数组容量
private static final int DEFAULT_CAPACITY = 10;
// ArrayList带容量大小的构造函数。
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
// 新建一个数组
this.elementData = new Object[initialCapacity];
}
// ArrayList构造函数。默认容量是10。
public ArrayList() {
this(10);
}
//每次添加时将数组扩容1,然后再赋值
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
// 确定ArrarList的容量。
// 若ArrayList的容量不足以容纳当前的全部元素,则设置 新的容量
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
总结:ArrayList每次对内容进行插入操作的时候,都会做扩容处理,这是ArrayList的优点(无容量的限制),同时也是缺点,线程不安全。(以下例子取材于鱼笑笑博客)
一个 ArrayList ,在添加一个元素的时候,它可能会有两步来完成:
在 Items[Size] 的位置存放此元素;
增大 Size 的值。
在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。 那好,现在我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了。
那么如何将其处理为线程安全的?或者说对应的线程安全类有哪些呢?接下来就涉及到我们同步容器
Java 集合系列06之 Vector详细介绍(源码解析)和使用示例
Vector实现了List接口,Vector实际上就是一个数组,和ArrayList非常的类似,但是内部的方法都是使用synchronized修饰过的方法。
Stack它的方法也是使用synchronized修饰了,继承了Vector,实际上就是栈
//Vector 的add()方法
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
但是vector 也不是绝对线程安全的,在某些情况下还是需要同步手段。详见jvm(13)-线程安全与锁优化
源码分析:
Entry对象唯一表示一个键值对,有四个属性:
-K key 键对象
-V value 值对象
-int hash 键对象的hash值
-Entry entry 指向链表中下一个Entry对象,可为null,表示当前Entry对象在链表尾部
备注:HashMap 和 HashTable 的实现都涉及到数据结构中的散列表。散列表的基本原理与实现
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
分析:
保证安全性:使用了synchronized修饰
不允许空值(在代码中特殊做了判断)
HashMap和HashTable都使用哈希表来存储键值对。在数据结构上是基本相同的,都创建了一个继承自Map.Entry的私有的内部类Entry,每一个Entry对象表示存储在哈希表中的一个键值对。
Collections类中提供了一系列的线程安全方法用于处理ArrayList等线程不安全的Collection类 。内部操作的方法使用了synchronized修饰符
同步容器对应的类一般只是相对线程安全的,在并发环境下仍可能存在问题,此时就需要并发容器JUC
参考: