在单线程序中,每次只能做一件事情,后面的事情需要等待前面的事情完成后才可以进行,但如果使用多线程程序,就会发生两个线程抢占资源的问题。所以在多线程编程中需要防止资源之间访问的冲突,而Java提供了线程同步机制来防止这种冲突。
实际开发中,使用多线程程序的情况很多。这种多线程的程序通常会发生问题,就以火车站售票系统为例,在代码中判断当前票数是否大于0,如果大于0则执行将该票出售给乘客的功能,但当两个线程同时访问这段代码时(假如只剩一张票),第一个线程将票售出,与此同时第二个线程也已经执行完成判断是否有票的操作,并得出票数大于0的结果,于是它也执行可售出操作,这样就会产生负数。所以在编写多线程程序时,应该考虑到线程安全问题。实质上线程安全问题来源于两个线程同时存取单一对象的数据,如以下例子,模仿售票系统功能:
public class ThreadSafeTest implements Runnable {
int num = 10;//当前票数
@Override
public void run() {
while (true) {
if (num > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("剩余票数:" + num--);
}
}
}
public static void main(String[] args) {
ThreadSafeTest test = new ThreadSafeTest();//实例化对象
//实例化4个线程
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
Thread t3 = new Thread(test);
Thread t4 = new Thread(test);
//启动
t1.start();
t2.start();
t3.start();
t4.start();
}
}
输出结果:
从结果可以看出,最后打印的剩余票数为负值。这是由于同时创建了4个线程,这个4个线程执行run()方法,在num变量为1时,线程1、线程2、线程3、线程4都对num变量有存储功能,当线程1执行run()方法时,还没来得及做递减操作,就指定它调用sleep()方法进行就绪状态,这是线程2、3、4都进入了run()方法,发现num变量依然大于0,但此时线程1休眠时间已到,将num变量值递减,同时线程2、3、4也都对num变量进行递减操作,从而产生了负值。
解决办法:
1、同步块
在Java中提供了同步机制,可以有效地防止资源冲突。关键字synchronized。
如修改之前的代码,将判断资源放在同步块里面:
public class ThreadSafeTest implements Runnable {
int num = 10;//当前票数
@Override
public void run() {
while (true) {
synchronized ("") {
if (num > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("剩余票数:" + num--);
}
}
}
}
public static void main(String[] args) {
ThreadSafeTest test = new ThreadSafeTest();//实例化对象
//实例化4个线程
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
Thread t3 = new Thread(test);
Thread t4 = new Thread(test);
//启动
t1.start();
t2.start();
t3.start();
t4.start();
}
}
输出结果:
打印到最后没有出现负值。这个同步块也被称为临界区,使用synchronized关键字建立,格式如下:
synchronized (obj) {
//同步代码块
}
通常将共享资源的操作放置在synchronized定义的区域内,当其他线程也获取到这个锁时,必须等待锁被释放时才能进入该区域。obj为任意一个对象,每个对象都存在一个标志位,并具有两个值:0和1。一个线程运行到同步块时首先检查该对象的标志位,如果为0状态,表明此同步块中存在其他线程在运行。这时该线程处于就绪状态,直到处于同步块中的线程执行完同步块中的代码为止。这时该对象的标志位被设置为1,该线程才能执行同步块中的代码,并将obj对象的标志位设置为0,防止其他线程执行同步块中的代码。
2、同步方法
同步方法就是在方法前修饰synchronized关键字。当某个对象调用同步方法时,该对象上的其他同步方法必须等待该同步方法执行完毕后才能被执行,必须将每个能访问共享资源的方法修饰为synchronized,否则就会出错。
如将共享资源操作放置在一个同步方法中:
public class ThreadSafeTest implements Runnable {
int num = 10;//当前票数
public synchronized void doit(){
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("剩余票数:" + --num);
}
}
@Override
public void run(){
while (true) {
doit();
}
}
public static void main(String[] args) {
ThreadSafeTest test = new ThreadSafeTest();//实例化对象
//实例化4个线程
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
Thread t3 = new Thread(test);
Thread t4 = new Thread(test);
//启动
t1.start();
t2.start();
t3.start();
t4.start();
}
}
输出结果:
运行结果跟使用同步块一致。