同步方法
通过前面小节的学习,了解到同步代码块可以有效解决线程的安全问题,当把共享资源的操作放在synchronized定义的区域内时,便为这些操作加了同步锁。同样,在方法前面也可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能,具体语法格式如下:
[修饰符] synchronized 返回值类型 方法名([参数1,……]){}
被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行。
接下来使用同步方法模拟售票系统,如文件1所示。
文件1 Example13.java
1 // 定义SaleThread3类实现Runnable接口
2 class SaleThread3 implements Runnable {
3 private int tickets = 10;
4 public void run() {
5 while (true) {
6 saleTicket(); // 调用售票方法
7 }
8 }
9 // 定义一个同步方法saleTicket()
10 private synchronized void saleTicket() {
11 if (tickets > 0) {
12 try {
13 Thread.sleep(100); // 模拟售票耗时过程
14 } catch (InterruptedException e) {
15 e.printStackTrace();
16 }
17 System.out.println(Thread.currentThread().getName()
18 + " 正在发售第 " + tickets-- + " 张票 ");
19 }
20 }
21 }
22 public class Example13 {
23 public static void main(String[] args) {
24 SaleThread3 saleThread = new SaleThread3();
25 // 创建并开启四个线程,模拟4个售票窗口
26 new Thread(saleThread, "窗口1").start();
27 new Thread(saleThread, "窗口2").start();
28 new Thread(saleThread, "窗口3").start();
29 new Thread(saleThread, "窗口4").start();
30 }
31 }
运行结果如图1所示。
图1 运行结果
文件1中,将售票代码抽取为售票方法saleTicket(),并用synchronized关键字把saleTicket()修饰为同步方法,然后在run()方法中调用该方法。从图1可以看出,同样没有出现0号和负数号的票,说明同步方法实现了和同步代码块一样的效果。
思考:
大家可能会有这样的疑问:同步代码块的锁是自己定义的任意类型的对象,那么同步方法是否也存在锁?如果有,它的锁是什么呢?答案是肯定的,同步方法也有锁,它的锁就是当前调用该方法的对象,也就是this指向的对象。这样做的好处是,同步方法被所有线程所共享,方法所在的对象相对于所有线程来说是唯一的,从而保证了锁的唯一性。当一个线程执行该方法时,其他的线程就不能进入该方法中,直到这个线程执行完该方法为止,从而达到了线程同步的效果。
有时候需要同步的方法是静态方法,静态方法不需要创建对象就可以直接用“类名.方法名()”的方式调用。这时候我们就会有一个疑问,如果不创建对象,静态同步方法的锁就不会是this,那么静态同步方法的锁是什么?Java中静态方法的锁是该方法所在类的class对象,该对象可以直接类名.class的方式获取。
同步代码块和同步方法解决多线程问题有好处也有弊端。同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只能有一条线程执行,但是线程在执行同步代码时每次都会判断锁的状态,非常消耗资源,效率较低。