• Java并发包——线程同步和锁


    Java并发包——线程同步和锁

    摘要:本文主要学习了Java并发包里有关线程同步的类和锁的一些相关概念。

    部分内容来自以下博客:

    https://www.cnblogs.com/dolphin0520/p/3923167.html

    https://blog.csdn.net/tyyj90/article/details/78236053

    线程同步方式

    对于线程安全我们前面使用了synchronized关键字,对于线程的协作我们使用Object.wait()和Object.notify()。在JDK1.5中java为我们提供了Lock来实现与它们相同的功能,并且性能优于它们,在JDK1.6时,JDK对synchronized做了优化,在性能上两种方式差距不大了。

    synchronized的缺陷

    synchronized修饰的代码块,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,如果没有释放则需要无限的等待下去。

    获取锁的线程释放锁只会有两种情况:

    1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有。

    2)线程执行发生异常,此时JVM会让线程自动释放锁。

    总结一下,也就是说Lock提供了比synchronized更多的功能。但是要注意以下几点:

    1)Lock不是Java语言内置的,synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问。

    2)synchronized不需要手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用。而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。

    Lock

    Lock接口位于java.util.concurrent.locks包中。

     1 public interface Lock {
     2     // 用来获取锁。如果锁已被其他线程获取,则进行等待。
     3     void lock();
     4 
     5     // 用来获取锁。允许在等待时由其它线程调用interrupt方法来中断等待而直接返回,这时不用获取锁,而会抛出一个InterruptedException。
     6     void lockInterruptibly() throws InterruptedException;
     7 
     8     // 用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false。
     9     boolean tryLock();
    10 
    11     // 用来尝试获取锁,如果拿到锁或者在等待期间内拿到了锁,则返回true。如果在某段时间之内获取失败,就返回false。
    12     boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    13 
    14     // 释放锁。
    15     void unlock();
    16 
    17     // 获取Condition对象。
    18     Condition newCondition();
    19 }

    lock方法

    首先lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。

    由于在前面讲到如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

    通常使用Lock来进行同步的话,是以下面这种形式去使用的:

    1 Lock lock = ... ;
    2 lock.lock();
    3 try {
    4     // 处理任务
    5 } catch(Exception e) {
    6 
    7 } finally {
    8     lock.unlock();// 释放锁
    9 }

    tryLock方法

    tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

    tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

    一般情况下通过tryLock来获取锁时是这样使用的:

     1 Lock lock = ... ;
     2 if (lock.tryLock()) {
     3     try {
     4         // 处理任务
     5     } catch (Exception e) {
     6 
     7     } finally {
     8         lock.unlock();// 释放锁
     9     }
    10 } else {
    11     // 获取失败处理其他事情
    12 }

    lockInterruptibly方法

    lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

    由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。

    一般的使用形式如下:

    1 public void method() throws InterruptedException {
    2     Lock lock = ... ;
    3     lock.lockInterruptibly();
    4     try {
    5         // 处理任务
    6     } finally {
    7         lock.unlock();
    8     }
    9 }

    注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为本身在前面的文章中讲过单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。

    因此当通过lockInterruptibly()方法获取某个锁时,如果不能获取到,只有进行等待的情况下,是可以响应中断的。

    而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

    ReentrantLock

    ReentrantLock类实现了Lock接口,并且ReentrantLock提供了更多的方法。

     1 public class Demo {
     2     public static void main(String[] args) {
     3         DemoThread dt = new DemoThread();
     4         Thread t1 = new Thread(dt, "窗口1");
     5         Thread t2 = new Thread(dt, "窗口2");
     6         t1.start();
     7         t2.start();
     8     }
     9 }
    10 
    11 class DemoThread implements Runnable {
    12     private int ticket = 3;
    13     Lock lock = new ReentrantLock();
    14 
    15     @Override
    16     public void run() {
    17         while (ticket > 0) {
    18             try {
    19                 Thread.sleep(1);
    20             } catch (InterruptedException e) {
    21                 e.printStackTrace();
    22             }
    23             
    24             lock.lock();
    25             try {
    26                 if (ticket > 0) {
    27                     System.out.println(Thread.currentThread().getName() + " 进入卖票环节 ");
    28                     System.out.println(Thread.currentThread().getName() + " 售卖的车票编号为: " + ticket--);
    29                 }
    30             } catch (Exception e) {
    31                 e.printStackTrace();
    32             } finally {
    33                 lock.unlock();
    34             }
    35         }
    36     }
    37 }

    注意在声明Lock的时候,要注意不要声明为局部变量。

    ReadWriteLock

    ReadWriteLock也是一个接口,用来定义读写锁。

    1 public interface ReadWriteLock {
    2     Lock readLock();
    3 
    4     Lock writeLock();
    5 }

    一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成两个锁来分配给线程,从而使得多个线程可以同时进行读操作。

    ReentrantReadWriteLock

    ReentrantReadWriteLock实现了ReadWriteLock接口,支持多个线程同时进行读操作。

     1 public class Demo {
     2     public static void main(String[] args) {
     3         DemoThread dt = new DemoThread();
     4         new Thread(() -> dt.showTicket(), "窗口1").start();
     5         new Thread(() -> dt.showTicket(), "窗口2").start();
     6         new Thread(() -> dt.showTicket(), "窗口3").start();
     7         new Thread(() -> dt.saleTicket(), "窗口4").start();
     8     }
     9 }
    10 
    11 class DemoThread {
    12     private int ticket = 3;
    13     ReadWriteLock lock = new ReentrantReadWriteLock();
    14 
    15     public void showTicket() {
    16         while (ticket > 0) {
    17             try {
    18                 Thread.sleep(1);
    19             } catch (InterruptedException e) {
    20                 e.printStackTrace();
    21             }
    22             lock.readLock().lock();
    23             try {
    24                 if (ticket > 0) {
    25                     System.out.println(Thread.currentThread().getName() + " 进入预售环节");
    26                     System.out.println(Thread.currentThread().getName() + " 预售的车票编号为: " + ticket);
    27                 }
    28             } catch (Exception e) {
    29                 e.printStackTrace();
    30             } finally {
    31                 lock.readLock().unlock();
    32             }
    33         }
    34         System.out.println(Thread.currentThread().getName() + " 进入结束环节");
    35     }
    36 
    37     public void saleTicket() {
    38         while (ticket > 0) {
    39             try {
    40                 Thread.sleep(1);
    41             } catch (InterruptedException e) {
    42                 e.printStackTrace();
    43             }
    44             lock.writeLock().lock();
    45             try {
    46                 if (ticket > 0) {
    47                     System.out.println(Thread.currentThread().getName() + " 进入售票环节");
    48                     System.out.println(Thread.currentThread().getName() + " 售卖的车票编号为: " + ticket--);
    49                 }
    50             } catch (Exception e) {
    51                 e.printStackTrace();
    52             } finally {
    53                 lock.writeLock().unlock();
    54             }
    55         }
    56         System.out.println(Thread.currentThread().getName() + " 进入结束环节");
    57     }
    58 }

    运行结果如下:

     1 窗口2 进入预售环节
     2 窗口1 进入预售环节
     3 窗口1 预售的车票编号为: 3
     4 窗口2 预售的车票编号为: 3
     5 窗口3 进入预售环节
     6 窗口3 预售的车票编号为: 3
     7 窗口4 进入售票环节
     8 窗口4 售卖的车票编号为: 3
     9 窗口4 进入售票环节
    10 窗口4 售卖的车票编号为: 2
    11 窗口2 进入预售环节
    12 窗口3 进入预售环节
    13 窗口1 进入预售环节
    14 窗口1 预售的车票编号为: 1
    15 窗口2 预售的车票编号为: 1
    16 窗口3 预售的车票编号为: 1
    17 窗口4 进入售票环节
    18 窗口4 售卖的车票编号为: 1
    19 窗口4 进入结束环节
    20 窗口3 进入结束环节
    21 窗口2 进入结束环节
    22 窗口1 进入结束环节

    从运行的结果来看,最多有三个线程在同时读,提高了读操作的效率。

    如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。

    如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。

    关于synchronized和Lock的比较

    1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现。

    2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。而Lock在发生异常时,如果没有主动释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁。

    3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断。

    4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

    5)Lock可以提高多个线程进行读操作的效率。

    6)synchronized的底层是一个基于CAS操作的等待队列,synchronized还实现了自旋锁,并针对不同的系统和硬件体系进行了优化,而Lock则完全依靠系统阻塞挂起等待线程。

    7)在资源竞争不是很激烈的情况下,synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态

    锁的分类

    在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类。介绍的内容如下:

    1 可重入锁
    2 独享锁/共享锁
    3 互斥锁/读写锁
    4 公平锁/非公平锁
    5 乐观锁/悲观锁
    6 分段锁
    7 偏向锁/轻量级锁/重量级锁
    8 自旋锁

    上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释。

    可重入锁

    可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。

    对于synchronized和ReentrantLock而言,都是可重入锁。

    可重入锁的一个好处是可一定程度避免死锁,如果不是可重入锁的话,可能造成死锁。

    独享锁/共享锁

    独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。

    对于synchronized和ReentrantLock而言,都是独享锁。

    但是对于ReadWriteLock而言,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。

    独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

    互斥锁/读写锁

    上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。

    互斥锁在Java中的具体实现就是ReentrantLock。读写锁在Java中的具体实现就是ReadWriteLock。

    公平锁/非公平锁

    公平锁是指多个线程按照申请锁的顺序来获取锁,非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序。

    对于synchronized而言,是一种非公平锁。

    对于ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。

    乐观锁/悲观锁

    乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

    悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题

    乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的

    悲观锁在Java中的使用,就是利用各种锁。

    乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

    分段锁

    分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。

    分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

    偏向锁/轻量级锁/重量级锁

    这三种锁是指锁的状态,并且是针对Synchronized。在JDK5通过引入锁升级的机制来实现高效Synchronized。

    这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

    偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

    轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

    重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

    自旋锁

    在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

    了解AQS

    什么是AQS

    AQS是英文单词AbstractQueuedSynchronizer的缩写,翻译过来就是抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock、Semaphore、CountDownLatch等等。

    实现方式

    AQS的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。

    原理

    AQS维护了一个state用来代表资源共享状态 private volatile int state; ,AQS提供了三种操作state的方法: int getState(); 、 void setState(int newState); 、 boolean compareAndSetState(int expect, int update); 。

    AQS通过内置的FIFO同步队列 static final class Node 来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。

    资源共享方式

    AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

    使用分析

    不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

    isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。

    tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。

    tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。

    tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。

    tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

    以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

    再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

    一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

  • 相关阅读:
    给出字符串
    Closest
    最短路计数(spfa)
    Cleaning Shifts
    Mr. Young's Picture Permutations
    Georgia and Bob
    GCD
    Cutting Game
    SP4191 MSKYCODE
    [CQOI2013]新Nim游戏
  • 原文地址:https://www.cnblogs.com/shamao/p/11020704.html
Copyright © 2020-2023  润新知