并发编程系列(三)—线程的同步和阻塞

并发编程系列之基础篇(三)—线程的同步和阻塞

前言

大家好,牧码心今天给大家推荐一篇并发编程系列之基础篇(三)—线程的同步和阻塞的文章,希望对你有所帮助。具体内容如下:

  • 同步和阻塞概要
  • 为什么需要同步
  • 实现同步的方式

同步和阻塞概要

在并发编程中,我们常会碰到线程的同步,异步,阻塞和非阻塞等场景,其中同步和异步,是线程之间的方式,两个线程之间要么是同步的,要么是异步的;阻塞和非阻塞是线程内的状态,在某个时刻,线程要么处于阻塞,要么处于非阻塞。那它们分别是什么呢?

  • 同步:指线程在发起调用请求时,需要等待被调用者返回结果,才会进行后面的步骤。

  • 异步:指线程在发起调用请求时,调用者不需要等待被调用者返回调用,即可进行下一步操作,被调用者通常依靠事件、回调等机制来通知调用者结果。

  • 阻塞:指线程在发起调用请求时,在返回结果前,此线程会被挂起,处于等待状态;

  • 非阻塞:指线程在发起调用请求时不能立刻得到返回结果,此线程不会被被挂起,处于运行态;

上述概念我们可以用一个例子说明:

比如你打餐饮店客服电话需要订位。同步方式则是你需要一直等客服给你排好座位后,你才能挂断电话。其中过程是阻塞的。异步方式则是你可以提前挂断电话去做其他事,餐饮店客服以回电话或消息通知你订位的结果,其中过程是非阻塞。

同时这几个概念又可以组合成以下4种方式:

  • 同步阻塞
  • 同步非阻塞
  • 异步阻塞
  • 异步非阻塞

本文主要线介绍实现线程同步和实现同步的方式。对于线程阻塞,中断等分析详见后续系列文章。

为什么需要线程同步

当有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序执行结果异常。比如一个银行账户同时被两个线程操作,一个取100块,一个存钱100块。假设账户原本有0块,如果取钱线程和存钱线程同时发生,则会出现账户金额不准确的现象。而多线程同步就是要解决这个问题。

  • 示例演示
public class Account {

    // 余额
    private double balance;
    // 存款
    public void addAmount(double amount,String threadName){

        balance+=amount;
        System.out.println(threadName+"-存款:"+amount);
    }
    // 取款
    public  void subAmount(double amount,String threadName){
        if(balance-amount<0){
            System.out.println("余额不足!");
            return;
        }
        balance-=amount;
        System.out.println(threadName+"-取款:"+amount);
    }
    // 查询余额
    public void queryAmount(String threadName){
        System.out.println(threadName+"-当前账户余额:"+balance);
    }

    public static void main(String[] args) {
        Account account=new Account();
        // 模拟存款
        Thread addThread=new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=0;i<3;i++){
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    account.addAmount(200,Thread.currentThread().getName()+"-"+i);
                    account.queryAmount(Thread.currentThread().getName()+"-"+i);
                    System.out.println("\n");
                }
            }
        });
        // 模拟取款
        Thread subThread=new Thread(new Runnable() {
            @Override
            public void run() {
                for(int j=0;j<3;j++){
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    account.subAmount(100,Thread.currentThread().getName()+"-"+j);
                    account.queryAmount(Thread.currentThread().getName()+"-"+j);
                    System.out.println("\n");
                }
            }
        });
        addThread.start();
        subThread.start();
    }
}
  • 运行结果
余额不足!
Thread-0-0-存款:200.0
Thread-0-0-当前账户余额:200.0
Thread-1-0-当前账户余额:200.0
Thread-0-1-存款:200.0
Thread-0-1-当前账户余额:400.0
Thread-1-1-取款:100.0
Thread-1-1-当前账户余额:300.0
Thread-0-2-存款:200.0
Thread-0-2-当前账户余额:500.
Thread-1-2-取款:100.0
Thread-1-2-当前账户余额:400.0

从运行结果看,若未使用线程同步机制则运行结果会出现无序和不正确的情形。

实现线程同步的方式

在java中,每一个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在。当我们调用某对象的synchronized方法时,就获取了该对象的同步锁。例如,synchronized(obj)就获取了“obj这个对象”的同步锁。
不同线程对同步锁的访问是互斥的。也就是说,某时间点,对象的同步锁只能被一个线程获取到!通过同步锁,我们就能在多线程中,实现对“对象/方法”的互斥访问。 例如,现在有两个线程A和线程B,它们都会访问“对象obj的同步锁”。假设,在某一时刻,线程A获取到“obj的同步锁”并在执行一些操作;而此时,线程B也企图获取“obj的同步锁” —— 线程B会获取失败,它必须等待,直到线程A释放了“该对象的同步锁”之后线程B才能获取到“obj的同步锁”从而才可以运行。

  • synchronized方式
    使用synchronized可以修饰方法,代码块,以及静态资源等。此时线程访问synchronized修饰的资源,需要先获得同步锁,否则就处于阻塞状态。

    • 修饰方法:会锁住整个方法,需要访问改方法时,需要获得同步锁。
    • 修饰代码块:锁的粒度更细,并且充当锁的对象不一定是this,也可以是其它对象,使用起来更加灵活。
    • 修饰静态资源:若锁的对象是静态变量或静态资源,则会锁住整个类。

    注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

  • volatile (特殊域变量)方式
    volatile 修饰的成员变量在每次被线程访问时,都需要从共享内存中重读该成员变量的值。而且当成员变量发生变化时,线程将变化值回写到共享内存。如线程为了提高效率,将某成员变量(如A)拷贝了一份(如B),线程中对A的访问其实访问的是B。只在某些动作时才进行A和B的同步,因此存在A和B不一致的情况。volatile就是用来避免这种情况的。 volatile告诉jvm,它所修饰的变量不保留拷贝,直接访问主内存中的。

    volatile 变量具有 内存可见性特性,有序性,但是不具备原子特性,不能替代synchronized ,需要使用它用于线程安全,则需要满足:
    1.该变量没有包含在具有其他变量的不变式中;
    2.对变量的写操作是原子操作,如不适合自增的i++ 包含了读去,修改等操作;

  • 重入锁方式
    在jdk 5.0开始新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。

  • ThreadLocal方式
    ThreadLocal 保证不同线程拥有不同实例,相同线程一定拥有相同的实例,即为每一个使用该变量的线程提供一个该变量值的副本,每一个线程都可以独立改变自己的副本,而不是与其它线程的副本冲突。ThreadLocal 是采用空间隔离多个线程的数据共享,从根本上就不在多个线程之间共享资源,这样当然不需要多个线程进行同步了。

  • 原子类的方式
    需要使用线程同步的根本原因在于对普通变量的操作不是原子的。而原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作,即-这几种行为要么同时完成,要么都不完成。如jdk的java.util.concurrent包中提供了创建了原子类型变量的工具类。如AtomicInteger/AtomicLong … 等

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值