java 方法锁_java 多线程8 : synchronized锁机制 之 方法锁

本文通过实例代码探讨了Java中多线程环境下可能出现的脏读问题,并展示了如何通过`synchronized`关键字实现方法锁以确保线程安全。分析了synchronized的锁对象、锁重入以及异常自动释放锁的特性,强调了同步方法在多线程并发控制中的作用。
摘要由CSDN通过智能技术生成

脏读

一个常见的概念。在多线程中,难免会出现在多个线程中对同一个对象的实例变量或者全局静态变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数据其实是被更改过的。注意这里 局部变量是不存在脏读的情况

多线程线程实例变量非线程安全

看一段代码:

public classThreadDomain13

{private int num = 0;public voidaddNum(String userName)

{try{if ("a".equals(userName))

{

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)

{

e.printStackTrace();

}

}

}

写两个线程分别去add字符串"a"和字符串"b":

public class MyThread13_0 extendsThread

{privateThreadDomain13 td;publicMyThread13_0(ThreadDomain13 td)

{this.td =td;

}public voidrun()

{

td.addNum("a");

}

}

public class MyThread13_1 extendsThread

{privateThreadDomain13 td;publicMyThread13_1(ThreadDomain13 td)

{this.td =td;

}public voidrun()

{

td.addNum("b");

}

}

写一个主函数分别运行这两个线程:

public static voidmain(String[] args)

{

ThreadDomain13 td= newThreadDomain13();

MyThread13_0 mt0= newMyThread13_0(td);

MyThread13_1 mt1= newMyThread13_1(td);

mt0.start();

mt1.start();

}

看一下运行结果:

a set over!b set over!b num= 200a num= 200

按照正常来看应该打印"a num = 100"和"b num = 200"才对,现在却打印了"b num = 200"和"a num = 200",这就是线程安全问题。我们可以想一下是怎么会有线程安全的问题的:

1、mt0先运行,给num赋值100,然后打印出"a set over!",开始睡觉

2、mt0在睡觉的时候,mt1运行了,给num赋值200,然后打印出"b set over!",然后打印"b num = 200"

3、mt1睡完觉了,由于mt0的num和mt1的num是同一个num,所以mt1把num改为了200了,mt0也没办法,对于它来说,num只能是100,mt0继续运行代码,打印出"a num = 200"

分析了产生问题的原因,解决就很简单了,给addNum(String userName)方法加同步即可:

多线程线synchronized关键字加到方法上

public classThreadDomain13

{private int num = 0;public synchronized voidaddNum(String userName)

{try{if ("a".equals(userName))

{

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)

{

e.printStackTrace();

}

}

}

看一下运行结果:

a set over!a num= 100b set over!b num= 200

多个对象多个锁

在同步的情况下,把main函数内的代码改一下:

public static voidmain(String[] args)

{

ThreadDomain13 td0= newThreadDomain13();

ThreadDomain13 td1= newThreadDomain13();

MyThread13_0 mt0= newMyThread13_0(td0);

MyThread13_1 mt1= newMyThread13_1(td1);

mt0.start();

mt1.start();

}

看一下运行结果:

a set over!b set over!b num= 200a num= 100

打印结果的方式变了,打印的顺序是交叉的,这又是为什么呢?

这里有一个重要的概念。关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,这里如果是把一段代码或方法(函数)当作锁,其实获取的也是对象锁,只是监视器(对象)不同而已,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁,其他线程都只能呈等待状态。但是这有个前提:既然锁叫做对象锁,那么势必和对象相关,所以多个线程访问的必须是同一个对象。

如果多个线程访问的是多个对象,那么Java虚拟机就会创建多个锁,就像上面的例子一样,创建了两个ThreadDomain13对象,就产生了2个锁。既然两个线程持有的是不同的锁,自然不会受到"等待释放锁"这一行为的制约,可以分别运行addNum(String userName)中的代码。

synchronized方法与锁对象

上面我们认识了对象锁,对象锁这个概念,比较抽象,确实不太好理解,看一个例子,在一个实体类中定义一个同步方法和一个非同步方法:

public classThreadDomain14_0

{public synchronized voidmethodA()

{try{

System.out.println("Begin methodA, threadName = " +Thread.currentThread().getName());

Thread.sleep(5000);

System.out.println("End methodA, threadName = " +Thread.currentThread().getName()+ ", end Time = " +System.currentTimeMillis());

}catch(InterruptedException e)

{

e.printStackTrace();

}

}public voidmethodB()

{try{

System.out.println("Begin methodB, threadName = " +Thread.currentThread().getName()+ ", begin time = " +System.currentTimeMillis());

Thread.sleep(5000);

System.out.println("End methodB, threadName = " +Thread.currentThread().getName());

}catch(InterruptedException e)

{

e.printStackTrace();

}

}

}

一个线程调用其同步方法,一个线程调用其非同步方法:

public class MyThread14_0 extendsThread

{privateThreadDomain14_0 td;publicMyThread14_0(ThreadDomain14_0 td)

{this.td =td;

}public voidrun()

{

td.methodA();

}

}

public class MyThread14_1 extendsThread

{privateThreadDomain14_0 td;publicMyThread14_1(ThreadDomain14_0 td)

{this.td =td;

}public voidrun()

{

td.methodB();

}

}

写一个main函数去掉用这两个线程:

public static voidmain(String[] args)

{

ThreadDomain14_0 td= newThreadDomain14_0();

MyThread14_0 mt0= newMyThread14_0(td);

mt0.setName("A");

MyThread14_1 mt1= newMyThread14_1(td);

mt1.setName("B");

mt0.start();

mt1.start();

}

看一下运行效果:

Begin methodA, threadName =A

Begin methodB, threadName= B, begin time = 1443697780869End methodB, threadName=B

End methodA, threadName= A, end Time = 1443697785871

从结果看到,第一个线程调用了实体类的methodA()方法,第二个线程完全可以调用实体类的methodB()方法。但是我们把methodB()方法改为同步就不一样了,就不列修改之后的代码了,看一下运行结果:

Begin methodA, threadName =A

End methodA, threadName= A, end Time = 1443697913156Begin methodB, threadName= B, begin time = 1443697913156End methodB, threadName=B

从这个例子我们得出两个重要结论:

1、A线程持有Object对象的Lock锁,B线程可以以异步方式调用Object对象中的非synchronized类型的方法

2、A线程持有Object对象的Lock锁,B线程如果在这时调用Object对象中的synchronized类型的方法则需要等待,也就是同步

synchronized锁重入

关键字synchronized拥有锁重入的功能。所谓锁重入的意思就是:当一个线程得到一个对象锁后,再次请求此对象锁时时可以再次得到该对象的锁的。看一个例子:

922179e7f4871c5a47416ad3e09fb26a.pngpublic classThreadDomain16

{public synchronized voidprint1()

{

System.out.println("ThreadDomain16.print1()");

print2();

}public synchronized voidprint2()

{

System.out.println("ThreadDomain16.print2()");

print3();

}public synchronized voidprint3()

{

System.out.println("ThreadDomain16.print3()");

}

}

922179e7f4871c5a47416ad3e09fb26a.png

922179e7f4871c5a47416ad3e09fb26a.pngpublic class MyThread16 extendsThread

{public voidrun()

{

ThreadDomain16 td= newThreadDomain16();

td.print1();

}

}

922179e7f4871c5a47416ad3e09fb26a.png

public static voidmain(String[] args)

{

MyThread16 mt= newMyThread16();

mt.start();

}

看一下运行结果:

ThreadDomain16.print1()

ThreadDomain16.print2()

ThreadDomain16.print3()

看到可以直接调用ThreadDomain16中的打印语句,这证明了对象可以再次获取自己的内部锁。这种锁重入的机制,也支持在父子类继承的环境中。

异常自动释放锁

最后一个知识点是异常。当一个线程执行的代码出现异常时,其所持有的锁会自动释放。模拟的是把一个long型数作为除数,从MAX_VALUE开始递减,直至减为0,从而产生ArithmeticException。看一下例子:

public classThreadDomain17

{public synchronized voidtestMethod()

{try{

System.out.println("Enter ThreadDomain17.testMethod, currentThread = " +Thread.currentThread().getName());long l =Integer.MAX_VALUE;while (true)

{long lo = 2 /l;

l--;

}

}catch(Exception e)

{

e.printStackTrace();

}

}

}

public class MyThread17 extendsThread

{privateThreadDomain17 td;publicMyThread17(ThreadDomain17 td)

{this.td =td;

}public voidrun()

{

td.testMethod();

}

}

public static voidmain(String[] args)

{

ThreadDomain17 td= newThreadDomain17();

MyThread17 mt0= newMyThread17(td);

MyThread17 mt1= newMyThread17(td);

mt0.start();

mt1.start();

}

看一下运行结果:

Enter ThreadDomain17.testMethod, currentThread = Thread-0Enter ThreadDomain17.testMethod, currentThread= Thread-1java.lang.ArithmeticException:/by zero

at com.xrq.example.e17.ThreadDomain17.testMethod(ThreadDomain17.java:14)

at com.xrq.example.e17.MyThread17.run(MyThread17.java:14)

java.lang.ArithmeticException:/by zero

at com.xrq.example.e17.ThreadDomain17.testMethod(ThreadDomain17.java:14)

at com.xrq.example.e17.MyThread17.run(MyThread17.java:14)

因为打印结果是静态的,所以不是很明显。在l--前一句加上Thread.sleep(1)结论会更明显,第一句打出来之后,整个程序都停住了,直到Thread-0抛出异常后,Thread-1才可以运行,这也证明了我们的结论。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值