解决并发问题的方法(有锁、无锁)

1 并发问题解决的方式

  • 无锁
    • 局部变量
    • 不可变对象
    • ThreadLocal
  • 有锁
    • synchronized
    • ReetrantLock

1.1 无锁的解决方式

1.1.1 局部变量

  • 善用局部变量可以避免出现线程安全问题。
  • 当每一个线程都运行同一行代码时,如果只是操作局部变量,则不可能会造成并发问题。因为每个线程操作的变量是局部的,并不存在交际。
  public void test(){
    int i = 0;
    i++;
    System.out.println(i);
  }

1.1.2 不可变对象

  • 这个对象本身是不可以被改变的。自然就不会害怕多个线程去访问它。
    String s = "Hello"
    // “Hello”就是一个不可改变的对象,他就是一个固定的字符串。

1.1.3 ThreadLocal

  • 当多个线程去访问THreadLocal对象是,都会单独的为访问的线程提供一个该对象的副本。即每个对象只会被一个线程操作,自然无并发问题。

1.1.4 CAS原子类

  • CAS = compare and swap 比较并交换
  • CAS中有三个基本操作数
    • 内存地址V
    • 旧的预期值A
    • 要修改的新数值B
  • CAS思想:只有V里面的值和A相等,才会把V里面的值改成B
  • Java中,采用CAS思想的类,都以Atomic开头,基于乐观锁,保证不会出现并发问题。
    // simple using of AtomicInteger
    private AtomicInteger counter = new AtomicInteger(0);
    public void atomicAdd(){
        counter.incrementAndGet();
    }
    // source code of Atomic 
    public class AtomicInteger extends Number implements java.io.Serializable{
        private static final long serialVsersionUID = ...;
        
        // setup to use Unsafe.compareAndSwapInt for updates
        private static final Unsafe unsafe = Unsafe.getUnsafe(); 
        // unsafe 提供硬件级别的原子操作,由于Java无法直接操控底层代码,为此Java需要使用native方法来拓展这部分功能,unsafe就是其中的一个操作入口。
        // unsafe提供了分配、释放内存,挂起、恢复程序,定位对象字段内存地址,修改对象字段值,CAS操作。
        private static final long valueOffset;

        static {
            try{
                valueOffset = unsafe.objectFieldOffset
                    (AtomicInteger.class.getDeclaredField(value));
            } catch(Exception ex){
                throw new Error(ex)
            }
        }
    }
    // source code of getAndAddInt
    // getAndAddInt method is a cas operate in unsafe class
    public final int getAndAddInt(Object var1, long var2, int var4){
        int var5;
        do{
            // use var5 to get the old value
            var5 = this.getIntVolatile(var1, var2);
            
            // compareAndSwapInt = cas
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
    // a counter of interface which do not have concurrency problem.
    public static class AccessCounter{
        // count the times of the acccess to this interface
        AtomicInteger accessCount = new AtomicInteger(0);

        public void access(){
            accessCount.incrementAndGet();
            sout("reslut is: " + accountCount.get());
        }
    }

CAS操作实际模拟

  1. 两个线程同时对一个变量K(初始值0)加一。
  2. 线程A,线程B,获得K的旧值0.
  3. 线程A,把K加1以后,再次访问主存中的K,发现现在K的值(0)与自己记录的旧值(0)相等,所以生效,把自己操作的结果写入主存(k=0 --> k=1)
  4. 线程B,把K加1以后,再次访问主存中的K,发现现在的K值(1)与自己记录的旧值(0)不等,所以失效。再次记录主存中K的值,作为一个“新”的旧值。
  5. 线程B,把K加1以后,再次访问主存中的K,发现现在的K值(1)与自己记录的旧值(1)相等,所以生效,把自己的操作的结果写入主存(k=1 --> K=2)

1.2 有锁的解决方式

1.2.1 synchronized & reentrandLock

  • 采用悲观锁的策略。
  • synchronized是通过语言层面来实现,reentrandLock是通过编程层面来实现。
    public class Counter{
        private int i = 0;
        
        private ReentrantLock lock = new ReentrantLock();

        // lock by reentrantLock
        public void lockByReentrantLock(){
            lock.lock();
            try{
                add();
            } finally{
                lock.unlock();
            }
        }

        // lock by synchronized
        public synchronized void lockBySynchronized(){
            add();
        }

        private void add(){
            i++;
        }
    }
  • 加锁的原理:
  • 线程A和线程B同时更新一个资源
  • A抢到了锁,开始更新。
  • B发现锁被人抢走了,进入等待队列。
  • A更新完成,释放锁,传递消息给等待队列。
  • B因为是等待队列里第一名,所以抢到锁,开始更新。
上一篇:ZOJ1006 Do the Untwist


下一篇:记一个Java多线程相关的面试题