解决线程不安全问题
更多精彩文章欢迎关注公众号“Java之康庄大道”
当多个线程并发访问同一个资源对象时,可能会出现线程不安全的问题,比如现有100个高铁座位,现在有请三个窗口(A,B,C)同时售票.,此时使用多线程技术来实现这个案例.
package com.yunqing.ssm.test; /** * 存在线程安全问题 */ public class TestRunnable { public static void main(String[] args) { Ticket tick = new Ticket(); Thread t1 = new Thread(tick,"A窗口"); Thread t2 = new Thread(tick,"B窗口"); Thread t3 = new Thread(tick,"C窗口"); t1.start(); t2.start(); t3.start(); } } class Ticket implements Runnable{ int ticket = 100; @Override public void run() { while (true){ try { //模拟网络延迟 Thread.currentThread().sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } if (ticket>0) System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket--); } } }
以上代码运行结果:
为什么编号为84的座位号被3个窗口售出了?
当A窗口打印84座位号,还没打印完的时候,其他两个线程就也进入到了84号座位票的分配操作中,所以导致线程安全问题。
要解决上述多线程并发访问多一个资源的安全性问题,就必须得保证打印座位号和座位号总数减1操作,必须同步完成.即是说,A线程进入操作的时候,B和C线程只能在外等着,A操作结束,A和B和C才有机会进入代码去执行.
解决多线程并发访问资源的安全问题,有三种方式:
方式1:同步代码块
方式2:同步方法
方式3:锁机制(Lock)
方式1:同步代码块:
语法:
synchronized(同步锁)
{
需要同步操作的代码
}
同步锁:
为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制.也称为同步监听对象/同步锁/同步监听器/互斥锁。
实际上,对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁,谁拿到锁,谁就可以进入代码块,其他线程只能在代码块外面等着,而且注意,在任何时候,最多允许一个线程拥有同步锁.
Java程序运行可以使用任何对象作为同步监听对象,但是一般的,我们把当前并发访问的共同资源作为同步监听对象.
package com.yunqing.ssm.test; /** * 存在线程安全问题 */ public class TestRunnable { public static void main(String[] args) { Ticket tick = new Ticket(); Thread t1 = new Thread(tick,"A窗口"); Thread t2 = new Thread(tick,"B窗口"); Thread t3 = new Thread(tick,"C窗口"); t1.start(); t2.start(); t3.start(); } } class Ticket implements Runnable{ int ticket = 100; @Override public void run() { while (true){ try { //模拟网络延迟 Thread.currentThread().sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } //this代表Ticket对象,Ticket对象是多线程共享资源 synchronized (this){ if (ticket>0) System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket--); } } } }
方式2:同步方法:
使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着.
Synchronized public void doWork(){
///TODO
}
同步锁是谁:
对于非static方法,同步锁就是this.
对于static方法,我们使用当前方法所在类的字节码对象(Ticket.class).
package com.yunqing.ssm.test;
/**
* 存在线程安全问题
*/
public class TestRunnable {
public static void main(String[] args) {
Runnable a = new Ticket();
new Thread(a,"A窗口").start();
new Thread(a,"B窗口").start();
new Thread(a,"C窗口").start();
}
}
class Ticket implements Runnable{
int ticket = 100;
synchronized private void doWork() throws InterruptedException {
if (ticket>0){
System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket);
ticket--;
Thread.sleep(100);
}
}
@Override
public void run() {
for (int i=1;i<=100;i++){
try {
doWork();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
注意:
不要使用synchronized修饰run方法,修饰之后,某一个线程就执行完了所有的功能. 好比是多个线程出现串行.
解决方案:把需要同步操作的代码定义在一个新的方法中,并且该方法使用synchronized修饰,再在run方法中调用该新的方法即可.
实际上,同步代码块和同步方法差不了多少,在本质上是一样的,两者都用了一个关键字synchronized,synchronized保证了多线程并发访问时的同步操作,避免线程的安全性问题,但是有一个弊端,就是使用synchronized的方法/代码块的性能比不用要低一些,因此如果要用synchronized,建议尽量减小synchronized的作用域。
方式3:同步锁(锁机制)
Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象.
package com.yunqing.ssm.test;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 存在线程安全问题
*/
public class TestRunnable {
public static void main(String[] args) {
Runnable a = new Ticket();
new Thread(a,"A窗口").start();
new Thread(a,"B窗口").start();
new Thread(a,"C窗口").start();
}
}
class Ticket implements Runnable{
int ticket = 100;
//创建锁对象
private final Lock lock = new ReentrantLock();
private void doWork(){
//进入方法,立马加锁
lock.lock();
try {
if (ticket>0){
System.out.println(Thread.currentThread().getName()+"售票,票号为:"+ticket);
ticket--;
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
@Override
public void run() {
for (int i=1;i<=100;i++){
doWork();
}
}
}