Java并发教程–锁定:内在锁

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


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;
    }
    
    @Override
    public 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;
        }
        
        @Override
        public 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;
        }
        
        @Override
        public 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;
        }
        
        @Override
        public void run() {
            try {
                privateLock.executeAnotherTask();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

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

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

7.结论

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

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

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值