Java中锁的分类与使用方法

2025-05-29 0 70

Lock和synchronized

  • 是一种工具,用于控制对共享资源的访问
  • Lock和synchronized,这两个是最创建的,他们都可以达到线程安全的目的,但是使用和功能上有较大不同
  • Lock不是完全替代synchronized的,而是当使用synchronized不合适或不足以满足要求的时候,提供高级功能
  • Lock 最常见的是ReentrantLock实现

为啥需要Lock

  1. syn效率低:的释放情况少,试图获得时不能设定超时,不能中断一个正在试图获得的线程
  2. 不够灵活,加和释放的时机单一,每个仅有一个单一的条件(某个对象),可能是不够的
  3. 无法知道是否成功获取到

主要方法

Lock();

最普通的获取,最佳实践是finally中释放,保证发生异常的时候一定被释放

  1. /**
  2. * 描述:Lock不会像syn一样,异常的时候自动释放
  3. * 所以最佳实践是finally中释放,保证发生异常的时候一定被释放
  4. */
  5. private static Lock lock = new ReentrantLock();
  6. public static void main(String[] args) {
  7. lock.lock();
  8. try {
  9. //获取本保护的资源
  10. System.out.println(Thread.currentThread().getName() + "开始执行任务");
  11. } finally {
  12. lock.unlock();
  13. }
  14. }

tryLock(long time,TimeUnit unit);超时就放弃

用来获取,如果当前没有被其它线程占用,则获取成功,则返回true,否则返回false,代表获取失败

  1. /**
  2. * 描述:用TryLock避免死
  3. */
  4. static class TryLockDeadlock implements Runnable {
  5. int flag = 1;
  6. static Lock lock1 = new ReentrantLock();
  7. static Lock lock2 = new ReentrantLock();
  8. @Override
  9. public void run() {
  10. for (int i = 0; i < 100; i++) {
  11. if (flag == 1) {
  12. try {
  13. if (lock1.tryLock(800, TimeUnit.MILLISECONDS)) {
  14. try {
  15. System.out.println("线程1获取到了1");
  16. Thread.sleep(new Random().nextInt(1000));
  17. if (lock2.tryLock(800,TimeUnit.MILLISECONDS)){
  18. try {
  19. System.out.println("线程1获取到了2");
  20. System.out.println("线程1成功获取到了2把");
  21. break;
  22. }finally {
  23. lock2.unlock();
  24. }
  25. }else{
  26. System.out.println("线程1获取2失败,已重试");
  27. }
  28. } finally {
  29. lock1.unlock();
  30. Thread.sleep(new Random().nextInt(1000));
  31. }
  32. } else {
  33. System.out.println("线程1获取1失败,已重试");
  34. }
  35. } catch (InterruptedException e) {
  36. e.printStackTrace();
  37. }
  38. }
  39. if (flag == 0) {
  40. try {
  41. if (lock2.tryLock(3000, TimeUnit.MILLISECONDS)) {
  42. try {
  43. System.out.println("线程2获取到了2");
  44. Thread.sleep(new Random().nextInt(1000));
  45. if (lock1.tryLock(800,TimeUnit.MILLISECONDS)){
  46. try {
  47. System.out.println("线程2获取到了1");
  48. System.out.println("线程2成功获取到了2把");
  49. break;
  50. }finally {
  51. lock1.unlock();
  52. }
  53. }else{
  54. System.out.println("线程2获取1失败,已重试");
  55. }
  56. } finally {
  57. lock2.unlock();
  58. Thread.sleep(new Random().nextInt(1000));
  59. }
  60. } else {
  61. System.out.println("线程2获取2失败,已经重试");
  62. }
  63. } catch (InterruptedException e) {
  64. e.printStackTrace();
  65. }
  66. }
  67. }
  68. }
  69. public static void main(String[] args) {
  70. TryLockDeadlock r1 = new TryLockDeadlock();
  71. TryLockDeadlock r2 = new TryLockDeadlock();
  72. r1.flag = 1;
  73. r2.flag = 0;
  74. new Thread(r1).start();
  75. new Thread(r2).start();
  76. }
  77. }
  78. 执行结果:
  79. 线程1获取到了1
  80. 线程2获取到了2
  81. 线程1获取2失败,已重试
  82. 线程2获取到了1
  83. 线程2成功获取到了2
  84. 线程1获取到了1
  85. 线程1获取到了2
  86. 线程1成功获取到了2

lockInterruptibly(); 中断

相当于tryLock(long time,TimeUnit unit) 把超时时间设置为无限,在等待的过程中,线程可以被中断

  1. /**
  2. * 描述:获取的过程中,中断了
  3. */
  4. static class LockInterruptibly implements Runnable {
  5. private Lock lock = new ReentrantLock();
  6. @Override
  7. public void run() {
  8. System.out.println(Thread.currentThread().getName() + "尝试获取");
  9. try {
  10. lock.lockInterruptibly();
  11. try {
  12. System.out.println(Thread.currentThread().getName() + "获取到了");
  13. Thread.sleep(5000);
  14. } catch (InterruptedException e) {
  15. System.out.println(Thread.currentThread().getName() + "睡眠中被中断了");
  16. } finally {
  17. lock.unlock();
  18. System.out.println(Thread.currentThread().getName() + "释放了");
  19. }
  20. } catch (InterruptedException e) {
  21. System.out.println(Thread.currentThread().getName() + "等期间被中断了");
  22. }
  23. }
  24. public static void main(String[] args) {
  25. LockInterruptibly lockInterruptibly = new LockInterruptibly();
  26. Thread thread0 = new Thread(lockInterruptibly);
  27. Thread thread1 = new Thread(lockInterruptibly);
  28. thread0.start();
  29. thread1.start();
  30. try {
  31. Thread.sleep(2000);
  32. } catch (InterruptedException e) {
  33. e.printStackTrace();
  34. }
  35. thread0.interrupt();
  36. }
  37. }
  38. 执行结果:
  39. Thread0尝试获取
  40. Thread1尝试获取
  41. Thread0获取到了
  42. Thread0睡眠中被中断了
  43. Thread0释放了
  44. Thread1获取到了
  45. Thread1释放了

Java分类:

Java中锁的分类与使用方法

乐观和悲观

乐观

比较乐观,认为自己在处理操作的时候,不会有其它线程来干扰,所以并不会住操作对象

  • 在更新的时候,去对比我修改期间的数据有没有被改变过,如没有,就正常的修改数据
  • 如果数据和我一开始拿到的不一样了,说明其他人在这段时间内改过,会选择放弃,报错,重试等策略
  • 乐观的实现一般都是利用CAS算法来实现的

劣势:

可能造成ABA问题,就是不知道是不是修改过

使用场景:

适合并发写入少的情况,大部分是读取的场景,不加的能让读取的性能大幅提高

悲观

比较悲观,认为如果我不住这个资源,别人就会来争抢,就会造成数据结果错误,所以它会住操作对象,Java中悲观的实现就是syn和Lock相关类

劣势:

  • 阻塞和唤醒带来的性能劣势
  • 如果持有的线程被永久阻塞,比如遇到了无限循环,死等活跃性问题,那么等待该线程释放的那几个线程,永远也得不到执行
  • 优先级反转,优先级低的线程拿到不释放或释放的比较慢,就会造成这个问题

使用场景:

适合并发写入多的情况,适用于临界区持时间比较长的情况:

  1. 临界区有IO操作
  2. 临界区代码复杂或者循环量大
  3. 临界区竞争非常激烈

可重入

可重入就是说某个线程已经获得某个,可以再次获取而不会出现死

ReentrantLock 和synchronized 都是可重入

  1. // 递归调用演示可重入
  2. static class RecursionDemo{
  3. public static ReentrantLock lock = new ReentrantLock();
  4. private static void accessResource(){
  5. lock.lock();
  6. try {
  7. System.out.println("已经对资源处理了");
  8. if (lock.getHoldCount() < 5){
  9. System.out.println("已经处理了"+lock.getHoldCount()+"次");
  10. accessResource();
  11. }
  12. }finally {
  13. lock.unlock();
  14. }
  15. }
  16. public static void main(String[] args) {
  17. new RecursionDemo().accessResource();
  18. }
  19. }
  20. 执行结果:
  21. 已经对资源处理了
  22. 已经处理了1
  23. 已经对资源处理了
  24. 已经处理了2
  25. 已经对资源处理了
  26. 已经处理了3
  27. 已经对资源处理了
  28. 已经处理了4
  29. 已经对资源处理了

ReentrantLock的其它方法

  • isHeldByCurrentThread 可以看出是否被当前线程持有
  • getQueueLength()可以返回当前正在等待这把的队列有多长,一般这两个方法是开发和调试时候使用,上线后用到的不多

公平和非公平

  • 公平指的是按照线程请求的顺序,来分配
  • 非公平指的是,不完全按照请求的顺序,在一定情况下,可以插队
  • 非公平可以避免唤醒带来的空档期
  1. /**
  2. * 描述:演示公平和非公平
  3. */
  4. class FairLock{
  5. public static void main(String[] args) {
  6. PrintQueue printQueue = new PrintQueue();
  7. Thread[] thread = new Thread[10];
  8. for (int i = 0; i < 10; i++) {
  9. thread[i] = new Thread(new Job(printQueue));
  10. }
  11. for (int i = 0; i < 5; i++) {
  12. thread[i].start();
  13. try {
  14. Thread.sleep(100);
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. }
  19. }
  20. }
  21. class Job implements Runnable{
  22. PrintQueue printQueue;
  23. public Job(PrintQueue printQueue) {
  24. this.printQueue = printQueue;
  25. }
  26. @Override
  27. public void run() {
  28. System.out.println(Thread.currentThread().getName()+"开始打印");
  29. printQueue.printJob(new Object());
  30. System.out.println(Thread.currentThread().getName()+"打印完成");
  31. }
  32. }
  33. class PrintQueue{
  34. // true 公平,false是非公平
  35. private Lock queueLock = new ReentrantLock(true);
  36. public void printJob(Object document){
  37. queueLock.lock();
  38. try {
  39. int duration = new Random().nextInt(10)+1;
  40. System.out.println(Thread.currentThread().getName()+"正在打印,需要"+duration+"秒");
  41. Thread.sleep(duration * 1000);
  42. } catch (InterruptedException e) {
  43. e.printStackTrace();
  44. } finally {
  45. queueLock.unlock();
  46. }
  47. queueLock.lock();
  48. try {
  49. int duration = new Random().nextInt(10)+1;
  50. System.out.println(Thread.currentThread().getName()+"正在打印,需要"+duration+"秒");
  51. Thread.sleep(duration * 1000);
  52. } catch (InterruptedException e) {
  53. e.printStackTrace();
  54. } finally {
  55. queueLock.unlock();
  56. }
  57. }
  58. }
  59. 执行结果:
  60. Thread0开始打印
  61. Thread0正在打印,需要10
  62. Thread1开始打印
  63. Thread2开始打印
  64. Thread3开始打印
  65. Thread4开始打印
  66. Thread1正在打印,需要2
  67. Thread2正在打印,需要2
  68. Thread3正在打印,需要2
  69. Thread4正在打印,需要4
  70. Thread0正在打印,需要2
  71. Thread0打印完成
  72. Thread1正在打印,需要7
  73. Thread1打印完成
  74. Thread2正在打印,需要8
  75. Thread2打印完成
  76. Thread3正在打印,需要3
  77. Thread3打印完成
  78. Thread4正在打印,需要8
  79. Thread4打印完成
  80. true改为false演示非公平
  81. Lock queueLock = new ReentrantLock(false);
  82. 执行结果:
  83. Thread0正在打印,需要7
  84. Thread1开始打印
  85. Thread2开始打印
  86. Thread3开始打印
  87. Thread4开始打印
  88. Thread0正在打印,需要9
  89. Thread0打印完成
  90. Thread1正在打印,需要3
  91. Thread1正在打印,需要2
  92. Thread1打印完成
  93. Thread2正在打印,需要4
  94. Thread2正在打印,需要7
  95. Thread2打印完成
  96. Thread3正在打印,需要10
  97. Thread3正在打印,需要2
  98. Thread3打印完成
  99. Thread4正在打印,需要7
  100. Thread4正在打印,需要8
  101. Thread4打印完成

共享和排它

  • 排它,又称为独占,独享
  • 共享,又称为读,获得共享之后,可以查看但无法修改和删除数据,其他线程此时也可以获取到共享,也可以查看但无法修改和删除数据
  • 共享和排它的典型是读写ReentrantReadWriteLock,其中读是共享,写是独享

读写的作用:

  • 在没有读写之前,我们假设使用ReentrantLock,那么虽然我们保证了线程安全,但是也浪费了一定的资源:多个读操作同时进行,并没有线程安全问题
  • 在读的地方使用读,在写的地方使用写,灵活控制,如果没有写的情况下,读是无阻塞的,提高了程序的执行效率

读写的规则:

  1. 多个线程值申请读,都可以申请到
  2. 要么一个或多个一起读,要么一个写,两者不会同时申请到,只能存在一个写
  1. /**
  2. * 描述:演示可以多个一起读,只能一个写
  3. */
  4. class CinemaReadWrite{
  5. private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
  6. private static ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
  7. private static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
  8. private static void read(){
  9. readLock.lock();
  10. try {
  11. System.out.println(Thread.currentThread().getName() + "得到了读,正在读取");
  12. Thread.sleep(1000);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. } finally {
  16. System.out.println(Thread.currentThread().getName() + "释放了读");
  17. readLock.unlock();
  18. }
  19. }
  20. private static void write(){
  21. writeLock.lock();
  22. try {
  23. System.out.println(Thread.currentThread().getName() + "得到了写,正在写入");
  24. Thread.sleep(1000);
  25. } catch (InterruptedException e) {
  26. e.printStackTrace();
  27. } finally {
  28. System.out.println(Thread.currentThread().getName() + "释放了写");
  29. writeLock.unlock();
  30. }
  31. }
  32. public static void main(String[] args) {
  33. new Thread(()-> read(),"Thrad1").start();
  34. new Thread(()-> read(),"Thrad2").start();
  35. new Thread(()-> write(),"Thrad3").start();
  36. new Thread(()-> write(),"Thrad4").start();
  37. }
  38. }
  39. 执行结果:
  40. Thrad1得到了读,正在读取
  41. Thrad2得到了读,正在读取
  42. Thrad2释放了读
  43. Thrad1释放了读
  44. Thrad3得到了写,正在写入
  45. Thrad3释放了写
  46. Thrad4得到了写,正在写入
  47. Thrad4释放了写

和写的交互方式:

插队策略:

  • 公平:不允许插队
  • 非公平:写可以随时插队,读仅在等待队列头节点不是想获取写线程的时候可以插队

自旋和阻塞

  • 让当前线程进行自旋,如果自旋完成后前面定同步资源的线程已经释放了,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋
  • 阻塞和自旋相反,阻塞如果遇到没拿到的情况,会直接把线程阻塞,知道被唤醒

自旋缺点:

  • 如果被占用的时间很长,那么自旋的线程只会白浪费处理器资源
  • 在自旋的过程中,一直消耗cpu,所以虽然自旋的起始开销低于悲观,但是随着自旋的时间增长,开销也是线性增长的

原理:

  • Java1.5版本及以上的并发框架java.util.concurrent 的atmoic包下的类基本都是自旋的实现
  • AtomicInteger的实现:自旋的实现原理是CAS,AtomicInteger中调用unsafe 进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改过程中遇到其他线程竞争导致没修改成功,就在while里死循环直至修改成功
  1. /**
  2. * 描述:自旋演示
  3. */
  4. class SpinLock{
  5. private AtomicReference<Thread> sign = new AtomicReference<>();
  6. public void lock(){
  7. Thread currentThread = Thread.currentThread();
  8. while (!sign.compareAndSet(null,currentThread)){
  9. System.out.println("自旋获取失败,再次尝试");
  10. }
  11. }
  12. public void unLock(){
  13. Thread currentThread = Thread.currentThread();
  14. sign.compareAndSet(currentThread,null);
  15. }
  16. public static void main(String[] args) {
  17. SpinLock spinLock = new SpinLock();
  18. Runnable runnable = new Runnable(){
  19. @Override
  20. public void run(){
  21. System.out.println(Thread.currentThread().getName()+"开始尝试自旋");
  22. spinLock.lock();
  23. System.out.println(Thread.currentThread().getName()+"获取到了自旋");
  24. try {
  25. Thread.sleep(1);
  26. } catch (InterruptedException e) {
  27. e.printStackTrace();
  28. }finally {
  29. spinLock.unLock();
  30. System.out.println(Thread.currentThread().getName()+"释放了自旋");
  31. }
  32. }
  33. };
  34. Thread thread1 = new Thread(runnable);
  35. Thread thread2 = new Thread(runnable);
  36. thread1.start();
  37. thread2.start();
  38. }
  39. }
  40. 执行结果:
  41. Thread0开始尝试自旋
  42. Thread0获取到了自旋
  43. Thread1开始尝试自旋
  44. 自旋获取失败,再次尝试
  45. 自旋获取失败,再次尝试
  46. 自旋获取失败,再次尝试
  47. 自旋获取失败,再次尝试
  48. 自旋获取失败,再次尝试
  49. 自旋获取失败,再次尝试
  50. 自旋获取失败,再次尝试
  51. 自旋获取失败,再次尝试
  52. 自旋获取失败,再次尝试
  53. 自旋获取失败,再次尝试
  54. 自旋获取失败,再次尝试
  55. Thread0释放了自旋
  56. Thread1获取到了自旋
  57. Thread1释放了自旋

使用场景:

  • 自旋一般用于多核服务器,在并发度不是特别高的情况下,比阻塞的效率要高
  • 另外,自旋适用于临界区比较短小的情况,否则如果临界区很大(线程一旦拿到,很久之后才会释放),那也是不合适的

总结

到此这篇关于Java的分类与使用方法的文章就介绍到这了,更多相关Java使用内容请搜索快网idc以前的文章或继续浏览下面的相关文章希望大家以后多多支持快网idc!

原文链接:https://blog.csdn.net/sinat_35395498/article/details/115571519

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

快网idc优惠网 建站教程 Java中锁的分类与使用方法 https://www.kuaiidc.com/108267.html

相关文章

发表评论
暂无评论