文档章节

Java线程之并发

士别三日
 士别三日
发布于 06/07 15:48
字数 2416
阅读 4
收藏 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
深圳
程序员
Java面试:投行的15个多线程和并发面试题

本文由ImportNew -一杯哈希不加盐 翻译自dzone。欢迎加入翻译小组。转载请见文末要求。 多线程和并发问题已成为各种 Java 面试中必不可少的一部分。如果你准备参加投行的 Java 开发岗位面试,...

ImportNew
08/23
0
0
【转】15个顶级Java多线程面试题及回答

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

一只死笨死笨的猪
2014/09/30
0
0
java多线程之ThreadLocal

java中的java.lang.ThreadLocal,为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序,ThreadLocal并不是一个Thread,而是Thread的局部变量。...

飞翔的兔兔
2017/07/11
0
0
15个顶级Java多线程面试题及回答

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

LCZ777
2014/05/27
0
0
synchronized与ThreadLocal

synchronized是实现java的同步机制。同步机制是为了实现同步多线程对相同资源的并发访问控制。保证多线程之间的通信。 同步的主要目的是保证多线程间的数据共享。同步会带来巨大的性能开销,...

bigYuan
2013/07/18
0
2

没有更多内容

加载失败,请刷新页面

加载更多

首个智能运维项目开源!腾讯织云Metis,用算法替代人为指定规则

10月20日,腾讯织云Metis 智能运维学件平台在OSCAR开源先锋日上宣布,正式对外开源。Metis 是AIOps(Algorithmic IT Operations),即智能运维领域的首个开源产品。智能运维主张通过算法从海...

腾讯开源
7分钟前
0
0
Java面试170题

1、面向对象的特征有哪些方面? 2、访问修饰符public,private,protected,以及不写(默认)时的区别? 3、String 是最基本的数据类型吗? 4、float f=3.4;是否正确? 5、short s1 = 1; s1 = ...

lanyu96
8分钟前
0
0
利用Ant脚本生成war包

使用ant脚本前的准备 1、下载一个ant安装包。如:apache-ant-1.8.4-bin.zip。解压到E盘。 2、配置环境变量。新增ANT_HOME:E:\apache-ant-1.8.4;PATH增加:E:\apache-ant-1.8.4\bin。 3、检...

狼王黄师傅
11分钟前
0
0
优雅的写出类

前言 虽然现在已经是ES6的时代,但是,还是有必要了解下ES5是怎么写一个类的。 本文详述JavaScript面向对象编程中的类写法,并分步骤讲述如何写出优雅的类。 一、例子 例子为一个轻提示组件T...

peakedness丶
13分钟前
0
0
Python基础案例教程

一、超市买薯片 # 用户输入薯片的单价danjia = float(input("薯片的单价"))# 用户输入购买袋数daishu = int(input("购买的袋数"))# 计算总价zongjia = danjia * daishu# 输出结果...

linuxprobe16
今天
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部