线程安全问题
对于上次的小实例,是强调过不考虑线程安全问题的。
如果我们通过调用线程类的sleep()方法,模拟CUP突然暂停对程序A的执行转而执行线程B。这在实际中是有可能发生的。
class Shopping implements Runnable {
private int num = 10;
public void run() {
while (true) {
if (num > 0) {
try {
//模拟CUP暂停对某线程的执行转而执行另一线程
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "抢血拼到了第" + num-- + "双鞋");
} else
break;
}
}
}
public class Test {
public static void main(String[] args) {
Shopping shopping = new Shopping();
Thread t1 = new Thread(shopping, "线程1");
Thread t2 = new Thread(shopping, "线程2");
t1.start();
t2.start();
}
}
多运行几次,有时候会出现这样的打印结果:
线程1抢血拼到了第10双鞋
线程2抢血拼到了第9双鞋
线程2抢血拼到了第8双鞋
线程1抢血拼到了第7双鞋
线程2抢血拼到了第6双鞋
线程1抢血拼到了第5双鞋
线程1抢血拼到了第4双鞋
线程2抢血拼到了第3双鞋
线程1抢血拼到了第2双鞋
线程2抢血拼到了第1双鞋
线程2抢血拼到了第0双鞋
很明显,这里就出现了线程安全问题,因为是没有第0双鞋的。
出现问题的原因,可能是这样。
线程1首先获得资源,此时num等于1大于0,进入if语句,线程1暂停执行转而执行线程2去了。得到num还是等于1,大于0,进入if语句,同样,此时线程2被暂停转而执行线程1去了。
紧接着线程1一鼓作气,执行了num–,num变成了0,线程1结束。但是可怕的地方来了,线程2从睡眠中醒来,继续执行下面的语句就是num–,问题是此时num等于0啊,那么就产生了这样的结果“线程2抢血拼到了第0双鞋”。
其中num是两个线程的共享数据。想详细了解,参见这篇博文:线程共享数据与线程私有数据。
synchronize关键字
那么要从根本上解决这个问题,我们就必须保证多个线程在对共享数据进行操作时完全同步。也就是说,当线程1在写入时,线程2不仅不能写入而且不能读。因为在线程1写完之前,线程2读取的一定是一个过期数据。Java提供了一个重要的关键字synchronize来实现这个功能。
关键字synchronize的作用是对需要同步的代码加锁,使得每一次,只能有一个线程进入同步代码块,从而保证线程共享数据的安全。
synchronize有多种用法,这里做一个简单的整理:
- 作用于代码块:对给定对象加锁,进入同步代码块前要获得给定对象的锁。
- 作用于实例方法:相当于对当前实例加锁,进入方法前要获得当前实例的锁。
- 作用于静态方法:相当于对当前类加锁,进入方法前要获得当前类的锁。
1.作用于代码块
下述代码,将关键字synchronize作用于代码块,因此,每次当线程进入被synchronize包裹的代码块时,就会要求请求obj实例的锁。如果当前有其他线程正持有这把锁,那么新到的线程就必须等待。这样,就保证每次只有一个线程能进入synchronize作用的代码块。
class Shopping implements Runnable {
private int num = 10;
static Shopping shop = new Shopping();
public void run() {
while (true) {
synchronized (shop) {
if (num > 0) {
try {
//模拟CUP暂停对某线程的执行转而执行另一线程
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "抢血拼到了第" + num-- + "双鞋");
} else
break;
}
}
}
}
public class Test {
public static void main(String[] args) {
Shopping shopping = new Shopping();
Thread t1 = new Thread(shopping, "线程1");
Thread t2 = new Thread(shopping, "线程2");
t1.start();
t2.start();
}
}
2.作用于实例方法
我们还可以使用第二种用法改写上述代码:
class Shopping implements Runnable {
private int num = 10;
//stop的作用是跳出循环
boolean stop = false;
public void run() {
while (true) {
if (stop)
break;
stop = shop(stop);
}
}
//synchronize作用于实例方法
public synchronized boolean shop(boolean stop) {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "抢血拼到了第" + num-- + "双鞋");
} else
stop = true;
return stop;
}
}
public class Test {
public static void main(String[] args) {
Shopping shopping = new Shopping();
Thread t1 = new Thread(shopping, "线程1");
Thread t2 = new Thread(shopping, "线程2");
t1.start();
t2.start();
}
}
上述代码中,synchronize作用于一个实例方法。也就是说进入shop()方法前,线程必须获得当前对象实例的锁。在本例中就是对象shopping。
3.作用于静态方法
如果要使用第三种用法,即作用于静态方法上,此时int类型的变量num也要是static类型,不然访问不到。
class Shopping implements Runnable {
//静态变量
private static int num = 10;
boolean stop = false;
public void run() {
while (true) {
if (stop)
break;
stop = shop(stop);
}
}
//synchronize作用于静态方法
public static synchronized boolean shop(boolean stop) {
if (num > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()
+ "抢血拼到了第" + num-- + "双鞋");
} else
stop = true;
return stop;
}
}
public class Test {
public static void main(String[] args) {
//这种方式当然可以实现,暂且注释掉,重点看另一种方式。
/*Shopping shopping = new Shopping();
Thread t1 = new Thread(shopping, "线程1");
Thread t2 = new Thread(shopping, "线程2");*/
Shopping s1 = new Shopping();
Shopping s2 = new Shopping();
Thread t1 = new Thread(s1, "线程1");
Thread t2 = new Thread(s2, "线程2");
t1.start();
t2.start();
}
}
从两个技术点进行分析:
- 虽然上诉代码,new了两个Shopping类型的实例。但是int型的变量num是静态变量,所有Shopping类型的实例共享一份;
- 虽然两个线程指向了不同的Shopping实例。但是synchronize作用于静态方法时,相当于对当前类即Shopping类加锁,进入方法前要获得当前类的锁,s1、s2都是Shopping类的实例,同为Shopping类的锁,因此,线程间可以正确同步。
这两个技术点缺一不可,下面我们分三种情况讨论:
- int类型变量num不是静态变量。这种情况不详细讨论,有明显错误——静态方法不能访问非静态变量。
- 把shop()方法只用synchronize修饰不用static修饰。此时,两个线程的Shopping类型实例不是同一个对象。因此,线程t1会在进入同步方法前加锁s1对象,而线程t2也加锁自己的对象s2。
即,这两个线程使用的是两把不同的锁。因此,线程安全时无法保证的。
多运行几次,还是会有这样的情况出现:
线程1抢血拼到了第0双鞋
- 前两种情况的叠加,既不使用static修饰变量num,也不修饰shop()方法。此时的运行结果是这样:
线程1抢血拼到了第10双鞋
线程2抢血拼到了第10双鞋
线程2抢血拼到了第9双鞋
线程1抢血拼到了第9双鞋
线程1抢血拼到了第8双鞋
线程2抢血拼到了第8双鞋
线程2抢血拼到了第7双鞋
线程1抢血拼到了第7双鞋
线程2抢血拼到了第6双鞋
线程1抢血拼到了第6双鞋
线程1抢血拼到了第5双鞋
线程2抢血拼到了第5双鞋
线程2抢血拼到了第4双鞋
线程1抢血拼到了第4双鞋
线程2抢血拼到了第3双鞋
线程1抢血拼到了第3双鞋
线程1抢血拼到了第2双鞋
线程2抢血拼到了第2双鞋
线程2抢血拼到了第1双鞋
线程1抢血拼到了第1双鞋
完全是各玩各的。即此时在Java堆中有两个Shopping类型实例s1和s2,线程t1和线程t2在操作各自实例中的num。