并发编程 - 通过案例分析synchronized

1、synchronized修饰的对象


《Java中Synchronized的用法》中对synchronized修饰对象给出了很好的说明与案例,本文不再赘述,仅附上该文章得到的结论。

1.1、修饰一个代码块

  • 被修饰的代码块称为同步语句块。
  • 作用范围:大括号{}括起来的代码。
  • 作用对象:调用这个代码块的对象。

在平常的开发中,我们更倾向于使用这种更细粒度的方式去synchronized。

public void test() {
	// ...
	synchronized(this) {
		// ...
	}
	// ...
}

1.2、修饰一个方法

  • 被修饰的方法称为同步方法。
  • 作用范围:整个方法。
  • 作用对象:调用这个方法的对象。
public synchronized void test() {
	// ...
}

1.3、修饰一个静态的方法

  • 作用范围:整个静态方法。
  • 作用对象:是这个类的所有对象。
public synchronized static void method() {
	// ...
}

1.4、修饰一个类

  • 作用范围:synchronized后面括号括起来的部分
  • 作用对象:这个类的所有对象。
class ClassName {
	public void method() {
		synchronized(ClassName.class) {
			// ...
		}
	}
}

2、synchronized锁定对象案例分析


一定要清楚一个概念,那就是synchronized锁定的是对象,而不是引用。我们通过案例来“深刻理解”这句话的意思:

public class LockReferenceDemo {

    public Object obj = new Object();

    public void act() {
        System.out.println(Thread.currentThread().getName() + " start!");
        synchronized (obj) { // 锁定了obj指向的对象
            while (true) {
            	// 输出当前线程 及 obj对象地址
                System.out.println(Thread.currentThread().getName() + " - " + obj);
                // 延迟1s执行
                try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) {}
            }
        }
    }

    public static void main(String[] args) {
        final LockReferenceDemo demo = new LockReferenceDemo();
        // 定义并启动线程1
        Thread thread1 = new Thread(demo::act, "Thread-1");
        thread1.start();
		// 延迟1s执行
        try { TimeUnit.SECONDS.sleep(1); } catch (Exception e) {}
		// 启动Thread-2前,修改obj引用的对象
        demo.obj = new Object(); 
        // 定义并启动线程2
        Thread thread2 = new Thread(demo::act, "Thread-2");
        thread2.start();
    }
}

输出结果:

Thread-1 start!
Thread-1 - java.lang.Object@4b7f7470
Thread-1 - java.lang.Object@4b7f7470
Thread-2 start!
Thread-2 - java.lang.Object@5e130a4c
Thread-1 - java.lang.Object@5e130a4c
Thread-2 - java.lang.Object@5e130a4c

我们可以看到,Thread-2正常运行,同时,demo.obj = new Object()这一步没有终止Thread-1的执行。

这是因为,对象的引用是存在栈桢中的,而对象是存在堆中的。synchronized只是锁住了堆中的对象,并没有锁栈桢中的引用demo.obj = new Object()这一步只是将引用obj指向了一个新的对象。

我们可以把“获取锁”这个修饰词替换成“挂锁”去理解整个过程。在整个过程中,Thread-1发现原对象(假设为A)上没有锁,就在A上挂了一把锁。

这个时候,如果没有demo.obj = new Object()这一步,则Thread-2也需要尝试在A上挂锁,发现该对象上已经挂了一把锁,那么Thread-2就必须等待Thread-1将A对象上的锁释放后才能挂锁。

而有了demo.obj = new Object()这一步之后就不一样了,Thread-1在A中挂了一把锁,经过demo.obj = new Object()执行后,obj引用引向了新的对象(假设为B),B显然是没有挂锁的,所以Thread-2就可以在新的对象中挂锁并正常执行。

在main方法中,Thread-1与Thread-2之间我添加了TimeUnit.SECONDS.sleep(1);去延迟执行,目的在于,若代码执行过快,在Thread-1还没来得及执行synchronized时,demo.obj = new Object()这一步已然发生,那么Thread-1就会直接锁定B对象,这从侧面反映出synchronized锁定的是对象这个事实。

此时还有一个严重的问题,在输出的时候,我们发现,当demo.o = new Object()执行之后,Thread-1输出的obj的地址变成了新的对象的地址。这是因为Thread-1调用obj是调用obj指向的对象,而不是Thread-1锁定的对象。我们可以通过新建引用去接收引用参数的方式去解决这个问题:

public class LockReferenceDemo {
  	Object obj = new Object();
	void act() {
        System.out.println(Thread.currentThread().getName() + " start!");
        synchronized (obj) {
            Object o1 = obj; // 用新的引用引向原对象,线程就可以访问o1指向的原对象继续操作。
            while (true) {
                System.out.println(Thread.currentThread().getName() + " - " + o1);
                // ...
            }
        }
    }
    // main
}

输出结果:

Thread-1 start!
Thread-1 - java.lang.Object@1437c889
Thread-1 - java.lang.Object@1437c889
Thread-2 start!
Thread-2 - java.lang.Object@2303252c
Thread-1 - java.lang.Object@1437c889
Thread-2 - java.lang.Object@2303252c

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值