zoukankan      html  css  js  c++  java
  • Java并发(二)-实现同步

    并发带来的问题

    先看一个单例类,后文中都会用到:

    public class SimpleWorkingHardSingleton {
    	private static SimpleWorkingHardSingleton simpleSingleton = new SimpleWorkingHardSingleton();
    	
    	// 数量
    	private int count;
    	
    	private SimpleWorkingHardSingleton() {
    		count = 0;
    	}
    	
    	public static SimpleWorkingHardSingleton getInstance() {
    		return simpleSingleton;
    	}
    
    	public int getCount() {
    		return count;
    	}
    	
    	public void addCount(int increment) {
    		this.count += increment;
    		System.out.println(this.count);
    	}
    
    }
    

    使用原子变量同步

    上文中,我们已经知道这个类的getCount方法对count的操作是线程不安全的,我们可以用一些原子变量来实现原子性:

    public class SimpleWorkingHardSingleton {
    	private static SimpleWorkingHardSingleton simpleSingleton = new SimpleWorkingHardSingleton();
    	
    	// 数量
    	private AtomicLong atomicCount = new AtomicLong(0);
    	
    	private SimpleWorkingHardSingleton() {
    		count = 0;
    	}
    	
    	public static SimpleWorkingHardSingleton getInstance() {
    		return simpleSingleton;
    	}
    
    	public AtomicLong getAtomicCount() {
    		return atomicCount;
    	}
    	
    	public void addAtomicCount(long increment) {
    		this.atomicCount.getAndAdd(increment);
    	}
    
    }
    

    可以看到,在这个类中,我们把count使用AtomicLong原子类。java的jdk包实现了一系列的原子类,这些原子类型的操作都是原子的。那么count的增加就不会分为3步(获取,增加,赋值)了,这个原子的操作是原子类内部实现的,我们在使用过程中只需知道这个操作过程是原子的、不可分割的即可。在使用原子类型的情况下:count变量是会达到预期的效果的。

    原子变量失效情况

    这里所说的原子变量的失效情况是指当类中使用了多个原子变量,如果一个操作要改变多个原子变量,那么还是会出现同步问题:

    public class SimpleWorkingHardSingleton {
    	private static SimpleWorkingHardSingleton simpleSingleton = new SimpleWorkingHardSingleton();
    	
    	// 数量
    	private AtomicLong atomicCount = new AtomicLong(0);
        
        private AtomicLong atomicCountCopy = new AtomicLong(0);
    	
    	private SimpleWorkingHardSingleton() {
    		count = 0;
    	}
    	
    	public static SimpleWorkingHardSingleton getInstance() {
    		return simpleSingleton;
    	}
    
    	public AtomicLong getAtomicCount() {
    		return atomicCount;
    	}
        
        public AtomicLong getAtomicCountCopy() {
    		return atomicCountCopy;
    	}
    	
    	public void addAtomicCount(long increment) {
    		this.atomicCount.getAndAdd(increment);
            this.atomicCountCopy.getAndAdd(increment);
    	}
    }
    

    这种情况下,atomicCount和atomicCountCopy各自的增加是原子的,但是两个变量都增加这个过程是两步,不是原子的。若是a、b两根线程在运行addAtomicCount方法,a线程执行完atomicCount的增加,此时a线程挂起,b线程执行,并且执行了atomicCount和atomicCountCopy的增加,那么此时atomicCountCopy就要比atomicCount小1了,因为a线程还有一半的任务没有执行呢。

    java关键字synchronized实现同步

    java提供了一种内置的锁机制同步代码块(synchronized block),它包括两部分:锁对象和由锁对象保护的代码块。

    1. 若synchronized修饰了一段代码,则负责保护一段代码;
      synchronized (lock) {
          // 操作或访问由lock保护的代码块
      }
      
    2. 若修饰了一个方法,则负责保护这个方法的全部代码,锁是当前对象;若synchronized修饰静态方法,那么同步代码块的锁是Class
    public class SimpleWorkingHardSingleton {
    	private static SimpleWorkingHardSingleton simpleSingleton = new SimpleWorkingHardSingleton();
    	
    	// 数量
    	private int count;
        
        private int countCopy;
    	
    	private SimpleWorkingHardSingleton() {
    		count = 0;
    	}
    	
    	public static SimpleWorkingHardSingleton getInstance() {
    		return simpleSingleton;
    	}
    
    	public int getCount() {
    		return count;
    	}
        
        public int getCountCopy() {
        	return countCopy;
        }
    	
    	public synchronized void addCount(int increment) {
        	/*
        	try {
    			Thread.sleep(3000);
    		} catch (InterruptedException e) {
    			System.err.println(e);
    		}
            */
    		this.count += increment;
            this.countCopy += increment;
    		System.out.println(this.count);
    	}
    
    }
    

    上文代码中synchronized对整个方法进行了修饰,那么保护的代码就是方法中的全部代码;这样在多线程环境中,会有序递增地输出count。但是这样有一个潜在问题就是性能问题;
    synchronized对整个方法进行了修饰,就会导致这个方法每次只有一个线程可以运行,这就会导致性能问题;假如这个方法中有一个耗时3s的io操作,我们用Thread.sleep(3000);来模拟。然而synchronized保护的代码块本不应该包含这3s的操作,因此代码应该写成:

    public void addCount(int increment) {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            System.err.println(e);
        }
        synchronized (this) {
            this.count += increment;
            System.out.println(this.count);
        }
    }
    

    上文中两个变量不同步的情况,就可以用synchronized同步代码块来解决;而且使用synchronized要注意,先保证正确性,即可能产生并发问题的共享变量都要放在同步代码块当中;然后再追求性能,即对尽可能短的代码进行保护,也不能太过细化因为锁的使用和释放都是需要代价的。

    一个稍微复杂的场景(多看例子多模仿系列)

    /**
     * 实现带缓存功能的因子分解
     */
    public class CachedFactorizer {
    	private static CachedFactorizer cachedFactorizer = new CachedFactorizer();
        // 上一个处理的数字
    	private long lastNumber;
        // 上一个数字分解的结果
    	private long[] lastFactors;
        // 处理数字的次数
    	private long hits;
        // 缓存命中的次数
    	private long cacheHits;
    	
    	private CachedFactorizer() {
    		
    	}
    	
    	public synchronized long getHits() {
    		return hits;
    	}
    
    	public synchronized double getCacheHitRatio() {
    		return (double)cacheHits / (double)hits;
    	}
    
    	public static CachedFactorizer getInstance() {
    		return cachedFactorizer;
    	}
    	
    	public long[] factor(int target) {
    		// 伪代码,假装实现了因子分解
    		return new long[] {};
    	}
    	
    	public void doFactor(int target) {
        	Thread.sleep(300);
    		synchronized (this) {
    			hits++;
    			if (target == lastNumber) {
    				cacheHits++;
    			} else {
               		lastNumber = target;
    				lastFactors = factor(target);
                }
    		}
    	}
    }
    
    1. 其实可以在doFactor方法前用synchronized修饰,然而这样不符合性能问题;所以应该用synchronized修饰代码块即可
    2. getHits和getCacheHitRatio方法加上了synchronized修饰,用的锁就是this,所以和doFactor里面的锁是一样的;因而达到的效果是在doFactor内进行因子计算时候,getHits和getCacheHitRatio方法在阻塞状态

    java锁机制的重入

    当一个线程请求另一个线程持有的锁的时候,那么请求的线程会阻塞;重入的概念是:当线程去获取自己所拥有的锁,那么会请求成功;重入的原理是:为每个锁关联一个计数器和持有者线程,当计数器为0时候,这个锁被认为是没有被任何线程持有;当有线程持有锁,计数器自增,并且记下锁的持有线程,当同一线程继续获取锁时候,计数器继续自增;当线程退出代码块时候,相应地计数器减1,直到计数器为0,锁被释放;此时这个锁才可以被其他线程获得。

    public class Parent {
    	public synchronized void do() {
        
        }
    }
    
    public class Child extends Parent {
    	@Override
    	public synchronized void do() {
        	blabla
            super.do();
        }
    }
    

    如果没有重入机制,那么Child对象在执行do方法时候会发生死锁,因为它拿不到自己持有的锁

    参考内容

    1. 书籍《Java并发编程实战》
  • 相关阅读:
    FocusScope学习三: 对FocusScope 的探究与总结
    FocusScope学习二: 很好的理解FocusScope的工作原理
    不同XML之间节点的拷贝
    计算几何DotVector
    计算几何Graham法凸包
    计算几何UVa10652
    线性筛三合一,强大O(n)
    计算几何AngRadVector
    线性筛euler,强大O(n)
    矩阵bzoj1898
  • 原文地址:https://www.cnblogs.com/yanwenxiong/p/9506474.html
Copyright © 2011-2022 走看看