在以前的文章中,我们回顾了在不同线程之间共享数据的一些主要风险(例如原子性和可见性 )以及如何设计类以安全地共享( 线程安全的设计 )。 但是,在许多情况下,我们将需要共享可变数据,其中一些线程将写入而其他线程将充当读取器。 可能的情况是,您只有一个域,与其他域无关,需要在不同线程之间共享。 在这种情况下,您可以使用原子变量。 对于更复杂的情况,您将需要同步。

1.咖啡店的例子

让我们从一个简单的示例开始,例如CoffeeStore 。 此类开设了一家商店,客户可以在此购买咖啡。 客户购买咖啡时,会增加一个计数器,以跟踪所售商品的数量。 商店还注册谁是最后一个来商店的客户。

public class CoffeeStore {private String lastClient;private int soldCoffees;private void someLongRunningProcess() throws InterruptedException {Thread.sleep(3000);}public void buyCoffee(String client) throws InterruptedException {someLongRunningProcess();lastClient = client;soldCoffees++;System.out.println(client + " bought some coffee");}public int countSoldCoffees() {return soldCoffees;}public String getLastClient() {return lastClient;}
}

在以下程序中,四个客户决定来商店购买咖啡:

public static void main(String[] args) throws InterruptedException {CoffeeStore store = new CoffeeStore();Thread t1 = new Thread(new Client(store, "Mike"));Thread t2 = new Thread(new Client(store, "John"));Thread t3 = new Thread(new Client(store, "Anna"));Thread t4 = new Thread(new Client(store, "Steve"));long startTime = System.currentTimeMillis();t1.start();t2.start();t3.start();t4.start();t1.join();t2.join();t3.join();t4.join();long totalTime = System.currentTimeMillis() - startTime;System.out.println("Sold coffee: " + store.countSoldCoffees());System.out.println("Last client: " + store.getLastClient());System.out.println("Total time: " + totalTime + " ms");
}private static class Client implements Runnable {private final String name;private final CoffeeStore store;public Client(CoffeeStore store, String name) {this.store = store;this.name = name;}@Overridepublic void run() {try {store.buyCoffee(name);} catch (InterruptedException e) {System.out.println("interrupted sale");}}
}

主线程将使用Thread.join()等待所有四个客户端线程完成。 一旦客户离开,我们显然应该算出我们商店中售出的四种咖啡,但是您可能会得到与上面的类似的意外结果:

Mike bought some coffee
Steve bought some coffee
Anna bought some coffee
John bought some coffee
Sold coffee: 3
Last client: Anna
Total time: 3001 ms

我们丢了一杯咖啡,最后一位客户(John)也不是那一位(Anna)。 原因是由于我们的代码未同步,因此线程交错。 我们的buyCoffee操作应设为原子操作。

2.同步如何工作

同步块是由锁保护的代码区域。 当线程进入同步块时,它需要获取其锁,并且一旦获取,它就不会释放它,直到退出该块或引发异常。 这样,当另一个线程尝试进入同步块时,只有所有者线程释放它后,它才能获取其锁。 这是Java机制,可确保仅在给定时间在线程上执行同步的代码块,从而确保该块内所有动作的原子性。

好的,所以您使用锁来保护同步块,但是什么是锁? 答案是任何Java对象都可以用作锁,称为内在锁。 现在,我们将看到使用同步时这些锁的一些示例。

3.同步方法

同步方法由两种类型的锁保护:

  • 同步实例方法 :隐式锁定为“ this”,这是用于调用该方法的对象。 此类的每个实例将使用自己的锁。
  • 同步静态方法 :锁是Class对象。 此类的所有实例将使用相同的锁。

像往常一样,用一些代码可以更好地看到这一点。

首先,我们将同步一个实例方法。 它的工作方式如下:我们有两个线程(线程1和线程2)共享该类的一个实例,而另一个线程(线程3)使用了另一个实例:

public class InstanceMethodExample {private static long startTime;public void start() throws InterruptedException {doSomeTask();}public synchronized void doSomeTask() throws InterruptedException {long currentTime = System.currentTimeMillis() - startTime;System.out.println(Thread.currentThread().getName() + " | Entering method. Current Time: " + currentTime + " ms");Thread.sleep(3000);System.out.println(Thread.currentThread().getName() + " | Exiting method");}public static void main(String[] args) {InstanceMethodExample instance1 = new InstanceMethodExample();Thread t1 = new Thread(new Worker(instance1), "Thread-1");Thread t2 = new Thread(new Worker(instance1), "Thread-2");Thread t3 = new Thread(new Worker(new InstanceMethodExample()), "Thread-3");startTime = System.currentTimeMillis();t1.start();t2.start();t3.start();}private static class Worker implements Runnable {private final InstanceMethodExample instance;public Worker(InstanceMethodExample instance) {this.instance = instance;}@Overridepublic void run() {try {instance.start();} catch (InterruptedException e) {System.out.println(Thread.currentThread().getName() + " interrupted");}}}
}

由于doSomeTask方法是同步的,因此您希望在给定的时间只有一个线程将执行其代码。 但这是错误的,因为它是一个实例方法。 不同的实例将使用不同的锁,如输出所示:

Thread-1 | Entering method. Current Time: 0 ms
Thread-3 | Entering method. Current Time: 1 ms
Thread-3 | Exiting method
Thread-1 | Exiting method
Thread-2 | Entering method. Current Time: 3001 ms
Thread-2 | Exiting method

由于线程1和线程3使用不同的实例(因此使用了不同的锁),因此它们都同时进入该块。 另一方面,线程2使用与线程1相同的实例(和锁)。 因此,它必须等到线程1释放锁。

现在,让我们更改方法签名并使用静态方法。 除以下行外, StaticMethodExample具有相同的代码:

public static synchronized void doSomeTask() throws InterruptedException {

如果执行main方法,将得到以下输出:

Thread-1 | Entering method. Current Time: 0 ms
Thread-1 | Exiting method
Thread-3 | Entering method. Current Time: 3001 ms
Thread-3 | Exiting method
Thread-2 | Entering method. Current Time: 6001 ms
Thread-2 | Exiting method

由于同步方法是静态的,因此它由Class对象锁保护。 尽管使用了不同的实例,所有线程仍需要获取相同的锁。 因此,任何线程都必须等待上一个线程释放锁。

4.回到咖啡店的例子

我现在修改了Coffee Store示例,以使其方法同步。 结果如下:

public class SynchronizedCoffeeStore {private String lastClient;private int soldCoffees;private void someLongRunningProcess() throws InterruptedException {Thread.sleep(3000);}public synchronized void buyCoffee(String client) throws InterruptedException {someLongRunningProcess();lastClient = client;soldCoffees++;System.out.println(client + " bought some coffee");}public synchronized int countSoldCoffees() {return soldCoffees;}public synchronized String getLastClient() {return lastClient;}
}

现在,如果我们执行该程序,我们将不会失去任何销售:

Mike bought some coffee
Steve bought some coffee
Anna bought some coffee
John bought some coffee
Sold coffee: 4
Last client: John
Total time: 12005 ms

完善! 好吧,真的是吗? 现在程序的执行时间为12秒。 您肯定已经注意到在每次销售期间都会执行someLongRunningProcess方法。 它可以是与销售无关的操作,但是由于我们同步了整个方法,所以现在每个线程都必须等待它执行。 我们可以将这段代码放在同步块之外吗? 当然! 下一节将介绍同步块。

5.同步块

上一节向我们展示了我们可能并不总是需要同步整个方法。 由于所有同步代码都强制对所有线程执行进行序列化,因此我们应最小化同步块的长度。 在我们的咖啡店示例中,我们可以省去长时间运行的过程。 在本节的示例中,我们将使用同步块:

在SynchronizedBlockCoffeeStore中 ,我们修改buyCoffee方法,以将长时间运行的进程排除在同步块之外:

public void buyCoffee(String client) throws InterruptedException {someLongRunningProcess();synchronized(this) {lastClient = client;soldCoffees++;System.out.println(client + " bought some coffee");}
}public synchronized int countSoldCoffees() {return soldCoffees;}public synchronized String getLastClient() {return lastClient;}

在上一个同步块中,我们将“ this”用作其锁。 它与同步实例方法中的锁相同。 当心使用另一个锁,因为我们正在此类的其他方法( countSoldCoffeesgetLastClient )中使用此锁。

让我们看看执行修改后的程序的结果:

Mike bought some coffee
John bought some coffee
Anna bought some coffee
Steve bought some coffee
Sold coffee: 4
Last client: Steve
Total time: 3015 ms

在保持代码同步的同时,我们大大减少了程序的时间。

6.使用私人锁

上一节对实例对象使用了锁定,但是您可以将任何对象用作其锁定。 在本节中,我们将使用私人锁,看看使用私人锁会有什么风险。

在PrivateLockExample中 ,我们有一个由私有锁(myLock)保护的同步块:

public class PrivateLockExample {private Object myLock = new Object();public void executeTask() throws InterruptedException {synchronized(myLock) {System.out.println("executeTask - Entering...");Thread.sleep(3000);System.out.println("executeTask - Exiting...");}}
}

如果一个线程进入executeTask方法将获取myLock锁。 在由相同的myLock锁保护的此类中进入其他方法的任何其他线程都必须等待才能获取它。

但是现在,让我们想象一下,有人想要扩展此类以添加自己的方法,并且由于需要使用相同的共享数据,因此这些方法也需要同步。 由于该锁在基类中是私有的,因此扩展类将无法访问它。 如果扩展类同步其方法,则将通过“ this”进行保护。 换句话说,它将使用另一个锁。

MyPrivateLockExample扩展了上一个类,并添加了自己的同步方法executeAnotherTask

public class MyPrivateLockExample extends PrivateLockExample {public synchronized void executeAnotherTask() throws InterruptedException {System.out.println("executeAnotherTask - Entering...");Thread.sleep(3000);System.out.println("executeAnotherTask - Exiting...");}public static void main(String[] args) {MyPrivateLockExample privateLock = new MyPrivateLockExample();Thread t1 = new Thread(new Worker1(privateLock));Thread t2 = new Thread(new Worker2(privateLock));t1.start();t2.start();}private static class Worker1 implements Runnable {private final MyPrivateLockExample privateLock;public Worker1(MyPrivateLockExample privateLock) {this.privateLock = privateLock;}@Overridepublic void run() {try {privateLock.executeTask();} catch (InterruptedException e) {e.printStackTrace();}}}private static class Worker2 implements Runnable {private final MyPrivateLockExample privateLock;public Worker2(MyPrivateLockExample privateLock) {this.privateLock = privateLock;}@Overridepublic void run() {try {privateLock.executeAnotherTask();} catch (InterruptedException e) {e.printStackTrace();}}}
}

该程序使用两个工作线程,分别执行executeTaskexecuteAnotherTask 。 输出显示线程如何交错,因为它们没有使用相同的锁:

executeTask - Entering...
executeAnotherTask - Entering...
executeAnotherTask - Exiting...
executeTask - Exiting...

7.结论

我们已经使用Java的内置锁定机制回顾了内部锁定的使用。 这里的主要关注点是需要使用共享数据的同步块。 必须使用相同的锁。

这篇文章是Java Concurrency Tutorial系列的一部分。 检查此处以阅读本教程的其余部分。

  • 您可以在Github上找到源代码。

翻译自: https://www.javacodegeeks.com/2014/09/java-concurrency-tutorial-locking-intrinsic-locks.html

Java并发教程–锁定:内在锁相关推荐

  1. Java并发教程–重入锁

    Java的synced关键字是一个很棒的工具–它使我们能够以一种简单可靠的方式来同步对关键部分的访问,而且也不难理解. 但是有时我们需要对同步进行更多控制. 我们要么需要分别控制访问类型(读取和写入) ...

  2. Java并发教程–锁定:显式锁定

    1.简介 在许多情况下,使用隐式锁定就足够了. 有时,我们将需要更复杂的功能. 在这种情况下, java.util.concurrent.locks包为我们提供了锁定对象. 当涉及到内存同步时,这些锁 ...

  3. Java并发教程–信号量

    这是我们将要进行的Java并发系列的第一部分. 具体来说,我们将深入探讨Java 1.5及更高版本中内置的并发工具. 我们假设您对同步和易失性关键字有基本的了解. 第一篇文章将介绍信号量-特别是对信号 ...

  4. Java并发教程– CountDownLatch

    Java中的某些并发实用程序自然会比其他并发实用程序受到更多关注,因为它们可以解决通用问题而不是更具体的问题. 我们大多数人经常遇到执行程序服务和并发集合之类的事情. 其他实用程序不太常见,因此有时它 ...

  5. Java并发教程–阻塞队列

    如第3部分所述,Java 1.5中引入的线程池提供了核心支持,该支持很快成为许多Java开发人员的最爱. 在内部,这些实现巧妙地利用了Java 1.5中引入的另一种并发功能-阻塞队列. 队列 首先,简 ...

  6. Java并发教程–可调用,将来

    从Java的第一个发行版开始,Java的美丽之处之一就是我们可以轻松编写多线程程序并将异步处理引入我们的设计中. Thread类和Runnable接口与Java的内存管理模型结合使用,意味着可以进行简 ...

  7. Java并发教程–线程池

    Java 1.5中提供的最通用的并发增强功能之一是引入了可自定义的线程池. 这些线程池使您可以对诸如线程数,线程重用,调度和线程构造之类的东西进行大量控制. 让我们回顾一下. 首先,线程池. 让我们直 ...

  8. java 并发锁_Java并发教程–重入锁

    java 并发锁 Java的synced关键字是一个很棒的工具–它使我们可以通过一种简单可靠的方式来同步对关键部分的访问,而且也不难理解. 但是有时我们需要对同步进行更多控制. 我们要么需要分别控制访 ...

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

    java 并发多线程显式锁概念简介 什么是显式锁 多线程下篇(一) 目前对于同步,仅仅介绍了一个关键字synchronized,可以用于保证线程同步的原子性.可见性.有序性 对于synchronize ...

最新文章

  1. 使用KMeanCluster对多个区域进行聚类,并结合Matplotlib绘制中心点、最大最小距离点
  2. java 读取文件,内容方置Person 并写到另外地址
  3. mysql数据库自动转储_mysql数据库数据定时封装转储
  4. Xcode7 项目转 Xcode6 时 出现问题
  5. 44、生鲜电商平台-Java后端生成Token架构与设计详解
  6. mysql 存储过程调用权限消失的问题,恢复权限
  7. Kubernetes本地集群和Google Kubernetes Engine的区别
  8. jquery.uploadify参数
  9. C++|Qt工作笔记-C++获取当前系统时间,Qt获取当前系统时间及各标准间转化
  10. 第25月第3天 Mxshop项目记录01
  11. vc中操作Xml--使用CMarkup类
  12. POJ1209 UVA158 Calendar题解
  13. 质数c语言2357,用“2357”判断“100以内的质数合数”.doc
  14. Python3网络爬虫教程7——SSL数字证书
  15. Android - 一种相似图片搜索算法的实现
  16. ubuntu下exiftool安装
  17. C4996:#(The POSIX name for this item is deprecated. Instead, use the ISO C++ conformant name:)
  18. 分分钟带你学会DNS、WEB、DHCP服务器的搭建
  19. Ubuntu 5.0内核降级至4.0
  20. win7蓝屏_电脑蓝屏0x0000007b怎么稳定解决?

热门文章

  1. 银行营业网点管理系统——implt包(CityAreaDaoImpl )
  2. 2019蓝桥杯省赛---java---B---6(特别数的和)
  3. openglshader实现虚拟场景_opengl+shader
  4. rabbitmq-java生产者消费者
  5. java国际化——Locale+数字格式
  6. 有效期判断功能 java_有效的Java第三版有哪些新功能?
  7. jersey 入门示例_Jersey Web Service Hello World Java示例
  8. vaadin_Vaadin提示:以声明方式构建UI
  9. 本地缓存防止缓存击穿_防止缓存爆炸的快速提示
  10. 在Java中使用FileChannel和ByteBuffer对文件进行读写