一.为什么需要同步
经常作为的demo的卖票程序中,极有可能碰到一种意外,就是同一张票号被打印两次或多次,也可能出现打印出的票号为0或是负数的情况。
if (tickets>0)
System.out.println(Thread.currentThread().getName()+"出售票"+tickets--);
假设tickets的值为1的时候,线程1刚执行完if (tickets>0) 这行代码,正准备执行下面的代码,就在这时,操作系统将CPU切换到了线程2上执行,此时tickets的值仍为1,线程2执行完上面两行代码,tickets 的值变为0后,CPU又切回到了线程1上执行,但此时线程1不会再执行if (tickets>0) 这行代码,因为先前已经比较过了,并且比较的结果为真,线程1将直接往下执行这行代码。
System.out.println(Thread.currentThread().getName()+"出售票"+tickets--);
但此刻tickets 的值已变为0,屏幕打印出来的将是0。
要想立即见到这种意外,可以在程序中调用Thread.sleep()静态方法来刻意造成线程间的这种切换。Thread.sleep()方法将迫使线程执行到该处后暂停执行,让出CPU给别的线程,在指定的时间(这里是毫秒)后,CPU回到刚才暂停的线程上执行。
package dataStructure;
public class ThreadDemo {
public static void main(String[] args) {
TestThread t =new TestThread();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
class TestThread implements Runnable{
private int tickets =20 ;
public void run() {
while (true) {
if (tickets>0) {
try {
Thread.sleep(100);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println(Thread.currentThread().getName()+"出售票"+tickets--);
}
}
}
}
在本程序中,故意实现线程执行完if (tickets>0)语句后,执行Thread.sleep(100),以让出CPU给别的线程。从运行结果可以看到,票号被打印出来了负数,这说明有同一张票被卖了几次的意外发生。造成这种意外的根本原因就是因为资源数据访问不同步引起的。
二.同步代码块
为了线程安全,就涉及到线程同步,
第一步就是要保证下面代码的原子性
if (tickets>0)
System.out.println(Thread.currentThread().getName()+"出售票"+tickets--);
同步代码块定义语法如下:
...
synchronized (对象) {
需要同步的代码;
}
package dataStructure;
public class ThreadDemo {
public static void main(String[] args) {
TestThread t =new TestThread();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
class TestThread implements Runnable{
private int tickets =20 ;
public void run() {
while (true) {
synchronized (this) {
if (tickets>0) {
try {
Thread.sleep(100);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println(Thread.currentThread().getName()+"出售票"+tickets--);
}
}
}
}
}
本程序将这些需要具有原子性的代码放入synchronied语句内,形成了同步代码块。在同一时刻只能有一个线程可 以进入同步代码块内运行,只有当该线程离开同步代码块后,其他线程才能进入同步代码块内运行。
三.同步方法
package dataStructure;
public class ThreadDemo {
public static void main(String[] args) {
TestThread t =new TestThread();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
new Thread(t).start();
}
}
class TestThread implements Runnable{
private int tickets =20 ;
public void run() {
while (true) {
sale();
}
}
public synchronized void sale() {
if (tickets>0) {
try {
Thread.sleep(100);
} catch (Exception e) {
// TODO: handle exception
}
System.out.println(Thread.currentThread().getName()+"出售票"+tickets--);
}
}
}
可见,编译运行后的结果同上面同步代码块方式的运行结果完全一样, 也就是说在方法定义前使用synchronized关键字也能够很好地实现线程间的同步。
在同一类中,使用synchronized关键字定义的若干方法,可以在多个线程之间同步。当有一个线程进入了有synchronized修饰的方法时,其他线程就不能进入同一个对象使用synchronized来修饰所有方法,直到第1个线程执行完它所进入的synchronized修饰的方法为止。