synchronized关键字

前言

多线程并发过程中,存在不可避免地竞态条件和内存可见性问题,为了保证结果的正确性,确保相应过程的同步执行,可以使用synchronized关键字作为其中之一的解决方案。

从1.0版本开始,Java中的每一个对象都有一个内部锁。如果一个方法使用synchronized关键字修饰,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。

使用wait方法添加一个线程到等待队列中,notifyAll/notify方法解除等待线程的阻塞状态。

小总结

总而言之,每个对象都有一个内部锁,并且该锁有一个内部条件,由锁来管理那些试图进入synchronized方法的线程(没锁就进不了方法),由条件来管理那些调用wait的线程(不满足条件就wait)。

基本用法

synchronized关键字可以用于修饰类的实例方法、静态方法和代码块。

修饰实例方法

举个栗子

public class Counter {
	private int count;
	public synchronized void incr() {
		count++;
	}
	public synchronized int getCount() {
		return count;
	}
}
 

Counter是一个简单的计数器类,加了synchronized后,方法内的代码就变成了原子操作(要执行就一起执行,不执行就都不执行),当多个线程并发更新同一个Counter对象时,也不会出错。

synchronized发挥的作用

那么synchronized发挥了什么作用呢?
是保护实例方法,使得同时只有一个线程可以访问实例方法嘛?

显然不是,因为就算多个线程同时执行同一个synchronized实例方法,也是可以的,只要它们访问的对象不同即可。
比如

Counter c1 = new Counter();
Counter c2 = new Counter();
Thread t1 = new CouterThread(c1);
Thread t2 = new CounterThread(c2);
t1.start();
t2.start();

这样也是可以的,因为这里的c1和c2是不同的对象,是允许同时执行incr这个实例方法的。
也就是说,
实际上保护的是同一个对象的方法调用,确保只有一个线程执行,具体而言,保护的是当前实例对象,this。
this对象,有一个锁,以及等待获得该锁的等待队列,该锁只能被一个线程持有,其他需要获得该锁的线程需要等待(位于等待队列中的线程处于BLOCKED状态)。

总结

此时的synchronized,保护的是对象而非代码。
只要访问的是同一个对象的synchronized实例方法,即使是不同的代码,也会被同步顺序访问。比如同一个对象的getCount()和incr()方法就不能同时执行,只能顺序访问。
但是针对不同的对象,可以同时执行它们中相同实例方法。

修饰静态方法

举个栗子

public class StaticCounter {
	private static int count = 0;
	public static synchronized void incr() {
		count++;	
	}
	public static synchronized int getCount() {
		return count;
	}
}

在实例方法中,我们说synchronized保护的是this,那么静态方法呢?
由于访问静态方法,一般都是通过类进行访问的,所以说,此时保护的是类对象。
实际上,每个对象都有一个锁和一个等待队列,连类对象也不例外。

总结

不过,由于synchronized静态方法和synchronized实例方法保护的对象不同,所以允许两个线程,一个访问synchronized静态方法,一个访问synchronized实例方法。

修饰代码块

除了修饰方法外,synchronized还可以用于包装代码块

public class Counter {
	private int count;
	public void incr() {
		synchronized (this) {
			count++;
		}
	}
	public int getCount() {
		synchronized (this) {
			return count;
		}
	}
}

synchronized括号里就是保护的对象,对于实例方法,是this,{}里面是同步执行的代码。
而静态方法,相当于

public class StaticCounter {
	private static int count;
	public static void incr() {
		synchronized (StaticCounter.class) {
			count++;
		}
	}
	public static int getCount() {
		synchronized (StaticCounter.class) {
			return count;
		}
	}
}

synchronized同步的对象可以是任意对象,任意对象都有一个锁和等待队列,或者说,任何对象都可以作为锁对象。

深入一步

1. 可重入性

synchronized是可重入的。
什么是可重入呢?
指的是,对于一个获得对象锁的线程而言,如果它再去调用另外一个需要相同的锁的代码时,可以直接调用。

实现方式

可重入是通过记录锁的持有线程和持有数量来实现的。

当调用被synchronized保护的代码时,检查对象是否已被锁,如果是,再检查是否被当前线程锁定,如果是,增加持有数量,如果不是被当前线程锁定,才加入等待队列,当释放锁时,减少持有数量,当数量变为0时才释放整个锁。

内存可见性

synchronized除了保证原子操作外,还可以保证内存可见性。

实现方式

在释放锁时,所有的写入都会被写回内存,而获得锁后,会从内存中读取当前最新数据。
不过,如果只是为了保证内存可见性,可以使用更轻量级的volatile修饰符。

死锁

这是所有锁,都避不开的一个问题。
所谓死锁,简单来说,是指两个线程各自持有一个锁,却都想去申请对方持有的锁,最后相互等待,谁都执行不下去的现象。

解决方式
  1. 首先应该避免在持有一个锁的同时去申请另一个锁;
  2. 如果确实需要多个锁,所有代码都应该按照相同的顺序去申请,比如说,都先申请lockA,再申请lockB;
  3. 使用显式锁接口Lock,它支持尝试获取锁tryLock和带有时间限制的获取锁方法(使用这些方法可以在获取不到锁的时候释放已持有的锁,然后再次尝试获取锁或干脆放弃,以避免死锁)。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值