多线程Synchronized锁的使用与线程之间的通讯
一、什么是线程安全问题
多线程同时对同一个全局变量做写操作,可能会受到其他线程的干扰,就会发生线程安全问题。
Java中的全局变量是存放在堆内存中的,而堆内容对于所有线程来说是共享的。
比如下面一个简单的代码案例:
publicclassThreadCountimplementsRunnable{privateint count =10;@Overridepublicvoidrun(){while(true){if(count >1){try{// 模拟两个线程的阻塞状态Thread.sleep(30);}catch(Exception e){thrownewRuntimeException(e);}
count--;System.out.println(Thread.currentThread().getName()+" == "+ count);}}}publicstaticvoidmain(String[] args){ThreadCount threadCount =newThreadCount();// 启动两个线程执行任务 newThread(threadCount).start();newThread(threadCount).start();}}
代码比较简单,我们看下面控制台的打印:
Thread-1==9Thread-0==9Thread-0==7Thread-1==7Thread-1==6Thread-0==6Thread-1==5Thread-0==5Thread-1==4Thread-0==4Thread-0==2Thread-1==2Thread-1==1Thread-0==1
可以看到两个线程之间产生了冲突,产生了线程安全问题。
二、如何解决线程安全问题
如何解决线程安全问题呢?或者说如何实现线程的同步呢?
核心思想:加锁
在同一个JVM中,多个线程需要竞争锁的资源。
那么哪些代码需要加锁呢?
可能会发生线程安全性问题的代码需要加锁。
还是上面的例子,我们在哪里加锁合适呢?
(1)锁加在run()方法上
@Overridepublicsynchronizedvoidrun(){while(true){if(count >1){try{// 模拟两个线程的阻塞状态Thread.sleep(30);}catch(Exception e){thrownewRuntimeException(e);}
count--;System.out.println(Thread.currentThread().getName()+" == "+ count);}}}
这样可不可以呢?是可以的,但是我们来思考一个问题,如果synchronized加在了run()方法上,那么该执行过程是单线程还是多线程呢?
答案是单线程。我们可以看到以下控制台打印:
Thread-0==9Thread-0==8Thread-0==7Thread-0==6Thread-0==5Thread-0==4Thread-0==3Thread-0==2Thread-0==1
这是为什么呢?
原因是因为synchronized加在了run()方法上,获取到锁的线程不会释放锁,会一直持有锁,所以方法的执行就变成了单线程的;没有获取锁的线程,如果一直没有获取锁,中间需要经历一个锁的升级过程,最后会一直阻塞等待锁的释放。
(2)锁加在操作共享资源的代码上
@Overridepublicvoidrun(){while(true){if(count >1){try{Thread.sleep(30);}catch(Exception e){thrownewRuntimeException(e);}synchronized(this){
count--;System.out.println(Thread.currentThread().getName()+" == "+ count);}}}}
直接看控制台的输出结果:
Thread-1==9Thread-0==8Thread-0==7Thread-1==6Thread-1==5Thread-0==4Thread-0==3Thread-1==2Thread-1==1Thread-0==0
这就解决了线程安全问题。
过程就是第一个线程和第二个线程同时去竞争this锁,假设第一个线程获取到锁,那么第二个线程就会阻塞等待,等第一个线程执行完操作资源后,释放锁之后才会获取到锁,执行操作。
三、synchronized锁的基本用法
1.修饰代码块,指定加锁对象,对指定对象加锁,进入同步代码块前要获取 给定对象 的锁。
2.修饰实例方法,作用于当前实例加锁,进入同步代码块前要获取 当前实例 的锁。
3.修饰静态方法,作用于当前类对象(当前类.class)加锁,进入同步代码块前要获得 当前类对象 的锁。
1、修饰代码块(this锁)
publicclassThreadCountimplementsRunnable{privateint count =10;@Overridepublicvoidrun(){while(true){sub();}}publicvoidsub(){if(count >1){try{Thread.sleep(30);}catch(Exception e){thrownewRuntimeException(e);}// 修饰代码块,即this锁,进入同步代码块之前需要获取对象锁synchronized(this){
count--;System.out.println(Thread.currentThread().getName()+" == "+ count);}}}publicstaticvoidmain(String[] args){// 同一个实例,执行不同线程,是线程安全的,不会出现问题//ThreadCount threadCount = new ThreadCount();//new Thread(threadCount).start();//new Thread(threadCount).start();// 不同实例,执行不同线程,会出现线程安全问题,这就是对象锁// 代码比较简单,可自行执行测试ThreadCount threadCount1 =newThreadCount();ThreadCount threadCount2 =newThreadCount();newThread(threadCount1).start();newThread(threadCount2).start();}}
2、修饰实例方法(this锁)
@Overridepublicvoidrun(){while(true){try{Thread.sleep(30);}catch(Exception e){thrownewRuntimeException(e);}sub();}}// 将synchronized加在实例方法上,则使用的还是this锁publicsynchronizedvoidsub(){if(count >1){
count--;System.out.println(Thread.currentThread().getName()+" == "+ count);}}
3、修饰静态方法
privatestaticint count =10;...// 使用当前的 类名.class 锁publicstaticsynchronizedvoidsub(){if(count >1){
count--;System.out.println(Thread.currentThread().getName()+" == "+ count);}}
相当于
publicstaticvoidsub(){synchronized(ThreadCount.class){if(count >1){
count--;System.out.println(Thread.currentThread().getName()+" == "+ count);}}}
四、死锁问题
(1)死锁
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A,按照先锁a再获得锁b的的顺序获得锁,而在此同时又有另外一个线程B,按照先锁b再锁a的顺序获得锁。如下图所示:
(2)死锁产生的必要条件
产生死锁的必要条件:
- 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
- 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
(3)诊断synchronized死锁
以下这段代码会产生死锁问题,我们当作测试案例:
publicclassDeadlockThreadimplementsRunnable{privateint count =1;privatefinalString lock ="lock";@Overridepublicvoidrun(){while(true){
count++;if(count %2==0){synchronized(lock){a();}}else{synchronized(this){b();}}}}publicsynchronizedvoida(){System.out.println(Thread.currentThread().getName()+",a方法...");}publicvoidb(){synchronized(lock){System.out.println(Thread.currentThread().getName()+",b方法...");}}publicstaticvoidmain(String[] args){DeadlockThread deadlockThread =newDeadlockThread();Thread thread1 =newThread(deadlockThread);Thread thread2 =newThread(deadlockThread);
thread1.start();
thread2.start();}}
诊断死锁我们可以使用jdk8自带的诊断工具jconsole.exe
如图,双击打开,选择对应的进程。
这里我们本地,不需要登录,直接选择不安全方式连接。
连接成功之后,点击线程,点击检测死锁,该工具就可以帮我们自动检测到产生死锁的线程,如图:
还能够显示出死锁线程的具体信息,锁的拥有者,以及对应的代码行数:
五、线程如何实现同步
线程如何实现同步?
或者说线程如何保证线程安全性问题?
- 使用synchronized锁,JDK1.6开始,锁的升级过程
偏向锁 --> 轻量级锁 --> 重量级锁
- 使用Lock锁(JUC),需要自己实现锁的升级过程,底层是基于AQS+CAS实现
- 使用ThreadLocal,但是需要注意内存泄漏的问题
- 原子类CAS非阻塞式
六、多线程之间的通信
1、等待/通知机制
等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.Object 上,方法如下:
1.notify():通知一个在对象上等待的线程,使其从main()方法返回,而返回的前提是该线程获取到了对象的锁;
2.notifyAll():通知所有等待在该对象的线程;
3.wait():调用该方法的线程进入WAITING状态,只有等待其他线程的通知或者被中断,才会返回。需要注意调用wait()方法后,会主动释放对象的锁。
2、生产者和消费者模型
下面我们通过一个案例来展示生产者消费者模型,模拟过程为两个线程,一个输入线程一个输出线程,输入线程输入内容,输出线程立马打印:
publicclassThreadTest{// 共享变量classRes{publicString userName;publicchar sex;}/**
* 输入线程
*/classInputThreadextendsThread{privateRes res;publicInputThread(Res res){this.res = res;}@Overridepublicvoidrun(){int count =0;while(true){if(count ==0){
res.userName ="zal";
res.sex ='男';}else{
res.userName ="zzal";
res.sex ='女';}
count =(count +1)%2;}}}/**
* 输出线程
*/classOutPutThreadextendsThread{privateRes res;publicOutPutThread(Res res){this.res = res;}@Overridepublicvoidrun(){while(true){System.out.println(res.userName +", "+ res.sex);}}}publicstaticvoidmain(String[] args){newThreadTest().print();}publicvoidprint(){// 全局对象Res res =newRes();// 输入线程InputThread inputThread =newInputThread(res);OutPutThread outPutThread =newOutPutThread(res);
inputThread.start();
outPutThread.start();}}
然后我们看控制台输出打印,发现了问题。
这就意味着该代码出现了线程安全性问题,那么为了解决线程安全性问题,我们就需要对线程进行加锁,那么锁哪些代码块呢?
肯定是锁Res对象。
代码改进如下:
publicclassThreadTest{classRes{publicString userName;publicchar sex;}/**
* 输入线程
*/classInputThreadextendsThread{privateRes res;publicInputThread(Res res){this.res = res;}@Overridepublicvoidrun(){int count =0;while(true){synchronized(res){if(count ==0){
res.userName ="zal";
res.sex ='男';}else{
res.userName ="zzal";
res.sex ='女';}}
count =(count +1)%2;}}}/**
* 输出线程
*/classOutPutThreadextendsThread{privateRes res;publicOutPutThread(Res res){this.res = res;}@Overridepublicvoidrun(){while(true){synchronized(res){System.out.println(res.userName +", "+ res.sex);}}}}publicstaticvoidmain(String[] args){newThreadTest().print();}publicvoidprint(){// 全局对象Res res =newRes();// 输入线程InputThread inputThread =newInputThread(res);OutPutThread outPutThread =newOutPutThread(res);
inputThread.start();
outPutThread.start();}}
我们对输入线程和输出线程的res对象都加了锁,并且锁住的是同一个对象,这下不会再出现线程安全问题了,运行截图如下:
可是又出现了新的问题,那就是输入和输出一片一片的打印,并不能实现我们输入线程输入,输出线程立马输出的功能。
出现问题的原因就是当输入线程获取锁的时候,那么输出线程就不能获取锁,就会进入阻塞状态,而当输出线程进行输出的时候,输入线程就不能输入了,所以就会出现这种现象。
最后,我们使用生产者和消费者模型进行改进代码,代码如下:
publicclassThreadTest{classRes{publicString userName;publicchar sex;/**
* flag 标志
* 当flag = false时,输入线程输入,输出线程等待
* 当flag = true时,输出线程输出,输入线程等待
*/publicboolean flag =false;}/**
* 输入线程
*/classInputThreadextendsThread{privateRes res;publicInputThread(Res res){this.res = res;}@Overridepublicvoidrun(){int count =0;while(true){synchronized(res){if(res.flag){try{
res.wait();}catch(InterruptedException e){
e.printStackTrace();}}if(count ==0){
res.userName ="zal";
res.sex ='男';}else{
res.userName ="zzal";
res.sex ='女';}// 输出线程可以输出值
res.flag =true;// 唤醒输出线程
res.notify();}
count =(count +1)%2;}}}/**
* 输出线程
*/classOutPutThreadextendsThread{privateRes res;publicOutPutThread(Res res){this.res = res;}@Overridepublicvoidrun(){while(true){synchronized(res){// 如果res.flag = false,则输出线程主动释放锁// 同时会阻塞线程if(!res.flag){try{
res.wait();}catch(InterruptedException e){thrownewRuntimeException(e);}}System.out.println(res.userName +", "+ res.sex);
res.flag =false;// 唤醒输出线程
res.notify();}}}}publicstaticvoidmain(String[] args){newThreadTest().print();}publicvoidprint(){// 全局对象Res res =newRes();// 输入线程InputThread inputThread =newInputThread(res);OutPutThread outPutThread =newOutPutThread(res);
inputThread.start();
outPutThread.start();}}
版权归原作者 只是六号z 所有, 如有侵权,请联系我们删除。