一、概述
讲到synchronized大家都知道这是为了解决多线程对同一个资源竞争导致问题而出现的,synchronized的使用分为同步方法和同步块,那么对于多线程的同步问题我们只需要在方法上或方法里面的代码块加入synchronized就可以了吗?我们如何提高synchronized的使用效率?对于这两个问题,我们通过买票的例子来讲解synchronized的正确用法。
二、线程不安全
先看下线程不安全的写法,假如现在有20张票,同时有三个线程竞争,代码如下:
1、方式一
public class Web12306 implements Runnable {
private final String TAG = Web12306.class.getSimpleName();
//总票数
private int ticketNum = 20;
//是否有票
private volatile boolean flag = true;
@Override
public void run() {
while (flag) {
buyTicket1();
}
}
/**
* 线程不安全
*/
private void buyTicket1() {
if (ticketNum <= 0) {
flag = false;
return;
}
//模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.i(TAG, "【" + Thread.currentThread().getName() + "】" + "买票, 剩余票数:" + ticketNum--);
}
}
Web12306 web12306 = new Web12306();
Thread thread1 = new Thread(web12306, "thread-1");
Thread thread2 = new Thread(web12306, "thread-2");
Thread thread3 = new Thread(web12306, "thread-3");
thread1.start();
thread2.start();
thread3.start();
结果如下:
看到没有,最后出现了负数,这显然是不正确的。此时我们会想到使用synchronized,所以这样改:
2、方式二
/**
* 线程不安全
* 同步块,锁定的是ticketNum,ticketNum对象一直在变,锁不住
*/
private void buyTicket2() {
synchronized (Integer.valueOf(ticketNum)) {
if (ticketNum <= 0) {
flag = false;
return;
}
//模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.i(TAG, "【" + Thread.currentThread().getName() + "】" + "买票, 剩余票数:" + ticketNum--);
}
}
上面我们是在ticketNum上加锁,票是唯一的,好像没啥问题,但仔细想一下,好像又有问题,因为ticketNum在不断变化,锁定的目标对象一直在变,所以也是不行的,结果跟上面一样会出现负数。
三、线程安全
我们会想到用同步方法来实现线程安全
3、方式三
/**
* 线程安全
* 同步方法,锁定的是this,即Web12306
* 缺点:锁定的范围太大,性能不高
*/
private synchronized void buyTicket3() {
if (ticketNum <= 0) {
flag = false;
return;
}
//模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.i(TAG, "【" + Thread.currentThread().getName() + "】" + "买票, 剩余票数:" + ticketNum--);
}
这个的确可以实现,但是我们需要理解synchronized锁定的是什么,不是任何时候在方法上加个synchronized就能解决问题的,buyTicket3()方法是在Web12306类中的run()方法里面调用的,即我们锁定的对象是Web12306,票就是在Web12306里面,这个是唯一的,所以此时在方法前面加synchronized是可以的。
注意:有一点需要搞清楚,不管任何时候,我们在使用synchronized时要锁定的并不是这个方法或者代码块,而是多个线程需要竞争的资源,因为资源是唯一的。
看下打印结果:
4、方式四
/**
* 线程安全
* 同步块,锁定的是this,即Web12306
* 缺点:锁定的范围太大,性能不高
*/
private void buyTicket4() {
synchronized (this) {
if (ticketNum <= 0) {
flag = false;
return;
}
//模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.i(TAG, "【" + Thread.currentThread().getName() + "】" + "买票, 剩余票数:" + ticketNum--);
}
}
这种方式使用同步块实现,原理跟上面的一样,锁定的都是Web12306对象。虽说这两种方式都能实现线程同步,但是效率上还是可以提高,先分析一下代码:
问题:如果现在只剩一张票,线程1进入到同步块里面,此时在模拟延时,线程2和线程3过来,发现对象锁住了,在外面等着,后面线程1买完了票出来了(注意:此时已经没票了)。线程2获取到了锁,进来发现没票了只能return出去。线程2和线程3白等了,造成了资源浪费。
解决:应该在线程同步前先判断是否有票,如果没票直接return出去,不需要等待获取对象锁,这个有点类似单例模式的双重检测,我们来看正确的使用方式。
5、方式五
/**
* 线程安全
* 同步块,锁定的是this,即Web12306
* 优点:锁定的范围合理,性能高
*/
private void buyTicket5() {
//考虑没票的情况
if (ticketNum <= 0) {
flag = false;
return;
}
synchronized (this) {
//考虑只有一张票的情况
if (ticketNum <= 0) {
flag = false;
return;
}
//模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.i(TAG, "【" + Thread.currentThread().getName() + "】" + "买票, 剩余票数:" + ticketNum--);
}
}
好了,关于synchronized的用法就讲到这里,希望对大家有帮助。