线程安全问题

为什么会产生线程不安全问题?

下面从一个简单地代码示例说明一下为什么会产生线程不安全问题。

package cn.xmx.ioc.threadtest;

public class TicketDemo implements Runnable{
    private int tickets = 10; //记录剩余票数
    private  int num = 0 ; //记录买到的票数

    public void run() {
        while (true) {
            //没有余票时跳出循环
            if (tickets > 0) {
                tickets--;
                num++;
                try {
                    Thread.sleep(500);//模拟网络延迟
                } catch (InterruptedException e) {
                }
                System.out.println(Thread.currentThread().getName() + "抢到第"+num+"张票,剩余" + tickets+"张票。");
            }
        }
    }

    public static void main(String[] args) {
        TicketDemo ticketDemo = new TicketDemo();
        Thread t1 = new Thread(ticketDemo,"张三");
        Thread t2 = new Thread(ticketDemo,"李四");
        Thread t3 = new Thread(ticketDemo,"王五");
        Thread t4 = new Thread(ticketDemo,"康宁");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

在main方法中,创建四个线程模拟四个人开始抢票,并启动线程,运行的结果如图:
代码运行结果图
从运行结果中会发现:1.多人抢到同一张票,2.不是从第一张票开始,3.有些票号没有被抢到。这就是线程的安全问题。

总结得出:
当多个线程操作一个有多条线程代码的共享数据时,有可能会导致线程安全问题的。
注意:这只是有可能,也可能线程安全问题不会显示出来(在每个线程刚好运行完后才被别的线程抢夺到cpu资源的使用权的情况)

解决思路

什么是线程同步?

从示例代码中,我们能想到,要解决这类问题,就需要保证一个人在抢票过程未结束前,不允许其他人同时抢票。
所以从编程上来说,需要将这些操作共享数据的代码封装起来,当有线程在执行这些代码的时候,其他线程不能参与运算。
只有在当前线程执行完后,才允许其他的线程参与运算。
我们将这个方法称为线程同步。

线程同步的实现

1.同步方法

使用synchronized修饰的方法来控制对类成员变量的访问。
每一个类实例对应一把锁,方法一旦执行,就独占该锁,知道从该方法返回时才将锁释放,以后其他的线程才能获得该锁,重新进入可执行的状态。
这种机制确保了同一时刻对应一个实例,从而有效的避免了类成员变量的访问冲突。
同步方法的格式:
访问修饰符 synchronized 返回类型 方法名 (参数列表){
//省略方法体
}

其代码修改如下:

public void run() {
        while (true) {
            sale();
        }
    }

    public synchronized void sale(){
        if (tickets > 0) {
            tickets--;
            num++;
            try {
                Thread.sleep(500);//模拟网络延迟
            } catch (InterruptedException e) {
            }
            System.out.println(Thread.currentThread().getName() + "抢到第"+num+"张票,剩余" + tickets+"张票。");
        }
    }

执行结果如下图:
执行结果

2.同步代码块

同步代码块的使用格式:
synchronized(syncObject)
{
需要被同步的代码 ;
}

需要注意三点:

  1. 其中syncObject指的是锁对象,该锁对象可以是任意对象。
    (同步函数使用的锁是 this。静态的同步函数使用的锁是该函数所属 字节码文件对象 ,可以用 getClass()方法获取,也可以用 当前类名.class 表示。
    同步函数和同步代码块的区别:同步函数的锁是固定的this。同步代码块的锁是任意的对象。
    建议使用同步代码块。)
  2. 必须保证多线程使用的锁对象是同一个。
  3. 锁对象的作用:把同步代码块锁住,只能让一个线程在同步代码中执行。
//使用同步代码块的线程类
public class TicketDemo implements Runnable{
    private int tickets = 10; //记录剩余票数
    private  int num = 0 ; //记录买到的票数
    public void run() {
        while (true) {
            synchronized(TicketDemo.class){ //同步代码块
            	if (tickets > 0) {
            		tickets--;
            		num++;
            		try {
                		Thread.sleep(500);//模拟网络延迟
            		} catch (InterruptedException e) {
            		}
            	System.out.println(Thread.currentThread().getName() + "抢到第"+num+"张票,剩余" + tickets+"张票。");
        	}
        }
    }

原理:
线程1抢到的cpu资源的使用权,执行run方法,遇到了synchronized代码块,这时线程1会检测代码块是否有锁对象,发现有,就会获取到锁对象,进入到同步方法中执行。
然后线程2抢到了cpu的使用权,执行run方法,遇到了synchronized代码块,这时线程2会检测代码块没有锁对象,这时线程2会进入到阻塞状态,会一直等待线程1还锁对象。

单例设计模式中的线程安全问题

//饿汉式
class Single
{
    private static final Single s = new Single();
    private Single(){}
    public static Single getInstance()
    {
        return s;
    }
}
 
//懒汉式
/*
*加入同步是为了解决多线程安全问题。
*
*加入双重判断不用每次都判断是否上锁,是为了解决效率问题。
**/
 
class Single
{
    private static Single s = null;
    private Single(){}
    //线程安全问题---synchronized ,double check 
    public static Single getInstance()
    {
        if(s==null)
        {
            synchronized(Single.class)     
            {
                if(s==null)
                    s = new Single();
            }
        }
        return s;
    }
}

开发用饿汉式,没有线程安全问题。——饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。

面试懒汉式,记住如何解决线程安全问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值