|
2.1 synchronized同步方法 |
关键字synchronized可用来保障原子性、可见性和有序性。
在第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所示。
图2-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所示。
图2-2 单例模式中的实例
上面的实验是两个线程同时访问同一个业务对象中的一个没有同步的方法,如果两个线程同时操作业务对象中的实例变量,则有可能出现非线程安全问题,此示例的知识点在前面已经介绍过,只需要在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所示。
图2-3 同步了,线程安全了
结论:两个线程同时访问同一个对象中的同步方法时一定是线程安全的。在本实验中,由于线程是同步访问的,并且a线程先执行,所以先输出a,然后输出b,但是完全有可能b线程先运行,那么就先输出b,后输出a,不管哪个线程先运行,这个线程进入用synchronized声明的方法时就上锁,方法执行完成后自动解锁,之后下一个线程才会进入用synchronized声明的方法里,不解锁其他线程执行不了用synchronized声明的方法。
在方法中使用synchronized关键字实现同步的原因是使用了flag标记ACC_SYN-CHRONIZED,当调用方法时,调用指令会检查方法的ACC_SYNCHRONIZED访问标志是否设置,如果设置了,执行线程先持有同步锁,然后执行方法,最后在方法完成时释放锁。
测试代码如下:
public class Test { synchronized public static void testMethod() { } public static void main(String[] args) throws InterruptedException { testMethod(); } }
在cmd中使用javap命令将class文件转成字节码指令,参数-v用于输出附加信息,参数-c用于对代码进行反汇编。
使用javap.exe命令如下:
javap -c -v Test.class
生成这个class文件对应的字节码指令,指令的核心代码如下:
public synchronized void myMethod(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=1, locals=2, args_size=1 0: bipush 100 2: istore_1 3: return LineNumberTable: line 5: 0 line 6: 3 LocalVariableTable: Start Length Slot Name Signature 0 4 0 this Ltest56/Test; 3 1 1 age I
在反编译的字节码指令中,对public synchronized void myMethod()方法使用了flag标记ACC_SYNCHRONIZED,说明此方法是同步的。
如果使用synchronized代码块,则使用monitorenter和monitorexit指令进行同步处理,测试代码如下:
public class Test2 { public void myMethod() { synchronized (this) { int age = 100; } } public static void main(String[] args) throws InterruptedException { Test2 test = new Test2(); test.myMethod(); } }
在cmd中使用命令:
javap -c -v Test2.class
生成这个class文件对应的字节码指令,指令核心代码如下:
public void myMethod(); descriptor: ()V ags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter 4: bipush 100 6: istore_2 7: aload_1 8: monitorexit 9: goto 15 12: aload_1 13: monitorexit 14: athrow 15: return
字节码中使用monitorenter和monitorexit指令进行同步处理。
经过前面若干章节的测试,我们可以明白同步与异步的区别。
同步:按顺序执行A和B这两个业务,就是同步。
异步:执行A业务的时候,B业务也在同时执行,就是异步。
下面来看一个实验。创建的项目名称为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所示。
图2-4 两个线程类代码
类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(); } }
这里创建了两个HasSelfPrivateNum.java类的对象,程序运行结果如图2-5所示。
图2-5 无同步各有各锁
上面的示例演示了两个线程分别访问同一个类的两个不同实例的相同名称的同步方法,输出“a set over!”后继续输出“b set over!”,却不是前面测试中输出的结果:
a set over! a num=100 b set over! b num=200
如果先输出:
a set over! a num=100
然后输出:
b set over! b num=200
就是同步的。
如果输出“a set over!”后继续输出“b set over!”,就是以异步的方式运行的。
本示例创建了两个业务对象,在系统中产生两个锁,线程和业务对象属于一对一的关系,每个线程执行自己所属业务对象中的同步方法,不存在争抢关系,所以运行结果是异步的,另外,在这种情况下,synchronized可以不需要,因为不会出现非线程安全问题。
只有多个线程执行相同的业务对象中的同步方法时,线程和业务对象属于多对一的关系,为了避免出现非线程安全问题,所以使用了synchronized。
从上面的程序运行结果来看,虽然在HasSelfPrivateNum.java中使用了synchronized关键字,但输出顺序不是同步的,而是交叉的,为什么是这样的结果呢?关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法当作锁,所以在上面的示例中,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁Lock,那么其他线程只能处于等待状态,前提是多个线程访问的是同一个对象。但如果多个线程访问多个对象,也就是每个线程访问自己所属的业务对象(上面的示例就是这种情况),则JVM会创建多个锁,不存在锁争抢的情况。另外,更具体地讲,由于本示例创建了两个业务对象,所以产生两份实例变量,每个线程访问自己的实例变量,所以加不加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所示。
图2-6 两个线程可一同进入
两个线程可一同进入methodA()方法,因为该方法并没有同步化。
更改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所示。
图2-7 排队进入方法
通过上面的示例得到结论,调用用关键字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所示。
图2-8 调用不同方法的线程类
文件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所示。
图2-9 线程B异步调用非同步方法
通过上面的示例可以得知,虽然线程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所示。
图2-10 同步运行
结论如下:
1)A线程先持有object对象的Lock锁,B线程可以以异步的方式调用object对象中的非synchronized类型的方法。
2)A线程先持有object对象的Lock锁,B线程如果在这时调用object对象中的synchronized类型的方法,则需要等待,也就是同步。
3)在方法声明处添加synchronized并不是锁方法,而是锁当前类的对象。
4)在Java中只有“将对象作为锁”这种说法,并没有“锁方法”这种说法。
5)在Java语言中,“锁”就是“对象”,“对象”可以映射成“锁”,哪个线程拿到这把锁,哪个线程就可以执行这个对象中的synchronized同步方法。
6)如果在X对象中使用了synchronized关键字声明非静态方法,则X对象就被当成锁。
2.1.4节示例中已经演示了在多个线程调用同一个方法时为了避免数据出现交叉的情况,使用synchronized关键字来进行同步。
虽然在赋值时进行了同步,但在取值时有可能出现一些意想不到的情况,这种情况就是脏读(dirty read),发生脏读的原因是在读取实例变量时,此值已经被其他线程更改过了。
创建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所示。
出现脏读是因为public void getValue()方法并不是同步的,所以可以在任意时刻进行调用,解决办法是加上同步synchronized关键字,代码如下:
synchronized public void getValue() { System.out.println("getValue method thread name=" + Thread.currentThread().getName() + " username=" + username + " password=" + password); }
图2-11 出现脏读情况
程序运行结果如图2-12所示。
图2-12 不出现脏读了
方法setValue()和getValue()被依次执行,通过这个示例可以知道脏读是通过synch-ronized关键字解决的。
当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这两个实例变量已经同时被赋值,不存在脏读的基本环境。
多个线程执行同一个业务对象中的不同同步方法时,是按顺序同步的方式调用的。
脏读前一定会出现不同线程一起去写实例变量的情况,这就是不同线程“争抢”实例变量的结果。
关键字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所示。
图2-13 程序运行结果
“可重入锁“是指自己可以再次获取自己的内部锁。例如,一个线程获得了某个对象锁,此时这个对象锁还没有释放,当其再次想要获取这个对象锁时还是可以获取的,如果不可重入锁,则方法service2()不会被调用,方法service3()更不会被调用。
锁重入也支持父子类继承的环境。
创建实验用的项目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); super.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所示。
此示例说明,当存在父子类继承关系时,子类是完全可以通过锁重入调用父类的同步方法的。
图2-14 重入到父类中的锁
当一个线程执行的代码出现异常时,其所持有的锁会自动释放。
创建实验用的项目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所示。
图2-15 两个自定义线程类代码
运行类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所示。
图2-16 程序运行结果
线程a出现异常并释放锁,线程b进入方法正常输出。本示例说明,当出现异常时,锁可以自动释放。
要注意,类Thread.java中的suspend()方法和sleep(millis)方法被调用后并不释放锁。
重写方法如果不使用synchronized关键字,即是非同步方法,使用后变成同步方法。
创建测试用的项目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所示。
图2-17 两个线程代码
类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所示。
图2-18 程序运行结果
从输出结果可以看到,线程以异步的方式进行输出,所以需要在子类的重写方法中添加synchronized关键字。程序运行结果如图2-19所示。
图2-19 同步了
public static native boolean holdsLock(Object obj)方法的作用是当currentThread在指定的对象上保持锁定时,才返回true。
创建测试用的代码如下:
package test9; public class Test1 { public static void main(String[] args) { System.out.println("A " + Thread.currentThread().holdsLock(Test1.class)); synchronized (Test1.class) { System.out.println("B " + Thread.currentThread().holdsLock(Test1.class)); } System.out.println("C " + Thread.currentThread().holdsLock(Test1.class)); } }
程序运行结果如下:
A false B true C false