线程同步
前面说到的线程都是独立、且异步执行,而多线程编程的特点就是多个线程能够读写相同的变量或数据结构 。所以,编写多线程程序时,必须注意每个线程是否干扰了其他线程的工作。
下面首先给出一个多个线程在使用相同资源时会出现问题的例子。
示例1:
public class Site implements Runnable {
private int count = 10; // 剩余票数
private int num = 0; // 记录买到第几张
@Override
public void run() {
while (count > 0) { //没有余票时,跳出循环
//第一步,修改数据
num++;
count--;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//第二步,显示信息
System.out.println(Thread.currentThread().getName() +
"抢到第" + num + "张票,剩余" + count + "张票!");
}
}
}
public class TestSite {
public static void main(String[] args) {
Site site=new Site();
Thread person1 = new Thread(site, "唐甜");
Thread person2 = new Thread(site, "抢票软件");
Thread person3 = new Thread(site, "黄牛");
person1.start();
person2.start();
person3.start();
}
}
以上代码的运行结果如下:
可以看到,最终显示结果出现了以下问题:
- 不是从第一张票开始
- 存在多人抢到同一张票的情况
- 有些票号没有被抢到
这是由于多个线程并行工作操作同一共享资源时,带来的数据不安全问题。这是由于多个线程并行工作操作同一共享资源时,带来的数据不安全问题。
当多个线程需要访问同一资源时,需要以某种顺序来确保该资源某一时刻只能被一个线程使用,这就称为线程同步(synchronized)。
线程同步的实现
在 Java 程序中最简单实现同步的方法就是上锁。为了防止同时访问共享资源,线程在使用资源的前后可以给该资源上锁和开锁。
假想有一个房间,任一时刻只允许有一个人进入,就需要给这个房间上锁。房间只有一把钥匙,若没有钥匙就不能进入该房间。进入房间后锁门,其他人不能进入。退出房间后锁门,下一个人得到钥匙才能进入。
给共享变量上锁就使得 Java 线程能够快速方便地通信和同步。某个线程若给一个对象上了锁,就可以知道没有其他线程能够访问该对象,直到上锁的线程被唤醒、完成工作并开锁。那些试图访问一个上锁对象的线程通常会进入睡眠状态,直到上锁的线程开锁。一旦锁被打开,这些睡眠进程就会被唤醒并移到准备就绪队列中。
采用线程同步来控制线程的执行方法有两种,即同步方法和同步代码块。
Java使用synchronized关键字来获得锁。
1.同步方法
在方法声明中加入synchronized关键字,声明这是一个同步方法。
修改上面Site类的代码如下:
示例2
public class Site implements Runnable {
private int count = 10; //记录剩余票数
private int num = 0; //记录买到第几张票
private boolean flag = false; //记录是否售完
public void run() {
while (!flag) {
sale();
}
}
//同步方法:售票
public synchronized void sale() {
if (count <= 0) {
flag = true;
return;
}
//第一步,修改数据
num++;
count--;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//第二部,显示信息
System.out.println(Thread.currentThread().getName() + "抢到第"
+ num + "张票,剩余" + count + "张票.");
}
}
此为使用synchronized声明了sale()为同步方法
运行结果如下:
上述方法为非静态同步方法,即synchronized修饰的是类的实例方法,锁定的当前对象,如果是类的其他对象就没有这个约束了,而当synchronized修饰的方法是静态方法时,即
public static synchronized void sale(){//略}
此时锁定的是当前类,可以控制类的所有实例的访问。
同步方法的缺陷:如果将一根运行时间比较长的方法声明为synchronized将会影响效率。例如,将实例1中的run()方法声明为synchronized,由于在线程的整个声明周期内它一直在运行,因此就有可能导致run()方法会执行很长时间,那么其他的线程就需要一直等到run()方法结束了才能执行。
2.同步代码块
代码块即使用{}括起来的一段代码,使用synchronized关键字修饰的代码块,称为同步代码块。其语法如下:
synchronized(syncObject){
//需要同步的代码块
}
示例3
public class Site implements Runnable {
private int count = 10; // 剩余票数
private int num = 0; // 记录买到第几张
public void run() {
while (count > 0) { //没有余票时,跳出循环
synchronized (this) { //同步代码块
//第一步,修改数据
num++;
count--;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
//第二步,显示信息
System.out.println(Thread.currentThread().getName() +
"抢到第" + num + "张票,剩余" + count + "张票!");
}
}
}
}
以上代码将修改数据和显示信息的操作进行了同步,实现效果与使用同步方法相同。同步代码块可以更精准的限制访问区域。
以下几点需注意:
1. 当多个并发线程访问同一对象的同步代码块时,同一时刻只能有一个线程得到执行,其他线程必须等待当前线程执行完毕之后才能执行该代码块。
2. 当一个线程访问对象的一个同步代码块时,其他线程对对象中所有其他同步代码块的访问将被阻塞。即该线程获得这个对象的锁,其他线程对该对象所有同步代码块的访问都被暂时阻塞。
3. 当一个线程访问一个对象的同步代码块时,其他线程仍可以访问该对象的非同步代码块。
小结:
本章开始,才真正意义上接触到多线程编程,举例说明了多线程编程有可能会出现的数据不安全情况,并简单介绍了解决办法——线程同步synchronized。下一章将介绍多线程编程中,应对控制访问一组(大量)资源时,常用的一种方法,信号量计数。