Java--线程同步&线程通信

上一篇--五态模型&控制线程

线程同步:

同步监视器(synchronized): 

Java多线程引出了临界区问题。当两个进程并发修改同一资源时就有可能造成异常。Java引入同步监视器来解决这个问题。使用同步监视器有两种方法:同步代码块和同步方法。

同步代码块:

synchronized(obj){
    //此处代码是同步代码块
}

上述代码的obj就是同步监视器,同一时刻只能有一个线程获得同步监视器的锁定,当同步代码块执行完毕后,该线程会自动释放该同步监视器的锁定。

通常使用可能被并发访问的共享资源作为同步监视器obj,如银行取款问题就用银行账户作为同步监视器。

同步方法:

public synchronized void drow(double drawAmount){
    //同步方法体
}

使用synchronized来修饰某个方法,该方法就是同步方法。同步方法无需显式指定同步监视器,它的同步监视器就是this,也就是调用该方法的对象。

不可变类总是线程安全的,因为它的对象状态不可被改变;可变类对象的方法就需要使用额外的方法(如上述方法)来保证线程安全。

synchronized关键字可以修饰方法和代码块,但不可以修饰构造器、成员变量等。

可变类的线程安全是以牺牲运行效率为代价的,所以不要对线程安全类的所有方法都进行同步。如果可变类有两种运行环境--单线程和多线程,那么应该为该可变类提供两种版本,即线程不安全版本和线程安全版本。

同步监视器的释放

下面这些情况会释放同步监视器

  • 同步方法、同步代码块执行结束;
  • 线程在同步代码块或同步方法中遇到break、return终止执行;
  • 线程在同步代码块或同步方法中出现了未处理的Error或Exception;
  • 线程在同步代码块或同步方法中执行了同步监视器对象的wait()方法。

下面这些情况不会释放同步监视器

  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()和Thread.yield()暂停线程的执行;
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()将该线程挂起。

同步锁(lock): 

同步锁是比同步监视器更强大的线程同步机制----通过显式定义同步锁对象来实现同步,同步锁由Lock对象充当。

Lock和ReadWriteLock是Java 5提供的两个根接口,Lock有ReentrantLock(可重入锁)实现类,ReadWriteLock有ReentrantReadWriteLock实现类。

public class Account{
    ...
    //定义锁对象,以ReentrantLock 为例
    private final ReentrantLock lock = new ReentrantLock();
    ...
    public void draw(double drawAmount){
        //加锁
        lock.lock();
        try{
            ...
        }catch(InterruptedException e){
            ...
        }finally{
            //释放锁
            lock.unlock();
        }
    }
}

ReentrantLock 锁具有可重入性,即一个线程可以对已被加锁的ReentrantLock 锁再次加锁,ReentrantLock 对象维持一个计数器来追踪lock()方法的嵌套调用,线程在调用lock()后必须调用unlock()释放锁。所以一段被锁保护的代码可以调用另一个被相同锁保护的代码。

线程通信:

考虑一种“生产者消费者问题”:一个银行账户,系统要求存款者和取款者不断地交替进行操作。

传统的线程通信:

为了实现这种功能,可以借助Object类的wait()、notify()、notifyAll()方法。注意这三个方法不属于Thread类,但必须由同步监视器对象调用。

对于使用synchronized修饰的同步方法,因为同步监视器就是this,因此可以直接调用这三个方法;对于使用synchronized修饰的代码块,同步监视器是synchronized括号里的对象,必须由这个对象调用这些方法。

  • wait():导致该线程等待,知道其他线程调用该同步监视器的notify()或notifyAll()唤醒该线程。调用wait()方法的当前线程会释放对该同步监视器的锁定。
  • notify():唤醒在该同步监视器上等待的单个线程,如果多个线程在该同步监视器上等待,随机唤醒一个。只有当前线程放弃对该同步监视器的锁定后才可以执行被唤醒的线程。
  • notifyAll():唤醒所有在此同步监视器上等待的线程。只有当前线程放弃对该同步监视器的锁定后才可以执行被唤醒的线程。
//取钱方法,该方法是同步方法
public synchronized void draw(double drawAmount){
    try{
        //如果flag为假,表示还未存钱,取钱方法阻塞
        if(!flag)
            wait();
        else{
            System.out.println("取钱");
            flag = false;    //转换标志
            notifyAll();    //唤醒其他线程
        }
    }catch(InterruptedException e){
        ex.printStackTrace();
    }
}
//存钱方法,该方法是同步方法
public synchronized void deposit(double depositAmount){
    try{
        if(flag)
            wait();
        else{
            System.out.println("存钱");
            flag = true;
            notifyAll();
        }
    }catch(InterruptedException e){
        ex.printStackTrace();
    }
}

使用Condition控制线程通信:

如果程序不使用同步监视器而是Lock对象,那么就不能调用上述三个方法了。Java提供了Condition类来保持协调。Condition可以让那些已经得到Lock对象的线程释放Lock对象,也可以唤醒其他处于等待的线程。在这种情况下,Lock替代了同步方法或同步代码块,Condition替代了同步监视器的功能。

Condition实例被绑定在一个Lock对象上,要获得特定Lock对象实例的Condition实例,只需调用Lock实例的newCondition()方法。Condition类提供了如下三个方法:

  • await(): 类似wait()
  • signal():类似notify()
  • signalAll(): 类似notifyAll()
public class Account{
    ...
    //显式定义Lock实例
    private final Lock lock = new ReentrantLock();
    //获得指定Lock对象对应的Condition
    private final Condition cond = lock.newCondition();
    ...
    public void draw(double drawAmount){    //取钱方法
        lock.lock();    //加锁
        try{
            if(!falg)    //还未存钱时,取钱等待
                cond.await();
            else{
                System.out.println("取钱");
                flag = false;    //转换标志
                cond.signalAll();   //唤醒其他线程
            }
        }catch(InterruptedException e){
            ex.printStackTrace();
        }finally{
            lock.unlock();    //释放锁
        }
    }
    public void deposit(double drawAmount){   //存钱方法
        lock.lock();
        try{
            if(falg)
                cond.await();
            else{
                System.out.println("存钱");
                flag = true;
                cond.signalAll();
            }
        }catch(InterruptedException e){
            ex.printStackTrace();
        }finally{
            lock.unlock();
        }
    }

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java编程技术

一个有关定时生产与消费的问题

按照上面的逻辑看的话,每个队列里面最多有一个元素。其实不然,因为在多线程模型中每个线程占用cpu执行的时间是按照时间片来划分的,每个线程执行完自己的时间片后会被...

9010
来自专栏云计算教程系列

如何使用Grep

Grep是一个命令行实用程序,可以使用常见的正则表达式语法搜索和过滤文本。它无处不在,动词“to grep”已经成为“搜索”的同义词。它grep是一个有用的工具...

15730
来自专栏chenssy

【死磕Java并发】—–Java内存模型之happens-before

在上篇博客(【死磕Java并发】—–深入分析volatile的实现原理)LZ提到过由于存在线程本地内存和主内存的原因,再加上重排序,会导致多线程环境下存在可见性...

34790
来自专栏Java帮帮-微信公众号-技术文章全总结

hibernate延迟加载详解

hibernate延迟加载详解 Hibernae 的延迟加载是一个非常常用的技术,实体的集合属性默认会被延迟加载,实体所关联的实体默认也会被延迟加载。hiber...

32330
来自专栏余林丰

虚拟机类加载机制(2)——类加载器

《深入理解Java虚拟机》一书中将类的加载过程放到了类加载器前面一节,但在这里我想先讲“类加载器”。在上一篇类加载时机中我们用大量篇幅来讲解了类加载过程中的5个...

22360
来自专栏IT技术精选文摘

Shell入门指南

16230
来自专栏Android相关

Java线程池---ThreadPoolExecutor解析

在ThreadPoolExecutor中的ctl变量中已经解释了线程池中ctl变量中,高3位代表线程池当前的状态,而低28位表示线程池中线程的总数。

14930
来自专栏chenssy

【死磕Java并发】-----Java内存模型之happens-before

在上篇博客(【死磕Java并发】—–深入分析volatile的实现原理)LZ提到过由于存在线程本地内存和主内存的原因,再加上重排序,会导致多线程环境下存在可见性...

12520
来自专栏Python攻城狮

Python系统编程-进程1.进程1.多任务的引入2.多任务的概念

有很多的场景中的事情是同时进行的,比如开车的时候手和脚共同来驾驶汽车,再比如唱歌跳舞也是同时进行的;

9930
来自专栏芋道源码1024

【死磕Java并发】—–Java内存模型之happens-before

在上篇博客(【死磕Java并发】—–深入分析volatile的实现原理)LZ提到过由于存在线程本地内存和主内存的原因,再加上重排序,会导致多线程环境下存在可见性...

30750

扫码关注云+社区

领取腾讯云代金券