JAVA锁机制

103 阅读5分钟

锁的分类

  • 公平锁与非公平锁
  • 独占锁与共享锁
  • 悲观锁与乐观锁
  • 自旋锁与可重入锁
  • 偏向锁与轻量级锁与重量级锁

悲观锁与乐观锁

悲观锁:当线程访问一个临界资源时,不管它是否还会不会被其它线程访问,会首先进行上锁,当任务执行完再释放锁。没有获取到锁的线程会进入到阻塞等待状态,阻塞后获取到临界资源则进入就绪状态,在cpu调度后则进入运行状态。线程阻塞再到唤醒运行成本较高。

乐观锁:当线程访问一个临界资源时,先假设其是空闲的,当遇到有资源竞争时,通过自旋轮询目标状态,并在空闲时进行访问。乐观锁属于无锁机制,没有竞争锁的流程,涉及线程也不会阻塞等待。

公平锁与非公平锁

公平锁:按线程请求临界资源的先来后到顺序分配锁,采用队列实现。

  • 优点:所有线程能够按请求顺序得到资源,不会饿死在队列中。
  • 缺点:个别任务执行较长时,会导致较多的任务阻塞,由此带来更多的任务阻塞唤醒的cpu开销。

非公平锁:不是按照请求顺序来分配锁,而是通过资源竞争的方式来获取锁。

  • 优点:增加任务吞叶量,减少对线程的阻塞唤醒开销。非公平锁通常比公平锁效率高。
  • 缺点:可能导致某些线程始终得不到执行,导致饿死。

独占锁与共享锁

独占锁:只允许一个线程访问临界资源,其它的线程都会阻塞等待。

共享锁:多个线程同时持有锁。如ReentrantReadWirteLock,读读共享、读写互斥、写入互斥、写读互斥。

自旋锁与可重入锁

自旋锁:没有获取到锁的线程不阻塞,而是通过CAS(Compare and swap)循环控制不断尝试的获取锁。自旋锁为不可重入锁,同一线程需要获取锁也是需要自旋等待。

  • CAS参数(a,expectValue,newValue): a是目标内存地址,expectValue为要修改的原值,newValue为目标值。表示把目标内存地址的值修改为newValue值,而expectValue主要用于验证,a地址的原始值是否是expectValue.
  • ABA问题:ABA问题,是指一个线程进行两次锁操作过程中,有另外一个或多个线程将值改变之后又改回去了。中间锁的过程发生了变化,在并发中可能出现问题。虽然目标值是一样,但是中间间隙已经发生了很多操作,导致结果状态异常。
  • 解决ABA问题:原子引用AtomicReference会存在ABA问题,可用带版本的原子引用AtomicStampedReference解决ABA问题。可根据业务需求选择。原理就是增加一个版本号进行控制,在值与版本号一致的情况下才认为是一样的。
// 新建一个原子引用,用于多个线程在抢占资源时,做锁操作。
AtomicReference<Thread> threadAtomicReference = new AtomicReference<>();
// 获取锁
threadAtomicReference.compareAndSet(null,thread1);
// 释放锁
threadAtomicReference.compareAndSet(thread1,null);

threadAtomicReference.compareAndSet(null,thread2);
threadAtomicReference.compareAndSet(thread2,null);

可重入锁:同一个线程中,有多个同步方法都需要同一把锁,不需要重复获取,可直接进行。可重入锁机制主要用于避免死锁,即线程自己等待自己的情况。

偏向锁与轻量级锁与重量级锁

Synchronized锁(内置锁)升级过程中的锁的不同状态。锁升级是不可逆的过程。

锁是针对对象而言,锁住的是一个对象,表现在对象头中。对象头中有_mark(markword)信息区,markword(64位or32位),其中锁标志位占两位,其余为其它信息。

image.png

  • 线程访问一个同步资源,会首先查看对象头信息,检查锁标志位。
    • 锁标志位(01)-->是否偏向锁--->0为无锁,1为偏向锁
    • 当有一个线程访问对象时,升级为偏向锁并记录线程ID(主要用于当线程重入时,不用再获取锁).
  • 有其它线程来竞争访问该对象或同步块时,锁升级为轻量级锁
    • 锁标志位(00)-->竞争线程不会阻塞,进入自旋尝试获取锁,访问资源。
    • 线程栈中分配锁记录,拷贝对象头中的markword到原持有偏向锁的线程栈中,将对象中轻量级锁指向原持有偏向锁的线程的锁记录
    • 竞争线程也拷贝对象头中的markword到线程栈中的锁记录,并尝试自旋获取锁,将对象中的轻量级锁指向线程栈中的锁记录。
  • 锁膨胀,自旋一定次数(如10次)获取锁失败则升级为重量级锁
    • 锁标志位(11)--> 重量级锁(monitor),在jdk1.6之前,可以直接认为对应到操作系统中的互斥量(mutex).这种同步成本较高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等,因些后来被称为重量级锁。
  • 锁释放,各线程执行完了,同步块又变成无锁状态。(这大概是从锁记录中判断,并释放对象锁)

参考: www.cnblogs.com/mingyueyy/p… www.jianshu.com/p/36eedeb3f…

有哪些锁

  • LockSupport
  • Semaphore
  • Lock
    • ReadLock
    • WriteLock
    • ReadLockView
    • WriteLockView
    • ReentrantLock
  • ReadWriteLock
    • ReadWriteLockView
    • ReentrantReadWriteLock
  • Synchronized

其它

AtomicStampedReference的简单用例

public class AtomicMarkableReferenceTest {
    private static final Integer UPDATE_NUM = 100;
    private static AtomicStampedReference<Integer> atomicStampedReference=
            new  AtomicStampedReference<>(127,1);

    public static void main(String[] args) {
        new Thread(()->{
            Integer value = atomicStampedReference.getReference();
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + " : 当前值为:" + value + " 版本号为:" +
                    stamp);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace(); }
            // 注意:如果引用类型是 Long、Integer、Short、Byte、Character 一定一定要注意值的缓存区间!
            // 比如 Long、Integer、Short、Byte 缓存区间是在-128~127,会直接存在常量池中,而不在这个区间内对象的值
            // 则会每次都 new 一个对象,那么即使两个对象的值相同,CAS 方法都会返回 false
            // 所以注意,这里expectedReference最好使用引用value,避免值不在缓存区间中,而导致新建的对象与原对象不匹配。(地址不一样)
            if(atomicStampedReference.compareAndSet(127,UPDATE_NUM,1,stamp+1)){
                System.out.println(Thread.currentThread().getName()+"当前值:"
                        +atomicStampedReference.getReference()+"版本号:"+atomicStampedReference.getStamp());
            }else {
                System.out.println("版本号不同更新失败");
            }
        },"线程A").start();
    }
}

原子类实现CAS无锁同步机制

public class AtomicTryLock {
    private AtomicLong atomicLong= new AtomicLong(0);
    private Thread lockCurrentThread;

    public boolean lock(){
        // 自旋,尝试获取锁
        for(;;){
            boolean b = atomicLong.compareAndSet(0, 1);
            if(b){
                lockCurrentThread=Thread.currentThread();
                return true;
            }
            System.out.println(Thread.currentThread().getName());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public boolean unlock(){
        if(lockCurrentThread!=Thread.currentThread()){
            return false;
        }
        return atomicLong.compareAndSet(1,0);
    }

    public static void main(String[] args) {
        AtomicTryLock atomicTryLock = new AtomicTryLock();
        IntStream.range(1, 5).forEach(i->new Thread(()->{
            try {
                boolean lock = atomicTryLock.lock();
                if(lock){
                    System.out.println(Thread.currentThread().getName()+"获取锁成功");
                }else{
                    System.out.println(Thread.currentThread().getName()+"获取锁失败");
                }
            }catch (Exception e){
            }finally {
                atomicTryLock.unlock();
            }
        }).start());
    }
}