同步控制——synchronized关键字

同步是java中一个很重要的知识点,也是比较难以理解的知识点。经常在学会了,学忘了,学废了之间循环,抽点时间系统的整理一下同步的知识点,梳理一下逻辑,本文主要讲解Synchronized关键字
照顾下太长不看的童鞋,先把分析置顶:
synchronized相关

  1. .synchronized关键字在修饰代码块的this和变量的时候同步的作用域是同一个对象,如果创建两个对象的话synchronized关键字就不起作用了,不管是new关键字也好,花里胡哨的反射也好,都不顶用,除非两个对象在堆中指定的是同一个地址,比如Test a = new Test(); Test b = a; 这种情况下synchronized生效,亲测,篇幅问题不展示了,有兴趣可以自己试试。
  2. synchronized关键字修饰代码块修饰的是class对象的时候,不论修饰的是本类的class还是其他类的class,同一个实例对象的情况下synchronized都是起作用的,并且在用两个不同的实例对象分别调用方法的时候也可以起到同步的作用。
  3. synchronized修饰普通方法时同1,实际上是对本类的具体的一个对象锁定
  4. synchronized修饰静态方法时同2,实际上是对本类的class对象锁定

先看看这个synchronized关键字,synchronized有五种比较常用的方法

  1. 修饰代码块(this)
  2. 修饰代码块(变量)
  3. 修饰代码块(class)
  4. 修饰普通方法
  5. 修饰静态方法

1.修饰代码块(this)

先看没有同步的时候的代码和效果

public class SynchronizedTest {

    public void testSynchronized(){
        try {
       		//synchronized (this) {
            System.out.println("before test synchronized");
            Thread.sleep(5000);
            System.out.println("do test synchronized");
            //}
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void testSynchronizedDelay(){
        try {
        	//synchronized (this) {
            System.out.println("before test synchronizedDelay");
            Thread.sleep(10000);
            System.out.println("do test synchronizedDelay");
            //}
        }catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        //最普通的执行
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("test synchronized start");
                synchronizedTest.testSynchronized();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("test synchronizedDelay start");
                synchronizedTest.testSynchronizedDelay();
            }
        }).start();
    }
}

输出结果

test synchronized start
before test synchronized
test synchronizedDelay start
before test synchronizedDelay       //此处过了5秒打印下一句
do test synchronized   				//这里又过了5秒打印下一句
do test synchronizedDelay			//总共用时10秒

没有做过同步处理的方法,两个线程异步同时执行,互不干扰
把上面代码的两处注释打开,被synchronized修饰部分代码块之后的效果

public class SynchronizedTest {

    public void testSynchronized(){
        try {
            synchronized (this) {
                System.out.println("before test synchronized");
                Thread.sleep(5000);
                System.out.println("do test synchronized");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void testSynchronizedDelay(){
        try {
            synchronized (this) {
                System.out.println("before test synchronizedDelay");
                Thread.sleep(10000);
                System.out.println("do test synchronizedDelay");
            }
        }catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        //最普通的执行
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("test synchronized start");
                synchronizedTest.testSynchronized();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("test synchronizedDelay start");
                synchronizedTest.testSynchronizedDelay();
            }
        }).start();
    }
}

执行结果

1.test synchronized start
2.before test synchronized				
3.test synchronizedDelay start			
4.do test synchronized					
5.before test synchronizedDelay			
6.do test synchronizedDelay				

对应序号
1.线程1开始运行
2.线程1进入同步代码块
3.线程2开始运行,运行到同步代码块开始阻塞
4.线程1运行结束,退出同步代码块,此处用时5秒
5.线程2进入同步代码块,此时线程1已经结束
6.线程2退出同步代码块,此时总用时15秒
修改上述的main方法,创建两个不同的对象

public static void main(String[] args) {
        //最普通的执行
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        SynchronizedTest synchronizedTest1 = new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("test synchronized start");
                synchronizedTest.testSynchronized();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("test synchronizedDelay start");
                synchronizedTest1.testSynchronizedDelay();
            }
        }).start();
    }

执行结果

test synchronized start
before test synchronized
test synchronizedDelay start
before test synchronizedDelay
do test synchronized
do test synchronizedDelay

可以看到,如果是两个不同的对象,synchronized是不起作用的。

2.修饰代码块(变量)

不做同步的效果就不再展示了,反正都一样,下面来看一下synchronized修饰过变量之后的结果

public void testSynchronizedWithAttribute(){
        try {
            synchronized (tipStr) {
                System.out.println("before test synchronized");
                Thread.sleep(5000);
                System.out.println("do test synchronized");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    public void testSynchronizedWithAttributeDelay(){
        try {
            synchronized (tipStr) {
                System.out.println("before test synchronizedDelay");
                Thread.sleep(10000);
                System.out.println("do test synchronizedDelay");
            }
        }catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("test synchronized start");
                synchronizedTest.testSynchronizedWithAttribute();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("test synchronizedDelay start");
                synchronizedTest.testSynchronizedWithAttributeDelay();
            }
        }).start();
    }

运行结果
test synchronized start
before test synchronized
test synchronizedDelay start
do test synchronized
before test synchronizedDelay
do test synchronizedDelay
可以看到,synchronized关键字起作用了,实际上,synchronized修饰成员变量的时候和效果和修饰代码块是一样的。不过就是synchronized括号中的一个是this,一个是具体的变量,但是作用范围都是本类具体的某一个实例。在修饰成员变量的时候,如果和上面代码块一样创建了两个实例,synchronized关键字也是失效的。
不过有一种特殊情况,如果成员变量是基本类型,比如int,long这种,用synchronized修饰这些变量是会报错的。
在这里插入图片描述
synchronized括号中的变量必须是引用类型。如果需要修饰数字类的成员变量的话,可以使用Integer等基本类型的扩展类。

3.修饰代码块(class)

synchronized修饰代码块还有一种用法是修饰类的class属性,首先是修饰本类的class

public void testSynchronizedWithClass(){
        try {
        	//1
            synchronized (SynchronizedTest.class) {   
                System.out.println("before test synchronized");
                Thread.sleep(5000);
                System.out.println("do test synchronized");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    public void testSynchronizedWithClassDelay(){
        try {
            synchronized (SynchronizedTest.class) {
                System.out.println("before test synchronizedDelay");
                Thread.sleep(10000);
                System.out.println("do test synchronizedDelay");
            }
        }catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("test synchronized start");
                synchronizedTest.testSynchronizedWithClass();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("test synchronizedDelay start");
                synchronizedTest.testSynchronizedWithClassDelay();
            }
        }).start();
    }

执行结果

test synchronized start
before test synchronized
test synchronizedDelay start
do test synchronized
before test synchronizedDelay
do test synchronizedDelay

在synchronized修饰本类class对象的时候,类的同一个实例对象是有同步的效果的,接下来看看修饰class对象的时候不同实例对象是不是也会像前两者一样,不起作用。修改下main方法,创建两个实例对象,分别执行。

test synchronized start
before test synchronized
test synchronizedDelay start
do test synchronized
before test synchronizedDelay
do test synchronizedDelay

可以发现,这个时候,同步的作用还是有的,并不是和前两种情况一样。并且在synchronized关键字修饰其他类的对象的时候,把上面代码注释1处的SynchronizedTest.class换成项目中另一个类(随便选一个类,比如LogUtils.class),执行结果不变,同步还是有效的。

4. 修饰普通方法

篇幅问题,接下来的直接伪代码了,修饰普通方法

	public synchronized void testSynchronized(){
        	...
        	Thread.sleep(5000);
	}

    public synchronized  void testSynchronizedDelay(){
        	...
            Thread.sleep(10000);
    }

像上面这种修饰普通方法的情况,与修饰this和变量的时候一样,都是同一实例变量下起到同步的效果,在两个不同的实例对象下无效

5.修饰静态方法

	public synchronized static  void testSynchronized(){
        	...
        	Thread.sleep(5000);
	}

    public synchronized static void testSynchronizedDelay(){
        	...
            Thread.sleep(10000);
    }

修饰静态方法的时候产生的效果和synchronized修饰代码块(class)一致,即使是两个不同的实例对象,还是会起到同步的作用。

6.synchronized原理

先来看一个最简单的例子,在main方法中直接插入一个同步代码块,然后我们看下这个例子的编译后的字节码

public static void main(String[] args) {
        synchronized (SynchronizedSimpleTest.class){
            System.out.println("1123");
        }
    }

字节码信息
在这里插入图片描述
我们在java中创建的对象都存在一个属于他本身的monitor(监视器锁),synchronized关键字在实现同步控制的过程中,最主要的一环其实就是对一个对象的monitor争夺。在一个线程争抢到monitor的时候对应的字节码就是图中红线划出的monitorenter这一步,此时线程占据共享资源,开始执行之后的逻辑代码,同时把monitor的计数器(count)加1(monitor实际上也是一个对象,在hotSpot源码的ObjectMonitor.hpp文件,做个了解),其他线程在看到monitor计数为1时就进入了我们平时说的等待状态,等待持有monitor的线程退出,该线程推出的时候会执行图中红框框出的monitorExit步骤,释放monitor对象的所有权把monitor的count减1,其他等待中的对象看到计数器为0之后继续争抢monitor,以此循环直到所有的线程全部运行完毕。
这里有人可能会有疑问了,为什么monitorenter只执行以此,但是monitorexit却执行了两次呢,网上有几个大哥说是获取monitorexit释放之后再次获取了一次,但是因为该线程是上一个持有者,再次获取不需要monitorenter。实际上并不是这样的, 如果第14行monitorexit是正常的退出,第15行goto直接定向到第二十三行return就结束了,第20行的monitorexit实际上是隐式的异常处理,针对的是程序出错的时候不会导致线程一直占有monitor而其他线程一直等待的情况。代码举例

public void  testSynchronized(){
        try {
            synchronized (this) {
                System.out.println("before test synchronized");
                Thread.sleep(5000);
                int a = 10/0;
                System.out.println("do test synchronized");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void testSynchronizedDelay(){
        try {
            synchronized (this) {
                System.out.println("before test synchronizedDelay");
                Thread.sleep(10000);
                System.out.println("do test synchronizedDelay");
            }
        }catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
	public static void main(String[] args) {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("test synchronized start");
                synchronizedTest.testSynchronized();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("test synchronizedDelay start");
                synchronizedTest.testSynchronizedDelay();
            }
        }).start();
    }

执行结果

test synchronized start
before test synchronized
test synchronizedDelay start
before test synchronizedDelay
Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
	at com.yzj.dobetter.synchronization.SynchronizedTest.testSynchronized(SynchronizedTest.java:22)
	at com.yzj.dobetter.synchronization.SynchronizedTest$1.run(SynchronizedTest.java:141)
	at java.lang.Thread.run(Thread.java:748)
do test synchronizedDelay

第一个方法里写一个异常,两个线程同时执行,第一个线程进入同步代码块,休眠5秒之后报错,此时程序就会走到类似于上面的第二个monitorexit处,然后抛出异常,如果没有第二个monitorexit的话第一个线程抛出异常,第二个线程就一直处于等待,也就不会有最后的do test synchronizedDelay了

回到正题,看完了简单的例子,再看看synchronized修饰方法的字节码,首先是java代码

	public synchronized void  testSynchronizedMethod(){
        try {
            System.out.println("before test synchronized");
            Thread.sleep(5000);
            System.out.println("do test synchronized");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized void testSynchronizedMethodDelay(){
        try {
            System.out.println("before test synchronizedDelay");
            Thread.sleep(10000);
            System.out.println("do test synchronizedDelay");
        }catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
	public static void main(String[] args) {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("test synchronized start");
                synchronizedTest.testSynchronizedMethod();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("test synchronizedDelay start");
                synchronizedTest.testSynchronizedMethodDelay();
            }
        }).start();
    }

然后看字节码,main方法的字节码

两个方法的字节码
在这里插入图片描述
在这里插入图片描述
可以看到main方法中红框框出的部分就是实际上开启线程然后运行两个方法的地方。而当synchronized修饰在方法上的时候,在字节码中实际上并没有monitorenter和monitorexit的步骤,而是在方法的标识位中多了图中框出的ACC_SYNCHRONIZED的标识,jvm实际上就是用这个标识来确认一个方法是否是同步方法的。当一个方法被synchronized修饰的时候,所在的线程会先去尝试获取monitor,成功获取到的时候才会真正进入方法体内执行具体逻辑,同样的,方法运行完毕之后也会执行monitorexit释放资源。这种方式实际上和字节码中显式的monitorenter/monitorexit没有本质上的区别。(网上有人说monitorenter/monitorexit这两个指令是jvm通过调用操作系统的mutex原语实现的,这个操作会涉及到cpu在用户态和内核态之间的转换和传值,是java1.6之前synchronized关键字性能较差的罪魁祸首,做个了解)

那为什么synchronized修饰普通方法和修饰静态方法的时候,产生的效果不同呢?原因其实很简单,添加在普通方法的synchronized实际的锁是该类的一个具体的实例对象,而synchronized修饰静态方法时实际上锁的是该类的class对象。一般而言,同一个类加载器加载一个类产生的class对象只有一个,因此,在synchronized修饰class或者修饰静态方法时,实际上的锁是指定的类的class对象。

关于synchronized的同步控制最需要搞清楚一个概念,synchronized并不是锁,它只是一个程序需要同步的标志,真正产生锁这个效应的是对象在生成时就已经自带的monitor对象,因此,除了基本类型,每一个对象都可以充当锁这个角色。

最后,告诉你个小知识。还记得我们在学java的时候接触的第一行程序吗System.out.println("Hello World");就是这行,让我们看下out.println()方法的具体实现
在这里插入图片描述
out对象在启动时由java运行环境初始化,可以在执行期间由开发人员更改,但是很少有人去修改这个对象,然而println()这个方法是一个同步方法,在大型项目中大量写System.out.println()会产生一些性能方面的影响。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. synchronized关键字在使用层面的理解 synchronized关键字Java中用来实现线程同步的关键字,可以修饰方法和代码块。当线程访问被synchronized修饰的方法或代码块时,需要获取对象的锁,如果该锁已被其他线程获取,则该线程会进入阻塞状态,直到获取到锁为止。synchronized关键字可以保证同一时刻只有一个线程能够访问被锁定的方法或代码块,从而避免了多线程并发访问时的数据竞争和一致性问题。 2. synchronized关键字在字节码中的体现 在Java代码编译成字节码后,synchronized关键字会被编译成monitorenter和monitorexit指令来实现。monitorenter指令对应获取锁操作,monitorexit指令对应释放锁操作。 3. synchronized关键字在JVM中的实现 在JVM中,每个对象都有一个监视器(monitor),用来实现对象锁。当一个线程获取对象锁后,就进入了对象的监视器中,其他线程只能等待该线程释放锁后再去竞争锁。 synchronized关键字的实现涉及到对象头中的标志位,包括锁标志位和重量级锁标志位等。当一个线程获取锁后,锁标志位被设置为1,其他线程再去获取锁时,会进入自旋等待或者阻塞等待状态,直到锁标志位被设置为0,即锁被释放后才能获取锁。 4. synchronized关键字在硬件方面的实现 在硬件层面,锁的实现需要通过CPU指令和总线锁来实现。当一个线程获取锁时,CPU会向总线发送一个锁请求信号,其他CPU收到该信号后会进入自旋等待状态,直到锁被释放后才能获取锁。总线锁可以保证多个CPU之间的原子操作,从而保证锁的正确性和一致性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值