synchronized是Java关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
1、synchronized修饰方法
更加准确的说法应该是修饰实例方法,而不包括静态方法。关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法当作锁。原因在于,要调用一个实例方法必须要new一个对应的实例对象,通过此实例对象才能访问实例方法。要实现同步,那么不同线程的锁必须是访问的同一个对象。
public synchronized void method(){
//synchronized关键字修饰方法
}
2、synchronized修饰代码块
当一个线程访问object的一个synchronized同步代码块时,另一个线程仍然可以访问该object对象中的非synchronized(this)代码块。一半是异步,一半是同步,不在synchronized块中就是异步执行,在synchronized块中就是同步执行。但是synchronized持有的锁仍然是调用它的对象。synchronized(this)代码块锁定的是当前对象。在多个线程持有对象监视器为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this对象x)同步代码块中的代码.但是我们可以把任何对象当作对象监视器,格式 synchronized(非this对象),作用只有一种:synchronized(非this对象)同步代码块。
public void method() {
synchronized (this)
{
//synchronized修饰代码块
}
}
3、静态synchronized方法与synchronized(class)代码块
由于静态成员不专属于任何一个实例对象,因此对象锁对于它们是没有意义的。静态成员的并发问题事实上是多线程间访问同一个静态资源的并发问题,因此通过class对象锁可以控制静态成员的并发操作。需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象。因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁. 另外,从内存的角度来讲,静态成员所分配的内存空间也并不和任何一个对象的内存空间重叠,而是单独在一块内存里面分配。
//静态synchronized方法
public synchronized static void method1(){
}
public void method2(){
//synchronized(class)代码块
synchronized(TestA.class)
}
synchronized是对类的当前实例进行加锁,防止其他线程同时访问该类的该实例的所有synchronized块,注意这里是“类的当前实例”,类的两个不同实例就没有这种约束了。那么static synchronized恰好就是要控制类的所有实例的访问了。
static synchronized是限制线程同时访问JVM中该类的所有实例同时访问对应的代码快。实际上,在类中某方法或某代码块中有 synchronized,那么在生成一个该类实例后,该类也就有一个监视快,放置线程并发访问该实例synchronized保护块,而static synchronized则是所有该类的实例公用一个监视快了,也就是两个的区别了,也就是synchronized相当于this.synchronized,而static synchronized相当于Something.synchronized。
各种变量的线程安全问题
引用:https://blog.youkuaiyun.com/topdeveloperr/article/details/80485900
- 静态变量:线程非安全。 静态变量即类变量,位于方法区,为所有对象共享,共享一份内存,一旦静态变量被修改,其他对象均对修改可见,故线程非安全。
- 实例变量:单例模式(只有一个对象实例存在)线程非安全,非单例线程安全。 实例变量为对象实例私有,在虚拟机的堆中分配,若在系统中只存在一个此对象的实例,在多线程环境下,“犹如”静态变量那样,被某个线程修改后,其他线程对修改均可见,故线程非安全;如果每个线程执行都是在不同的对象中,那对象与对象之间的实例变量的修改将互不影响,故线程安全。
- 局部变量:线程安全。 每个线程执行时将会把局部变量放在各自栈帧的工作内存中,线程间不共享,故不存在线程安全问题。
可重入锁
是当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。synchronized关键字拥有锁重入的功能,在一个synchronized方法/块内部调用本对象的其他synchronized方法/块时,是永远可以得到锁的,原因是Java中线程获得对象锁的操作是以线程为单位的,而不是以调用为单位的。同一个线程获得一个对象锁之后,再次访问这个对象的其他同步方法,所需的对象锁没有发生变化。
实现可重入锁
每个锁关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都都可能获得该锁而调用相应方法。当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1。此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronized方法/块时,计数器会递减,如果计数器为0则释放该锁。
4、未优化前synchronized的缺陷
引用:https://blog.youkuaiyun.com/u013851082/article/details/61200103
无法中断一个正在等候获得锁的线程,也无法通过投票得到锁,如果不想等下去,也没法得到锁。
ReentrantLock 类实现Lock接口,它拥有与 synchronized 相同的并发性和内存语义,但是添加了锁投票、定时锁等候和可中断锁等候的一些特性。ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。
引用:https://blog.youkuaiyun.com/weixin_39910081/article/details/80147754
ReenTrantLock独有的能力
- 等待可中断:持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待
- 公平锁:多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁。所谓的公平锁就是先等待的线程先获得锁。Synchronized只能是非公平锁,ReentrantLockReenTrantLock可以指定是公平锁还是非公平锁,但公平锁表现的性能不是很好
- 锁绑定多个条件:一个ReentrantLock对象可以同时绑定对个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程
在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议用synchronized,Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。如果你需要实现ReenTrantLock的三个独有功能时,选择使用ReenTrantLock。
ReentrantLock适用场景
- 某个线程在等待一个锁的控制权的这段时间需要中断
- 需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程,锁可以绑定多个条件。
- 具有公平锁功能,每个到来的线程都将排队等候。
6、synchronized的优化
jdk1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
自旋锁
让该线程等待一段时间(自旋操作:执行一段无意义的循环即可),不会被立即挂起,看持有锁的线程是否会很快释放锁。
自适应自旋锁
JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。 有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。
7、Synchronized和ReentrantLock对比总结
相同点:都是阻塞式、都是可重入锁
不同点:
synchronized是java关键字(JVM控制)、ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成.
synchronized是悲观锁机制,独占锁。而Locks.ReentrantLock是每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
结束。。