java线程 锁_Java多线程(二) 多线程的锁机制

本文探讨了多线程编程中的并发问题,如线程重入引发的内存泄漏和程序错误。通过实例分析了单例模式和生产消费问题,并介绍了Java的synchronized和volatile锁机制如何控制线程同步与通信,以避免数据竞争和线程交错。重点讲解了如何使用synchronized关键字和volatile关键字来确保代码的正确执行和资源的合理利用。
摘要由CSDN通过智能技术生成

当两条线程同时访问一个类的时候,可能会带来一些问题。并发线程重入可能会带来内存泄漏、程序不可控等等。不管是线程间的通讯还是线程共享数据都需要使用Java的锁机制控制并发代码产生的问题。本篇总结主要著名Java的锁机制,阐述多线程下如何使用锁机制进行并发线程沟通。

1、并发下的程序异常

先看下下面两个代码,查看异常内容。

异常1:单例模式

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

1 packagecom.scl.thread;2

3 public classSingletonException4 {5 public static voidmain(String[] args)6 {7 //开启十条线程进行分别测试输出类的hashCode,测试是否申请到同一个类

8 for (int i = 0; i < 10; i++)9 {10 new Thread(newRunnable()11 {12 @Override13 public voidrun()14 {15 try

16 {17 Thread.sleep(100);18 }19 catch(InterruptedException e)20 {21 e.printStackTrace();22 }23 System.out.println(Thread.currentThread().getName() + " " +MySingle.getInstance().hashCode());24 }25 }).start();26 }27 }28 }29

30 classMySingle31 {32 private static MySingle mySingle = null;33

34 privateMySingle()35 {36 }37

38 public staticMySingle getInstance()39 {40 if (mySingle == null) { mySingle = newMySingle(); }41 returnmySingle;42 }43 }

view code

运行结果如下:

cef5cfac64d29734d037b80ef0dfaf47.png

由上述可见,Thread-7与其他结果不一致,证明了在多线程并发的情况下这种单例写法存在问题,问题就在第40行。多个线程同时进入了空值判断,线程创建了新的类。

异常2:线程重入,引发程序错误

现在想模拟国企生产规则,每个月生产100件产品,然后当月消费20件,依次更替。模拟该工厂全年的生产与销售

备注:举这个实例是为后面的信号量和生产者消费者问题做铺垫。可以另外举例,如开辟十条线程,每条线程内的任务就是进行1-10的累加,每条线程输出的结果不一定是55(线程重入导致)

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

1 packagecom.scl.thread;2

3 //每次生产100件产品,每次消费20件产品,生产消费更替12轮

4 public classThreadCommunicateCopy5 {6 public static voidmain(String[] args)7 {8 final FactoryCopy factory = newFactoryCopy();9 new Thread(newRunnable()10 {11

12 @Override13 public voidrun()14 {15 try

16 {17 Thread.sleep(2000);18 }19 catch(InterruptedException e)20 {21 e.printStackTrace();22 }23

24 for (int i = 1; i <= 12; i++)25 {26 factory.createProduct(i);27 }28

29 }30 }).start();31

32 new Thread(newRunnable()33 {34

35 @Override36 public voidrun()37 {38 try

39 {40 Thread.sleep(2000);41 }42 catch(InterruptedException e)43 {44 e.printStackTrace();45 }46

47 for (int i = 1; i <= 12; i++)48 {49 factory.sellProduct(i);50 }51

52 }53 }).start();54

55 }56 }57

58 classFactoryCopy59 {60 //生产产品

61 public void createProduct(inti)62 {63

64 for (int j = 1; j <= 100; j++)65 {66 System.out.println("第" + i + "轮生产,产出" + j + "件");67 }68 }69 //销售产品

70 public void sellProduct(inti)71 {72 for (int j = 1; j <= 20; j++)73 {74 System.out.println("第" + i + "轮销售,销售" + j + "件");75 }76

77 }78 }

View Code

结果如下:

faf3574c67aa83b9f10dbb69ffe3e9a8.png

该结果不能把销售线程和生产线程的代码分隔开,如果需要分隔开。可以使用Java的锁机制。下面总结下如何处理以上两个问题。

2、使用多线程编程目的及一些Java多线程的基本知识

使用多线程无非是期望程序能够更快地完成任务,这样并发编程就必须完成两件事情:线程同步及线程通信。

线程同步指的是:控制不同线程发生的先后顺序。

线程通信指的是:不同线程之间如何共享数据。

Java线程的内存模型:每个线程拥有自己的栈,堆内存共享 [来源:Java并发编程艺术 ],如下图所示。 锁是线程间内存和信息沟通的载体,了解线程间通信会对线程锁有个比较深入的了解。后面也会详细总结Java是如何根据锁的信息进行两条线程之间的通信。

0e51ad04809b1bf68efae0577c3376de.png

2、使用Java的锁机制

Java语音设计和数据库一样,同样存在着代码锁.实现Java代码锁比较简单,一般使用两个关键字对代码进行线程锁定。最常用的就是volatile和synchronized两个

2.1synchronized

synchronized关键字修饰的代码相当于数据库上的互斥锁。确保多个线程在同一时刻只能由一个线程处于方法或同步块中,确保线程对变量访问的可见和排它,获得锁的对象在代码结束后,会对锁进行释放。

synchronzied使用方法有两个:①加在方法上面锁定方法,②定义synchronized块。

模拟生产销售循环,可以通过synchronized关键字控制线程同步。代码如下:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

1 packagecom.scl.thread;2

3 //每次生产100件产品,每次消费20件产品,生产消费更替10轮

4 public classThreadCommunicate5 {6 public static voidmain(String[] args)7 {8 final FactoryCopy factory = newFactoryCopy();9 new Thread(newRunnable()10 {11

12 @Override13 public voidrun()14 {15 try

16 {17 Thread.sleep(2000);18 }19 catch(InterruptedException e)20 {21 e.printStackTrace();22 }23

24 for (int i = 1; i <= 12; i++)25 {26 factory.createProduct(i);27 }28

29 }30 }).start();31

32 new Thread(newRunnable()33 {34

35 @Override36 public voidrun()37 {38 try

39 {40 Thread.sleep(2000);41 }42 catch(InterruptedException e)43 {44 e.printStackTrace();45 }46

47 for (int i = 1; i <= 12; i++)48 {49 factory.sellProduct(i);50 }51

52 }53 }).start();54

55 }56 }57

58 classFactory59 {60 private boolean isCreate = true;61

62 public synchronized void createProduct(inti)63 {64 while (!isCreate)65 {66 try

67 {68 this.wait();69 }70 catch(InterruptedException e)71 {72 e.printStackTrace();73 }74 }75

76 for (int j = 1; j <= 100; j++)77 {78 System.out.println("第" + i + "轮生产,产出" + j + "件");79 }80 isCreate = false;81 this.notify();82 }83

84 public synchronized void sellProduct(inti)85 {86 while(isCreate)87 {88 try

89 {90 this.wait();91 }92 catch(InterruptedException e)93 {94 e.printStackTrace();95 }96 }97 for (int j = 1; j <= 20; j++)98 {99 System.out.println("第" + i + "轮销售,销售" + j + "件");100 }101 isCreate = true;102 this.notify();103 }104 }

View Code

上述代码通过synchronized关键字控制生产及销售方法每次只能1条线程进入。代码中使用了isCreate标志位控制生产及销售的顺序。

注意:即使代码不使用isCreate标志位进行控制,代码只会出现 :thread-0 生产---thread-0 生产--- thread-0 生产(生产完毕) ---thread-1 销售...这种情况,不会出现生产跟销售交替。原因:使用Synchronized关键字对方法进行约束,默认锁定的是对一个的object类,直到代码结束,才会把锁给释放。因此使用该关键字进行限制时不会出现线程交叠现象。

备注:默认的使用synchronized修饰方法, 关键字会以当前实例对象作为锁对象,对线程进行锁定。

单例模式的修改可以在getInstance方式中添加synchronized关键字进行约束,即可。

wait方法和notify方法将在第三篇线程总结中讲解。

2.2 volatile

volatile关键字主要用来修饰变量,关键字不像synchronized一样,能够块状地对代码进行锁定。该关键字可以看做对修饰的变量进行了读或写的同步操作。

如以下代码:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

1 packagecom.scl.thread;2

3 public classNumberRange4 {5 private volatile intunSafeNum;6

7 public intgetUnSafeNum()8 {9 returnunSafeNum;10 }11

12 public void setUnSafeNum(intunSafeNum)13 {14 this.unSafeNum =unSafeNum;15 }16

17 public intaddVersion()18 {19 return this.unSafeNum++;20 }21 }

View Code

代码编译后功能如下:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

1 packagecom.scl.thread;2

3 public classNumberRange4 {5 private volatile intunSafeNum;6

7 public synchronized intgetUnSafeNum()8 {9 returnunSafeNum;10 }11

12 public synchronized void setUnSafeNum(intunSafeNum)13 {14 this.unSafeNum =unSafeNum;15 }16

17 public intaddVersion()18 {19 int temp =getUnSafeNum();20 temp = temp + 1;21 setUnSafeNum(temp);22 returntemp;23 }24

25 }

View Code

由此可见,使用volatile变量进行自增或自减操作的时候,变量进行temp= temp+1这一步时,多条线程同时可能同时操作这一句代码,导致内容出差。线程代码内的原子性被破坏了。

单纯使用volatile来控制boolean或者某一个int类型的时候,感觉不出太大的作用。但当volatile在修饰一个对象的时候,对象必须按照步骤进行。在单线程的情况下new一个对象必须进行三步操作:①开辟存储空间 ②初始化 ③使用变量指向该内存。在并发的情况下,虚拟机创建对象可能依据这三步依次执行。可能③在②之前执行,那么就可能会导致程序抛出空指针异常。这时候可以使用volatile保证对象初始化原子性。

以上是对Java锁机制的总结,如有问题,烦请指出纠正。代码及例子很大一部分参考了《Java 并发编程艺术》[方腾飞 著]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值