多线程编程中出现的数据读写不同步的问题给多线程编程开发者带来了很大的困扰。一般来说,线程中数据读写不同步是由于多个线程对同一个对象中的实例变量进行并发访问造成的;我们将这种由于多个线程对同一个对象中的实例变量进行并发访问,造成数据被“脏读”的这种现象称为非线程安全;与之对应的,线程安全就是指对同一个对象中的实例变量的访问是经过同步处理的,不会造成实例变量数据的“脏读”。
为了解决多线程编程中出现的非线程安全问题,多线程编程中的同步技术就成为了学习多线程技术的重中之重。只有很好地处理多线程编程中的非线程安全问题,才能更好地利用多线程带来的优势去解决实际开发中遇到的问题。
多线程编程中有多种不同的同步技术可以解决多线程编程中的非线程安全问题。其中,学会使用synchronized关键字的用法,是解决多线程编程中非线程安全问题的比较适合入门的途径。
这里首先要强调的是,非线程安全问题只存在于多个线程对同一个对象中的实例变量进行并发访问的场景中,而对方法内部的私有变量进行访问,则不会出现非线程安全问题,这是因为,对于不同的线程访问同一个方法中的私有变量,其实访问的不是同一个变量,而是不同的线程都拥有这样一个私有变量,彼此之间,互不干扰;
其次,关键字synchronized取得的锁都是对象锁,不同的对象具有不同的锁,不要认为关键字synchronized加在的位置是方法或则代码块,就理所当然地认为锁是属于方法或代码块的;
下面对多线程编程中的同步技术的讨论都是基于多个线程并发访问同一个对象的实例变量的前提展开的。为了便于叙述和举例给出示例代码,一般只对两个线程并发访问同一对象的实例变量时可能出现的非线程安全问题进行讨论和解决;
一、synchronized同步方法(将synchronized关键字加在方法处)
示例代码:
传入线程构造函数的对象的LockObject类:
package com.synchronizedmethod;
public class LockObject {
private int number=0;//实例变量,也就是多个线程并发访问的数据
//synchronized public void setNumber() { //①
public void setNumber() {//②
try {
if(Thread.currentThread().getName().equals("A")){
number = 1;
System.out.println("线程"+Thread.currentThread().getName()+"设置number完毕");
Thread.sleep(1000);//此方法有可能要抛出异常,因此这里添加try/catch语句块将其包围
}
if(Thread.currentThread().getName().equals("B")){
number = 2;
System.out.println("线程"+Thread.currentThread().getName()+"设置number完毕");
}
System.out.println("线程"+Thread.currentThread().getName()+"number="+number);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//此方法为非同步方法,用以搭配对象中的同步方法,验证加锁方法与未加锁的方法的执行是异步执行的;同一个对象中的不同的加锁方法之间是同步执行的;
public void getNumber() {
System.out.println("线程"+Thread.currentThread().getName()+"进入getNumber方法"+"number="+number);
}
//以下的三个方法用来验证关键字synchronized拥有“锁重入”的功能
//synchronized关键字同步方法method1
synchronized public void method1() {
System.out.println("method1");
method2();//同步方法中method1调用同一类中另一个同步方法method2
}
//synchronized关键字同步方法method2
synchronized public void method2() {
System.out.println("method2");
method3();//同步方法中method1调用同一类中另一个同步方法method3
}
//synchronized关键字同步方法method3
synchronized public void method3() {
System.out.println("method3");
}
}
自定义线程类MyThread:
package com.synchronizedmethod;
public class MyThread extends Thread {
private LockObject lockObject;
public MyThread(LockObject lockObject) {
this.lockObject = lockObject;
}
@Override
public void run() {
lockObject.setNumber();
}
}
测试运行Test类:
package com.synchronizedmethod;
public class Test {
public static void main(String[] args) throws InterruptedException {
LockObject lockObject = new LockObject();
MyThread threadA = new MyThread(lockObject);//将实例对象lockObject放入构造函数中,实例化线程对象threadA
threadA.setName("A");
threadA.start();
Thread.sleep(500);
MyThread threadB = new MyThread(lockObject);//将实例对象lockObject放入构造函数中,实例化线程对象threadA
threadB.setName("B");
threadB.start();
}
}
在测试运行Test类中实例化的两个线程对象中都包含同一个实例对象lockObject作为各自的数据域,因此两个线程启动后,将会对实例对象lockObject中的数据域number进行并发访问,这里就有可能产生非线程安全问题;
运行结果如下图所示:
将①处的注释去掉,将位置②所在的行注释,再次运行的结果如下图所示:
从两次运行的结果我们可以看出,对方法setNumber()加上synchronized关键字后,不同的线程访问同一个对象lockObject的setNumber()方法时,必须取得对象lockObject的锁,才能调用setNumber(),如果当前线程对象B将要执行setNumber()方法时,发现对象lockObject的锁被另一线程A占用,则只能等待线程对象A执行完setNumber()方法之后,将锁释放,线程对象B才能去抢占这把锁,从而去执行setNumber()方法,这样就实现了不同线程对同一个方法的调用操作是同步执行;
修改测试运行Test类如下:
package com.synchronizedmethod;
public class Test {
public static void main(String[] args) throws InterruptedException {
LockObject lockObjectA = new LockObject();
LockObject lockObjectB = new LockObject();
MyThread threadA = new MyThread(lockObjectA);//将实例对象lockObjectA放入构造函数中,实例化线程对象threadA
threadA.setName("A");//将线程对象取名为A
threadA.start();
Thread.sleep(500);
MyThread threadB = new MyThread(lockObjectB);//将实例对象lockObjectB放入构造函数中,实例化线程对象threadB
threadB.setName("B");//将线程对象取名为B
threadB.start();
}
}
添加线程类ThreadA和ThreadB,代码如下:
package com.synchronizedmethod;
public class ThreadA extends Thread {
private LockObject lockObject;
public ThreadA(LockObject lockObject) {
this.lockObject = lockObject;
}
@Override
public void run() {
lockObject.setNumber();//setNumber()方法为同步方法
}
}
package com.synchronizedmethod;
public class ThreadB extends Thread {
private LockObject lockObject;
public ThreadB(LockObject lockObject) {
this.lockObject = lockObject;
}
@Override
public void run() {
lockObject.getNumber();//getNumber()方法不是同步方法
}
}
修改运行测试Test类,代码如下:
package com.synchronizedmethod;
public class Test {
public static void main(String[] args) throws InterruptedException {
LockObject lockObject = new LockObject();
ThreadA threadA = new ThreadA(lockObject);
threadA.setName("A");
threadA.start();
Thread.sleep(500);
ThreadB threadB = new ThreadB(lockObject);
threadB.setName("B");
threadB.start();
}
}
从这一运行结果可以看出,线程threadA和threadB分别执行同一实例对象中的同步方法setNumber()、非同步方法getNumber(),执行时是异步执行,这是因为线程threadB不需要获得对象锁即可访问非同步方法getNumber();
添加线程类ThreadC,代码如下:
package com.synchronizedmethod;
public class ThreadC extends Thread {
private LockObject lockObject;
public ThreadC(LockObject lockObject) {
this.lockObject = lockObject;
}
@Override
public void run() {
lockObject.method1();//method1方法为synchronized方法,并且method1方法调用了method2方法,method2也是synchronized方法,方法method2方法调用了method3方法,method3方法也是synchronized方法
}
}
修改测试运行Test类,代码如下:
package com.synchronizedmethod;
public class Test {
public static void main(String[] args) throws InterruptedException {
LockObject lockObject = new LockObject();
ThreadC threadC = new ThreadC(lockObject);
threadC.setName("C");
threadC.start();
}
}
从运行结果我们可以知道,当线程对象threadC获取加在lockObject对象上的锁,调用synchronized方法method1方法时,在释放该锁之前,可以继续访问该对象可以调用的其他synchronized方法,如method2和method3;
添加子锁对象的类SubLockObject,代码如下:
package com.synchronizedmethod;
public class SubLockObject extends LockObject{
synchronized public void subMethod() {
System.out.println("子类中的同步方法可以调用父类中的同步方法");
try {
for(int i = 0; i < 5 ; i++) {
Thread.sleep(1000);
System.out.println(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void method() {
this.method1();
}
}
添加线程类ThreadD、ThreadE,代码如下:
package com.synchronizedmethod;
public class ThreadD extends Thread {
private SubLockObject sublockObject;
public ThreadD(SubLockObject sublockObject) {
this.sublockObject = sublockObject;
}
@Override
public void run() {
sublockObject.subMethod();
}
}
package com.synchronizedmethod;
public class ThreadE extends Thread {
private SubLockObject sublockObject;
public ThreadE(SubLockObject sublockObject) {
this.sublockObject = sublockObject;
}
@Override
public void run() {
sublockObject.method();
}
}
修改测试运行类Test,代码如下:
package com.synchronizedmethod;
public class Test {
public static void main(String[] args) throws InterruptedException {
SubLockObject sublockObject = new SubLockObject();
ThreadD threadD = new ThreadD(sublockObject);
threadD.setName("D");
threadD.start();
ThreadE threadE = new ThreadE(sublockObject);
threadE.setName("E");
threadE.start();
}
}
从运行结果,我们可以得出:从父类继承得到的同步方法,在子类中同样具有同步性质,但是如果重写了从父类继承得到的方法时,并没有添加synchronized关键字时,则子类中的该方法将不具备同步性质;
修改子锁对象的类SubLockObject,代码如下:
package com.synchronizedmethod;
public class SubLockObject extends LockObject{
synchronized public void subMethod() {
System.out.println("子类中的同步方法可以调用父类中的同步方法");
try {
for(int i = 0; i < 5 ; i++) {
Thread.sleep(1000);
System.out.println(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void method1() {
System.out.println("method1方法在子类中已重写");
}
}