以前研究过并发编程,但是没有深入,这次有时间了深入研究下。并发编程中只要掌握两个点就好了,一个是线程之间的互斥,一个是线程的通信。
1、互斥: 互斥的概念就是当线程A在执行某个方法时,只有当A完全执行完之后其他的线程才可以访问这个方法,如果A没有执行完,那么其他要访问这个方法的线程将阻塞。
2、通信:如果要很多线程都阻塞了,那么当A执行完之后应该怎么办呢?是让A线程继续执行还是随机选取一个线程执行?这里就是线程之间的通信。
互斥
现在使用的互斥的工具有两个,一个是synchronized,一个是lock(java.util.conrrent.locks.Lock)。
synchronized:可以用在方法上,也可以用在方法体内部。这个的意思是获取某个对象的监视器,进入这个方法(或者被synchronized包含的方法体)必须要获得这个对象的监视器(monitor)。如果是用在方法上的,比如 public void synchronzed aa(){} 那么获得的是这个方法的实例的对象的监视器(也就是aa这个方法所在类的当前实例的监视器);在方法体内比如 public void aa(){ synchronized(someObject) {//do something}},就可以设置要同步的对象(也就是要获取谁的监视器)。当某个对象的监视器没有释放(也就是当前有个线程在执行某个带有synchronized的方法并且没有执行完),其他的线程不能获得监视器也就无法执行这个方法。只有当这个线程执行完成或者是调用了wait()方法之后才会释放这个监视器,然后先前被阻塞的线程获得这个监视器并继续执行。
lock:为什么有了synchronized还要有lock呢?一定是因为lock可以完成synchronized不可以完成的。synchronized有一个很大的缺陷,即某些只读的操作也会被阻塞。举个例子,对于某些缓存的框架,比如hibernate的二级缓存在查询时会先进性判断id=1的在缓存中是否存在,如果不存在的话就会从DB中查找,如果存在则直接从缓存中获得,这样就会提高效率。在这个例子中如果不适用同步的限制,就会出现多次查找的问题,比如下图中,如果有多个线程同时执行这个get(String id)方法,就会可能出现查询多次的情况。
public Class Test12161615{ //用于从DB中查找的dao实例 private Dao dao = xxxx; //缓存 Map<String,Object> cache = new HashMap<String,Object); //根据id查找 public Object get(String id){ Object o = cache.get(id); if(o == null){ //如果没有同步,假设有A线程执行到这里后B线程执行该方法,B到这里后也停止C在执行......o==null的条件都成立,所以就会执行多次查找,所以上锁是必须的。 o = dao.queryById(id); cache.put(o.getId(),o); } return o; } }
如果上的是synchronized的锁,可以解决上面的问题,但是又会出现新的问题。
public Class Test12161615{ //用于从DB中查找的dao实例 private Dao dao = xxxx; //缓存 Map<String,Object> cache = new HashMap<String,Object); //根据id查找 //添加了synchronized之后,某个时间点上只有一个线程可以进入这个方法,所以解决了多次查询的问题。但是当cache中有要查询的id时,即多个线程只是查询的线程时也会被阻塞,造成资源的浪费。 public synchronized Object get(String id){ Object o = cache.get(id); if(o == null){ o = dao.queryById(id); cache.put(o.getId(),o); } return o; } }
所以要用lock来解决这个问题,lock之所以不同于synchronized是因为他区分了读锁和写锁,写锁和写锁,写锁和读锁是互斥的,但是读锁和读锁是不互斥的,这样就解决了并发读时的阻塞问题,并且不会引起多次写的问题。
public Class Test12161615{ //用于从DB中查找的dao实例 private Dao dao = xxxx; //缓存 Map<String,String> cache = new HashMap<String,String); Lock lock = new ReentrantReadWriterLock(); public String getFromCache(String key){ String value = null; try { //读取数据,上读锁 lock.readLock().lock(); value = cache.get(key); if(value == null){ //没有数据,要写,上写锁,先释放读锁,因为读锁不能升级为写锁。 lock.readLock().unlock(); try { lock.writeLock().lock();//上写锁 if( value == null){//再次判断,因为其他线程可能已经执行了这个方法, value = getFromDb(); } lock.readLock().lock();//降级为读锁,写锁可以降为读锁。 } finally { lock.writeLock().unlock();//释放写锁,这个时候其他的读线程可以读取了。 } } return value;//在释放读锁之前返回,保证不会被修改。 } finally { lock.readLock().unlock(); } } }
这样就会解决了,需要注意的是ReentranReadWriteLock的用法。读锁和写锁是可以互换的,读锁不可以升级为写锁,即如果已经上了读锁,必须解锁读锁才可以上写锁,否则会报错。但是写锁可以降低为读锁,所以如果已经上了写锁,再次上读锁是可以的。
至此,可以学好线程之间的互斥了,并且用synchronized 或者 lock来实现。