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
相关文章
|
22小时前
|
安全 Java 调度
Java线程:深入理解与实战应用
Java线程:深入理解与实战应用
7 0
|
5天前
|
安全 Java
深入理解 Java 多线程和并发工具类
【4月更文挑战第19天】本文探讨了Java多线程和并发工具类在实现高性能应用程序中的关键作用。通过继承`Thread`或实现`Runnable`创建线程,利用`Executors`管理线程池,以及使用`Semaphore`、`CountDownLatch`和`CyclicBarrier`进行线程同步。保证线程安全、实现线程协作和性能调优(如设置线程池大小、避免不必要同步)是重要环节。理解并恰当运用这些工具能提升程序效率和可靠性。
|
5天前
|
安全 Java
java多线程(一)(火车售票)
java多线程(一)(火车售票)
|
6天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
6天前
|
安全 Java 程序员
Java中的多线程并发编程实践
【4月更文挑战第18天】在现代软件开发中,为了提高程序性能和响应速度,经常需要利用多线程技术来实现并发执行。本文将深入探讨Java语言中的多线程机制,包括线程的创建、启动、同步以及线程池的使用等关键技术点。我们将通过具体代码实例,分析多线程编程的优势与挑战,并提出一系列优化策略来确保多线程环境下的程序稳定性和性能。
|
14天前
|
存储 Java 数据库连接
java多线程之线程通信
java多线程之线程通信
|
25天前
|
存储 缓存 NoSQL
Redis单线程已经很快了6.0引入多线程
Redis单线程已经很快了6.0引入多线程
31 3
|
28天前
|
消息中间件 安全 Linux
线程同步与IPC:单进程多线程环境下的选择与权衡
线程同步与IPC:单进程多线程环境下的选择与权衡
57 0
|
1月前
|
Java 调度 C#
C#学习系列相关之多线程(一)----常用多线程方法总结
C#学习系列相关之多线程(一)----常用多线程方法总结
|
1月前
|
安全 编译器 C#
C#学习相关系列之多线程---lock线程锁的用法
C#学习相关系列之多线程---lock线程锁的用法

热门文章

最新文章