本节书摘来自华章社区《Java多线程编程核心技术》一书中的第2章,第2.1节synchronized同步方法,作者高洪岩,更多章节内容可以访问云栖社区“华章社区”公众号查看
2.1 synchronized同步方法
在第1章中已经接触“线程安全”与“非线程安全”相关的技术点,它们是学习多线程技术时一定会遇到的经典问题。“非线程安全”其实会在多个线程对同一个对象中的实例变量进行并发访问时发生,产生的后果就是“脏读”,也就是取到的数据其实是被更改过的。而“线程安全”就是以获得的实例变量的值是经过同步处理的,不会出现脏读的现象。此知识点在第1章也介绍,但本章将细化线程并发访问的内容,在细节上更多接触在并发时变量值的处理方法。
2.1.1 方法内的变量为线程安全
“非线程安全”问题存在于“实例变量”中,如果是方法内部的私有变量,则不存在“非线程安全”问题,所得结果也就是“线程安全”的了。
下面的示例项目就是实现方法内部声明一个变量时,是不存在“非线程安全”问题的。
创建t1项目,HasSelfPrivateNum.java文件代码如下:
package service;
public class HasSelfPrivateNum {
public void addI(String username) {
try {
int num = 0;
if (username.equals("a")) {
num = 100;
System.out.println("a set over!");
Thread.sleep(2000);
} else {
num = 200;
System.out.println("b set over!");
}
System.out.println(username + " num=" + num);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
文件ThreadA.java代码如下:
package extthread;
import service.HasSelfPrivateNum;
public class ThreadA extends Thread {
private HasSelfPrivateNum numRef;
public ThreadA(HasSelfPrivateNum numRef) {
super();
this.numRef = numRef;
}
@Override
public void run() {
super.run();
numRef.addI("a");
}
}
文件ThreadB.java代码如下:
package extthread;
import service.HasSelfPrivateNum;
public class ThreadB extends Thread {
private HasSelfPrivateNum numRef;
public ThreadB(HasSelfPrivateNum numRef) {
super();
this.numRef = numRef;
}
@Override
public void run() {
super.run();
numRef.addI("b");
}
}
文件Run.java代码如下:
package test;
import service.HasSelfPrivateNum;
import extthread.ThreadA;
import extthread.ThreadB;
public class Run {
public static void main(String[] args) {
HasSelfPrivateNum numRef = new HasSelfPrivateNum();
ThreadA athread = new ThreadA(numRef);
athread.start();
ThreadB bthread = new ThreadB(numRef);
bthread.start();
}
}
程序运行后的效果如图2-1所示。
![ef4155fcef8857658df2b2efe895ff096d844397](https://yqfile.alicdn.com/ef4155fcef8857658df2b2efe895ff096d844397.png?x-oss-process=image/resize,w_1400/format,webp)
可见,方法中的变量不存在非线程安全问题,永远都是线程安全的。这是方法内部的变量是私有的特性造成的。
2.1.2 实例变量非线程安全
如果多个线程共同访问1个对象中的实例变量,则有可能出现“非线程安全”问题。
用线程访问的对象中如果有多个实例变量,则运行的结果有可能出现交叉的情况。此情况在第1章中非线程安全的案例演示过。
如果对象仅有1个实例变量,则有可能出现覆盖的情况。
创建t2项目,HasSelfPrivateNum.java文件代码如下:
package service;
public class HasSelfPrivateNum {
private int num = 0;
public void addI(String username) {
try {
if (username.equals("a")) {
num = 100;
System.out.println("a set over!");
Thread.sleep(2000);
} else {
num = 200;
System.out.println("b set over!");
}
System.out.println(username + " num=" + num);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
文件ThreadA.java代码如下:
package extthread;
import service.HasSelfPrivateNum;
public class ThreadA extends Thread {
private HasSelfPrivateNum numRef;
public ThreadA(HasSelfPrivateNum numRef) {
super();
this.numRef = numRef;
}
@Override
public void run() {
super.run();
numRef.addI("a");
}
}
文件ThreadB.java代码如下:
package extthread;
import service.HasSelfPrivateNum;
public class ThreadB extends Thread {
private HasSelfPrivateNum numRef;
public ThreadB(HasSelfPrivateNum numRef) {
super();
this.numRef = numRef;
}
@Override
public void run() {
super.run();
numRef.addI("b");
}
}
文件Run.java代码如下:
package test;
import service.HasSelfPrivateNum;
import extthread.ThreadA;
import extthread.ThreadB;
public class Run {
public static void main(String[] args) {
HasSelfPrivateNum numRef = new HasSelfPrivateNum();
ThreadA athread = new ThreadA(numRef);
athread.start();
ThreadB bthread = new ThreadB(numRef);
bthread.start();
}
}
程序运行后的结果如图2-2所示。
![f483f8286c293689f73df83510da8dfceeefa2be](https://yqfile.alicdn.com/f483f8286c293689f73df83510da8dfceeefa2be.png?x-oss-process=image/resize,w_1400/format,webp)
本实验是两个线程同时访问一个没有同步的方法,如果两个线程同时操作业务对象中的实例变量,则有可能会出现“非线程安全”问题。此示例的知识点在前面已经介绍过,只需要在public void addI(String username)方法前加关键字synchronized即可。更改后的代码如下:
package service;
public class HasSelfPrivateNum {
private int num = 0;
synchronized public void addI(String username) {
try {
if (username.equals("a")) {
num = 100;
System.out.println("a set over!");
Thread.sleep(2000);
} else {
num = 200;
System.out.println("b set over!");
}
System.out.println(username + " num=" + num);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
程序再次运行结果如图2-3所示。
![01eec64c1afe4370e74edc6a8d82131c21c235c1](https://yqfile.alicdn.com/01eec64c1afe4370e74edc6a8d82131c21c235c1.png?x-oss-process=image/resize,w_1400/format,webp)
实验结论:在两个线程访问同一个对象中的同步方法时一定是线程安全的。本实验由于是同步访问,所以先打印出a,然后打印出b。
2.1.3 多个对象多个锁
再来看一个实验,创建项目名称为twoObjectTwoLock,创建HasSelfPrivateNum.java类,代码如下:
package service;
public class HasSelfPrivateNum {
private int num = 0;
synchronized public void addI(String username) {
try {
if (username.equals("a")) {
num = 100;
System.out.println("a set over!");
Thread.sleep(2000);
} else {
num = 200;
System.out.println("b set over!");
}
System.out.println(username + " num=" + num);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
上面的代码中有同步方法addI,说明此方法应该被顺序调用。
创建线程ThreadA.java和ThreadB.java代码,如图2-4所示。
![ed6a483774c37ca0dbbcca9d75f74df17d75dcbd](https://yqfile.alicdn.com/ed6a483774c37ca0dbbcca9d75f74df17d75dcbd.png?x-oss-process=image/resize,w_1400/format,webp)
类Run.java代码如下:
package test;
import service.HasSelfPrivateNum;
import extthread.ThreadA;
import extthread.ThreadB;
public class Run {
public static void main(String[] args) {
HasSelfPrivateNum numRef1 = new HasSelfPrivateNum();
HasSelfPrivateNum numRef2 = new HasSelfPrivateNum();
ThreadA athread = new ThreadA(numRef1);
athread.start();
ThreadB bthread = new ThreadB(numRef2);
bthread.start();
}
}
创建了2个HasSelfPrivateNum.java类的对象,程序运行的结果如图2-5所示。
![966635400c67794b27b6e1f9a143cee66b9d82d1](https://yqfile.alicdn.com/966635400c67794b27b6e1f9a143cee66b9d82d1.png?x-oss-process=image/resize,w_1400/format,webp)
上面示例是两个线程分别访问同一个类的两个不同实例的相同名称的同步方法,效果却是以异步的方式运行的。本示例由于创建了2个业务对象,在系统中产生出2个锁,所以运行结果是异步的,打印的效果就是先打印b,然后打印a。
从上面程序运行结果来看,虽然在HasSelfPrivateNum.java中使用了synchronized关键字,但打印的顺序却不是同步的,是交叉的。为什么是这样的结果呢?
关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,所以在上面的示例中,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁Lock,那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象。
但如果多个线程访问多个对象,则JVM会创建多个锁。上面的示例就是创建了2个HasSelfPrivateNum.java类的对象,所以就会产生出2个锁。
同步的单词为synchronized,异步的单词为asynchronized。
2.1.4 synchronized方法与锁对象
为了证明前面讲述线程锁的是对象,创建实验用的项目synchronizedMethodLockObject,类MyObject.java文件代码如下:
package extobject;
public class MyObject {
public void methodA() {
try {
System.out.println("begin methodA threadName="
+ Thread.currentThread().getName());
Thread.sleep(5000);
System.out.println("end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
自定义线程类ThreadA.java代码如下:
package extthread;
import extobject.MyObject;
public class ThreadA extends Thread {
private MyObject object;
public ThreadA(MyObject object) {
super();
this.object = object;
}
@Override
public void run() {
super.run();
object.methodA();
}
}
自定义线程类ThreadB.java代码如下:
package extthread;
import extobject.MyObject;
public class ThreadB extends Thread {
private MyObject object;
public ThreadB(MyObject object) {
super();
this.object = object;
}
@Override
public void run() {
super.run();
object.methodA();
}
}
运行类Run.java代码如下:
package test.run;
import extobject.MyObject;
import extthread.ThreadA;
import extthread.ThreadB;
public class Run {
public static void main(String[] args) {
MyObject object = new MyObject();
ThreadA a = new ThreadA(object);
a.setName("A");
ThreadB b = new ThreadB(object);
b.setName("B");
a.start();
b.start();
}
}
程序运行后的效果如图2-6所示。
更改MyObject.java代码如下:
package extobject;
public class MyObject {
synchronized public void methodA() {
try {
System.out.println("begin methodA threadName="
+ Thread.currentThread().getName());
Thread.sleep(5000);
System.out.println("end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
如上面代码所示,在methodA方法前加入了关键字synchronized进行同步处理。程序再次运行效果如图2-7所示。
![77abb5bff4d7f6567dce4bcf4242cd01d8f51729](https://yqfile.alicdn.com/77abb5bff4d7f6567dce4bcf4242cd01d8f51729.png?x-oss-process=image/resize,w_1400/format,webp)
通过上面的实验得到结论,调用用关键字synchronized声明的方法一定是排队运行的。另外需要牢牢记住“共享”这两个字,只有共享资源的读写访问才需要同步化,如果不是共享资源,那么根本就没有同步的必要。
那其他的方法在被调用时会是什么效果呢?如何查看到Lock锁对象的效果呢?继续新建实验用的项目synchronizedMethodLockObject2,类文件MyObject.java代码如下:
package extobject;
public class MyObject {
synchronized public void methodA() {
try {
System.out.println("begin methodA threadName="
+ Thread.currentThread().getName());
Thread.sleep(5000);
System.out.println("end endTime=" + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void methodB() {
try {
System.out.println("begin methodB threadName="
+ Thread.currentThread().getName() + " begin time="
+ System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
两个自定义线程类分别调用不同的方法,代码如图2-8所示。
![6933a2f07d68301782d83498f09ef588b50ae427](https://yqfile.alicdn.com/6933a2f07d68301782d83498f09ef588b50ae427.png?x-oss-process=image/resize,w_1400/format,webp)
文件Run.java代码如下:
package test.run;
import extobject.MyObject;
import extthread.ThreadA;
import extthread.ThreadB;
public class Run {
public static void main(String[] args) {
MyObject object = new MyObject();
ThreadA a = new ThreadA(object);
a.setName("A");
ThreadB b = new ThreadB(object);
b.setName("B");
a.start();
b.start();
}
}
程序运行结果如图2-9所示。
通过上面的实验可以得知,虽然线程A先持有了object对象的锁,但线程B完全可以异步调用非synchronized类型的方法。
继续实验,将MyObject.java文件中的methodB()方法前加上synchronized关键字,代码如下:
synchronized public void methodB() {
try {
System.out.println("begin methodB threadName="
+ Thread.currentThread().getName() + " begin time="
+ System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
本示例是两个线程访问同一个对象的两个同步的方法,运行结果如图2-10所示。
![2c87f7e22c1f4adb57c34294f3625de4d1ba35ed](https://yqfile.alicdn.com/2c87f7e22c1f4adb57c34294f3625de4d1ba35ed.png?x-oss-process=image/resize,w_1400/format,webp)
此实验的结论是:
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文件代码如下:
package entity;
public class PublicVar {
public String username = "A";
public String password = "AA";
synchronized public void setValue(String username, String password) {
try {
this.username = username;
Thread.sleep(5000);
this.password = password;
System.out.println("setValue method thread name="
+ Thread.currentThread().getName() + " username="
+ username + " password=" + password);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void getValue() {
System.out.println("getValue method thread name="
+ Thread.currentThread().getName() + " username=" + username
+ " password=" + password);
}
}
同步方法setValue()的锁属于类PublicVar的实例。
创建线程类ThreadA.java的代码如下:
package extthread;
import entity.PublicVar;
public class ThreadA extends Thread {
private PublicVar publicVar;
public ThreadA(PublicVar publicVar) {
super();
this.publicVar = publicVar;
}
@Override
public void run() {
super.run();
publicVar.setValue("B", "BB");
}
}
文件Test.java代码如下:
package test;
import entity.PublicVar;
import extthread.ThreadA;
public class Test {
public static void main(String[] args) {
try {
PublicVar publicVarRef = new PublicVar();
ThreadA thread = new ThreadA(publicVarRef);
thread.start();
Thread.sleep(200);// 打印结果受此值大小影响
publicVarRef.getValue();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
程序运行后的结果如图2-11所示。
![53fa14d3883d1185e4f5de333e96637c935c9187](https://yqfile.alicdn.com/53fa14d3883d1185e4f5de333e96637c935c9187.png?x-oss-process=image/resize,w_1400/format,webp)
出现脏读是因为public void getValue()方法并不是同步的,所以可以在任意时候进行调用。解决办法当然就是加上同步synchronized关键字,代码如下:
synchronized public void getValue() {
System.out.println("getValue method thread name="
+ Thread.currentThread().getName() + " username=" + username
+ " password=" + password);
}
程序运行后的结果如图2-12所示。
![587eff3c9929ffa9fbb3d4aff4f6a7bbafcae358](https://yqfile.alicdn.com/587eff3c9929ffa9fbb3d4aff4f6a7bbafcae358.png?x-oss-process=image/resize,w_1400/format,webp)
可见,方法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代码如下:
package myservice;
public class Service {
synchronized public void service1() {
System.out.println("service1");
service2();
}
synchronized public void service2() {
System.out.println("service2");
service3();
}
synchronized public void service3() {
System.out.println("service3");
}
}
线程类MyThread.java代码如下:
package extthread;
import myservice.Service;
public class MyThread extends Thread {
@Override
public void run() {
Service service = new Service();
service.service1();
}
}
运行类Run.java代码如下:
package test;
import extthread.MyThread;
public class Run {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
程序运行结果如图2-13所示。
![18ef71204fd2f14ff052458137b2b6e84d03e7e7](https://yqfile.alicdn.com/18ef71204fd2f14ff052458137b2b6e84d03e7e7.png?x-oss-process=image/resize,w_1400/format,webp)
“可重入锁”的概念是:自己可以再次获取自己的内部锁。比如有1条线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。
可重入锁也支持在父子类继承的环境中。
创建实验用的项目synLockIn_2,类Main.java代码如下:
package myservice;
public class Main {
public int i = 10;
synchronized public void operateIMainMethod() {
try {
i--;
System.out.println("main print i=" + i);
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
子类Sub.java代码如下:
package myservice;
public class Sub extends Main {
synchronized public void operateISubMethod() {
try {
while (i > 0) {
i--;
System.out.println("sub print i=" + i);
Thread.sleep(100);
this.operateIMainMethod();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
自定义线程类MyThread.java代码如下:
package extthread;
import myservice.Main;
import myservice.Sub;
public class MyThread extends Thread {
@Override
public void run() {
Sub sub = new Sub();
sub.operateISubMethod();
}
}
运行类Run.java代码如下:
package test;
import extthread.MyThread;
public class Run {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
}
}
程序运行后的效果如图2-14所示。
![8b0515a768ed270bbd69fca2f87bbcd9f983725d](https://yqfile.alicdn.com/8b0515a768ed270bbd69fca2f87bbcd9f983725d.png?x-oss-process=image/resize,w_1400/format,webp)
此实验说明,当存在父子类继承关系时,子类是完全可以通过“可重入锁”调用父类的同步方法的。
2.1.7 出现异常,锁自动释放
当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
创建实验用的项目throwExceptionNoLock,类Service.java代码如下:
package service;
public class Service {
synchronized public void testMethod() {
if (Thread.currentThread().getName().equals("a")) {
System.out.println("ThreadName=" + Thread.currentThread().getName()
+ " run beginTime=" + System.currentTimeMillis());
int i = 1;
while (i == 1) {
if (("" + Math.random()).substring(0, 8).equals("0.123456")) {
System.out.println("ThreadName="
+ Thread.currentThread().getName()
+ " run exceptionTime="
+ System.currentTimeMillis());
Integer.parseInt("a");
}
}
} else {
System.out.println("Thread B run Time="
+ System.currentTimeMillis());
}
}
}
两个自定义线程代码如图2-15所示。
![109fa60a9d0c63b294cac4493f94cc30d22b431d](https://yqfile.alicdn.com/109fa60a9d0c63b294cac4493f94cc30d22b431d.png?x-oss-process=image/resize,w_1400/format,webp)
运行类Run.java代码如下:
package controller;
import service.Service;
import extthread.ThreadA;
import extthread.ThreadB;
public class Test {
public static void main(String[] args) {
try {
Service service = new Service();
ThreadA a = new ThreadA(service);
a.setName("a");
a.start();
Thread.sleep(500);
ThreadB b = new ThreadB(service);
b.setName("b");
b.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
程序运行后的效果如图2-16所示。
![2d21ece6d291a9ceed166ef56a1cc2ed64f8bd2b](https://yqfile.alicdn.com/2d21ece6d291a9ceed166ef56a1cc2ed64f8bd2b.png?x-oss-process=image/resize,w_1400/format,webp)
线程a出现异常并释放锁,线程b进入方法正常打印,实验的结论就是出现异常的锁被自动释放了。
2.1.8 同步不具有继承性
同步不可以继承。
创建测试用的项目synNotExtends,类Main.java代码如下:
package service;
public class Main {
synchronized public void serviceMethod() {
try {
System.out.println("int main 下一步sleep begin threadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("int main 下一步sleep end threadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
类Sub.java代码如下:
package service;
public class Sub extends Main {
@Override
public void serviceMethod() {
try {
System.out.println("int sub 下一步sleep begin threadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
Thread.sleep(5000);
System.out.println("int sub 下一步sleep end threadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
super.serviceMethod();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
类MyThreadA.java和MyThreadB.java代码如图2-17所示。
![ecd42ba9c4bf37690f6c77fad9604784b07f61e8](https://yqfile.alicdn.com/ecd42ba9c4bf37690f6c77fad9604784b07f61e8.png?x-oss-process=image/resize,w_1400/format,webp)
类Test.java代码如下:
package controller;
import service.Sub;
import extthread.MyThreadA;
import extthread.MyThreadB;
public class Test {
public static void main(String[] args) {
Sub subRef = new Sub();
MyThreadA a = new MyThreadA(subRef);
a.setName("A");
a.start();
MyThreadB b = new MyThreadB(subRef);
b.setName("B");
b.start();
}
}
程序运行后的效果如图2-18所示。
![9ff12941c65162e9bc2815564ddcd5a609ab5bae](https://yqfile.alicdn.com/9ff12941c65162e9bc2815564ddcd5a609ab5bae.png?x-oss-process=image/resize,w_1400/format,webp)