临界区与锁并发编程中不可避免的不可避免线程共享同一个资源

关键部分和锁

在并发编程中,多个线程共享同一个资源是不可避免的。为了防止数据不一致的发生,人们引入了临界区的概念。临界区是用于访问共享资源的代码块,一次只有一个运行的线程进入它。

那么如何实现这个临界区呢?这使用了我们的锁。当一个进程想要访问一个临界区时,它首先会检查是否有另一个线程进入,也就是看它是否可以获取锁。如果没有其他线程进入,则进入临界区,其他线程无法进入,相当于加锁。反而会被挂起,等待其他线程离开临界区,被JVM选中该线程进入(因为可能还有其他线程在等待)。

使用 Synchronized 解决并发问题

Synchronize 是一种重量级锁,会降低程序性能,所以如果对数据一致性没有要求就不要使用。如果使用 Synchronize 关键字声明方法同步代码块的锁是什么,则该方法的代码块被视为临界区。当线程调用对象的同步方法或访问同步代码块时,线程获取对象的锁,其他线程暂时无法访问该方法。对象上的锁会在其他线程执行该方法或代码块之前被释放。

接下来我们将创建两个线程 A 和 B 来同时访问一个对象:A 从账户中取款,B 从账户中存款。首先是不要使用 Synchronized 关键字。

创建账户类

它有一个私有变量 balance 表示金额,addAmount 和subtractAmount 分别对金额进行加减运算。

创建A线程和B线程,分别从账户中存款和取款。

最后在 main 中测试

ThreadA 进行了 10 次入账操作,每次存入 1000 元,而 ThreadB 进行了 10 次相同金额的提款操作。那么根据我们的推测,最终账户的金额应该保持不变,但程序的结果并不是我们想要的数字。为什么是这样?因为我们在对数据进行操作的时候,可能还有另一个线程在操作,逻辑上应该一个接一个地执行的方法变成同时执行了,所以会出错。

现在我们在addAmount和subtractAmount中加入synchronized关键字,保证数据的一致性,这样程序就不会出问题了。

如果您使用同步来保护代码块,则需要将对象引用作为参数传递。一般来说同步代码块的锁是什么,传入 this 关键字作为对执行该方法的对象的引用就足够了。

究竟什么是锁?

可能上面的例子中你因为不小心只给其中一个方法添加了关键字,那么你就会看到这种现象:

如果你想在保护代码块中传入一个对象,它应该是应该被锁定的对象。你可能会想:我执行了subtractAmout,它应该等我执行addAmount 后才执行。它对帐户对象没有锁定,因此不应将其插入中间。但是,只有在方法加了锁时,线程在执行该方法时,才会尝试获取锁,看线程是否进入临界区。无需获取锁即可访问非同步方法。如果删除同步,则与仅添加一个相同。同步方法遵循与异步方法不同的规则。也就是说,你可以在调用对象的同步方法的同时,调用其他的异步方法。

两个线程如何同时访问同一个对象的两个同步方法?

在摆弄这个关键字时,您可能会惊讶地发现静态方法有多么不同。如果对象中的静态方法被同步修改,其他线程可以在访问静态方法的同时访问对象中的非静态方法(当然,静态方法一次只能被一个线程访问)。也就是说,两个线程可以同时访问一个对象中的两个同步方法。

等等,你不是说锁定对象吗?究竟锁定了什么?锁确实是对象,但是对于我们说的静态方法T.class(T是类名),非静态方法锁就是this,也就是类的实例对象,两者是不一样的.

上面的代码等价于:

其实加锁的本质就是在加锁对象的对象头中写入当前线程id。我们可以用下面的代码进行验证,每次都传入new Object()。

因为线程每次调用方法都会锁定新对象,所以锁定是无效的。甚至编译器也可能优化掉同步,因为这相当于用多个锁保护同一个资源。当编译器看到大家都加了锁,那我还不如不加,反正都是一样的。

另外需要注意的是 synchronized 是一个可重入锁。也就是说,线程在访问对象的同步方法时,调用其他同步方法时不需要获取它的访问权限。因为我们实际上是锁定了对象,所以当前线程的ID记录在对象头中。

总结

修改函数,锁住当前类的实例化对象修改静态方法,锁住当前类的Class对象修改同步代码块,锁住括号内的对象,锁住锁的对象头目的。中写入当前线程id,每个线程要调用这个同步方法,都会先去锁对象的对象头查看当前线程id是否是自己的。

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论