锁像synchronized同步块一样,是一种线程同步机制。让自Java 5开始,java.util.concurrent.locks包提供了另一种方式实现线程同步机制——Lock。那么问题来了既然都可以通过synchronized来实现同步访问了,那么为什么还需要提供Lock呢?这个问题我们下面讨论java.util.concurrent.locks包中包含了一些锁的实现,所以我们不需要重复造轮子了。但是我们仍然需要去了解怎样使用这些锁,且了解这些实现背后的理论也是很有用处的。
 
ASP站长网本文将从下面几个方面介绍
 
锁的相关概念
java.util.concurrent.locks下常用的几种锁
锁的相关概念
    在学习或者使用Java的过程中进程会遇到各种各样的锁的概念:公平锁、非公平锁、自旋锁、可重入锁、偏向锁、轻量级锁、重量级锁、读写锁、互斥锁等待。下边总结了对各种锁的解释
 
公平锁/非公平锁
    公平锁是指多个线程在等待同一个锁时按照申请锁的先后顺序来获取锁。相反的非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。
 
     公平锁的好处是等待锁的线程不会饿死,但是整体效率相对低一些;非公平锁的好处是整体效率相对高一些,但是有些线程可能会饿死或者说很早就在等待锁,但要等很久才会获得锁。其中的原因是公平锁是严格按照请求所的顺序来排队获得锁的,而非公平锁时可以抢占的,即如果在某个时刻有线程需要获取锁,而这个时候刚好锁可用,那么这个线程会直接抢占,而这时阻塞在等待队列的线程则不会被唤醒。
 
        对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。例:new ReentrantLock(true)是公平锁
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
 
可重入锁
    也叫递归锁,是指在外层函数获得锁之后,内层递归函数仍然可以获取到该锁。即线程可以进入任何一个它已经拥有锁的代码块。在JAVA环境下 ReentrantLock 和synchronized 都是可重入锁。可重入锁最大的作用是避免死锁。
 
   具体区别下文阐述。
 
自旋锁
    在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。
 
      JDK6中已经变为默认开启自旋锁,并且引入了自适应的自旋锁。自适应意味着自旋的时间不在固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。自旋是在轻量级锁中使用的,在重量级锁中,线程不使用自旋。
 
偏向锁、轻量级锁和重量级锁
      这三种锁是指锁的状态,并且是针对Synchronized。在Java 5后通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。如下图
 
       
 
                                这里的无锁和偏向锁在对象头的倒数第三bit中分别采用0和1标记
 
偏向锁是JDK6中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。但是对于锁竞争激励的场合,我其效果不佳。最坏的情况下就是每次都是不同的线程来请求相同的锁,这样偏向模式就会失效。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
悲观锁和乐观锁
      乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度
 
乐观锁认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。即假定不会发生并发冲突,只在提交操作时检测是否违反数据完整性。(使用版本号或者时间戳来配合实现)。在java中就是 是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。即假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在java中就是各种锁编程。
从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
共享锁和独占锁
共享锁:如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排它锁。获准共享锁的事务只能读数据,不能修改数据。
独占锁:如果事务T对数据A加上独占锁后,则其他事务不能再对A加任何类型的锁。获得独占锁的事务即能读数据又能修改数据。如Synchronized
互斥锁和读写锁
  独占锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
 
 互斥锁:就是指一次最多只能有一个线程持有的锁。在JDK中synchronized和JUC的Lock就是互斥锁。
 读写锁:读写锁是一个资源能够被多个读线程访问,或者被一个写线程访问但不能同时存在读线程。Java当中的读写锁通过ReentrantReadWriteLock实现。ReentrantReadWriteLock运行一个资源可以被多个读操作访问,或者一个写操作访问,但两者不能同时进行。
java.util.concurrent.locks下常用的几种锁
ReentrantLock
   ReentrantLock,可重入锁,是一种递归无阻塞的同步机制。它可以等同于synchronized的使用,但是ReentrantLock提供了比synchronized更强大、灵活的锁机制,可以减少死锁发生的概率。
 
   ReentrantLock还提供了公平锁和非公平锁的选择,构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。
 
   获取锁
    一般使用如下方式获取锁
 
ReentrantLock lock = new ReentrantLock();
lock.lock();
 lock方法:
 
   public void lock() {
        sync.lock();
    }
      Sync为Sync为ReentrantLock里面的一个内部类,它继承AQS。关于AQS的相关知识可以自行补充一下。Sync有两个子类分别是FairSync(公平锁)和 NofairSync(非公平锁)。默认使用NofairSync,下面是ReentrantLock的构造类
 
public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    下边是一个简单的重入锁使用案例
 
public class ReentrantLockDemo implements Runnable {
    public static final Lock lock = new ReentrantLock();
    public static int i = 0;
 
    @Override
    public void run() {
        for (int j = 0; j < 1000000; j++) {
            lock.lock();
            try {
                i++;
            } finally {
                lock.unlock();
            }
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo demo = new ReentrantLockDemo();
        Thread t1 = new Thread(demo);
        Thread t2 = new Thread(demo);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
}
 
       上述代码的第8~12行,使用了重入锁保护了临界区资源i,确保了多线程对i的操作。输出结果为2000000。可以看到与synchronized相比,重入锁必选手动指定在什么地方加锁,什么地方释放锁,所以更加灵活。
 
要注意是,再退出临界区的时候,需要释放锁,否则其他线程就无法访问临界区了。这里为啥叫可重入锁是因为这种锁是可以被同一个线程反复进入的。比如上述代码��使用锁部分可以写成这样
 
           lock.lock();
            lock.lock();
            try {
                i++;
            } finally {
                lock.unlock();
                lock.unlock();
            }
        在这种情况下,一个线程联连续两次获取同一把锁,这是允许的。但是需要注意的是,如果同一个线程多次获的锁,那么在释放是也要释放相同次数的锁。如果释放的锁少了,相当于该线程依然持有这个锁,那么其他线程就无法访问临界区了。释放的次数多了也会抛出java.lang.IllegalMonitorStateException异常。
 
      除了使用上的灵活,ReentrantLock还提供了一些高级功能如中断。限时等待等。
 
     中断响应
     对用synchrozide来说,如果一个线程在等待,那么结果只有两种情况,要么获得这把锁继续执行下去要么一直等待下去。而使用重入锁,提供了另外一种可能,那就是线程可以被中断。也就是说在这里可以取消对锁的请求。这种情况对解决死锁是有一定帮组的。
 
     下面代码产生了一个死锁,但是我们可以通过锁的中断,解决这个死锁。
 
public class ReentrantLockDemo implements Runnable {
    //重入锁ReentrantLock
    public static ReentrantLock lock1 = new ReentrantLock();
    public static ReentrantLock lock2 = new ReentrantLock();
    int lock;
    public ReentrantLockDemo(int lock) {
        this.lock = lock;
    }
 
    @Override
    public void run() {
        try {
            if (lock == 1) {
                lock1.lockInterruptibly();
                Thread.sleep(500);
                lock2.lockInterruptibly();
                System.out.println("this is thread 1");
            } else {
                lock2.lockInterruptibly();
                Thread.sleep(500);
                lock1.lockInterruptibly();
                System.out.println("this is thread 2");
            }
        } catch (Exception e) {
            //e.printStackTrace();
        } finally {
            if (lock1.isHeldByCurrentThread()) {
                lock1.unlock();//释放锁
            }
            if (lock2.isHeldByCurrentThread()) {
                lock2.unlock();
            }
            System.out.println(Thread.currentThread().getId() + ":线程退出");
        }
 
    }
 
    public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo r1 = new ReentrantLockDemo(1);
        ReentrantLockDemo r2 = new ReentrantLockDemo(2);
        Thread t1 = new Thread(r1);
        Thread t2 = new Thread(r2);
        t1.start();
        t2.start();
        Thread.sleep(1000);
        //t2线程被中断,放弃锁申请,释放已获得的lock2,这个操作使得t1线程顺利获得lock2继续执行下去;
        //若没有此段代码,t2线程没有中断,那么会出现t1获取lock1,请求lock2,而t2获取lock2,请求lock1的相互等待死锁情况
        t2.interrupt();
    }
}
        线程t1和t2启动后,t1先占用lock1然后在请求lock2;t2先占用lock2,然后请求lock1,因此很容易形成线程之间的相互等待。着这里使用的是ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
 
      最后由于t2线程被中断,t2会放弃对lock1的1请求,同时释放lock2。这样可以使t1继续执行下去,结果如下图
 
   
 
   锁申请等待限时
   除了等待通知以外,避免死锁还有另外一种方式,那就是限时等待。通过给定一个等待时间,让线程自动放弃。
 
public class TimeLockDemo implements Runnable {
    private static ReentrantLock reentrantLock = new ReentrantLock();
 
    @Override
    public void run() {
 
        try {
            if (reentrantLock.tryLock(5, TimeUnit.SECONDS)) {
                Thread.sleep(6000);
            } else {
                System.out.println("Gets lock failed");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (reentrantLock.isHeldByCurrentThread()){
                reentrantLock.unlock();
            }
        }
    }
 
    public static void main(String[] args) {
        TimeLockDemo demo1 = new TimeLockDemo();
        TimeLockDemo demo2 = new TimeLockDemo();
        Thread t1 = new Thread(demo1);
        Thread t2 = new Thread(demo2);
        t1.start();
        t2.start();
    }
}

dawei

【声明】:九江站长网内容转载自互联网,其相关言论仅代表作者个人观点绝非权威,不代表本站立场。如您发现内容存在版权问题,请提交相关链接至邮箱:bqsm@foxmail.com,我们将及时予以处理。