Java基础-线程锁(一)

Android知识总结

一、volatile ,最轻量的同步机制

1)、Java内存模型(JMM)

java内存模型示意图

从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

1、可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
由于线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,那么对于共享变量V,它们首先是在自己的工作内存,之后再同步到主内存。可是并不会及时的刷到主存中,而是会有一定时间差。很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了 。
要解决共享对象可见性这个问题,我们可以使用volatile关键字或者是加锁。
2、原子性
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。


当线程中volatile修饰的字段发生变化时会强制把更新的数据写到内中,当其他线程用到时会强制从内存中更新数据。因此volatile保证了变量的可见性。

2)、volatile特性
可以把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步

  • 线程可见性
  • 可见性也就是说一旦某个线程修改了该被volatile修饰的变量,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,可以立即获取修改之后的值。
  • 在Java中为了加快程序的运行效率,对一些变量的操作通常是在该线程的寄存器或是CPU缓存上进行的,之后才会同步到主存中,而加了volatile修饰符的变量则是直接读写主存。
  • 禁止进行指令重排
  • 指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序。主要因为CPU可以一次执行多条指令,在单线程情况下没有影响;在多线程中会出现重排的影响。
  • 指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率。需要注意的是,这里所说的不改变执行结果,指的是不改变单线程下的程序执行结果。
  • 阻止编译时和运行时的指令重排。编译时JVM编译器遵循内存屏障的约束,运行时依靠CPU屏障指令来阻止重排。
  • 当程序执行到 volatile 变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
  • 在进行指令优化时,不能将在对 volatile 变量访问的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。

3)、 volatile的实现原理
volatile关键字修饰的变量会存在一个“lock:”的前缀。

Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。

同时该指令会将当前处理器缓存行的数据直接写会到系统内存中,且这个写回内存的操作会使在其他CPU里缓存了该地址的数据无效。

volatile 变量自身具有下列特征:

  • 可见性:
    对于一个volatile变量的读,总是能看到(任意线程)对这个volatile变量的写入。
    volatile在CPU的缓存和内存间加了一个总线锁机制,当A缓存中的数据发上变化时,写到内存后。总线就会发出一个通知,让其他CPU清楚缓存,重新从CPU中读取数据,从而保证数据一致性。
  • 原子性:
    对任意单个的volatile变量的读/写具有原子性,但类似于volatile++这种符合操作不具有原子性。
    如:
    1)、一个写线程和多个读线程
    2)、一个线程的读写操作
    3)、多个写线程,非volatile++操作;而是互不影响的写操作。

对于 long 和 double 型变量的特殊规则
Java 要求对于主内存和工作内存之间的八个操作都是原子性的,但是对于 64 位的数据类型,有一条宽松的规定:允许虚拟机将没有被 volatile 修饰的 64 位数据的读写操作划分为两次 32 位的操作来进行,即允许虚拟机实现选择可以不保证 64 位数据类型的 load、store、read 和 write 这 4 个操作的原子性。这就是 long 和 double 的非原子性协定。

4) 、代码

    private volatile static int count = 0;
    private static void subCount() {
         System.out.println(count++);;  //有问题,因为volatile 不具有原子操作,不能进行有关联的写操作。
    }
    public static void main(String[] argc) throws InterruptedException {
        for (int i = 0; i < 1000; i ++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    subCount();
                }
            }).start();
        }
     }

二、原子变量类 AtomicInteger

  • AtomicInteger用于对整形数据进行原子操作,保证整形数据的加减操作线程安全。但是,它不能替代Integer类。
  • AtomicInteger基于CAS原理实现非阻塞的原子操作。

CAS (Compare and Swap) 操作(又名乐观锁、自旋锁、轻量级锁、无锁机制)
CAS操作三大问题:

  • 1)、ABA问题
      因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
      ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A
      AtomicStampedReferenceAtomicMarkableReference这两个类可以解决ABA问题。
  • 2)、循环时间长开销大
      自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
  • 3)、只能保证一个共享变量的原子操作
      当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
      还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。
      从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

CAS操作图

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
CAS在内核中C++代码中调用汇编实现了原子性

1)、代码

    private static int count = 1000;
    private static AtomicInteger at = new AtomicInteger(count);

    private static void subCount() {
        at.decrementAndGet();
    }
    public static void main(String[] argc) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    subCount();
                }
            }).start();
        }
        Thread.sleep(500);
        System.out.println(at.get());
    }

引用类型的原子操作类
解决只能保证一个共享变量的原子操作。

public class UseAtomicReference {
    static AtomicReference atomicUserRef;

    public static void main(String[] args) {
        UserInfo user = new UserInfo("Mark", 15);//要修改的实体的实例
        atomicUserRef = new AtomicReference(user);
        UserInfo updateUser = new UserInfo("Bill", 17);
        atomicUserRef.compareAndSet(user, updateUser);

        System.out.println(atomicUserRef.get());
        System.out.println(user);
    }

    //定义一个实体类
    static class UserInfo {
        private volatile String name;
        private int age;

        public UserInfo(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public int getAge() {
            return age;
        }

        @Override
        public String toString() {
            return "UserInfo{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}

带版本戳的原子操作类
解决ABA问题。

public class UseAtomicStampedReference {
    static AtomicStampedReference asr
            = new AtomicStampedReference("mark",0);

    public static void main(String[] args) throws InterruptedException {
        //拿到当前的版本号(旧)
        final int oldStamp = asr.getStamp();
        final String oldReference = asr.getReference();
        System.out.println(oldReference+"============"+oldStamp);

        Thread rightStampThread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+":当前变量值:"
                        +oldReference + "-当前版本戳:" + oldStamp + "-"
                  + asr.compareAndSet(oldReference,
                        oldReference + "+Java", oldStamp,
                        oldStamp + 1));
            }
        });

        Thread errorStampThread = new Thread(new Runnable() {
            @Override
            public void run() {
                String reference = asr.getReference();
                System.out.println(Thread.currentThread().getName()
                        +":当前变量值:"
                        +reference + "-当前版本戳:" + asr.getStamp() + "-"
                        + asr.compareAndSet(reference,
                        reference + "+C", oldStamp,
                        oldStamp + 1));
            }
        });
        rightStampThread.start();
        rightStampThread.join();
        errorStampThread.start();
        errorStampThread.join();

        System.out.println(asr.getReference()+"============"+asr.getStamp());
    }
}

2)、方法接口

  • int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果

  • boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。

  • int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。

  • int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值

  • int getAndAdd(int delta) : 获取当前的值,并加上预期的值。

  • int getAndDecrement() : 获取当前的值,并自减, 返回自增前的值。

三、显示锁( ReentrantLock), 实现Lock接口

ReentrantLock 里面实现了AbstractQueuedSynchronizer (AQS)的内部类。

    private static int count = 1000;

    private static void subCount() {
        //调用lock的实现类ReentrantLock
        Lock lock = new ReentrantLock();
        lock.lock();
        try {
          System.out.println(count--);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); //解锁必须在 finally 中实现
        }
    }

    public static void main(String[] argc) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    subCount();
                }
            }).start();
        }
    }
  • 1)、非公平锁\公平锁

ReentrantLock 默认是非公平锁

 public ReentrantLock() {
        sync = new NonfairSync();
  }

ReentrantLock 当设置为true是公平锁,设置flase是非公平锁。

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
  • 2)、锁的可重入

重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞,该特性的实现需要解决以下两个问题。

  • 1)线程再次获取锁。锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
  • 2)锁的最终释放。线程重复n次获取了锁,随后在第n次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,而锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。
      nonfairTryAcquire方法增加了再次获取同步状态的处理逻辑:通过判断当前线程是否为获取锁的线程来决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回true,表示获取同步状态成功。同步状态表示锁被一个线程重复获取的次数。
      如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。

示例

    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = -5179523762034025860L;

        abstract void lock();

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                //没人占用锁,锁是自由状态,进行加锁 state -> 1
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }

            } else if (current == getExclusiveOwnerThread()) { 
                //表示加锁失败,当等于当前线程锁重入,直接把状态+1表示重入次数+1
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

        protected final boolean isHeldExclusively() {
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

        final ConditionObject newCondition() {
            return new ConditionObject();
        }

        // Methods relayed from outer class

        final Thread getOwner() {
            return getState() == 0 ? null : getExclusiveOwnerThread();
        }

        final int getHoldCount() {
            return isHeldExclusively() ? getState() : 0;
        }

        final boolean isLocked() {
            return getState() != 0;
        }
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            s.defaultReadObject();
            setState(0); // reset to unlocked state
        }
    }

四、Semaphore

五、LongAdder

分段CAS锁优化机制,最后的结果所有分段的值相加。

六、实例

  • 从而我们可以看出volatile虽然具有可见性但是并不能保证原子性。
  • 性能方面,synchronized关键字是防止多个线程同时执行一段代码,就会影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized。
  • 但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。
   private static volatile TrackCache instance = null;

    private TrackCache() {
    }

    public static TrackCache getInstance() {
        if (instance != null) {
            return instance;
        } else {
            synchronized (TrackCache.class) {
                if (instance == null) {
                    instance = new TrackCache();
                }
            }
        }
        return instance;
    }

参考

synchronized中重量级锁、偏向锁和轻量级锁的区别
Java技术之AQS详解

你可能感兴趣的:(Java基础-线程锁(一))