什么是线程,线程安全问题如何解决,同步机制与Lock锁有何异同

什么是线程?什么是线程安全问题?当出现线程安全问题时又该如何解决?这篇文章将以一个银行账户取款问题对上述问题做出详细解答。如有不足,还请指正。

案例引入

银行有一个账户。有两个储户分别向同一个账户存3000元,每次存1000,存3次。每次存完打印账户余额。

什么是线程

首先我们来理解什么是进程,程序运行的一个完整的周期,(产生、存在、消亡)为一个进程。在这个银行存款的案例中,创建银行对象和储户对象的的创立,存款行为的执行,内存资源的分配,内存资源的释放完成一个进程。
进程可以进一步细分为线程,是程序内部的一条执行路径。在这个案例中,有两个储户都完成了上述创立,存入,分配内存资源的过程,如果这个两个储户的行为是同时进行的,那么这两个储户的行为分别为一个线程,并把这种同时发生叫做并发(单核CPU)。

单核CPU是如何同时处理多个线程的

我们知道对于单核CPU,一个时间单元内是只能处理一个线程的任务,那么单核CPU是如何做到并发的呢。
线程的调度使用时间片的策略
时间片
事实上,对于单核CPU这种线程这种同时并非严格意义上的同一时刻,而是由于CPU快速切换执行不同任务(每个任务执行极短时间,挂起,然后执行下一个,循环直至全部完成),由于CPU的频率极高,所以看起来像是同时执行多个任务。

线程安全问题的产生

有了上述单核CPU对于并发处理的解释我们就不难理解为何会出现线程安全问题了。
以上述案例为例,在一个时间片内,CPU接受储户1(线程1)对账户的存入指令,余额增加1000,本来下一时刻该执行该账户余额输出1000,但此时当前时间片结束,储户2(线程2)抢占了CPU,线程1挂起。线程2执行对该账户存入1000,假使下一时间片,线程2再次抢到CPU,此时账户余额输出2000。下一时间片,线程1抢到CPU,执行被中断的账户余额输出2000。此时,就出现了对于储户1来说,存完1000元后,显式余额2000,凭空多出1000。这就是线程安全问题。(真好!)
下面我们一个程序演示一下上述问题

public class BankAccountt1{
    public static void main(String[] args) {
        Account1 acc1 = new Account1();
        Account1 acc2 = new Account1();

        acc1.setName("账户1:");//设置线程名:账户1
        acc2.setName("账户2:");
        acc1.start();//执行run函数,每次给money+1000并打印
        acc2.start();
    }
}
class Account1 extends Thread{//继承Thread方式创建线程
    static int money = 0;
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            money += 1000;
            System.out.println(getName() + "当前余额" + money);
        }

    }
}

输出结果:

账户1:当前余额2000
账户2:当前余额2000
账户1:当前余额3000
账户2:当前余额4000
账户1:当前余额5000
账户2:当前余额6000

我们可以看到,程序并非执行完账户1的三次输出,再执行账户2的三次输出,这正是并发的体现。同时我们也可以看到,账户1存入1000后,输出2000,出现线程安全问题。
我们也发现此时输出结果与我上述描述刚好一致,这是巧合,事实上,两个线程有同等的概率抢占CPU,可能出现结果正确的现象,也可能出现其它各种奇怪的输出。

解决线程安全问题

方式1 :同步代码块

定义在run()方法中,以代码块的形式出现。多个线程需要以同一对象作为锁
(以上述继承Thread方式创建线程为例,主程序同上)

//(1)用同步代码块方式解决线程安全问题
class Account1 extends Thread{
    static int money = 0;

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            synchronized (Account1.class) {//使用Account.class对象作为当前锁
                money += 1000;
                System.out.println( getName() + "当前余额:" + money);
            }
        }
    }
}

方式2 :同步方法

将操作共享数据的代码定义为一个以synchornized修饰的方法。默认的锁为this(非静态方法,this指当前该对象的实例;静态方法,this指当前类对象)

//(2)用同步方法解决线程安全问题
class Account1 extends Thread{
    static int money = 0;

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            addMoney();
        }
    }

    public static synchronized void addMoney(){ //静态方法,默认当前类Account1为当前锁
        money += 1000;
        System.out.println( Thread.currentThread().getName() + "当前余额:" + money);
    }
}

方法3:Lock方式

接口java.util.concorrent.locks.Lock是控制多个线程对共享数据进行操作的工具,ReentrantLock类是实现了上述接口。它与synchronized具有相同的并发性和内存语义。一次只有一个对象可以获得lock(对象)锁。需要显式调用lock锁并显式释放

//(3)用LOCK方式解决线程安全问题
class Account1 extends Thread{
    private static int money = 0;
    private static final ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            lock.lock();
            try {//此处使用try…finally的目的是防止同步代码出现异常,锁无法释放
                money += 1000;
                System.out.println( Thread.currentThread().getName() + "当前余额:" + money);
            } finally {
                lock.unlock();
            }
        }
    }
}

输出结果:

账户2:当前余额:1000
账户1:当前余额:2000
账户2:当前余额:3000
账户1:当前余额:4000
账户1:当前余额:5000
账户2:当前余额:6000

synchronized(同步)方式和Lock方式对比

  1. lock是显式锁,需要手动开启和关闭;synchronized是隐式锁,出了作用域自动关闭
  2. lock只有代码块锁,synchroonized既有代码块锁还有方法锁
  3. 使用lock锁,JVM花费更少的时间来调度线程,性能更好。
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值