Java中关键字synchronized的使用方法详解
作者:超新星燃烧 发布时间:2022-04-14 06:18:54
synchronized是Java里的一个关键字,起到的一个效果是“监视器锁”~~,它的功能就是保证操作的原子性,同时禁止指令重排序和保证内存的可见性!
public class TestDemo {
static class Counter{
public int count = 0;
public void add(){
count++;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
counter.add();
}
}
};
Thread t2 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
counter.add();
}
}
};
//启动两个线程
t1.start();
t2.start();
//等待两个线程结束
t1.join();
t2.join();
System.out.println(counter.count);
}
}
此时的线程就是不安全的,如何解决呢?
给我们的Counter对象里的add方法加上synchronized关键字,针对这个方法进行了加锁操作。进入代码块(调用方法)自动加锁,出了代码块(方法结束),自动解锁。
public class TestDemo {
static class Counter{
public int count = 0;
//修饰方法
synchronized public void add{
count++;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
counter.add();
}
}
};
Thread t2 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
counter.add();
}
}
};
//启动两个线程
t1.start();
t2.start();
//等待两个线程结束
t1.join();
t2.join();
System.out.println(counter.count);
}
}
那么这里的代码是如何保证正确的呢?
使用synchronized 就相当于在我们执行的指令里又加入了2条新指令。
LOCK (加锁)
UNLOCK (解锁)
LOCK操作特性:只有一个线程能执行成功!如果第一个线程执行成功,第二个线程也尝试LOCK,就会阻塞等待,直到第一个线程执行UNLOCK 释放锁~
通过LOCK和UNLOCK 就把 LOAD ADD SAVE 这三个指令,给打包成了一个原子的操作(中间不能被打断,也不能被其他线程穿插)。
这里的加锁也是保证原子性的核心操作,所以线程里的没组指令就会顺序执行,不在穿插执行,就保证了线程1执行完之后再去执行线程2。
举个例子:
就好比张三和李四去ATM里去取钱,当张三进去取钱时,进去后就会锁门,李四就会在外面等待,直到张三取完钱出来后,李四在进去取钱。
synchronized 也会禁止编译器进行“内存可见性”和“指令重排序”的优化~ 同时程序运行的效率就会降低,
也会导致线程之间相互去等待,就涉及到系统的一些调度,也会引入一些时间成本。
synchronized修饰的对象有以下几种:
修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
public class TestDemo{
public void methond() {
// 进入代码块会锁 this 指向对象中的锁;
// 出代码块会释放 this 指向的对象中的锁
synchronized (this) {
}
}
public static void main(String[] args) {
TestDemo demo = new TestDemo();
demo.methond();
}
}
修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
public class TestDemo{
public synchronized void methond() {
}
public static void main(String[] args) {
TestDemo demo = new TestDemo();
demo.methond();
// 进入方法会锁 demo 指向对象中的锁;
// 出方法会释放 demo 指向的对象中的锁
}
}
修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
public class TestDemo{
public synchronized static void methond() {
}
public static void main(String[] args) {
methond();
// 进入方法会锁 TestDemo.class 指向对象中的锁;
//出方法会释放 TestDemo.class 指向的对象中的锁
}
}
修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
public class TestDemo{
public static void methond() {
// 进入代码块会锁 TestDemo.class 指向对象中的锁;
//出代码块会释放 TestDemo.class 指向的对象中的锁
synchronized (TestDemo.class) {
}
}
public static void main(String[] args) {
TestDemo demo = new TestDemo();
demo.methond();
}
}
总结:
无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;
如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
拓展:
public class TestDemo {
static class Counter{
public int count = 0;
public void add(){
synchronized (this){
count++;
}
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
synchronized (counter){
counter.add();
}
}
}
};
Thread t2 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
synchronized (counter){
counter.add();
}
}
}
};
//启动两个线程
t1.start();
t2.start();
//等待两个线程结束
t1.join();
t2.join();
System.out.println(counter.count);
}
}
此时可以看出上述代码,加了两次锁,会发生什么呢?
但是运行代码发现程序依然正确运行?? 为什么
但是上述分析死锁的思路是对的
只是因为synchronized内部使用特殊手段来处理了这种情况 。
这样的操作特性我们叫做 可重入锁
synchronized 内部记录了当前这把锁是哪个线程持有的~
如果当前加锁线程和持有锁的线程是同一个线程~
此时就并不是真的进行“加锁操作”,而是把一个计数器加一;
如果后续该线程继续尝试获取锁,继续判定加锁线程和持有锁线程是不是同一个线程,只要是同一个线程,就不真的加锁,而是计数器+1;
如果该线程调用解锁操作,也不是立刻就解锁,而是计数器减1
直到计数器减成0了,才认为真的要“释放锁”,才允许其他线程来获取锁~~
来源:https://blog.csdn.net/wlm123666/article/details/119580473


猜你喜欢
- 相信最近看过我的文章的朋友对于Microsoft.Extensions.ObjectPool不陌生;复用、池化是在很多高性能场景的优化技巧,
- 在许多Java面试中,我们经常会看到关于Java类加载机制的考察,例如下面这道题:class Grandpa{static{System.o
- Collection是最基本的集合接口,一个Collection代表一组Object,即Collection的元素(Elements)。一些
- ArrrayList是Java中经常被用到的集合,弄清楚它的底层实现,有利于我们更好地使用它。下图是ArrayList的UML图从图中我们可
- Spring的七个核心模块,供大家参考,具体内容如下1、Spring core:核心容器核心容器提供spring框架的基本功能。Spring
- 闹钟的简单实现,只有显示时间和设置闹钟。AlarmViewpackage com.example.lenovo.clock2; import
- 前言我们在使用spring security的时候可以通过好几种方法获取用户信息, 但是今天这篇文章介绍的是一个笔者觉得最优雅的实现; 借鉴
- 简单介绍 多个线程可以通过调用ManualResetEvent对象的WaitOne方法进入等
- 前言 前一段时间得闲的时候优化了一下我之前的轮子[DotNetCoreRpc]小框架,其中主要
- 一、项目简述本系统功能包括:数据统计、收件录入、发件录入、到件录入、派件录入、问题件录入、退件录入、留仓录入、装车录入、发车录入、到车录入、
- 在springboot的开发中,有时候我们会有不同的配置,例如日志打印,数据库连接等,开发,测试,生产每个环境可能配置都不一致,还好,spr
- 前两天给同事做 code review,感觉自己对 Java 的 Generics 掌握得不够好,便拿出 《Effective Java》1
- 文件上传页面<%@ page language="java" contentType="text/htm
- java volatile关键字在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多
- 前言通过ioctl跟binder驱动交互,实现以最快的方式唤醒新的保活服务,最大程度防止保活失败。同时,我也将跟您分享,我是怎么做到在不甚了
- 一、将已经编译后的java中Class文件进行打包;打包命令JAR 如:将某目录下的所有class文件夹全部进行打包处理;使用的命令:jar
- OAuth是一个关于授权(authorization)的开放网络标准,在全世界得到广泛应用,目前的版本是2.0版。本文对OAuth 2.0的
- Cloneable这个接口设计得十分奇葩,不符合正常人的使用习惯,然而用这个接口的人很多也很有必要,所以还是有必要了解一下这套扭曲的机制。以
- 文件上传也是常见的功能,趁着周末,用Spring boot来实现一遍。前端部分前端使用jQuery,这部分并不复杂,jQuery可以读取表单
- 前言前几天在技术群里看到有同学在讨论关于dynamic是否会存在装箱拆箱的问题,我当时第一想法是"会"。至于为啥会有很多