java 并发多线程显式锁概念简介 什么是显式锁 多线程下篇(一)

简介: java 并发多线程显式锁概念简介 什么是显式锁 多线程下篇(一) 目前对于同步,仅仅介绍了一个关键字synchronized,可以用于保证线程同步的原子性、可见性、有序性 对于synchronized关键字,对于静态方法默认是以该类的class对象作为锁,对于实例方法默认是当前对象this,对于.

java 并发多线程显式锁概念简介 什么是显式锁 多线程下篇(一)

目前对于同步,仅仅介绍了一个关键字synchronized,可以用于保证线程同步的原子性、可见性、有序性
对于synchronized关键字,对于静态方法默认是以该类的class对象作为锁,对于实例方法默认是当前对象this,对于同步代码块,需要指定锁对象
对于整个同步方法或者代码块,不再需要显式的进行加锁,默认这一整个范围都是在锁范围内
可以理解为,隐含的在代码开始和结尾处,进行了隐式的加锁和解锁
所以synchronized又被称为隐式锁
对于synchronized关键字的隐式锁,不需要显式的加锁和释放,即使出现了问题,仍旧能够对锁进行释放
synchronized是一种阻塞式的,在前面也提到过,对于synchronized修饰的同步,如果无法进入监视器则是BLOCKED状态,无疑,性能方面可想而知
而且,这种隐式锁,在同一个代码片段内只有一个监视器,灵活性不够
 
为了优化synchronized的一些不便,Java又提出来了显式锁的概念Lock
顾名思义,显式,是相对隐式来说的,也就是对于加锁和解锁,需要明确的给出,而不会自动的进行处理

示例回顾

回忆下是之前《多线程协作wait、notify、notifyAll方法简介理解使用 》一文中使用的例子
ps:下面的例子是优化过的,其中if判断换成了while 循环检测,notify换成了notifyAll
复制代码
package test1;
import java.util.LinkedList;
/**
* 消息队列MessageQueue 测试
*/
public class T14 {
public static void main(String[] args) {
final RefactorMessageQueue mq = new RefactorMessageQueue(5);
System.out.println("***************task begin***************");
//创建生产者线程并启动
for (int i = 0; i < 20; i++) {
new Thread(() -> {
while (true) {
mq.set(new Message());
}
}, "producer"+i).start();
}
//创建消费者线程并启动
new Thread(() -> {
while (true) {
mq.get();
}
}, "consumer").start();
}
}
/**
* 消息队列
*/
class RefactorMessageQueue {
/**
* 队列最大值
*/
private final int max;
/*
* 锁
* */
private final byte[] lock = new byte[1];
/**
* final确保发布安全
*/
final LinkedList<Message> messageQueue = new LinkedList<>();
/**
* 构造函数默认队列大小为10
*/
public RefactorMessageQueue() {
max = 10;
}
/**
* 构造函数设置队列大小
*/
public RefactorMessageQueue(int x) {
max = x;
}
public void set(Message message) {
synchronized (lock) {
//如果已经大于队列个数,队列满,进入等待
while (messageQueue.size() > max) {
try {
System.out.println(Thread.currentThread().getName() + " : queue is full ,waiting...");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果队列未满,生产消息,随后通知lock上的等待线程
//每一次的消息生产,都会通知消费者
System.out.println(Thread.currentThread().getName() + " : add a message");
messageQueue.addLast(message);
lock.notifyAll();
}
}
public void get() {
synchronized (lock) {
//如果队列为空,进入等待,无法获取消息
while (messageQueue.isEmpty()) {
try {
System.out.println(Thread.currentThread().getName() + " : queue is empty ,waiting...");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//队列非空时,读取消息,随后通知lock上的等待线程
//每一次的消息读取,都会通知生产者
System.out.println(Thread.currentThread().getName() + " : get a message");
messageQueue.removeFirst();
lock.notifyAll();
}
}
}
复制代码
分析下这个示例中的一些概念
使用了synchronized用作同步,锁对象为  private final byte[] lock = new byte[1];
有多个生产者和一个消费者,为了进行通信使用了监视器(也就是锁对象)的wait和notifyAll方法进行通信
ps:前文也说过为何要用notifyAll而不是notify
简单说两个点:
  • synchronized关键字
  • 监视器方法
借助于这两个点,可以完成多线程之间的协作与通信(多个生产者一个消费者)
监视器方法的调用需要在监视器内,也就是同步方法内
而且上面的例子中的监视器都是同一个就是锁对象,wait是当前线程在监视器上wait,notifyAll方法则是唤醒所有在此监视器上等待的线程
很显然,其实生产者应该唤醒生产者,消费者应该唤醒消费者
可是,多线程协作使用的是同一个队列,所以需要使用同一把锁
又因为监视器方法必须在同步方法内而且也必须是持有监视器才能调用相应的监视器方法,所以只能使用同一个监视器了
也就是只能将这些线程组织在同一个监视器中,就不好做到“其实生产者应该唤醒生产者,消费者应该唤醒消费者”

显式锁逻辑

再回过头看显式锁,他是如何做到各方面灵活的呢?
从上面的分析来看主要就是因为隐式锁与监视器之间的比较强的关联关系
synchronized修饰的代码片段使用的是同一把锁,同步方法内的监视器方法也只能调用这个锁的,也就是说在使用上来看,用什么锁,就要用这个锁的监视器,强关联
问题的一种解题思路就是解耦,显式锁就是这种思路 
Lock就好比是synchronized关键字,只不过你需要显式的进行加锁和解锁
惯用套路如下
复制代码
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
复制代码
本来使用synchronized隐式的加锁和解锁,换成了Lock的lock和unlock方法调用
那么监视器呢?
与锁关联的监视器又是什么,又如何调用监视器的方法呢?
Lock提供了Condition newCondition();方法
返回类型为Condition,被称之为条件变量,可以认为是锁关联的监视器
借助于Condition,就可以达到原来监视器方法调用的效果,Condition方法列表如下,看得出来,是不是很像wait和notify、notifyAll?目标是一致的
image_5c7dcac8_2722
所以可以说,显式锁的逻辑就是借助于Lock接口以及Condition接口,实现了对synchronized关键字以及锁对应的监视器的另外的一种实现
从而提供了更大的灵活性
还是之前的示例,尝试试用一下显式锁
复制代码
package test2;
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class T26 {
public static void main(String[] args) {
final RefactorMessageQueue mq = new RefactorMessageQueue(5);
System.out.println("***************task begin***************");
//创建生产者线程并启动
for (int i = 0; i < 20; i++) {
new Thread(() -> {
while (true) {
mq.set(new Message());
}
}, "producer" + i).start();
}
//创建消费者线程并启动
new Thread(() -> {
while (true) {
mq.get();
}
}, "consumer").start();
}
/**
* 消息队列中存储的消息
*/
static class Message {
}
/**
* 消息队列
*/
static class RefactorMessageQueue {
/**
* 队列最大值
*/
private final int max;
/*
* 锁
* */
private final Lock lock = new ReentrantLock();
/**
* 条件变量
*/
private final Condition condition = lock.newCondition();
/**
* final确保发布安全
*/
final LinkedList<Message> messageQueue = new LinkedList<>();
/**
* 构造函数默认队列大小为10
*/
public RefactorMessageQueue() {
max = 10;
}
/**
* 构造函数设置队列大小
*/
public RefactorMessageQueue(int x) {
max = x;
}
public void set(Message message) {
lock.lock();
try {
//如果已经大于队列个数,队列满,进入等待
while (messageQueue.size() > max) {
try {
System.out.println(Thread.currentThread().getName() + " : queue is full ,waiting...");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果队列未满,生产消息,随后通知lock上的等待线程
//每一次的消息生产,都会通知消费者
System.out.println(Thread.currentThread().getName() + " : add a message");
messageQueue.addLast(message);
condition.signalAll();
} finally {
}
lock.unlock();
}
public void get() {
lock.lock();
try {
//如果队列为空,进入等待,无法获取消息
while (messageQueue.isEmpty()) {
try {
System.out.println(Thread.currentThread().getName() + " : queue is empty ,waiting...");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//队列非空时,读取消息,随后通知lock上的等待线程
//每一次的消息读取,都会通知生产者
System.out.println(Thread.currentThread().getName() + " : get a message");
messageQueue.removeFirst();
condition.signalAll();
} finally {
lock.unlock();
}
}
}
}
复制代码
改变的核心逻辑就是锁和条件变量
复制代码
/*
* 锁
* */
private final Lock lock = new ReentrantLock();
/**
* 条件变量
*/
private final Condition condition = lock.newCondition();
复制代码
  • 使用lock.lock();以及lock.unlock(); 替代了synchronized(lock)
  • 使用condition的await和signalAll方法替代了lock.wait()和   lock.notifyAll
看起来与使用synchronized关键字好像差不多,这没什么毛病
显式锁的设计本来就是为了弥补隐式锁的,虽说不是说作为一种替代品,但是功能逻辑的相似性是必然的
注意到,使用条件变量,与隐式锁中都是只有一个监视器,所有的线程仍旧都是被唤醒
前面提到过,其实生产者应该唤醒消费者,消费者才应该唤醒生产者
是不是可以两个变量?
对于生产者来说,只要非满即可,如果满了等待,非满生产然后唤醒消费者
对于消费者来说,只要非空即可,如果空了等待,非空消费然后唤醒生产者
 
可以定义两个条件变量,如下所示完整代码
其实只是定义了两个监视器
复制代码
package test2;
import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class T27 {
  public static void main(String[] args) {
    final RefactorMessageQueue mq = new RefactorMessageQueue(5);
    System.out.println("***************task begin***************");
    //创建生产者线程并启动
    for (int i = 0; i < 20; i++) {
      new Thread(() -> {
        while (true) {
          mq.set(new Message());
        }
      }, "producer" + i).start();
    }
    //创建消费者线程并启动
    new Thread(() -> {
      while (true) {
        mq.get();
      }
    }, "consumer").start();
  }
  /**
  * 消息队列中存储的消息
  */
  static class Message {
  }
  /**
  * 消息队列
  */
  static class RefactorMessageQueue {
    /**
    * 队列最大值
    */
    private final int max;
    /*
    * 锁
    * */
    private final Lock lock = new ReentrantLock();
    /**
    * 条件变量,用于消费者,非空即可消费
    */
    private final Condition notEmptyCondition = lock.newCondition();
    /**
    * 条件变量,用于生产者,非满即可生产
    */
    private final Condition notFullCondition = lock.newCondition();
    /**
    * final确保发布安全
    */
    final LinkedList<Message> messageQueue = new LinkedList<>();
    /**
    * 构造函数默认队列大小为10
    */
    public RefactorMessageQueue() {
      max = 10;
    }
    /**
    * 构造函数设置队列大小
    */
    public RefactorMessageQueue(int x) {
      max = x;
    }
    public void set(Message message) {
      lock.lock();
      try {
        //如果已经大于队列个数,队列满,进入等待
        while (messageQueue.size() > max) {
          try {
            System.out.println(Thread.currentThread().getName() + " : queue is full ,waiting...");
            //如果满了,生产者在“非满”这个条件上等待
            notFullCondition.await();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
        //如果队列未满,生产消息,随后通知lock上的等待线程
        //每一次的消息生产,都会通知消费者
        System.out.println(Thread.currentThread().getName() + " : add a message");
        messageQueue.addLast(message);
        //生产后,增加了消息,非空条件满足,需要唤醒消费者
        notEmptyCondition.signalAll();
      } finally {
      }
      lock.unlock();
    }
    public void get() {
      lock.lock();
      try {
        //如果队列为空,进入等待,无法获取消息
        while (messageQueue.isEmpty()) {
          try {
            System.out.println(Thread.currentThread().getName() + " : queue is empty ,waiting...");
            //如果空了,消费者需要在“非空”条件上等待
            notEmptyCondition.await();
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
        }
        //队列非空时,读取消息,随后通知lock上的等待线程
        //每一次的消息读取,都会通知生产者
        System.out.println(Thread.currentThread().getName() + " : get a message");
        messageQueue.removeFirst();
        //消费后,减少了消息,所以非满条件满足,需要唤醒生产者
        notFullCondition.signalAll();
      } finally {
        lock.unlock();
      }
    }
  }
}
复制代码

总结

通过上面的示例,应该可以理解显式锁的思路
他与隐式锁并没有像名称上看起来这么对立(一个隐 一个显),他们的核心仍旧是为了解决线程的同步与线程间的通信协作
线程同步与通信的在Java中的底层核心概念为锁和监视器
不管是synchronized还是Lock,不管是Object提供的通信方法还是Condition中的方法,都还是围绕着锁和监视器的概念展开的
如同平时写代码,同样的功能,可能会有多种实现方式,显式锁和隐式锁也是类似的,他们的实现有着很多的不同,也都有各种利弊
所以才会有隐式锁和显式锁,在程序中很难找到“放之四海而皆准”的实现代码,所以才会有各种各样的解决方案
尽管早期synchronized关键字性能比较低,但是随着版本的升级,性能也有了很大的改善
所以官方也是建议如果场景满足,还是尽可能使用synchronized关键字而不是显式锁
显式锁是为了解决隐式锁而不好解决的一些场景而存在的,尽管本文并没有体现出来他们之间的差异(本文恰恰相反,对相同点进行了介绍)
但是显式锁有很多隐式锁不存在的优点,后续慢慢介绍,通过本文希望理解,显式锁也只是线程同步与协作通信的一种实现途径而已
相关实践学习
RocketMQ一站式入门使用
从源码编译、部署broker、部署namesrv,使用java客户端首发消息等一站式入门RocketMQ。
消息队列 MNS 入门课程
1、消息队列MNS简介 本节课介绍消息队列的MNS的基础概念 2、消息队列MNS特性 本节课介绍消息队列的MNS的主要特性 3、MNS的最佳实践及场景应用 本节课介绍消息队列的MNS的最佳实践及场景应用案例 4、手把手系列:消息队列MNS实操讲 本节课介绍消息队列的MNS的实际操作演示 5、动手实验:基于MNS,0基础轻松构建 Web Client 本节课带您一起基于MNS,0基础轻松构建 Web Client
相关文章
|
1天前
|
缓存 Java
【Java基础】简说多线程(上)
【Java基础】简说多线程(上)
5 0
|
1天前
|
Dubbo Java 应用服务中间件
Java从入门到精通:3.2.2分布式与并发编程——了解分布式系统的基本概念,学习使用Dubbo、Spring Cloud等分布式框架
Java从入门到精通:3.2.2分布式与并发编程——了解分布式系统的基本概念,学习使用Dubbo、Spring Cloud等分布式框架
|
1天前
|
并行计算 算法 安全
Java从入门到精通:2.1.3深入学习Java核心技术——掌握Java多线程编程
Java从入门到精通:2.1.3深入学习Java核心技术——掌握Java多线程编程
|
1天前
|
安全 Java 编译器
是时候来唠一唠synchronized关键字了,Java多线程的必问考点!
本文简要介绍了Java中的`synchronized`关键字,它是用于保证多线程环境下的同步,解决原子性、可见性和顺序性问题。从JDK1.6开始,synchronized进行了优化,性能得到提升,现在仍可在项目中使用。synchronized有三种用法:修饰实例方法、静态方法和代码块。文章还讨论了synchronized修饰代码块的锁对象、静态与非静态方法调用的互斥性,以及构造方法不能被同步修饰。此外,通过反汇编展示了`synchronized`在方法和代码块上的底层实现,涉及ObjectMonitor和monitorenter/monitorexit指令。
6 0
|
1天前
|
监控 安全 Java
在Java中如何优雅的停止一个线程?可别再用Thread.stop()了!
在Java中如何优雅的停止一个线程?可别再用Thread.stop()了!
7 2
|
1天前
|
Java 调度
Java面试必考题之线程的生命周期,结合源码,透彻讲解!
Java面试必考题之线程的生命周期,结合源码,透彻讲解!
8 1
|
1天前
|
安全 Java
Java基础教程(15)-多线程基础
【4月更文挑战第15天】Java内置多线程支持,通过Thread类或Runnable接口实现。线程状态包括New、Runnable、Blocked、Waiting、Timed Waiting和Terminated。启动线程调用start(),中断线程用interrupt(),同步用synchronized关键字。线程安全包如java.util.concurrent提供并发集合和原子操作。线程池如ExecutorService简化任务管理,Callable接口允许返回值,Future配合获取异步结果。Java 8引入CompletableFuture支持回调。
|
4月前
|
存储 监控 安全
Java虚拟机的锁优化策略
Java虚拟机的锁优化策略
28 0
|
7月前
|
Java 编译器
解密Java多线程中的锁机制:CAS与Synchronized的工作原理及优化策略
解密Java多线程中的锁机制:CAS与Synchronized的工作原理及优化策略
|
11天前
|
Java 程序员 编译器
Java中的线程同步与锁优化策略
【4月更文挑战第14天】在多线程编程中,线程同步是确保数据一致性和程序正确性的关键。Java提供了多种机制来实现线程同步,其中最常用的是synchronized关键字和Lock接口。本文将深入探讨Java中的线程同步问题,并分析如何通过锁优化策略提高程序性能。我们将首先介绍线程同步的基本概念,然后详细讨论synchronized和Lock的使用及优缺点,最后探讨一些锁优化技巧,如锁粗化、锁消除和读写锁等。