Java锁概论

2024-08-05 11:33:06 浏览数 (3)

Java中的锁主要用于保障多并发线程情况下数据的一致性。在多线程编程中为保障数据一致性,我们常需要在使用对象或方法之前加锁。这时若有其他线程也需要使用此对象或該方法,则产生要获得锁。如果某个线程发现锁正被其他线程使用,就会进入阻塞队列等待锁的释放,直到其它线程执行完毕并释放锁,該线程才有机会再次获取锁进行操作。这就保障了在同一时刻只有一个线程持有该对象的锁并修改对象,从而保障数据安全。

锁可分为乐观锁和悲观锁。

从获取资源的公平性角度可分为公平锁和非公平锁。

从是否共享资源角度可分为共享锁和独占锁。

从锁的状态角度可分为偏向锁、轻量级锁和重量级锁。

同时 ,在JVM中还巧妙设计了自旋锁,以更快地使用CPU资源。

乐观锁

乐观锁采用乐观的思想处理数据。

在每次读取数据时都认为别人不会修改该数据,所以不会上锁;但在更新时会判断在此期间别人有没有更新该数据,通常采用在写时先读出当前版本号然后加锁的方法。

Java中的乐观锁大部分是通过CAS(Compare And Swap)操作実現的。CAS是一种原子更新操作,在对数据操作之前首先会比较当前值跟传入的值是否一样,若同则更,否则不更。直接返回失败状态。

悲观锁

悲观锁采用悲观思想处理数据。

在每次读取数据时都认为别人会修改数据,∴每次在读写数据时都会上锁。这样,别人想读写这个数据时就会阻塞、等待直到拿到锁。

Java中悲观锁大部分基于AQS(Abstract Queued Synchronized)架构实现。AQS定义了一套多线程訪問共享资源的同步框架,許多同步类的実現都依赖于它,例如常用的Synchronized、ReentrantLock、Semaphore、CountDownLatch等。該框架下的锁会先尝試以CAS乐观锁去获取鎖;若获取不到,则会转为悲观锁(如ReentrantLock)。

自旋锁

自旋锁认为:如果持有锁的线程能在很短的时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态与用户态之间的切换进入阻塞、挂起状态,只需等一下(亦作自旋),在等待持有锁的线程释放锁后即可立即获取锁。这样就避免了用户线程在内核状态的切换上导致的锁的时间消耗。

线程在自旋时会占用CPU,在线程长时间自旋获取不到锁时,将会产生CPU浪费,甚至有时线程永远无法获取锁而导致CPU资源被永久占用,所以需要设定一个自旋等待的最大时间。在线程执行的时间超过自旋等待的最大时间后,线程会退出自旋模式并释放其持有的锁。

自旋锁优缺点:

优点:自旋锁可以减少CPU上下文的切换,对于占用锁的时间非常短或锁竞争不激烈的代码块来说性能大幅度提升,因为自旋的CPU耗时明显少于线程阻塞、挂起、再唤醒时两次CPU上下文切换所用的时间。

缺点:在持有锁的线程占用锁时间过长或锁的竞争过于激烈时,线程在自旋过程中会长时间获取不到锁资源,将引起CPU的浪费。所以系统中有复杂锁依赖的情况下不适合采用自旋锁。

自旋锁的时间阈值

自旋锁用于让当前线程占着CPU资源不释放,等到下次自旋获取资源后立即执行相关操作。但是如何选择自旋的执行时间呢?

如果自旋的执行时间太长,则会有大量的线程处于自旋状态且占用CPU资源,造成系统资源浪费。因此,对自旋的周期选择将直接影响到系统的性能!

JDK1.5为固定DE时间;JDK1.6引入了适应性自旋锁。适应性自旋锁的自旋时间不再是固定值,而是由上一次在同一锁上的自旋时间及锁的拥有者的状态来决定的,可基本认为一个线程上下文切换的时间就是一个最佳时间。

synchronized

synchronized关键字用于为Java对象、方法、代码块提供线程安全的操作。synchronized属于独占式的悲观锁、同时属于可重入锁。在使用synchronized修饰对象时,同一时刻只能有一个线程对該对象进行訪問;在synchronized修饰方法、代码块时,同一时刻只能有一个线程执行该方法体或代码块,其他线程只有等待当前线程执行完毕并释放锁资源之后才能訪問該對象或执行同步代码块。

Java中的每个对象都有个monitor对象,加锁就是在竞争monitor对象。对代码块加锁是通过在前后分别加上monitorenter 和 monitorexit指令実現的;对方法是否加锁是通过一个标记位来判断的。

synchronized的作用范围

  • synchronized作用于成员变量和非静态方法时,锁住的是对象的实例,即this对象。
  • synchronized作用于静态方法时,锁住的是Class実例,因为静态方法属于Class而不属性对象。
  • synchronized作用于一个代码块时,锁住的是所有代码块中配置的对象。

synchronized的用法简介

synchronized作用于成员变量和非静态方法时,锁住的是对象的实例:

代码语言:javascript复制
public class SynchronizedDemoTest {
    public static void main(String[] args) {
        final SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
        new Thread(()->{//run
            synchronizedDemo.generalMethod1();
        }).start();
        new Thread(()->{
            synchronizedDemo.generalMethod2();
        }).start();
    }
}

​​

代码语言:javascript复制
public class SynchronizedDemo {
    /**
     * synchronized修饰普通的同步方法,锁住的是当前実例对象
     * 方法1
     */
    public synchronized void generalMethod1(){
        try {
            for (int i = 1; i<3;i  ){
                System.out.println("generalMethod1 execute "   i   " time");
                Thread.sleep(3000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /*** 方法2 */
    public synchronized void generalMethod2(){
        try {
            for (int i = 1; i<3;i  ){
                System.out.println("generalMethod2 execute "   i   " time");
                Thread.sleep(3000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

以上程序定义了两个使用 synchronized 修饰的普通方法;然后在main函数中定义对象的実例并发执行各个方法。我们看到:线程2会等待线程1执行完毕才能执行,这是因为synchronized锁住了当前的对象实例 synchronizedDemo导致的。

稍将程序作一修改,定义两个実例分别调用两个方法,程序就能并发执行起来:

代码语言:javascript复制
public class SynchronizedDemoTest {
    public static void main(String[] args) {
        final SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
        final SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();
        new Thread(()->{//run
            synchronizedDemo.generalMethod1();
        }).start();
        new Thread(()->{
            synchronizedDemo2.generalMethod2();
        }).start();
    }
}

​ synchronized 作用于静态同步方法,锁住的是当前类的Class对象;具体使用代码如下,只需在以上方法中加上static即可:

代码语言:javascript复制
public class SynchronizedDemoTest {
    public static void main(String[] args) {
        final SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
        final SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();
        new Thread(()->{//run
            synchronizedDemo.generalMethod1();
        }).start();
        new Thread(()->{
            synchronizedDemo2.generalMethod2();
        }).start();
    }
}

/** */
public class SynchronizedDemo {
    /**
     * synchronized修饰静态同步方法,锁住的是当前类的Class对象
     */
    public static synchronized void generalMethod1(){
        try {
            for (int i = 1; i<3;i  ){
                System.out.println("generalMethod1 execute "   i   " time");
                Thread.sleep(3000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    public static synchronized void generalMethod2(){
        try {
            for (int i = 1; i<3;i  ){
                System.out.println("generalMethod2 execute "   i   " time");
                Thread.sleep(3000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

​ 以上代码首先定义两static的synchronized方法,然后定义了两个实例分别执行這両个方法。

通过日志能够清晰地看到,因为static方法是属于Class的,并且Class的相关数据在JVM中是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁住所有调用該方法的线程。

synchronized作用于一个代码块时,锁住的是在代码块中配置的对象。如下:

代码语言:javascript复制
public class SynchronizedBlockedDemoTest {
    public static void main(String[] args) {
        final SynchronizedBlockedDemo synchronizeddDemo = new SynchronizedBlockedDemo();
        new Thread(()->{//run 调用方法1
            synchronizeddDemo.blockMethod1();
        }).start();
        new Thread(()->{//调用方法2
            synchronizeddDemo.blockMethod2();
        }).start();
    }
}


/**
 * 代码块锁
 */
public class SynchronizedBlockedDemo {
    String lockA = "lockA";
    /**
     * synchronized作用于方法块,,锁住的是在{}中配置的对象
     * 方法1
     */
    public void blockMethod1(){
        try {
            synchronized(lockA){
                for (int i = 1; i<3;i  ){
                    System.out.println("blockMethod1 execute "   i   " time");
                    Thread.sleep(3000);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /*** 方法2 */
    public synchronized void blockMethod2(){
        try {
            synchronized(lockA){
                for (int i = 1; i<3;i  ){
                    System.out.println("blockMethod2 execute "   i   " time");
                    Thread.sleep(3000);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

​ 以上代码执行结果很簡单。由于两个方法都需要获取名为lockA的锁,因此线程2会等待线程1执行完成后才能获取到該锁并执行。

我们在写多线程程序时可能会出现A线程依赖B线程中的资源,而B线程又依赖于A线程中资源的情况,这时就可能死锁。在开发中要杜绝资源相互调用的情况。

synchronized的実現原理

在synchronized 内部包括ContentionList、EntryList、WaitSet、OnDeck、Owner、!Owner这六个区域,每个区域的数据都代表锁的不同状态:

  • ContentionList:锁竞争队列,所有请求锁的线程都被放在竞争队列中。
  • EntryList:竞争候选列表,在ContentionList中有资格成为候选者来竞争锁资源的线程被移动到了EntryList中。
  • WaitSet:等待集合,调用wait方法后被阻塞的线程将被放在WaitSet中。
  • OnDeck:竞争侯选者。在同一时刻最多只有一个线程在竞争锁资源,该线程的状态被称为OnDeck。
  • Owner:竞争到锁资源的线程被称为Owner状态线程。
  • !Owner:在Owner线程释放锁后,会从Owner状态变成!Owner状态。

①synchronized 在收到新的锁请求时首先自旋,如果通过自旋也没有获取锁资源,则将被放入锁竞争队列ContentionList中。

为防锁竞争时ContentionList尾部的元素被大量的并发线程进行CAS访问而影响性能,

②Owner线程会在释放锁资源时将ContentionList中的部分线程移动到EntryList中,

③并指定EntryList 中的某个线程(常为最先进入的线程)为OnDeck线程。Owner线程并没有直接把锁传递给OnDeck线程,而是把锁竞争的权利交给OnDeck,让OnDeck线程重新竞争锁。在Java中把该行为称为“竞争切换”。該行为牺牲了公平性,但提高了性能。

④获取到锁资源的OnDeck线程会变为Owner线程,而未获得到锁资源的线程仍然停留在EntryList中。

⑤Owner线程在被wait方法阻塞后,会被转移到WaitSet队列中,

⑥直到某个时刻被notify或notifyAll方法唤醒,会再次进入EntryList中。ContentionList、EntryList、WaitSet中的线程均为阻塞状态,该阻塞是由操作系统来完成的。(在Linux内核中是采用pthread_mutex_lock内核函数実現的)

⑦Owner线程在执行完毕后会释放锁的资源并变为!Owner状态。

在synchronized中,在线程进入ContentionList之前,等待的线程会先尝試以自旋的方式获取锁,如果获取不到进入ContentionList,該做法对于已进入队列的线程是不公平的,因此synchronized是非公平锁。另外,自旋获取锁的线程也可以直接抢占OnDeck线程的锁资源。

synchronized是一个重量级操作,需要调用操作系统的相关接口,性能较低,给线程加锁的时间甚至有可能超过获取锁后具体逻辑代码的操作时间。

JDK1.6后对synchronized做了很多优化,引入了适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等以提高锁的效率。锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫锁膨胀。在JDK1.6中默认开启了偏向锁和轻量级锁,可通过 -XX:UseBiasedLocking 禁用偏向锁。

ReentrantLock

ReentrantLock继承了Lock接口并実現了在接口中定义的方法,是一个可重入的独占锁。ReentrantLock通过自定义队列同步器(Abstract Queued Synchronized , AQS)来実現鎖的获取与释放。

独占锁指該鎖在同一时刻只能被一个线程获取,而获取锁的其他线程只能在同步队列中等待;可重入锁指该锁能够支持一个线程对同一个资源执行多次加锁操作。

ReentrantLock支持公平锁和非公平锁的実現。公平批线程竞争锁的机制是公平的,而非公平指不同的线程获取锁的机制是不公平的。

ReentrantLock不但提供了synchronized对锁的操作功能,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。

ReentrantLock用法

ReentrantLock有显式的操作过程 ,何时加锁、何时释放锁都在程序的控制之下。具体使用流程是定义一个ReentrantLock,在需要加锁的地方通过lock方法加锁,等资源使用完成之后再通过unlock方法释放锁。实现如下:

代码语言:javascript复制
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo implements Runnable {
    /**
     * ①  定义一个ReentrantLock
     */
    public static ReentrantLock lock = new ReentrantLock();
    public static Integer i = 0;
    @Override
    public void run() {
        for(int j = 0;j<11;j  ){
            //② 加锁
            lock.lock();    //可重入锁
            try {
                i  ;
            } finally {
                lock.unlock();//③ 释放锁
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo reentrantLock = new ReentrantLockDemo();
        Thread t1 = new Thread(reentrantLock);
        t1.start();
        t1.join();
        System.out.println(i);
    }
}

ReentrantLock之所以被称为可重入锁,是因为ReentrantLock可以反复进入。即允许连续両次获得同一把锁,两次释放同一把锁。

​ 注意,获取锁和释放锁的次数要相同,如果释放锁的次数多于获取锁的次数,JVM就会抛出java.lang.IllegalMonitorStateException异常;若释放锁的次数少于获取锁的次数,该线程就会一直持有該锁,其他线程将无法获取锁资源。

ReentrantLock如何避免死锁: 响应中断、可轮询锁、定时锁

(1)响应中断

在synchronized中如果有一个线程尝试获取一把锁,则其结果就是要么获取锁继续执行,要么保持等待。ReentrantLock还提供了可响应中断的可能,即在等待锁的过程中,线程可以根据需要取消对锁的请求。

具体実現代码:

代码语言:javascript复制
import java.util.concurrent.locks.ReentrantLock;

public class InterruptiblyLock {
    public ReentrantLock lock1 = new ReentrantLock();//①第一把锁lock1
    public ReentrantLock lock2 = new ReentrantLock();//②第二把锁lock2
    public Thread lock1(){
        Thread t = new Thread(()->{
            try {
                lock1.lockInterruptibly();//③ .1若当前线程未被中断,则获取锁
                try {
                    Thread.sleep(600); //④ .1此处执行具体丵务逻辑
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock2.lockInterruptibly();
                System.out.println(Thread.currentThread().getName()   "  执行完毕。");//
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //⑤ .1在业务逻辑执行结束后,检查当前线程是否持有该锁,若有则释放
                if(lock1.isHeldByCurrentThread()){
                    lock1.unlock();
                }
                if(lock2.isHeldByCurrentThread()){
                    lock2.unlock();
                }
                System.out.println(Thread.currentThread().getName()   "  退出");//退出
            }
        });
        t.start();
        return t;
    }
    public Thread lock2(){
        Thread t = new Thread(()->{
            try {
                lock2.lockInterruptibly();//③ .2若当前线程未被中断,则获取锁
                try {
                    Thread.sleep(600); //④ .2此处执行具体丵务逻辑
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                lock2.lockInterruptibly();
                System.out.println(Thread.currentThread().getName()   "  执行完毕。");//
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //⑤ .2在业务逻辑执行结束后,检查当前线程是否持有该锁,若有则释放
                if(lock1.isHeldByCurrentThread()){
                    lock1.unlock();
                }
                if(lock2.isHeldByCurrentThread()){
                    lock2.unlock();
                }
                System.out.println(Thread.currentThread().getName()   "  退出");//退出
            }
        });
        t.start();
        return t;
    }
}

以上代码中,在线程thread1与thread2启动后,thread1率先占用了lock1,再占用lock2;而thread2则先占用lock2,后占用lock1,这便形成了thread1和thread2之间的相互等待,在两个线程都启动时便处于死锁状态。在whild循环中,若等待时间过长,则这里可设定为3s;若可能发生了死锁等问题,thread2就会主动中断(interrupt),释放对lock1的申请,同时释放已获得的lock2,让thread1顺利获得lock2,继续执行下去。

(2)可轮询锁

通过boolean tryLock()获取锁。若有可用锁,则获取該锁,并返回true。若無,则立即返回false。

(3)定时锁

通过boolean tryLock(long time,TimeUnit timeUnit) throws InterruptedException获取定时锁。如果在给定的时间内获取到了可用锁,且当前线程未被中断,则获取该锁并返回true。否则,将禁用当前线程,并且在发生以下3种情况之前,該线程一直处于休眠状态:

当前线程获取到了可用锁并返回true;

当前线程在进入此方法时设置了该线程的中断状态,或当前线程在获取锁时被中断,则将抛出InterruptedException,并清除当前线程的已中断状态。

当前线程获取锁的时间超过了指定的等待时间,则将返回fals。若设定的时间leqslant 0,则该方法将完全不等待。

Lock接口的主要方法

Lock接口的主要方法如下:

void lock()

对对象加锁,如果锁未被其他线程使用,则当前线程将获取该锁;如果锁正在被其他线程持有,则将禁用当前线程,直到当前线程获取锁。

boolean tryLock()

试图给对象加锁,如果锁未被其他线程使用,则将获取该锁并返回true,否则返回false。tryLock()与lock()的区别在于tryLock()只是“试图”获取锁,如果没有可用锁,就会立即返回。lock()在锁不可用时会一直等待,直到获取到可用锁。

tryLock(long time,TimeUnit timeUnit)

创建定时锁,如果在给定的等待时间内有可用锁,则获取该锁。

void unlock()

释放当前线程所持有的锁。锁只能由持有者释放,如果当前线程并不持有该锁却执行该方法,则抛出异常。

Condition newCondition()

创建条件对象,获取等待通知组件。该组件和当前锁绑定,当前线程只有获取了锁才能调用该组件的 await(),在调用后当前线程将释放锁。

getHoldCount()

查询当前线程保持此锁的次数,也即此线程执行lock方法的次数。

getQueueLength()

返回等待获取此锁的线程估计数,譬如启动5个线程,1个线程获得锁,则此时返回4.

getWaitQueueLength(Condition condition)

返回在condition条件下等待该锁的线程数量。比如有5个线程用同一个condition对象,并且这5个线程都执行了condition对象的await方法,那么执行此方法将返回5。

hasWaiters(Condition condition)

查询是否有线程正在等待与给定条件有关的锁,即对于指定的condition对象,有多少线程执行了condition.await方法。

hasQueuedThread(Thread thread)

查询给定的线程是否等待获取该锁。

hasQueuedThreads()

查询是否有线程等待该锁。

isFair()

判断该锁是否为公平锁。

isHeldByCurrentThread()

查询当前线程是否持有该锁,线程执行lock方法的前后状态分别是false和true。

isLock()

判断此锁是否被线程占用

lockInterruptibly()

如果当前线程未被中断,则获取该锁。

公平锁与非公平锁

ReentrantLock支持公平锁和非公平锁两种方式。公平锁指锁的分配和竞争机制是公平的,即遵循先到先得原则。非公平锁指Jvm遵循随机、就近原则分配锁的机制。

ReentrantLock通过在构造器ReentrantLock(boolean fair) 中传递不同的参数来定义不同类型的锁,默认的実現是非公平锁。这是因为,非公平锁虽然放弃了锁的公平性,但执行效率明显高于公平锁。若系统没有特殊的要求,一般情况下建议使用非公平锁。

tryLock、lock和lockInterruptibly 区别

tryLock若有可用锁,则获取该锁并返回true,否则返回false,不会有延迟或等待;tryLock(long time,TimeUnit timeUnit) 可以增加时间限制,如果超过了指定的时间还没获得锁,则返回false。

lock若有可用锁,则获取该锁并返回true,否则会一直等待直到获取可用锁。

在锁中断时lockInterruptibly会抛出异常,而lock不会。

synchronized与ReentrantLock之比较

共同点:

  • 都用于控制多线程对共享对象的访问;
  • 都是可重入锁;
  • 都保证了可见性和互斥性;

不同点:

  • ReentrantLock显式获取和释放锁;synchronized隐式获取和释放锁。☆为了避免程序出现异常而无法正常释放锁,在使用ReentrantLock时务必在finally块中进行解锁操作。
  • ReentrantLock可响应中断、可轮回,为处理锁提供了更多的灵活性。
  • ※ReentrantLock是API级别的,synchronized是JVM级别的。
  • ReentrantLock可以定义公平锁。
  • ReentrantLock通过Condition可以绑定多个条件。
  • 二者底层实现不一样。synchronized是同步阻塞,采用的是悲观并发策略;Lock是同步非阻塞,采用的是乐观并发策略。
  • Lock是一个接口,而synchronized是Java关键字,synchronized是由内置语言实现的。
  • 通过Lock可以知道有没有成功获取锁;而通过synchronized却无从知道。
  • Lock可以通过分别定义读写锁提高多个线程读操作的效率。

Semaphore

Semaphore是一种基于计数的信号量。在定义信号量对象时可以设定一个阈值,基于该阈值,多个线程竞争获取许可信号,线程竞争到许可信号后开始执行具体的业务逻辑,业务逻辑在执行完成后释放该许可信号。在许可信号的竞争队列超过阈值后,新加入的申请许可信号的线程将被阻塞,直到有其他许可信号被释放。

Semaphore基本用法如下:

代码语言:javascript复制
import java.util.concurrent.Semaphore;

public class SemaphoreTest {
    public static void main(String[] args) {
        //① 创建一个计数阈值为5的信号量对象,即只能有5个线程同时访问。
        Semaphore sem = new Semaphore(5);
        try {
            sem.acquire();//② 申请许可
            try {
                // ③ 业务操作
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // ④ 释放许可
                sem.release();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } 
    }
}

Semaphore对锁的申请和释放与ReentrantLock类同,通过acquire方法和release方法来获取和释放许可信号资源。Semaphore.acquire方法默认和ReentrantLock.lockInterruptibly方法的效果一样,为可响应中断锁,即在等待许可信号资源的过程中可以被Thread.interrupt方法中断而取消对许可信号的申请。

此外,Semaphore也実現了可轮询的锁请求、定时锁的功能,以及公平锁与非公平锁的定义在构造方法中设定。

Semaphore的锁释放操作也需要手动执行。因此,为了避免线程因执行异常而无法正常释放锁,释放锁的操作必须在finally中完成。

Semaphore也可以用于実現一些对象池、资源池之构建,比如静态全局对象池、数据库连接池等。

此外,我们也可以创建计数为1的Semaphore,将其作为一种互斥锁的机制(也作二元信号量,表示两种互斥状态),同一时刻只能有一个线程获取该锁。

AtomicInteger

我们知道,在多线程程序中,诸如 i、i 等运算不具有原子性,因此不是安全的线程操作。我们可通过synchronized或ReentrantLock将該操作变成一个原子操作,但是synchronized 和ReentrantLock 均∈重量级锁,因此JVM为此类原子操作提供了一些原子操作同步类,使得同步操作(线程安全)更为方便、高效,這便是AtomicInteger。

AtomicInteger为提供原子操作的Integer的类,常见的原子操作类还有AtomicBoolean、AtomicLong、AtomicReference等,它们的実現原理相同,区别在于运算对象的类型。还可通过AtomicReference<V>将一个对象的所有操作都转化成原子操作。AtomicInteger的性能通常可达到synchronized 和ReentrantLock的好几倍。

具体用法举例:

代码语言:javascript复制
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerDemo implements Runnable {
    /**
     * ① 定义一个原子操作数
     */
    static AtomicInteger safeCountor = new AtomicInteger(0);
    @Override
    public void run() {
        for (int m=0; m<1000000;m  ) {
            safeCountor.getAndIncrement();  //② 对原子操作数执行自增操作
        }
    }
}
代码语言:javascript复制
public class AtomicIntegerDemoTest {
    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerDemo mt = new AtomicIntegerDemo();
        Thread t1 = new Thread(mt);
        Thread t2 = new Thread(mt);
        t1.start();
        t2.start();
        Thread.sleep(600);
        System.out.println(mt.safeCountor.get());
    }
}

可重入锁

可重入锁也叫作递归锁,指在同一线程中,在外层函数获取到该锁之后,内层的递归函数仍然可以继续获取该锁。在Java环境中,synchronized 和ReentrantLock都是可重入锁。

公平锁与非公平锁

  • 公平锁(Fair Lock)指在分配前检查是否有线程在排除等候获取该锁,优先将锁分配给排队时间最长的线程。
  • 非公平锁(Nonfair Lock) 指在分配锁时不考虑线程排队等待的情况,直接尝试获取锁,在获取不到锁时再排到队尾等待。

因为公平锁需要在多核的情况下维护一个锁线程等待队列,基于该队列进行锁的分配,因此效率比非公平锁低很多。Java中synchronized是非公平锁,ReentrantLock默认的lock方法采用的是非公平锁。

读写锁:ReadWriteLock

在Java中通过Lock接口及对象可以方便地为对象加锁和释放锁,但是这种锁不区分读写,叫做普通锁。为提高性能,Java提供了读写锁。读写锁分为读锁和写锁两种,多个读锁互不斥,读锁与写锁互斥。在读的地方使用读锁,在写的地方使用写锁;在没有写锁的情况下,读是无阻塞的。

如果系统要求共享数据可以同时支持很多线程并发读,但不能支持很多线程并发写,那么使用读锁能很大程度地提高效率;如果系统要求共享数据在同一时刻只能有一个线程在写,且在写的过程中不能读取該共享数据,则需要使用写锁。

一般做法是分别定义一个读锁与一个写锁,在读取共享数据时使用读锁,在使用完成后释放读锁;在写共享数据时使用写锁,在使用完成后释放写锁。在Java中,通过读写锁的接口 java.util.concurrent.locks.ReadWriteLock 的实现类 ReentrantReadWriteLock来完成对读写锁的定义与使用。具体如下:

代码语言:javascript复制
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class SafeCache {
    private final Map<String,Object> cache = new HashMap<String,Object>();
    private final ReentrantReadWriteLock rwlock = new ReentrantReadWriteLock();
    private final Lock readLock = rwlock.readLock();
    private final Lock writeLock = rwlock.writeLock();

    /**
     * 加读锁
     * @param key
     * @return
     */
    public Object get(String key){
        readLock.lock();
        try {
            return cache.get(key);
        } finally {
            readLock.unlock();
        }
    }

    /**
     * 写数据时加写锁
     * @param key
     * @param value
     * @return
     */
    public  Object put(String key,Object value){
        writeLock.lock();
        try {
            return cache.put(key,value);
        } finally {
            writeLock.unlock();
        }
    }
}

共享锁和独占锁

Java并发包提供的加锁模式分为独占锁与共享锁。

独占锁:也叫互斥锁,每次只允许一个线程持有该锁,ReentrantLock为独占锁的実現。

共享锁:允许多个线程同时获取该锁,并发访问共享资源。

ReentrantReadWriteLock的加锁和解锁操作最终都调用内部类Sync提供的方法。Sync对象通过继承AQS进行实现。AQS的内部类Node定义了两个常量SHARED和EXCLUSIVE,分别标识AQS队列中等待线程的锁获取模式。

独占锁是一种悲观的加锁策略,同一时刻只允许一个读线程读取锁资源,限制了读操作的并发性;因为并发读线程并不会影响数据一致性,因此共享锁采用了乐观的加锁策略,允许多个执行读操作的线程同时访问共享资源。

重量级锁和轻量级锁

重量级锁是基于操作系统的互斥量(Mutex Lock)而実現的锁,会导致进程在用户态与内核态之间切换,相对开销较大。

synchronized在内部基于监视器锁(Monitor)实现,监视器锁基于底层的操作系统的Mutex Lock 実現,因此synchronized属于重量级锁。重量级锁需要在用户态与核心态之间作转换,所以synchronized的运行并不高

JDK在1.6后,为减少获取锁和释放锁所带来的性能消耗及提高性能,引入了轻量级锁和偏向锁。

轻量级锁是相对于重量级锁而言的。轻量级锁的核心设计是在没有多线程竞争的前提下,减少重量级锁的使用以提高系统性能。轻量级锁适用于线程交替执行同步代码块的情况(即互斥操作)。若同一时刻有多个线程访问同一个锁,则将会导致轻量级锁膨胀为重量级锁。

偏向锁

除了在多线程之间存在竞争获取锁的情况,还会经常出现同一个锁被同一个线程多次获取的情况。偏向锁用于在某个线程获取某个锁之后,消除这个线程锁重入的开销,看起来似乎是这个线程得到了该锁的偏向。

偏向锁的主要目的是在同一个线程多次获取某个锁的情况下尽量减少轻量级锁的执行路径,因为轻量级锁的获取及释放需要多次CAS原子操作,而偏向锁只需在切换ThreadID时执行一次CAS原子操作,因此可提高锁的运行效率。

在出现多线程竞争锁的情况时,JVM会自动撤销偏向锁,因此偏向锁的撤销操作的耗时必须少于节省下来的CAS耗时。

综上所述,轻量级锁用于提高线程交替执行同步块是地的性能,偏向锁则在某个线程交替执行同步块时进一步提高性能。

锁的状态总共有四种:无锁、偏向锁。轻量级锁与重量级锁。随着锁竞争越来越激烈,锁可能从偏向锁升级到轻量级锁,再升级到重量级锁。但在Java中锁只单向升级,不会降级。

分段锁

分段锁并非一种实际的锁,而是一种思想,用于将数据分段并在每个分段上都单独加锁,把锁进一步细粒化,以提高并发效率。ConcurrentHashMap在内部便是使用分段锁実現的。

同步锁与死锁

在有多个线程同时被阻塞时,它们之间若相互等待对方释放锁资源,就会出现死锁。为避免,可为锁操作添加超时时间,在线程持有锁超时后自动释放该锁。

锁优化

减少锁持有的时间

减少锁持有时间指只在有线程安全要求的程序上加锁来尽量减少同步代码块对锁的持有时间。

减小锁的粒度

减小锁的粒度指将单个耗时较多的锁操作拆分为多个耗时较少的锁操作来增加锁的并行度,减少同一个锁上的竞争。在减少锁的竞争后,偏向锁、轻量级锁的使用率才会提高。减小锁粒度最典型的案例就是ConcurrentHashMap中的分段锁。

锁分离

锁分离指根据不同的应用场景将锁的功能进行分离,以应对不同的变化。最常见的锁分离思想就是读写锁(ReadWriteLock),它根据锁的功能将锁分离成读锁和写锁,这样读读不互斥,读写互斥,写写互斥,既保证了线程的安全性,又提高了性能。

操作分离思想可以进一步延伸为只要操作互不影响,就可以进一步拆分,比如LinkedBlockingQueue从头部取出数据,并从尾部加入数据。

锁粗化

指为了保障性能,会要求尽可能将锁的操作细化以减少线程持有锁的时间,但如果锁分得太细,将会导致系统频繁获取锁和释放锁,反而影响性能的提升。这这种情况下,建议将关联性强的锁操作集中起来处理,以提高系统整体的效率。

锁消除

在开发中经常会出现在不需要锁的情况下误用了锁操作而引起性能下降,这多是因为程序编码不规范引起的。这时需要检查并消除这些不必要的锁来提高系统性能。

0 人点赞