由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问。
10.4.1 多线程同步代码块的实例
同步是一种各线程间协调使用共享资源的一种方式,也就是说如果一个对象(或变量)同时被多个其他线程访问,那么这个对象是必须使用线程同步的。
线程同步又可以分为两种方式:同步代码块和同步方法。程序将允许访问控制的代码,放入synchronized语句内,形成了同步代码块。值得注意的是,它是通过synchronized关键字来声明。定义同步代码块的一般格式如下:
synchronized(syncObject){
/* *需要同步的代码 ; */
}
在Java语言中,为了保证线程对共享资源使用的完整性,用关键字synchronized为共享资源加锁来解决这个问题。其实每个对象都有一个“锁标志”,当某个对象的一个线程访问该对象的某个数据时,这个对象的所有被synchronized修饰的数据将被上锁(因为锁标志被当前线程占用了),只有当前线程访问完它要访问的synchronized数据时,当前线程才会释放锁标志,这样同一个对象的其他线程才有机会访问synchronized数据。因此在同一时刻只能有一个线程可以进入同步代码块内运行,只有当该线程离开同步代码块后,其他线程才能进入同步代码块内运行。
【代码剖析】这是一个关于线程同步代码块的问题,具体代码如下:
//文件名:text.java
public class text {
public static void main(String[] args) {
TextThread t = new TextThread();
//启动2个线程,实现了资源共享的目的
new Thread(t).start(); //启用线程
new Thread(t).start();
}
}
class TextThread implements Runnable {//用实现Runnable接口的方式创建线程
private int num = 5; //定义计数变量
public void run() {//重写Runnable接口的run()方法
while (true) {
synchronized (this) {//对共享资源加锁,实现资源同步的目的
if (num > 0) {
try {
Thread.sleep(100);//线程休眠100毫秒
} catch (Exception e) {//捕获异常
System.out.println(Thread.currentThread().getName()+"出错了");
}
System.out.println(Thread.currentThread().getName() + "数字为"+ num--);
}else{
System.out.println(Thread.currentThread().getName()+"退出了");
break; //如果num<0,退出while循环
}
}
}
}
}
运行结果如下:
Thread-0数字为5
Thread-0数字为4
Thread-0数字为3
Thread-0数字为2
Thread-0数字为1
Thread-0退出了
Thread-1退出了
【解释说明】从上面的运行结果中可以发现打印结果Thread-0和Thread-1并不是相互交替运行的。而是一个线程执行完毕后,其他线程才开始运行。这就实现了线程使用共享资源的完整性。
即当一个线程运行到被synchronized锁定的代码块时如(if (num > 0)),CPU不去执行其他线程中的同步代码块以下的代码块,必须等到下一句执行完后才能去执行其他线程中的有关代码块。这段代码就好比一座独木桥,任何时刻,都只能有一个人在桥上行走,程序中不能有多个线程同时在同步代码之间执行,这就是线程同步。
所以只有当Thread-0执行结束后,Thread-1才可以访问同步代码块。这时候num变量的值经过多次的自减运算后,变成了0,所以不符合if判断语句的条件。只能执行else。
10.4.2 多线程同步方法的实例
除了可以对代码块进行同步外,对函数也可以实现同步的。当一个方法被关键字synchronized声明之后,就只允许一个线程来操作这个方法。但是值得注意是,“一个线程”指的是一次只能让一个线程运行。这个方法就称为同步方法。一般同步方法定义的格式如下:
访问控制符 synchronized 返回值类型 方法名称(参数){
//方法体;
}
同步方法可民控制对类成员变量的访问。每个类的实例对象都对应着一把锁,每个同步方法都必须在获得该类实例对象的锁才能执行,方法一旦被执行,就独占该锁,而其他的线程只能等待,直到从该方法返回时才将锁释放。在同一类中,只要有可能访问类成员变量的方法均被声明为synchronized,这些synchronized方法,可以在多个线程之间同步,当有一个线程进入了有synchronized修饰的方法时,其他线程就不能进入同一个对象使用synchronized来修饰的所有方法,直到第一个线程执行完它所进入的synchronized修饰的方法为止。
【代码剖析】这是一个关于线程同步方法的问题,具体代码如下:
//文件名:ThreadDemo.java
public class ThreadDemo {
public static void main(String[] args) {
ThreadTest1 t1 = new ThreadTest1();//创建ThreadTest1类的实例对象
new Thread(t1).start();//创建线程并启动它
new Thread(t1).start();//创建线程并启动它
System.out.println(t1.call());//调用ThreadTest1类的同步方法call()
}
}
class ThreadTest1 implements Runnable {
private int x;
private int y;
//定义ThreadTest1的同步方法
public synchronized void run() {//重写Runnable接口的run(),并声明成synchronized
for (int i = 0; i < 4; i++) {
x++;
y++;
try {
Thread.sleep(200);//当前运行的线程休眠200毫秒
} catch (InterruptedException e) {
System.out.println("线程出错了");
}
System.out.println(Thread.currentThread().getName() + " x=" + x
+ ",y=" + y + " " + i);
}
}
public synchronized String call() {//自定义方法,并声明成synchronized
String name = Thread.currentThread().getName();
return "Hellow " + name;
}
}
运行结果如下:
Thread-0 x=1,y=1 0
Thread-0 x=2,y=2 1
Thread-0 x=3,y=3 2
Thread-0 x=4,y=4 3
Thread-1 x=5,y=5 0
Thread-1 x=6,y=6 1
Thread-1 x=7,y=7 2
Thread-1 x=8,y=8 3
Hellow main
【解释说明】从这段代码的结果中可以看出,Thread-0和Thread-1都完整地访问了共享资源。从代码的打印顺序上可以看出Thread-0访问完run()后,Thread-1才进入synchronized run()进行数据操作的。在Thread-0休眠的时间内,Thread-1也没有进入运行状态。直到Thread-0从synchronized run()方中返回把锁交给Thread-1。
而synchronized call(),创建的线程没有访问它,是在main()即程序的主线程中调用。所以在没在获得锁的情况下,也只是等待。这就说明了线程的同步很好地解决了多个线程共同访问同一片存储空间发生冲突的问题。
10.4.3 面试题5:说出线程同步的方法
【考题题干】请说出你所知道的线程同步的方法。
【参考答案】同步是一种各线程间协调使用共享资源的一种方式。各线程间的相互通信是实现同步的重要因素,所以Java提供了wait()和notify()等方法来使线程之间可以相互通信。
q wait():使线程处于等待状态,并且释放所持有的对象的lock。可以与notify()方法配套使用。它有两种形式,一种是以毫秒为单位的一段时间作为参数,另一种是没有参数。
q sleep():使一个正在运行的线程处于阻塞状态,可以以毫秒为单位的一段时间作为参数,它可以使得线程在设定的时间停止运行,但是在设定的时间一过,线程重新进入可执行状态。由于sleep()是一个静态方法,所以调用此方法要捕捉InterruptedException异常。
q notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
q allnotity():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。只有获得锁的那一个线程才能进入可执行状态。