文档章节

Java线程之并发

士别三日
 士别三日
发布于 06/07 15:48
字数 2416
阅读 5
收藏 0

1.线程同步

多线程的复杂度很大程度上都是来源于并发。并发必然涉及到状态共享,所以并发代码除了要实现业务逻辑,还要实现状态安全。状态安全包括三个方面:

  • 原子性:就是说当一组(一个或多个)状态被某个线程访问(通俗讲就是CRUD)时,这组状态不该被其他线程访问;
  • 可见性:当一个线程访问一组状态完成后,状态变化要立即对其他线程可见(每个线程都有自己的内存空间,如果没有同步,会先把修改后的状态缓存在线程自己的内存中,并不会马上冲刷到共享内存);
  • 有序性:当一个线程访问一组状态时,对各个状态的改变顺序应该和代码一致(如果没有同步代码,JVM可能会基于性能考虑对状态的赋值顺序作调整)。

当这三个条件得到满足,我们就说状态是安全的。在synchronized空间下的一组状态就可以满足这三个条件。被volatile修饰的单个状态也可以满足这三个条件(long和double的赋值默认都不是原子性的,这两种类型的变量如果被volatile修饰了就会变成具有赋值原子性)。volatile也要性能开销,和synchronized的最大区别是它不同步线程。

下图是JVM内存和线程的关系示意图:

讲完状态安全,其实就讲完了多线程的同步。下面再讲多线程的协作。

2.线程协作

多种线程利用同一组状态进行通信,实现特定的业务逻辑,这就是协作。每个synchronized空间都持有一把锁。这把锁可以是任何一个普通对象(Object)。每个锁对象都有一组用来协作的方法(扩展自Object类):wait()/wait(long timeout)/wait(long timeout, int nanos)/notify()/notifyAll()。wait让current thread进入锁对象的等待队列,notify唤醒锁对象的等待队列里面的线程。

3.如何识别并设计并发系统

现在做一个题目,来理解不同种类的线程怎样进行协作:

实现一把读写锁:当有线程在读的时候,允许读线程访问,但是不允许写线程访问;当有线程在写的时候,其他线程不可访问;当读线程释放锁后,如果同时有写线程和读线程在等待,优先执行写线程。

读写锁实现:

class ReadWriteLock {

    private int waitWritings;
    private int readings;
    private int writings;
    private boolean preferWriter;

    public synchronized boolean tryReadLock() {
        if(writings>0||(waitWritings>0&&preferWriter)) {
            return false;
        }
        readings++;
        return true;
    }

    public synchronized boolean tryWriteLock() {
        if(writings>0||readings>0) {
            return false;
        }
        writings++;
        return true;
    }

    public synchronized void readLock() {
        while(writings>0||(waitWritings>0&&preferWriter)) {
            try {
                wait();
            } catch (InterruptedException e) {

            }
        }
        readings++;
    }

    public synchronized void writeLock() {
        while(writings>0||readings>0) {
            try {
                waitWritings++;
                wait();
                waitWritings--;
            } catch (InterruptedException e) {

            }
        }
        writings++;
    }

    public synchronized void unReadLock() {
        readings--;
        preferWriter = true;
        notifyAll();
       }

    public synchronized void unWriteLock() {
        writings--;
        preferWriter = false;
        notifyAll();
       }

}

再补充一个数据类Couple,一个写线程类,一个读线程类。营造一种场景:写线程不停地修改Couple对象里面的夫妻信息,读线程不停读取Couple对象里面的夫妻信息。使用上面的读写锁进行状态的同步和协作:

class Couple {
    private ReadWriteLock lock = new ReadWriteLock();
    private String husband;
    private String wife;

    public void read() {
        try {
            lock.readLock();
            System.out.println("husband="+husband+";wife="+wife);
        } finally {
            lock.unReadLock();
        }
    }

    public void write(String husband,String wife) {
        try {
            lock.writeLock();
            this.husband = husband;
            this.wife = wife;
        } finally {
            lock.unWriteLock();
        }
    }
}

class ReadThread implements Runnable {

    private Couple couple;

    public ReadThread(Couple couple) {
        this.couple = couple;
    }

    @Override
    public void run() {
        while(true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {

            }
            couple.read();
        }
    }

}

class WriteThread implements Runnable {

    private Couple couple;
    private String husband;
    private String wife;


    public WriteThread(Couple couple,String husband,String wife) {
        this.couple = couple;
        this.husband = husband;
        this.wife = wife;
    }

    @Override
    public void run() {
        couple.write(husband, wife);
    }

}

最后补上测试代码和结果:

Couple couple = new Couple();
Thread rt = new Thread(new ReadThread(couple));
rt.start();
while(true) {
    Thread wt1 = new Thread(new WriteThread(couple,"萧峰","阿朱"));
    Thread wt2 = new Thread(new WriteThread(couple,"郭靖","黄蓉"));
    Thread wt3 = new Thread(new WriteThread(couple,"杨过","小龙女"));
    Thread wt4 = new Thread(new WriteThread(couple,"张无忌","赵敏"));
    Thread wt5 = new Thread(new WriteThread(couple,"令狐冲","任盈盈"));
    wt1.start();
    wt2.start();
    wt3.start();
    wt4.start();
    wt5.start();
}

husband=杨过;wife=小龙女
husband=萧峰;wife=阿朱
husband=张无忌;wife=赵敏
husband=张无忌;wife=赵敏
husband=令狐冲;wife=任盈盈
husband=杨过;wife=小龙女
husband=张无忌;wife=赵敏
husband=张无忌;wife=赵敏
husband=杨过;wife=小龙女
husband=郭靖;wife=黄蓉

夫妻信息在多线程环境下,没有发生紊乱。说明上面的读写锁实现生效了。但是,这个测试场景没有全覆盖读写锁的功能。节省篇幅,不做全功能测试。下面借助这个例子总结一下怎么识别和设计并发程序:

  1. 识别依据:当遇到多种线程利用同一组状态(一个或多个状态)进行通信,实现特定的业务逻辑的场景,就要考虑设计成并发程序;
  2. 抽象出共享状态:有没有写线程在访问代码(抽象为有多少个线程在写:writings)、有多少读线程访问代码(抽象为有多少个线程在读:readings)、有多少写线程在等待访问代码(抽象为:waitWritings)、是不是写优先(抽象为:preferWriter)。这些状态都要被同步起来,确保状态安全。
  3. 抽象出线程种类:写线程和读线程。当然,同一个线程可以同时为写线程和读线程,一个线程分饰两种角色。
  4. 抽象出每种线程的行为:写线程的行为有:阻塞获取写锁、非阻塞获取写锁和释放写锁;读线程的行为有:阻塞获取读锁、非阻塞获取读锁和释放读锁。
  5. 实现线程间通信规则:根据状态选择不同的行为。对于读线程获取读锁的时候,如果有线程在写或有线程在等待写并且写优先的情况下,不能获取读锁,否则可以获取;对于读线程释放锁的时候,要notify其他线程;对于写线程……

根据上面的五个步骤,就可以设计出一个符合安全要求和业务要求的并发程序。

4.Java并发包的锁

下面再学习一下Java并发包里面的锁,下图是类图结构,其实就是ReentrantLock和ReentrantReadWriteLock。

可重入锁:

读写锁:

Java并发包抽象出了锁对象(Lock)、条件对象(Condition)等,让同步和协作等操作可以转化为对普通对象的操作。一方面让代码看起来很直观,另一方面让操作更加精细化。为了证明这一点,下面用ReentrantLock重新实现一下上面的题目,并且加一些附加条件:

实现一把读写锁:当有线程在读的时候,允许读线程访问,但是不允许写线程访问;当有线程在写的时候,其他线程不可访问

附加条件:①对外护短模式:当写线程释放写锁的时候,如果有其它写线程在等待,只唤醒写线程,如果没有写线程,才唤醒读线程;当读线程释放读锁的时候,如果有其它读线程在等待,只唤醒读线程,如果没有读线程,才唤醒写线程。【这个附加条件有点开玩笑的意思,主要是为了展示Java并发包的精细化的特点,可能会导致其中一种线程总是拿不到锁,慎用!】②对内公平模式:让等待时间更久的线程优先拿到锁。

下面是实现代码,这里就不提供测试代码了:

class IntricatelyReadWriteLock {

    private int waitWritings;
    private int waitReadings;
    private int readings;
    private int writings;

    //公平的可重入锁
    private final ReentrantLock lock = new ReentrantLock(true);
    //写优先条件
    private final Condition writeFirst = lock.newCondition();
    //读优先条件
    private final Condition readFirst = lock.newCondition();

    public boolean tryReadLock() {
        try {
            lock.lock();

            if(writings>0) {
                return false;
            }
            readings++;
            return true;
        } finally {
            lock.unlock();
        }
    }

    public boolean tryWriteLock() {
        try {
            lock.lock();

            if(writings>0||readings>0) {
                return false;
            }
            writings++;
            return true;
        } finally {
            lock.unlock();
        }
    }

    public void readLock() {
        try {
            lock.lock();

            while(writings>0) {
                try {
                    waitReadings++;
                    readFirst.await();
                    waitReadings--;
                } catch (InterruptedException e) {

                }
            }
            readings++;
        } finally {
            lock.unlock();
        }
    }

    public void writeLock() {
        try {
            lock.lock();

            while(writings>0||readings>0) {
                try {
                    waitWritings++;
                    writeFirst.await();
                    waitWritings--;
                } catch (InterruptedException e) {

                }
            }

            writings++;
        } finally {
            lock.unlock();
        }

    }

    public void unReadLock() {
        try {
            lock.lock();

            readings--;
            if(waitReadings>0) {
                readFirst.signalAll();
            }else {
                writeFirst.signalAll();
            }
        } finally {
            lock.unlock();
        }
       }

    public void unWriteLock() {
        try {
            lock.lock();

            writings--;
            if(waitWritings>0) {
                writeFirst.signalAll();
            }else {
                readFirst.signalAll();
            }
        } finally {
            lock.unlock();
        }
       }

}

对比这两种实现,你会发现Java并发包里面的锁更加直观,而且协作更加精细化,对锁的各种管理控制也更强(比如设置公平性,获取等待线程队列……)。

5.Java并发包的同步器

最后,再介绍一下Java并发包里面提供的同步器。

  • CyclicBarrier 障栅。每个线程到一个执行点都会暂停,等到所有线程都到达,所有线程才会继续执行……可设置多个这样的执行点。
  • CountDownLath 倒计时门栓。等待所有线程执行完,再继续执行。
  • Exchanger 交换器。两个线程互相交换状态。
  • Semphore 信号量。限制访问资源的线程总数。
  • SynchronousQueue 同步队列。生产者和消费者总是同步执行,size永远是0。

这些同步器在很多特定的并发场景下非常有用。具体怎么使用本文不做介绍,可以自行学习。但是,记住这些同步器的功能,对你设计并发系统非常有用。

© 著作权归作者所有

共有 人打赏支持
士别三日

士别三日

粉丝 38
博文 30
码字总数 43081
作品 0
深圳
程序员
私信 提问
15个顶级Java多线程面试题及回答

Java 线程面试问题 在任何Java面试当中多线程和并发方面的问题都是必不可少的一部分。如果你想获得任何股票投资银行的前台资讯职位,那么你应该准备很多关于多线程的问题。在投资银行业务中多...

LCZ777
2014/05/27
0
0
java中高级大公司多线程面试题

1)在Java中Lock接口比synchronized块的优势是什么?你需要实现一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此来保持它的完整性,你会怎样去实现它? lock接口在多线程和并发编...

java成功之路
10/30
0
0
读书笔记之《Java并发编程的艺术》-并发编程基础

读书笔记部分内容来源书出版书,版权归本书作者,如有错误,请指正。 欢迎star、fork,读书笔记系列会同步更新 git https://github.com/xuminwlt/j360-jdk module j360-jdk-thread/me.j360....

Hi徐敏
2015/11/11
0
8
java.lang.ThreadLocal类研究

java.lang.ThreadLocal类研究 1、概述 ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是thread local variable(线程局部变量)。也许把它命名为...

SDK4
2011/09/17
0
2
读书笔记之《Java并发编程的艺术》-并发编程容器和框架(重要)

读书笔记部分内容来源书出版书,版权归本书作者,如有错误,请指正。 欢迎star、fork,读书笔记系列会同步更新 git https://github.com/xuminwlt/j360-jdk module j360-jdk-thread/me.j360....

Hi徐敏
2015/11/11
0
1

没有更多内容

加载失败,请刷新页面

加载更多

eureka多注册中心

eureka分区的深入讲解

miaojiangmin
8分钟前
0
0
RAM SSO功能重磅发布 —— 满足客户使用企业本地账号登录阿里云

阿里云RAM (Resource Access Management)为客户提供身份与访问控制管理服务。使用RAM,可以轻松创建并管理您的用户(比如雇员、企业开发的应用程序),并控制用户对云资源的访问权限。 对云资...

阿里云官方博客
11分钟前
0
0
Mysql 8.0 | #08004Client does not support authentication protocol requested by server

完整报错 #08004Client does not support authentication protocol requested by server; consider upgrading MySQL client 解决方法 Using the old mysql_native_password works:ALTER ......

云迹
13分钟前
0
0
kylin入门到实战

1.概述 kylin是一款开源的分布式数据分析工具,基于hadoop之上的sql查询接口,能支持超大规模的数据分析。响应时间在亚秒级别,其核心是预计算,计算结果存放在hbase中。 2.特性 可扩展超快O...

hblt-j
16分钟前
0
0
vagrant mac 安装和使用

varant 在mac上使用起来非常的方便,具体的下载步骤可以看这里https://segmentfault.com/a/1190000000264347 这里主要是记录一下配置文件 # -*- mode: ruby -*- # vi: set ft=ruby : # All V...

一千零一夜个为什么
16分钟前
0
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部