天天看点

《Java多线程编程核心技术》——2.1节synchronized同步方法

本节书摘来自华章社区《java多线程编程核心技术》一书中的第2章,第2.1节synchronized同步方法,作者高洪岩,更多章节内容可以访问云栖社区“华章社区”公众号查看

2.1 synchronized同步方法

在第1章中已经接触“线程安全”与“非线程安全”相关的技术点,它们是学习多线程技术时一定会遇到的经典问题。“非线程安全”其实会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是“脏读”,也就是取到的数据其实是被更改过的。而“线程安全”就是以获得的实例变量的值是经过同步处理的,不会出现脏读的现象。此知识点在第1章也介绍,但本章将细化线程并发访问的内容,在细节上更多接触在并发时变量值的处理方法。

2.1.1 方法内的变量为线程安全

“非线程安全”问题存在于“实例变量”中,如果是方法内部的私有变量,则不存在“非线程安全”问题,所得结果也就是“线程安全”的了。

下面的示例项目就是实现方法内部声明一个变量时,是不存在“非线程安全”问题的。

创建t1项目,hasselfprivatenum.java文件代码如下:

程序运行后的效果如图2-1所示。

《Java多线程编程核心技术》——2.1节synchronized同步方法

可见,方法中的变量不存在非线程安全问题,永远都是线程安全的。这是方法内部的变量是私有的特性造成的。

2.1.2 实例变量非线程安全

如果多个线程共同访问1个对象中的实例变量,则有可能出现“非线程安全”问题。

用线程访问的对象中如果有多个实例变量,则运行的结果有可能出现交叉的情况。此情况在第1章中非线程安全的案例演示过。

如果对象仅有1个实例变量,则有可能出现覆盖的情况。

创建t2项目,hasselfprivatenum.java文件代码如下:

程序运行后的结果如图2-2所示。

《Java多线程编程核心技术》——2.1节synchronized同步方法

本实验是两个线程同时访问一个没有同步的方法,如果两个线程同时操作业务对象中的实例变量,则有可能会出现“非线程安全”问题。此示例的知识点在前面已经介绍过,只需要在public void addi(string username)方法前加关键字synchronized即可。更改后的代码如下:

程序再次运行结果如图2-3所示。

《Java多线程编程核心技术》——2.1节synchronized同步方法

实验结论:在两个线程访问同一个对象中的同步方法时一定是线程安全的。本实验由于是同步访问,所以先打印出a,然后打印出b。

2.1.3 多个对象多个锁

再来看一个实验,创建项目名称为twoobjecttwolock,创建hasselfprivatenum.java类,代码如下:

上面的代码中有同步方法addi,说明此方法应该被顺序调用。

创建线程threada.java和threadb.java代码,如图2-4所示。

《Java多线程编程核心技术》——2.1节synchronized同步方法

类run.java代码如下:

创建了2个hasselfprivatenum.java类的对象,程序运行的结果如图2-5所示。

《Java多线程编程核心技术》——2.1节synchronized同步方法

上面示例是两个线程分别访问同一个类的两个不同实例的相同名称的同步方法,效果却是以异步的方式运行的。本示例由于创建了2个业务对象,在系统中产生出2个锁,所以运行结果是异步的,打印的效果就是先打印b,然后打印a。

从上面程序运行结果来看,虽然在hasselfprivatenum.java中使用了synchronized关键字,但打印的顺序却不是同步的,是交叉的。为什么是这样的结果呢?

关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,所以在上面的示例中,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁lock,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象。

但如果多个线程访问多个对象,则jvm会创建多个锁。上面的示例就是创建了2个hasselfprivatenum.java类的对象,所以就会产生出2个锁。

同步的单词为synchronized,异步的单词为asynchronized。

2.1.4 synchronized方法与锁对象

为了证明前面讲述线程锁的是对象,创建实验用的项目synchronizedmethodlockobject,类myobject.java文件代码如下:

程序运行后的效果如图2-6所示。

更改myobject.java代码如下:

如上面代码所示,在methoda方法前加入了关键字synchronized进行同步处理。程序再次运行效果如图2-7所示。

《Java多线程编程核心技术》——2.1节synchronized同步方法

通过上面的实验得到结论,调用用关键字synchronized声明的方法一定是排队运行的。另外需要牢牢记住“共享”这两个字,只有共享资源的读写访问才需要同步化,如果不是共享资源,那么根本就没有同步的必要。

那其他的方法在被调用时会是什么效果呢?如何查看到lock锁对象的效果呢?继续新建实验用的项目synchronizedmethodlockobject2,类文件myobject.java代码如下:

两个自定义线程类分别调用不同的方法,代码如图2-8所示。

《Java多线程编程核心技术》——2.1节synchronized同步方法

文件run.java代码如下:

程序运行结果如图2-9所示。

通过上面的实验可以得知,虽然线程a先持有了object对象的锁,但线程b完全可以异步调用非synchronized类型的方法。

继续实验,将myobject.java文件中的methodb()方法前加上synchronized关键字,代码如下:

本示例是两个线程访问同一个对象的两个同步的方法,运行结果如图2-10所示。

 

《Java多线程编程核心技术》——2.1节synchronized同步方法

此实验的结论是:

1)a线程先持有object对象的lock锁,b线程可以以异步的方式调用object对象中的非synchronized类型的方法。

2)a线程先持有object对象的lock锁,b线程如果在这时调用object对象中的synchronized类型的方法则需等待,也就是同步。

2.1.5 脏读

在2.1.4节示例中已经实现多个线程调用同一个方法时,为了避免数据出现交叉的情况,使用synchronized关键字来进行同步。

虽然在赋值时进行了同步,但在取值时有可能出现一些意想不到的意外,这种情况就是脏读(dirtyread)。发生脏读的情况是在读取实例变量时,此值已经被其他线程更改过了。

创建t3项目,publicvar.java文件代码如下:

程序运行后的结果如图2-11所示。

《Java多线程编程核心技术》——2.1节synchronized同步方法

出现脏读是因为public void getvalue()方法并不是同步的,所以可以在任意时候进行调用。解决办法当然就是加上同步synchronized关键字,代码如下:

程序运行后的结果如图2-12所示。

《Java多线程编程核心技术》——2.1节synchronized同步方法

可见,方法setvalue()和getvalue()被依次执行。通过这个案例不仅要知道脏读是通过synchronized关键字解决的,还要知道如下内容:

当a线程调用anyobject对象加入synchronized关键字的x方法时,a线程就获得了x方法锁,更准确地讲,是获得了对象的锁,所以其他线程必须等a线程执行完毕才可以调用x方法,但b线程可以随意调用其他的非synchronized同步方法。

当a线程调用anyobject对象加入synchronized关键字的x方法时,a线程就获得了x方法所在对象的锁,所以其他线程必须等a线程执行完毕才可以调用x方法,而b线程如果调用声明了synchronized关键字的非x方法时,必须等a线程将x方法执行完,也就是释放对象锁后才可以调用。这时a线程已经执行了一个完整的任务,也就是说username和password这两个实例变量已经同时被赋值,不存在脏读的基本环境。

脏读一定会出现操作实例变量的情况下,这就是不同线程“争抢”实例变量的结果。

2.1.6 synchronized锁重入

关键字synchronized拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象锁时是可以再次得到该对象的锁的。这也证明在一个synchronized方法/块的内部调用本类的其他synchronized方法/块时,是永远可以得到锁的。

创建实验用的项目synlockin_1,类service.java代码如下:

程序运行结果如图2-13所示。

《Java多线程编程核心技术》——2.1节synchronized同步方法

“可重入锁”的概念是:自己可以再次获取自己的内部锁。比如有1条线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。

可重入锁也支持在父子类继承的环境中。

创建实验用的项目synlockin_2,类main.java代码如下:

程序运行后的效果如图2-14所示。

《Java多线程编程核心技术》——2.1节synchronized同步方法

此实验说明,当存在父子类继承关系时,子类是完全可以通过“可重入锁”调用父类的同步方法的。

2.1.7 出现异常,锁自动释放

当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

创建实验用的项目throwexceptionnolock,类service.java代码如下:

两个自定义线程代码如图2-15所示。

《Java多线程编程核心技术》——2.1节synchronized同步方法

运行类run.java代码如下:

程序运行后的效果如图2-16所示。

《Java多线程编程核心技术》——2.1节synchronized同步方法

线程a出现异常并释放锁,线程b进入方法正常打印,实验的结论就是出现异常的锁被自动释放了。

2.1.8 同步不具有继承性

同步不可以继承。

创建测试用的项目synnotextends,类main.java代码如下:

类mythreada.java和mythreadb.java代码如图2-17所示。

《Java多线程编程核心技术》——2.1节synchronized同步方法

类test.java代码如下:

程序运行后的效果如图2-18所示。

《Java多线程编程核心技术》——2.1节synchronized同步方法