1.引言
在本篇博客中,我主要记录一下在多线程编程中存在的线程安全问题,以及如何去解决这种问题。
2 现实实例(出现问题)
首先我们举个例子:我们开发了一个售票系统,一共有100张票,然后我们用10个线程去买票,直到卖票结束位置。
- 定义我们的Ticket类
public class Ticket {
//一共有100张票
private int num=100;
public int getNum() {
return num;
}
//用于卖票,调用一下这个方法,票少一张
public boolean saleTicket() throws Exception
{
//用于判断,票是否卖没
boolean finish=false;
if(this.num>0){
//模拟卖票需要时间50ms
Thread.sleep(50);
num--;
}else
{
finish=true;
System.out.println("票卖结束了:剩余"+num);
}
return finish;
}
}
- 定义售票线程
public class MyThread extends Thread {
private Ticket ticket;
public MyThread(Ticket t) {
this.ticket = t;
}
@Override
public void run() {
boolean finish=false;
while (true) {
try {
finish=ticket.saleTicket();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(finish)
{
return;
}
}
}
}
- 启动2个线程来卖票
public class app {
public static void main(String[] args) throws InterruptedException {
Ticket t=new Ticket();
for(int i=0;i<2;i++)
{
MyThread td=new MyThread(t);
td.start();
}
}
}
- 运行结果
我们的票的数量怎么会出现负数呢?
这在现实生活中是不合理的,一共只有100张票,卖出超过了100张。
这也是我们在这篇博客中应该解决的问题
3 问题的原因
接下来我们用一张图来看一下这种bug出现的原因:
4 解决多线程安全问题
在解决线程安全问题之前,我们有比较了解一下:锁的概念。什么是锁?我们可以这么理解,在线程执行一个方法之前,首先要获得这个方法的锁,如果拿不到这个锁,该线程就只能等待,直到拿到该锁为止。在Java中,存在两种锁,一种是对象锁,一种是类锁。具体什么意思我们接下来看一下。
4.1 给售票方法直接加一个对象锁
- 代码修改如下(只是在方法上添加了一个关键字
synchronized
,注意此方法还是非静态方法。)
public synchronized boolean saleTicket() throws Exception
{
boolean finish=false;
if(this.num>0){
Thread.sleep(50);
num--;
}else
{
finish=true;
System.out.println("票卖结束了:剩余"+num);
}
return finish;
}
- 在此运行该方法:结果
解释:
1.当线程1进入saleTicket方法前,先判断是否可以拿到该方法的锁(是对象锁,具体点说,也就是this这个对象,因为这个方法是非静态方法),
如果拿到this中的锁,那么进入该方法,当线程1执行完saleTicket方法之后,释放this对象锁,此时其他线程才可以访问saleTicket方法。
2.当线程2想要进入saleTicket方法前,如果线程1正在访问该方法,线程2是无法得到this对象锁的,
线程2只能等待,等到线程1执行完方法,线程2才可以进入saleTicket方法。
4.2 给售票方法直接加锁的弊端
我们解决了上述的问题,发现了一个问题,异步线程编程了同步的方法,那么我们多线程还有什么意义呢?就好比搬砖,一个人搬砖需要2个小时,10个人搬砖还是需要两个小时(因为第二个人想要搬砖需要等待上一个搬砖结束。),这不是多此一举吗?这种弊端我们可以通过静态代码块来解决。
4.3 通过同步块给方法加锁
- 修改之后的代码如下
public boolean saleTicket() throws Exception
{
boolean finish=false;
if(this.num>0){
Thread.sleep(50);
//注意这里:我们只是将判断这一个简单的过程,同步化
synchronized (this) {
if(this.num>0){
num--;
}
}
}else
{
finish=true;
System.out.println("票卖结束了:剩余"+num);
}
return finish;
}
- 运行结果
修改之后的代码优点:
我们并不是将整个方法加锁,只是将代码的重要部分加锁,一些复杂的(耗时间的)不加锁,
这样既可以保证我们线程的安全,又可以提高我们的效率。这个大大提高了我们程序的效率
4.4 通过同步块给方法加锁补充
4.4.1 补充一
- 看下面的代码
public void saleTicket1()
{
synchronized (this) {
System.out.println(Thread.currentThread().getName()+"saleTicket1");
}
}
public synchronized void saleTicket2()
{
synchronized (this) {
System.out.println(Thread.currentThread().getName()+"saleTicket2");
}
}
这里有两个方法可以售票,同步代码块加锁都是用this对象进行加锁,所以多个线程不可以同时访问代码块的内容,
例如:
线程1访问方法saleTicket1的同时(也就是线程1未释放锁之前),
线程2也不可以访问saleTicket2方法(因为线程2拿不到this对象锁),必须要等待线程1将this锁释放。
4.4.2补充二
同样是上面的代码:如果线程1拿到了this锁(在saleTicket1方法中拿到的锁),那么线程1可以访问saleTicket2方法。例子
public void saleTicket1()
{
synchronized (this) {
saleTicket2();
}
}
public synchronized void saleTicket2()
{
System.out.println("在saleTicket1()方法中拿到锁,可以访问saleTicket2()方法");
}
- 运行结果
4.4.3 补充三
还是上面的代码:如果我们用不同的对象进行加锁,那么多个线程是可以同时访问的。
public class Ticket {
private Object o1=new Object();
private Object o2=new Object();
public void saleTicket1()
{
synchronized (o1) {
System.out.println(Thread.currentThread().getName()+"saleTicket1");
}
}
public synchronized void saleTicket2()
{
synchronized (o2) {
System.out.println(Thread.currentThread().getName()+"saleTicket2");
}
}
}
我们将saleTicket1用o1对象加锁,将saleTicket2用o2对象加锁,
那么多线程的时候,当线程1访问saleTicket1方法的时候(拿到的是o1对象锁)
线程2如果想访问saleTicket2方法,那么线程2将要去拿o2对象锁,两者是没有影响的。
5 总结
在本篇博客中,主要写了:
- 线程安全出现的原因
- 给方法加对象锁(没有讲解,如何加类锁)
- 给同步块加锁的一些补充
- 同步块加锁的一些优点