先来看一个程序:
package com.xy.thread;
class ThreadDemo3 extends Thread {
private static int tickets = 5; // 将票设置为静态的
public void run() {
while(tickets > 0) {
System.out.println(Thread.currentThread().getName() +
"出售票" + tickets);
tickets--;
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
new ThreadDemo3().start();
new ThreadDemo3().start();
new ThreadDemo3().start();
new ThreadDemo3().start();
}
}
【结果】
在前面讲解过的卖票程序中,很有可能碰到一种意外,就是同一个票号被打印两次或多次,也可能出现打印出的票号为0或负数的情况。 这种意外(运行结果不唯一)出现的原因在于这部分代码:
01 while(tickets>0)
02 {
03 System.out.println(Thread.currentThread().getName()+"出售票"+tickets);
04 tickets-=1;
05 }
假设tickets的值为1的时候,线程1刚执行完if( tickets > 0 )这行代码,正准备执行下面的代码(第03行以后的代码),操作系统却将CPU切换到了线程2上执行(这可能因为线程1在CPU中运行的时间片结束了),此时tickets的值没有来得及更新,其值仍为1。线程2执行完上面几行代码(01~05行), tickets的值变为0,这时CPU重新切换回线程1上执行,但此时线程1不会在判断tickets是否大于0,而是直接执行下面两条代码。
System.out.println( Thread.currentThread().getName() + “出售票” + tickets );
tickets -= 1;
而此时tickets的值也变为0,屏幕打印出来的仍然是0。这样,仅剩下的1张票,被线程1和线程2“一票两卖”,显然,这是不正确的。
如果多运行几次这段代码,就会发现,运行的结果可能完全不一样。例如,如果线程1完全运行完毕,这时才轮到线程2执行,那么最终的结果就是0,且“一票一卖”,但对于一个稳定的票务系统来说,我们不能赌运气。
为了模拟上面描述的这种情况,我们可以在程序中调用Thread.sleep()方法来刻意造成线程间的这种切换。Thread.sleep()方法将迫使线程执行到该处后暂停执行,让出CPU给别的线程,在指定的时间后的某个时刻CPU才会回到刚才暂停的线程上执行。修改后的代码如下:
package com.xy.thread;
class ThreadDemo7 implements Runnable {
private int tickets = 5;
public void run() {
while(tickets > 0) {
try {
Thread.sleep(100);
}
catch(Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
"出售票" + tickets);
tickets--;
}
}
}
public class ThreadSynchronizationDemo {
public static void main(String[] args) {
ThreadDemo7 t = new ThreadDemo7();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
【结果】
从运行结果可以看到,打印出了负数的票号以及几张票号相同的票,这说明有几张票被重复卖了出去。而且多次运行这个程序,得到的运行结果也是不唯一的。
造成这种意外的根本原因在于,因为没有对这些线程在访问临界资源(也即共享资源:tickets )做必要的控制。
下面介绍使用线程的同步来解决这种问题:
一、同步代码块
如何避免上面的情况出现呢?如何保证开发出的程序是线程安全的呢?这就要涉及到线程间的同步问题。要解决上面的问题,必须保证下面这段代码的原子性。
所谓的原子性是指,一段代码要么被执行,要么不被执行,不存在执行一部分被中断的情况。也就是说,这段代码的执行像原子一样,不可拆分。
回到上面的提到的代码:
01 while(tickets>0)
02 {
03 System.out.println(Thread.currentThread().getName()+"出售票"+tickets);
04 tickets-=1;
05 }
即当一个线程运行到while( tickets > 0 )后,CPU不去执行其他线程中可能影响当前线程中的下一句代码的执行结果的代码块。这段代码就好比一座独步桥,任何时刻都只能有一个人在桥上行走,即程序中不能有多个线程同时访问临界区,这就是线程的互斥——一种在临界区执行的特殊同步。
一般意义上的同步是指,多线程(进程)在代码执行的关键点上,互通消息、相互协作,共同把任务正确地完成。
同步代码块定义语法如下:
synchronized(对象)
{
// 需要同步的代码;
}
下面我们修改程序,使程序具有同步性,修改后的代码如下:
package com.xy.thread;
class ThreadDemo7 implements Runnable {
private int tickets = 5;
public void run() {
synchronized(this) { // 同步代码块
while(tickets > 0) {
try {
Thread.sleep(100);
}
catch(Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
"出售票" + tickets);
tickets--;
}
}
}
}
public class ThreadSynchronizationDemo {
public static void main(String[] args) {
ThreadDemo7 t = new ThreadDemo7();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
【结果】
第一次:
第二次:
将
while(tickets > 0) {
try {
Thread.sleep(100);
}
catch(Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
"出售票" + tickets);
tickets--;
}
这些具有原子性的代码(即临界区代码)放入synchronized语句中,形成了同步代码块。
在同一时刻只能由一个线程可以进入同步代码块内运行,只有当该线程离开同步代码块后,其他线程才能进入同步代码块内运行。
从上面的运行结果来看,5张票均实现了“一票一卖”的效果。 但是这时可能会出现负载不均衡的情况,即有的线程卖了5张票(如上图左所示的线程1、如上图右所示的线程0),而有的线程压根就没有卖到票(如线程2、3)。这是另外一个层面的问题,至少现在我们解决了正确卖票的问题。
二、同步方法
除了对代码块进行同步外,也可以对方法实现同步,只要是需要同步的方法定义前面加上synchronized关键字即可。同步方法定义语法如下:
访问控制符 synchronized 返回值类型方法名称( 参数)
{
// ...;
}
根据上述格式,我们再次修改,得到如下代码:
package com.xy.thread;
class ThreadDemo7 implements Runnable {
private int tickets = 5;
public void run() {
while(tickets > 0) {
sale();
}
}
public synchronized void sale() {
if(tickets > 0) {
try {
Thread.sleep(100);
}
catch(Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
"出售票" + tickets);
tickets--;
}
}
}
public class ThreadSynchronizationDemo {
public static void main(String[] args) {
ThreadDemo7 t = new ThreadDemo7();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
【结果】
把对临界变量(多线程共享变量)操作的代码封装成一个方法sale()。用关键词synchronized表明了这个方法的原子性——即对于一个线程而言,要么执行完毕这个方法,要么不执行这个方法。
由上面的运行可见,该程序的效果(售票结果)与同步代码块的运行结果完全一样,也就是说在方法定义前用synchronized关键字也能够很好地实现线程间的同步。
同步方法相当于下面形式的同步代码块:
访问控制符 返回值类型 方法名称( 参数)
{
synchronized( this ) //下面花括号{}内的为同步代码块
{
// ...;
}
}
由此可见,同步方法锁定的也是对象,而不是代码段。也就是说,在同一个类中,使用synchronized关键字定义的若干方法,当有一个线程进入了有synchronized修饰的方法时,其他线程就不能进入同一个对象(注意:是同一个对象,不是同一个类的不同对象) 使用synchronized来修饰的所有方法,直到第一个线程执行完它所进入的synchronized修饰的方法为止。