synchronized的使用

每个Object对象都有一把锁和等待队列,一个线程在进入synchronized代码块时,会尝试获取锁,如果获取不到则会把当前线程加入等待队列中。

执行synchronized的过程大致如下:

  1. 尝试获得锁,如果能够获得锁,继续下一步,否则加入等待队列,阻塞并等待唤醒。
  2. 执行实例方法体代码。
  3. 释放锁,如果等待队列上有等待的线程,从中取一个并唤醒,如果有多个等待的线程,唤醒哪一个是不一定的,不保证公平性。

当前线程不能获得锁的时候,它会加入等待队列等待,线程的状态会变为BLOCKED

synchronized保护的是对象而非代码,只要访问的是同一个对象的synchronized方法,即使是不同的代码,也会被同步顺序访问。

注意:synchronized方法不能防止非synchronized方法被同时执行。

功能

  • 原子性:synchronized可以实现原子操作,避免出现竞态条件。
  • 可重入性:对同一个执行线程,它在获得了锁之后,在调用其他需要同样锁的代码时,可以直接调用。可重入是通过记录锁的持有线程和持有数量来实现的,当调用被synchronized保护的代码时,检查对象是否已被锁,如果是,再检查是否被当前线程锁定,如果是,增加持有数量,如果不是被当前线程锁定,才加入等待队列,当释放锁时,减少持有数量,当数量变为0时才释放整个锁。
  • 内存可见性:synchronized可以解决内存可见性问题,在释放锁时,所有写入都会写回内存,而获得锁后,都会从内存中读最新数据。

使用场景

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
  3. 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  4. 修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

wait/notify协同机制

Object类中有两类方法wait和notify:

public final void wait() throws InterruptedException
public final native void wait(long timeout) throws InterruptedException;

public final native void notify();
public final native void notifyAll();

在wait等待期间都可以被中断,如果被中断,会抛出InterruptedException。

Object中除了用于锁的等待队列,还有另一个等待队列,表示条件队列,该队列用于线程间的协作。调用wait就会把当前线程放到条件队列上并阻塞,表示当前线程执行不下去了,它需要等待一个条件,这个条件它自己改变不了,需要其他线程改变。

当其他线程改变了条件后,应该调用Object的notify方法。

notify做的事情就是从条件队列中选一个线程,将其从队列中移除并唤醒,notifyAll和notify的区别是,它会移除条件队列中所有的线程并全部唤醒。

两个需要协调的线程都要访问协作的变量,容易出现竞态条件,所以相关代码都需要被synchronized保护。实际上,wait/notify方法只能在synchronized代码块内被调用,如果调用wait/notify方法时,当前线程没有持有对象锁,会抛出异常java.lang.IllegalMonitorStateException。

wait和notify底层交互流程

wait在synchronized方法内,但调用wait时,线程会释放对象锁。wait的具体过程是:

  1. 把当前线程放入条件等待队列,释放对象锁,阻塞等待,线程状态变为WAITING或TIMED_WAITING。
  2. 等待时间到或被其他线程调用notify/notifyAll从条件队列中移除,这时,要重新竞争对象锁:
  3. 如果能够获得锁,线程状态变为RUNNABLE,并从wait调用中返回。否则,该线程加入对象锁等待队列,线程状态变为BLOCKED,只有在获得锁后才会从wait调用中返回。

线程从wait调用中返回后,不代表其等待的条件就一定成立了,它需要重新检查其等待的条件,一般的调用模式是:

synchronized (obj) {
	while(条件不成立)
		obj.wait();
	…//执行条件满足后的操作
}

调用notify会把在条件队列中等待的线程唤醒并从队列中移除,但它不会释放对象锁,也就是说,只有在包含notify的synchronized代码块执行完后,等待的线程才会从wait调用中返回。

总结:wait/notify被不同的线程调用,但共享相同的锁和条件等待队列(相同对象的synchronized代码块内),它们围绕一个共享的条件变量进行协作,这个条件变量是程序自己维护的,当条件不成立时,线程调用wait进入条件等待队列,另一个线程修改了条件变量后调用notify,调用wait的线程唤醒后需要重新检查条件变量。从多线程的角度看,它们围绕共享变量进行协作,从调用wait的线程角度看,它阻塞等待一个条件的成立。我们在设计多线程协作时,需要想清楚协作的共享变量和条件是什么,这是协作的核心

使用场景

生产者/消费者协作模式

生产者线程和消费者线程通过共享队列进行协作,生产者将数据或任务放到队列上,而消费者从队列上取数据或任务,如果队列长度有限,在队列满的时候,生产者需要等待,而在队列为空的时候,消费者需要等待。

同时开始

类似运动员比赛,在听到比赛开始枪响后同时开始,在一些程序,尤其是模拟仿真程序中,要求多个线程能同时开始。

等待结束

主从协作模式也是一种常见的协作模式,主线程将任务分解为若干子任务,为每个子任务创建一个线程,主线程在继续执行其他任务之前需要等待每个子任务执行完毕。

异步结果

在主从协作模式中,主线程手工创建子线程的写法往往比较麻烦,一种常见的模式是将子线程的管理封装为异步调用,异步调用马上返回,但返回的不是最终的结果,而是一个一般称为Future的对象,通过它可以在随后获得最终的结果。

集合点

并行迭代计算中,每个线程负责一部分计算,然后在集合点等待其他线程完成,所有线程到齐后,交换数据和计算结果,再进行下一次迭代。

#java原理##并发编程#
27届毕业生-Java知识专辑 文章被收录于专栏

知其然知其所以然,只有掌握了底层原理,借助第一性原理,才可以在日常开发和项目中运用自如,潇洒走江湖。 专为27届毕业生准备,托起您的就业梦。 该专辑会不定时更新,建议27届同学订阅,入职后扎实的基本功可以帮您争取更好的机会和项目。

全部评论

相关推荐

zbk1:学院本找嵌入式我觉得不太行,不要被培训班忽悠了,老老实实读个研吧。
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务