java 线程锁


java 在多线程操作集合的时候,在什么情况下要加锁(读集合,给集合加值,删除集合值),每个操作都是自己写的方法。 不求代码,求详细理论,求详细解释, 各位大侠帮帮小弟

java 线程安全

loli糖昔 11 years, 7 months ago

要解答这个问题,需要从两方面来考虑:
1.什么是线程安全问题?
当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步(比如,加synchronized关键字)及在调用方代码不必做其他的协调,这个类的行为仍是正确的,那么称这个类是 线程安全的
以上是比较白皮书式的回答,有个例子会比较直白些。既然问到了集合,那就来看下HashMap里的一个具体的方法:
/**
* Returns the number of key-value mappings in this map.
*
* @return the number of key-value mappings in this map
*/
public int size() {
return size;
}
从JDK源码的comment可以知道这个方法返回的是一个map中键值映射对的数量,通俗点就是一个map的大小。那么这个size又是从哪里来的呢?继续看源代码得知原来是HashMap类的一个default的成员变量:
/**
* The number of key-value mappings contained in this map.
*/
transient int size;
那么哪个方法或是哪个写操作会来修改这个size的值呢?还得继续看源代码:),原来HashMap中很多关于写操作的方法都会牵扯到这个size变量,那我们就挑其中一个来看:

   
  /**
  
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}

modCount++;
addEntry(hash, key, value, i);
return null;
}

这里似乎没看到size变量,那我猜估计是在调用addEntry(hash, key, value, i);这个方法里:

   
  /**
  
* Adds a new entry with the specified key, value and hash code to
* the specified bucket. It is the responsibility of this
* method to resize the table if appropriate.
*
* Subclass overrides this to alter the behavior of put method.
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}

果然,只要调用一次put(K key, V value)方法就会使得size+1。那么在单线程的情况下不论是size还是put方法都是是线程安全的,然而在多个线程调用该方法的时候,如果你的代码中有对map.size()返回的结果有依赖,比如下面这个代码片段:

   
  ...
  
1 map.put(key,value)
2 if(map.size()>99){
3 //do something if the map size is greater than 99
4 }
...

这时多个线程同时执行这段的代码的话,就会有线程安全的问题了。
假设有A,B两个线程,在执行前,map的size已经是98了。当线程A执行到第二行要调用map.size()方法时(即要得到这个map的大小),本来预期的结果99,但在执行map.size()方法之前,线程B执行了第一行的代码,即再往map里添加了一对不同key值的键值映射对,这时map.size()返回的就是100,导致的结果是线程A走入if的代码块中执行了线程A本来不应该执行的操作。
t1 t3 t4
Thread A: map.put()-------------------> map.size()-----------------> if(map.size()>99)....
t2
Thread B: -------------> map.put().....

上述的例子就是最常见的竞争条件之一“check-then-act”,使用一个潜在的过期值作为决定下一步操作的依据。并且我们把“check-then-act”这样的行为称之为复合操作。
该例子中存在一个竞争条件导致其结果是不可靠的。当计算的正确性依赖于运行时当中的相关时序或者多线程的交替时,会产生竞争条件;换句话说,想得到正确的答案,要依赖与“幸运”的时序(即Thread B的map.put()发生在Thread A的if(map.size()>99)之后)。
所以为了避免竞争条件,必须阻止其他线程访问我们正在修改的变量,让我们可以确保:当其他线程想要查看或修改一个状态时(即该类的成员变量),必须在我们的线程开始之前或者完成之后,而不能在操作过程中。换句话说我们把整个的复合操作看成是一个整体,即接下来要解释的第二点-什么是原子操作?

2.什么是原子操作?
假设有操作A和B,如果从执行A的线程的角度看,当其他线程执行B时,要么B全部执行完成,要么一点都没有执行,这样A和B互为原子操作。一个原子操作是指:该操作对于所有的操作,包括该操作本身,在操作同一个状态时都是原子的。
还是老样子,上代码才能解释这些晦涩难懂的“官方语言” :)

   
  ...
  
1 synchronized(this){
2 map.put(key,value)
3 if(map.size()>99){
4 //do something if the map size is greater than 99
5 }
6 }
...

上述代码是给原来的复合操作(即check-then-act)加上了一个同步块,当线程A执行到第一行时,如果没有其他线程持有该类对象的内部锁或监视器锁时,线程A将持有该锁并执行同步块中的代码,在执行同步块中的代码同时,其他线程是无法进入同步块中执行相关的代码。那么我们说在同步块中的操作就是一个原子操作。

以上的代码片段只是抛砖引玉,synchronized加锁只是解决方案中的其中一种,synchronized中的锁是内部锁,也叫互斥锁或独占锁,是悲观锁的一种。他是假设某段代码的操作一定是有线程冲突的,不安全的,因此会让其他需要锁的线程挂起,等待持有锁的线程释放锁。
有内部锁那么肯定就有外部的锁,也叫显示锁。比如java.util.concurrent.locks.ReentrantLock;有悲观锁那么肯定就有乐观锁,比如AtomicInteger的compareAndSet()(即java的JNI借助cpu的CAS指令来实现的非阻塞算法)。
除此之外还可以使用Object自带的wait和notify或notifyAll的生产者-消费者模式来解决该线程问题。

总之有很多方法来保证并发情况下的线程安全,至于加不加锁,加什么样的锁需要视具体情况而定。

秀巴爾利斯 answered 11 years, 7 months ago

Your Answer