java多线程(对象和变量的并发访问)

时间:2022-11-14 18:06:09

在现实开发中,我们写的线程肯定会有不同的实例在执行,此时就可能会出现”非线程安全问题”,非线程安全就是:多个线程对同一个对象中的实例变量进行并发访问时候,有可能A和B线程同时读取到数据,先后进行更改,此时,该变量就不是我们期望的数据,也就是通常所说的”脏数据”

实例变量非线程安全

  • 需要注意的是,方法中的变量是不存在非线程安全问题的,这是因为方法内部的变量都是私有的。
  • 如果多个线程共同访问了一个对象中的实例变量,则可能会出现线程安全问题。看下面代码:
public class MultiThreadSet {
private int count;

public void setCount(String countStr) {
if ("first".equals(countStr)) {
count = 100;
System.out.println("first set over...");
} else {
count = 200;
System.out.println("second set over...");
}
System.out.println("setCount is :"+count);
}
}

java多线程(对象和变量的并发访问)

public static void main(String[] args) {
MultiThreadSet multiThreadSet = new MultiThreadSet();
ThreadA threadA = new ThreadA(multiThreadSet);
ThreadB threadB = new ThreadB(multiThreadSet);

threadA.start();
threadB.start();
}

此时执行程序,结果如下:
java多线程(对象和变量的并发访问)

synchronized public void setCount(String countStr) {
if ("first".equals(countStr)) {
count = 100;
System.out.println("first set over...");
} else {
count = 200;
System.out.println("second set over...");
}
System.out.println(countStr + "setCount is :"+count);
}

java多线程(对象和变量的并发访问)
可以看到,当多个线程访问同一对象中的同步方法时候,一定是线程安全的,那么如果是多个线程访问多个对象的同步方法,会是怎样呢?我们拭目以待:

MultiThreadSet multiThreadSetFirst = new MultiThreadSet();
MultiThreadSet multiThreadSetSecond = new MultiThreadSet();
ThreadA threadA = new ThreadA(multiThreadSetFirst);
ThreadB threadB = new ThreadB(multiThreadSetSecond);

threadA.start();
threadB.start();

java多线程(对象和变量的并发访问)
以上是两个线程分别访问同一个类的多个不同实例相同的同步方法,结果却是以异步的方式执行的
多个线程访问多个对象,JVM会创建多个锁,上关键字synchronized 取得的锁都是对象锁,哪个线程先执行带synchronized 关键字的方法,哪个线程就持有该方法所属的对象锁,那么其他线程只能等待,前提是多个线程访问的是同一个对象

锁对象

下面创建一个类,包含两个方法,一个使用synchronized修饰,一个是普通方法:

public class SyncLockMethod {

synchronized public void methodA() {
try {
System.out.println("methodA runs begin...... thread name is :"+Thread.currentThread().getName());
Thread.sleep(3000);
System.out.println("methodA runs end......");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

public void methodB() {
try {
System.out.println("methodB runs begin======thread name is :"+Thread.currentThread().getName());
Thread.sleep(3000);
System.out.println("methodB runs end======");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

分别创建两个线程ThreadA和ThreadB,然后在两个线程中调用不同的方法。

public class ThreadA extends Thread {
private SyncLockMethod syncLockMethod;

public ThreadA(SyncLockMethod syncLockMethod) {
super();
this.syncLockMethod = syncLockMethod;
}

@Override
public void run() {
syncLockMethod.methodA();
}
}

// =================================================
public class ThreadB extends Thread {
private SyncLockMethod syncLockMethod;

public ThreadB(SyncLockMethod syncLockMethod) {
this.syncLockMethod = syncLockMethod;
}

@Override
public void run() {
syncLockMethod.methodB();
}
}

创建SyncLockMethod 类的一个实例对象,分别传入两个线程中去执行该对象中的不同方法:

public static void main(String[] args) {
SyncLockMethod syncLockMethod = new SyncLockMethod();
MultiThreadSet multiThreadSetSecond = new MultiThreadSet();
ThreadA threadA = new ThreadA(syncLockMethod);
ThreadB threadB = new ThreadB(syncLockMethod);

threadA.start();
threadB.start();
}

此时运行结果如下:
java多线程(对象和变量的并发访问)
此时,我将methodB也改为同步方法:

synchronized public void methodB() {
try {
System.out.println("methodB runs begin======thread name is :"+Thread.currentThread().getName());
Thread.sleep(3000);
System.out.println("methodB runs end======");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

在执行上面的操作,结果如下:
java多线程(对象和变量的并发访问)

根据上面的对比操作,总结以下:

  • A线程先持有object对象的lock锁,B线程可以以异步的方式调用object对象中的非synchronized类型的方法
  • A线程先持有object对象的lock锁,B线程如果在这时调用object对象中的synchronized类型的方法,则需要等待

synchronized锁重入

synchronized锁重入:指的是当一个线程得到一个对象锁之后,再次请求该对象锁时候,可以再次得到该对象的锁。 看下面的栗子:

public class SynchronizedAgainService {

public synchronized void methodA() {
System.out.println("methodA runs .......");
methodB();
}
public synchronized void methodB() {
System.out.println("methodB runs ========");
methodC();
}
public synchronized void methodC() {
System.out.println("methodC runs +++++++++");
}
}


// =================================================

public class SynchronizedAgainThread extends Thread {

@Override
public void run() {
SynchronizedAgainService service = new SynchronizedAgainService();
service.methodA();
}

public static void main(String[] args) {
new SynchronizedAgainThread().start();
}

}

此时执行结果如下:
java多线程(对象和变量的并发访问)
可以看到当一个线程获得了某个对象的锁,此时这个对象的锁还没有释放,此时依然可以再次获得该对象撒花姑娘的锁,另外当存在父子类继承关系时候,子类完全可以通过”可重入锁”调用父类中的同步方法。

synchronized同步代码块

当两个并发线程访问同一个对象object中的synchronized(this)同步代码块时,一段时间内,只能有一个线程被执行,另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。

public class SubService {

public void printService() {
try {
// synchronized同步代码块,
synchronized (this) {
System.out.println("begin ParentSerice current name is :"+Thread.currentThread().getName());
Thread.sleep(2000);
System.out.println("end ParentSerice current name is :"+Thread.currentThread().getName());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}


// =============================================
public class ThreadA extends Thread {
private SubService subService;

public ThreadA(SubService subService) {
super();
this.subService = subService;
}

@Override
public void run() {
subService.printService();
}
}


// =============================================
public static void main(String[] args) {
SubService subService = new SubService();
ThreadA threadA = new ThreadA(subService);
ThreadB threadB = new ThreadB(subService);

threadA.setName("threadA");
threadB.setName("threadB");
threadA.start();
threadB.start();
}

此时运行结果如下:
java多线程(对象和变量的并发访问)
可以看到上面的代码使用了synchronized代码块,是的线程得以同步运行,但是执行效率还是很低下的。可以将需要同步的代码块最小化,使用synchronized包裹起来。
需要注意的是:同步synchronized(this)代码块锁定的是当前对象,当多个线程调用同一个对象中的不同名称的synchronized同步方法或者synchronized(this)同步代码块时候,调用的效果是按顺序执行的,也就是同步的。

将任意对象作为对象监视器

我们可以使用synchronized(非this对象)来同步代码块

  • 在多个线程持有”对象监视器”为同一个对象的前提下,同一时间只有一个线程可以执行synchronized(非this对象)同步代码块汇总的代码
public class MyService {

private String name;
private String pass;
// 使用全局的anyString对象作为synchronized的对象监视器,由于是全局的,所以在synchronized (anyString)中使用的是同一个监视器
private String anyString = new String();

public void setNamePass(String name,String pass) {
try {
synchronized (anyString) { // 同步代码块,如果是同一个对象监视器,则是同步执行的
System.out.println("begin the thread name is :"+Thread.currentThread().getName());
this.name = name;
Thread.sleep(3000);
this.pass = pass;
System.out.println("end the thread name is :"+Thread.currentThread().getName()+" name is :"+name+" pass is :"+pass);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

// ====================================================
public class ThreadA extends Thread {
private MyService myService;

public ThreadA(MyService myService) {
super();
this.myService = myService;
}

@Override
public void run() {
myService.setNamePass("aaa", "aaapass");
}
}

// =================================================
public class ThreadB extends Thread {
private MyService myService;

public ThreadB(MyService myService) {
super();
this.myService = myService;
}

@Override
public void run() {
myService.setNamePass("bbb", "bbbpass");
}
}

// ==============================================
public static void main(String[] args) {
MyService myService = new MyService();
ThreadA threadA = new ThreadA(myService);
ThreadB threadB = new ThreadB(myService);

threadA.setName("threadA");
threadB.setName("threadB");
threadA.start();
threadB.start();
}

此时程序执行结果如下:
java多线程(对象和变量的并发访问)
可以看到,由于这里使用了全局的对象作为对象监视器,所以不同的线程进来执行的同一对象方法,即是相同的对象监视器。所以可以做到代码开同步。下面我们将anyString放到setNamePass方法内部,看下效果:

public void setNamePass(String name,String pass) {
try {
// 这里不管是否调用的是同一对象的setNamePass方法,都会创建anyString对象,所以是没有办法做到代码块同步的。
String anyString = new String();
synchronized (anyString) { // 同步代码块,如果是同一个对象监视器,则是同步执行的
System.out.println("begin the thread name is :"+Thread.currentThread().getName());
this.name = name;
Thread.sleep(3000);
this.pass = pass;
System.out.println("end the thread name is :"+Thread.currentThread().getName()+" name is :"+name+" pass is :"+pass);
}
} catch (InterruptedException e) {
e.printStackTrace();
}

此时执行结果如下:
java多线程(对象和变量的并发访问)
可以看到,使用synchronized(非this对象)同步代码块时候,如果对象监视器不是同一个对象,运行的结果就是异步调用了。

这里,锁如果不是this对象,也有一定的有点,试想如果一个类中有很多synchronized方法,这时候,虽然能够实现同步,但是一个方法会阻塞其他方法的执行,但是如果使用同步代码块锁非this对象,则synchronized(非this) 代码块中的程序与同步方法是异步的,不与其他锁this同步方法争抢this锁,可以提高效率。

总结一下:
- 当多个线程同时执行synchronized(x) {}同步代码块时候,呈同步效果,前提是这多个线程必须要是同一个对象监视器
- 当其他线程执行x对象中synchronized同步方法时呈同步效果
- 当其他线程执行x对象方法里面的synchronized(this)代码块时,也呈现同步效果。

静态同步synchronized方法

关键字synchronized还可以应用在static静态方法上,如果这样,那么就是对当前的”*.java”对应的class类进行持锁。

public class MyService {

synchronized public static void printA() {
printInfo("printA");
}

synchronized public static void printB() {
printInfo("printB");
}

private static void printInfo(String methodName) {
try {
String anyString = new String();
System.out.println("begin "+methodName+" the thread name is :"+Thread.currentThread().getName());
Thread.sleep(3000);
System.out.println("end "+methodName+" the thread name is :"+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

// ====================================================
public class ThreadA extends Thread {
private MyService myService;

public ThreadA(MyService myService) {
super();
this.myService = myService;
}

@Override
public void run() {
myService.printA();
}
}

// ===================================================
public class ThreadB extends Thread {
private MyService myService;

public ThreadB(MyService myService) {
super();
this.myService = myService;
}

@Override
public void run() {
myService.printB();
}
}


// ====================================================
public static void main(String[] args) {
MyService myServiceFirst = new MyService();
MyService myServiceSecond = new MyService();
ThreadA threadA = new ThreadA(myServiceFirst);
ThreadB threadB = new ThreadB(myServiceSecond);

threadA.setName("threadA");
threadB.setName("threadB");
threadA.start();
threadB.start();
}

可以看到,上面在Myservice类中为printA和printB两个静态方法添加了synchronized,此时持有的就是当前class类的锁。这里由于是同一个锁,所以是同步打印的。
java多线程(对象和变量的并发访问)

同步synchronized方法无限等待

同步方法容易造成死循环,其实说到底,还是多个线程持有的锁是一样的。看下面代码:

public class MyService {

synchronized public void printA() {
System.out.println("printA beign...");
boolean isContinue = true;
while (isContinue) {

}
System.out.println("printA end...");
}

synchronized public void printB() {
System.out.println("printB beign...");
System.out.println("printB end...");
}

}

此时打印结果如下:
java多线程(对象和变量的并发访问)

可以看到程序走到这里进入了无线等待的状态,这是由于这里使用的是同一个对象作为锁,在printA方法中进入了无限循环的等待状态,此时没有释放锁,因此printB方法也不能获得当前锁,无法执行。因此我们只需要使用同步代码块,让两个方法,持有不同的锁对象

public class MyService {
private Object objA = new Object();
public void printA() {
synchronized (objA) {
System.out.println("printA beign...");
boolean isContinue = true;
while (isContinue) {

}
System.out.println("printA end...");
}
}
private Object objB = new Object();
public void printB() {
synchronized (objB) {
System.out.println("printB beign...");
System.out.println("printB end...");
}
}
}

java多线程(对象和变量的并发访问)

多线程死锁

java多线程是一个经典的多线程问题,因为不同的线程都在等待根本不可能被释放的锁,从而导致所有的任务都无法继续完成。下面代码演示两个同步代码块互相等待对象释放锁,从而导致死锁的问题。

public class DeadThread extends Thread {
private Object lockFirst = new Object();
private Object lockSecond = new Object();
private String userName;
public void setUserName(String userName) {
this.userName = userName;
}

@Override
public void run() {
if ("aaa".equals(userName)) {
synchronized (lockFirst) {
try {
System.out.println("username is :"+userName);
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockSecond) {
System.out.println("lockfirst ------>locksecond");
}
}
}
if ("bbb".equals(userName)) {
synchronized (lockSecond) {
try {
System.out.println("username is :"+userName);
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockFirst) {
System.out.println("locksecond ------>lockfirst");
}
}
}
}

public static void main(String[] args) {
DeadThread deadThreadFirst = new DeadThread();
deadThreadFirst.setUserName("aaa");
deadThreadFirst.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
DeadThread deadThreadSecond = new DeadThread();
deadThreadSecond.setUserName("bbb");
deadThreadSecond.start();
}
}

可以看到,两个if分支中的synchronized代码块,分别等待对象释放锁,就出现了死锁现象。

内部类与同步

测试:同步代码块synchronized(class2)对class2上锁以后,其他线程只能以同步的方式调用class2中的静态同步方法。

public class OutClass {
static class InnerClass1 {
public void method1(InnerClass2 class2) {
String threadName = Thread.currentThread().getName();
synchronized (class2) {
System.out.println(threadName+" 进入InnerClass1类的method1方法");
for (int i = 0; i < 10; i++) {
System.out.println("i = "+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(threadName+" 离开InnerClass1类的method1方法");
}
}

public void method2() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName+" 进入InnerClass1类的method2方法");
for (int i = 0; i < 10; i++) {
System.out.println("i = "+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(threadName+" 离开InnerClass1类的method2方法");

}
}

static class InnerClass2 {
public synchronized void method1() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName+" 进入InnerClass1类的method2方法");
for (int i = 0; i < 10; i++) {
System.out.println("i = "+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(threadName+" 离开InnerClass1类的method2方法");
}
}

public static void main(String[] args) {
InnerClass1 in1 = new InnerClass1();
InnerClass2 in2 = new InnerClass2();

Thread t1 = new Thread(new Runnable() {
public void run() {
in1.method1(in2);
}
},"t1");

Thread t2 = new Thread(new Runnable() {
public void run() {
in1.method2();
}
},"t2");

Thread t3 = new Thread(new Runnable() {
public void run() {
in2.method1();
}
},"t3");

t1.start();
t2.start();
t3.start();
}
}

此时程序运行结果如下:
java多线程(对象和变量的并发访问)

锁对象的改变

需要注意的是:在将任何数据类型作为同步锁时候,当多个线程同时持有锁对象,如果同时持有相同的锁对象,则这些线程之间就是同步的,如果分别获得锁对象,则这些线程之间就是异步的。看下面的栗子:

public class MyService {

private String mLock = "aaa";

public void printInfo() {
synchronized (mLock) {
try {
mLock = "bbb";
System.out.println("begin "+Thread.currentThread().getName()+" the thread name is :"+Thread.currentThread().getName());
Thread.sleep(3000);
System.out.println("end "+Thread.currentThread().getName()+" the thread name is :"+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

java多线程(对象和变量的并发访问)
测试:

public static void main(String[] args) {
MyService myService = new MyService();
ThreadA threadA = new ThreadA(myService);
ThreadB threadB = new ThreadB(myService);

threadA.setName("threadA");
threadB.setName("threadB");
threadA.start();
threadB.start();
}

此时运行效果如下:
java多线程(对象和变量的并发访问)
可以看到,此时threadA和threadB由于在同步块内部更改了锁对象,因此两个线程持有的锁是不同的,所以是异步执行的。
另外需要注意的是,只要对象不变,及时对象的属性被改变,运行的结果还是同步的。

volatile关键字

volatile关键字的主要作用是使变量在多个线程之间可见。volatile会强制从公共堆栈中取得变量的值,而不是从私有数据栈中取得变量的值。

使用volatile关键字增加了实例变量在多个线程之间的可见性,但是volatile不支持原子性。

volatile VS synchronized

下面将关键字volatile和synchronized进行一下比较:

  • volatile是线程同步的轻量级实现,所以volatile性能比synchronized要好,并且volatile只能修饰变量,而synchronized可以修饰方法以及代码块。
  • 多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
  • volatile可以保证数据的可见性,但是不能保证原子性。

ok,java多线程中关于对象和变量的并发访问就到这里了。希望大家喜欢。