Synchronized
- 前言
- 1.synchronized修饰符和线程控制
- 1.1synchronized修饰符和Object Monitor模式
- 1.2 synchronized修饰符可标注的位置
- 2. 方法
- 2.1 wait
- wait多态表达式
- notify()方法和notifyAII()方法
- interrupt中断信号
- 判断是否收到interrupt中断信号的方法
前言
悲观锁在Java中有两种典型的实现方式,一种是基于Object Monitor模式的资源操作方式,另一种是基于AQS技术的资源操作方式。他们共同的特点需要操作者获得资源的独占操作权才能对相关资源进行操作。
补充:乐观锁思想
- 该思想假设,并不是任何时候都有两个或多个操作者同时操作相同的资源,从而产生操作冲突。
- 该思想还假设,即使操作结果存在错误也没有关系。可以通过对比预期值和实际值来确认操作的正误,如果出现错误,则放弃本次操作,重新操作即可。
- 基于这种思想,操作者无须获得资源的操作独占权,也无须等待其他操作者释放资源的独占操作权。简单来说,这是一种无须加锁的,带有并发操作性的思想。在操作结束后,操作者只需比较实际操作结果是否符合预期操作结果,如果不符合,则放弃本次操作并重试。这种线程安全性的操作思想称为乐观锁思想
- Java中的乐观锁思想通常基于CAS(Compare and Swap,比较与交换)技术实现,但CAS进行比较的判定依据及比较后是否要进行重试,往往由操作者自行决定。所以在Java中,基于乐观锁工作的工具类都存在类似于for(;;)或while(true)的源码结构,这并不是BUG,而是为了匹配乐观锁的实现思想。例如,在ConcurrentHashMap集合中,基于乐观锁思想添加数据对象的源码片段如下。
1.synchronized修饰符和线程控制
- 不同的操作系统(Windows、UNIX、Linux等)支持的线程底层实现和操作效果不同。不过操作系统支持的线程状态至少可以归为四类,分别为就绪、执行、阻塞和结束,这四种状态可以互相切换。
- 在创建线程时,操作系统不会为它分配独立的资源。一个应用程序(进程)中的所有线程,都可以共享这个应用程序(进程)中的资源,如这个应用程序的CPU资源、I/O资源、内存资源。
1.1synchronized修饰符和Object Monitor模式
- Thread.stop()、Thread.suspend()方法在JDK 1.2后,被官方弃用。
如果要在异常情况下中断线程的运行(或通知线程进入中断逻辑),则需要向线程发出interrupt中断信号,下面会讲解interrupt中断信号的使用过程。 - 上图展示了java中的部分方法如何对线程间的切换施加影响。这些方法都和synchronized修饰符有关,后者代表一种经典的线程控制模型–Object Monitor模式。
- Object Monitor模式是一种典型的悲观锁实现,使用Java对象模型中的特定区域对线程状态、对象状态的描述进行线程操作。下面看一下在这种模式下的线程操作实例,代码如下。
在Object Monitor模式下,多个线程要对特定的对象进行操作,首先需要获取这个对象的独占操作权,然后进入synchronized修饰符对应的代码块(简称synchronized代码块)进行执行。未获得对象独占操作权的线程会在synchronized代码块外阻塞等待(可理解为一种临界状态),直到获取对象的独占操作权。当然,synchronized代码块内正在执行的线程也可以主动释放对象的独占操作权(如使用wait()方法),并且使自己进入阻塞状态,以便其他处于阻塞状态的线程重新抢占该对象的独占操作权。
上图中有两个线程,分别为线程1和线程2,这两个线程的执行过程都需要检查ThreadLock.WAIT_OBJECT对象的锁状态(检查独占操作权)。下面我们通过调试信息验证线程的执行过程。
(1)线程1和线程2开始工作,这时两个线程都没有获得ThreadLock. WAIT_OBJECT对象(上图中的Object)的独占操作权(上图中提到的“锁”)。
(2)线程1开始检查ThreadLock.WAIT_OBJECT对象的锁状态,发现没有任何线程占有这个对象的独占操作权,于是线程1通过改变ThreadLock.WAIT_OBJECT对象结构中特定区域的数据,获得了这个对象的独占操作权并进入synchronized代码块继续执行。
(3)接着线程2开始检查ThreadLock.WAIT_OBJECT对象的锁状态,发现ThreadLock.WAIT_OBJECT对象的独占操作权已经被其他线程占据,于是在检查位阻塞等待。
(4)线程1继续运行,直到synchronized代码块执行完毕,然后线程1释放ThreadLock.WAIT_OBJECT对象的独占操作权。线程2被通知ThreadLock.WAIT_OBJECT对象不再处于被独占状态,可以重新抢占该对象的独占操作权。如果线程2获取了ThreadLock.WAIT_OBJECT对象的独占操作权,就会解除阻塞状态,进入synchronized代码块继续执行(这之前线程2会处于阻塞状态)。
1.2 synchronized修饰符可标注的位置
可以在方法定义中添加synchronized修饰符,也可以在方法体中添加synchronized修饰符,还可以在static代码块中添加synchronized修饰符。synchronized修饰符的以下使用方法都是正确的。
不同位置的synchronized修饰符代表的意义不同。
在synchronized(){}语句中,可以在小括号中指定需要进行Object Monitor模式检查的对象。下面对以上几种synchronized修饰符的意义进行概要解释。
- 将synchronized修饰符加载在非静态方法上,其代表的意义和synchronized(this) { }语句代表的意义类似,即对拥有这个方法的对象进行Object Monitor模式下的锁状态检查。但两者的栈帧状态是有所区别的。
- 将synchronized修饰符加载在静态方法上,其代表的意义和synchronized (Class.class) { }语句代表的意义类似,即对拥有这个方法的类对象(类本身也是对象)进行Object Monitor模式下的锁状态检查。
- 在Object Monitor模式下需要关注对象的锁粒度。例如,基于synchronized (Class.class) { }语句的锁是不被推荐的,包括直接加在静态方法上的synchronized修饰符也不被推荐。因为控制粒度太过粗放,受影响的范围无法得到有效限制。
一个操作是否是线程安全的,需要开发人员真正理解synchronized修饰符在不同场景中的意义并正确使用,而不是盲目地使用synchronized修饰符。在两个线程的doOtherthing()方法中操作同一个对象NOWVALUE的示例代码如下,这段示例代码展示了synchronized修饰符的错误使用方法。
package com.company;
/**
* @version 1.0
* @date 2022/12/28
*/
public class SyncThread implements Runnable {
private Integer value;
/** 多个线程共同操作的整数值对象 */
private static Integer NOWVALUE;
public SyncThread(Integer value) {
this.value = value;
}
/** 对这个类的实例化对象检查 */
private synchronized void doOthering(){
NOWVALUE = this.value;
System.out.println("当前 NOWVALUE 的值为:" + NOWVALUE);
}
@Override
public void run() {
this.doOthering();
}
public static void main(String[] args) {
Thread syncThread1 = new Thread(new SyncThread(10));
Thread syncThread2 = new Thread(new SyncThread(100));
syncThread1.start();
syncThread2.start();
}
}
从DEBUG的情况来看,可能发生静态对象NOWVALUE的值出现脏读的情况,输出结果如下。
源码出现BUG的原因分析如下。
- syncThread1对象和syncThread2对象是SyncThread类的两个不同实例化对象。基于doOtherthing()方法的synchronized修饰符进行的锁状态检查,其目标对象并不是同一个对象。
- 如果读者要对SyncThread类的多个实例对象进行Object Monitor模式下的锁状态检查,那么应该对这个类的class对象进行Object Monitor模式下的锁状态检查。类似的语句应该是“privatesynchronized static void doOtherthing()”。
为了对SyncThread类的class对象进行锁状态检查,甚至无须在静态方法中标注synchronized修饰符,只需单独对SyncThread类的class对象标注synchronized修饰符。
package com.company;
/**
* @version 1.0
* @date 2022/12/28
*/
public class SyncThread implements Runnable {
private Integer value;
/**
* 多个线程共同操作的整数值对象
*/
private static Integer NOWVALUE;
public SyncThread(Integer value) {
this.value = value;
}
/**
* 对这个类的实例化对象检查
*/
private void doOthering() {
synchronized (SyncThread.class){
NOWVALUE = this.value;
System.out.println("当前 NOWVALUE 的值为:" + NOWVALUE);
}
}
@Override
public void run() {
this.doOthering();
}
public static void main(String[] args) {
Thread syncThread1 = new Thread(new SyncThread(10));
Thread syncThread2 = new Thread(new SyncThread(100));
syncThread1.start();
syncThread2.start();
}
}
2. 方法
2.1 wait
在Object Monitor模式下,要确保一个对象是线程安全的,除了正确使用synchronized修饰符,还需要多种相关方法协助控制(线程间同步),以便实现复杂的控制逻辑。其中一组重要方法是wait()方法、notify()方法、notifyAll()方法。
- wait方法
wait()方法由java.lang.Object类提供,该方法使用final修饰符进行修饰,代表不允许子类重载。wait()方法内部直接通过JNI调用JVM内核源码完成工作,即完成指定对象在Object Monitor模式下的状态改变。
使用wait()方法可以使当前线程在获取某个对象独占操作权的情况下,主动释放该对象的独占操作权,并且将当前线程的状态切换为阻塞状态(WAITING状态)。
这样,其他需要当前对象独占操作权且进入阻塞状态的线程,就可以重新抢占该对象的独占操作权了。
对象的wait()方法只能在synchronized代码块中调用。如果没有这样做,就会抛出“IllegalMonitorStateException”异常。
在这个synchronized代码块中,调用wait()方法的对象必须是Object Monitor模式的检查对象。以下示例代码片段是错误的,因为synchronized代码块指定的进行Object Monitor模式检查的对象并不是当前调用wait()方法的对象。
在正常情况下,使用wait()方法进入阻塞状态的线程,会主动释放当前对象的独占操作权,如果要再次进入就绪状态,就必须重新获得当前对象的独占操作权。这个规则没有例外,即使在抛出异常时也没有例外,相应源码如下。
在上述源码片段中,线程在通过wait()方法主动释放currentObject对象的独占操作权后,就会进入阻塞状态。即使出于某种原因,该线程在阻塞状态收到了interrupt中断信号,也需要在重新获取该对象的独占操作权后,才能运行catch语句中的源码。
wait多态表达式
wait()方法的多态表达包括wait(long)和wait(long, int),这两种方法的具体解释如下。
- wait(long):阻塞一段时间(单位为毫秒),如果这段时间内没有收到notify信号、notifyAll信号,也没有收到interrupt中断信号,则重新允许该线程参与对象独占操作权的抢占工作。如果抢占成功,则该线程继续执行。
- wait(long, int):该方法和上述方法类似,但要注意第二个参数,该参数传入一个纳秒数(1毫秒=1000000纳秒,CPU一个指令大约需要2~4纳秒),这个入参所代表的纳秒数是在阻塞时间结束后,允许当前线程继续等待的1毫秒内的时间偏移量,它的取值范围为0~999999。
- 解除wait方法的阻塞状态
有几种场景可以将使用wait()方法进入阻塞状态的线程状态重新切换为就绪状态。
・获得当前对象独占操作权的线程X在synchronized代码块中调用notify()方法或notifyAll()方法,并且当前线程重新获得对象的独占操作权。
・在使用wait()方法时指定一个最长的阻塞等待时间,在到时间后,当前线程会重新参与当前对象独占操作权的抢占工作,并且重新获取当前对象的独占操作权。
・当前线程收到interrupt中断信号,并且重新参与当前对象独占操作权的抢占工作。
notify()方法和notifyAII()方法
通过调用某个对象的notify()方法或notifyAll()方法,可以通知某个或所有因为没有该对象独占操作权而在Wait Set区域阻塞等待的线程(一般是主动调用wait()方法释放掉该对象独占操作权而进入阻塞状态的线程),可以重新参与该对象独占操作权的抢占工作了。两个方法的区别是,notify()方法会通知一个相关线程,notifyAll()方法会通知所有相关线程。
但从使用层面来看,这种工作方式具有一定的局限性。
在实际的并发场景中,程序员往往不能严格控制两个线程或多个线程的执行顺序。如果要控制两个线程或多个线程的执行顺序,则需要花费一定的设计成本。
例如,需要重新调整程序结构,或者引入其他线程,才能保证线程A调用wait()方法一定在线程B调用notify()方法/notifyAll()方法之前。本书后面会介绍一种无须严格限制两个同步线程的执行顺序,也能唤醒线程的方式。
interrupt中断信号
通过向一个指定的线程发送interrupt中断信号,可以通知这个线程进行中断,但是否要真的中断线程运行,或者在中断线程运行前是否要完成一些额外的工作,取决于程序员设计的具体处理逻辑。可以使用以下方法向指定线程发出一次interrupt中断信号(当然向自身线程发出interrupt中断信号也是可以的)。
判断是否收到interrupt中断信号的方法
- 线程在收到interrupt中断信号时可能处于就绪状态,也可能处于阻塞状态。如果线程处于就绪状态,则可以使用static boolean interrupted()方法或boolean isInterrupted()方法进行判断,如果interrupted()方法返回true,则表示当前线程收到了interrupt中断信号,线程中的相关逻辑可以根据实际情况决定是立即中断处理,还是继续处理,示例代码如下。
如果线程处于阻塞状态,则会抛出“java.lang.InterruptedException”异常,程序员可以在异常代码块中编写相关业务代码,决定是立即中断处理,还是继续处理,示例代码如下。
// TODO