Chapter10-并发和分布式编程

目录

Chapter10-并发和分布式编程
考点:
进程和线程
线程的创建和启动,runnable
内存共享模式、消息传递模式
时间分片、交错执行、竞争条件
线程的休眠、中断
线程安全threadsafe的四种策略**
Confinement、Immutability、ThreadSafe类型、Synchronization/Lock
死锁
以注释的形式撰写线程安全策略
多线程之间基于消息传递的实现机制、如何保证threadsafe

10.1、并发和线程安全
10.1.1、并发编程

并发性:多个计算同时发生。eg. 网络上的多个计算机;一台计算机上的多个应用;一个CPU上的多核处理器。
为了充分利用多核和多处理器,需要将程序转化为并行执行。
两种并发编程的模型:共享内存(在内存中读写共享数据,eg. 两个处理器共享内存;两个程序共享文件系统;两个Java线程共享Java对象);消息传递(通过channel交换消息,eg. 通过网络连接通讯;客户端和服务端;两个程序通过管道连接进行通讯)

10.1.2、进程、线程
10.1.2.1、进程 & 线程
并发模块
进程(process)线程(thread)
拥有整台计算机的资源;私有空间,彼此隔离;多进程之间不共享内存;进程之间通过消息传递进行协作;OS支持的IPC机制(pipe/socket)支持进程间通信(可以是不同机器的多个进程之间)程序内部的控制机制;共享内存(很难获取线程的私有内存空间);通过创建消息队列在线程之间进行消息传递
进程 == 程序 == 应用,进程 == 虚拟机,一个程序可能包含多个进程线程 == 虚拟CPU,每个应用至少有一个线程,即主线程,可以创建其他线程
JVM通常运行单一进程,但也可以创建新的进程(使用ProcessBuilder),命令行通过jps查看运行的java进程,可以kill该进程(安全)线程需要synchronization(改变对象时会锁住),kill线程是不安全的
10.1.2.2、线程的创建和启动

两种创建线程的方法:从Thread类派生子类,从Runnable接口构造Thread对象。
①从Thread类派生子类

public class HelloThread extends Thread {
    /* 实现run()方法 */
    public void run() {
        System.out.println("Hello from a thread!");
    }
    public static void main(String args[]) {
        HelloThread p = new HelloThread();
        /* 调用start方法启动 */
        p.start();
        //(new HelloThread()).start();
    }
}

②从Runnable接口构造Thread对象
Runnable接口只有一个run()方法,因此需要在Thread中执行代码。

public class HelloRunnable implements Runnable {
    /* 实现run()方法 */
    public void run() {
        System.out.println("Hello from a thread!");
    }
    public static void main(String args[]) {
        /* 将Runnable对象传递给Thread构造函数 */
        Thread p = new Thread(new HelloRunnable());
        p.start();
        //(new Thread(new HelloRunnable())).start();
    }
}

实现Runnable接口时,也可以实现为匿名函数形式

new Thread(new Runnable() {
    /* 实现run()方法 */
    public void run() {
        System.out.println("Hello from a thread!");
    }
/* 启动线程 */
}).start(); 

Summary
不管何种实现,创建线程都需要重写run()方法,启动线程都需要使用Threadstart()方法(不能直接调用run()方法,这将不会启动线程,只是简单的调用函数)。

Hint
在Java程序中,垃圾回收GC运行在自己的线程中。

10.1.3、交错和竞争
10.1.3.1、时间分片(Time slicing)& 交错

虽然有多线程,但只有一个核,每个时刻只能执行一个线程,通过时间分片,在多个进程、线程之间共享处理器。
并且时间分片是由OS自动调度的,因此,多个线程执行会出现交错现象(Interleaving)。
交错执行效果图如下:
这里写图片描述

通过如下代码进行测试:
这里写图片描述
可以发现first线程main线程**交错执行,但**second线程只有当主线程中循环执行完毕后才启动。

10.1.3.2、内存共享模式

线程之间共享内存,会导致bug。

例如:两个线程t1和t2,对int类型对象s=0进行加1操作,结果可能为1或2;
分析:①结果为1的情况,首先执行**t1,当t1将**s加1后,还未写入内存;此时切换到t2,将s从内存取出,s仍然为0将s加1;然后线程切换到t1,将s=1写入内存,执行完毕;执行t2将s=1写入内存,执行完毕;最后内存中s=1。
②结果为2的情况,即t1执行加1后,立即将s=1**写入内存,然后执行**t2,取出s=1,执行加1,然后将s=2写入内存;最后s=2。

10.1.3.3、竞争

交错之所以存在,就是因为每个时刻,各个进程之间都会进行竞争,从而决定下一时刻执行的进程(由OS进行调度)。
每个线程每个时刻可能执行一个或多个原子操作,单行、单条语句都未必是原子的,是否原子,有JVM确定。(比如,x+=1,不是原子的,执行了x+1x=x+1两个原子操作)
线程竞争也叫线程干扰,所以多个线程的执行过程和结果是不可预测的。
举例:

private static int x = 1;
    public static void methodA() {
        x *= 2;
        x *= 3;
    }
    public static void methodB() {
        x *= 5;
    }

串行:x = 30
并行:x = 30,10,15,6,5,(一定是先执行x *= 2,再执行x *= 3 ,而x *= 5可能在任意时刻执行,最后以及中间写入结果可能会覆盖)。

10.1.3.4、消息传递模式

消息传递机制也无法解决竞争条件问题,仍然存在消息传递时间上的交错。

10.1.4、线程休眠和中断

很难测试和调试因为竞争条件导致的bug,因为interleaving(交错)的存在,导致**很难复现**bug。
有时候增加print语句可能导致bug消失,因为printdubug操作比一般操作慢100-1000x,改变了交错和操作时间。

在此介绍一些干扰线程自动交错的操作。

10.1.4.1、线程休眠Thread.sleep()
          try {
            Thread.sleep(1000);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }

将某个线程休眠,意味着其他线程得到更多的执行机会;进入休眠的线程不会失去对现有monitor锁的所有权

10.1.4.2、线程中断Thread.interrupt()

t.interrupt()其他线程里向t发出中断信号
t.isInterrupted() 检查t是否已在中断状态
Thread.interrupted()判断当前线程是否收到*中断信号*
当某个线程被中断后,一般来说应停止其run()中的执行,取决于程序员run()处理

需要注意的是,线程正常运行时,不会理会中断信号,只有当sleep()(因此使用该方法时,需要使用try-catch 或抛出异常)或者使用Thread.interrupted()时,才会检测是否接收到中断信号,然后执行相应的处理。

举例1

class TestInterruptingThread3 extends Thread{
    public void run(){
        for(int i=1;i<=1000;i++)
        System.out.println(i);
    }
    public static void main(String args[]){
        TestInterruptingThread3 t1=new TestInterruptingThread3();
        t1.start();
        t1.interrupt();
    }
}

t1不会接受中断信号,因此正常执行。
举例2

class Task implements Runnable{
    private double d = 0.0;
    public void run() {
        try{
        while (true) {
            for (int i = 0; i < 900000; i++)
                d = d + (Math.PI + Math.E) / d;
            Thread.sleep(500);
            }
        } catch(InterruptedException e) {return;}
    }
}
    Thread t = new Thread(new Task());
    t.start();
    Thread.sleep(100); //当前线程休眠
    t.interrupt(); //试图中断t线程

t在执行Thread.sleep(500); 进行休眠时,检测是否接收到中断信号,然后进入异常处理,线程终止。

举例3

public class GeneralInterrupt extends Thread {
    public void run() {
        for (int i = 1; i <= 5; i++) {
            if (Thread.interrupted())
                System.out.println("code for interrupted thread");
            else
                System.out.println("code for normal thread");
        }
    }
    public static void main(String args[]) {
        GeneralInterrupt t1 = new GeneralInterrupt();
        GeneralInterrupt t2 = new GeneralInterrupt();
        t1.start();
        t1.interrupt();
        t2.start();
    }
}

t1执行过程中,如果检测到线程中断,则打印if语句块,否则打印else语句块,需要注意的是只有一次线程中断,if语句块只会打印一次。

10.1.4.3、Thread.yield()

使用该方法,线程告知调度器:我可以放弃CPU的占用权,从而可能引起调度器唤醒其他线程(总体思路是:线程调度器将选择一个不同的线程来运行,而不是当前的线程)。
尽量避免在代码中使用

    public void run() {
        ...
        for (int i = 0; i < 5; i++) {
            if ((i % 5) == 0)
                /* 执行其他线程 */
                Thread.yield();
        }
    }
10.1.4.4、Thread.join()

让当前线程保持执行,直到其执行结束(一般不需要这种显式指定线程执行次序,会变成串行执行)。

        Thread th1 = new Thread(new Runnable() {
             public void run() {
                for (int i = 0; i < 3; i++) {
                     System.out.println("th1");
                     Thread.sleep(5);
                 }
              }
    });
        Thread th2 = new Thread(new Runnable() {
              public void run() {
                for (int i = 0; i < 3; i++) {
                  System.out.println("th2");
                  Thread.sleep(5);
                }
          }
    });
        th1.start();
        try {
            th1.join();
        } catch (InterruptedException ie) {}
        th2.start();
        try {
            th2.join();
        } catch (InterruptedException ie) {}
    }
}

变成串行执行,结果为

th1
th1
th1
th2
th2
th2

需要注意的是该操作也会检测其他线程发来的中断信号(需要抛出异常或try-catch包围)。

10.1.5、线程安全的四种策略

线程安全:ADT或方法在多线程中要执行正确。

10.1.5.1、Confinement(限制数据共享)

将可变数据限制单一线程内部,避免竞争。
核心思想:线程之间不共享mutable数据类型
避免使用全局变量,由于竞争和交错问题,当两个线程同时修改全局变量时会出现问题。
举例

    private static Type globalv = null;
    /* Singleton Design, 单例模式 */
    public static Type getInstance(){
        if(globalv == null){      // line 1
            globalv = new Type(); // line 2
        }
        return simulator;         // line 3
    }

两个线程同时调用getInstance() ,由于竞争和交错,可能出现的情况有

thread 1     thread 2
 line 1        line 3
 line 1        line 2
 line 1        line 1

可能会导致两个线程都执行line 2 ,创建了两个对象,违反了不变量
Hint:当涉及到集合时,交错和竞争可能会出现空指针和越界情况。

10.1.5.2、Immutability(共享不可变数据)

使用不可变数据类型不可变引用不可变数据通常是线程安全的,避免多线程之间的race condition。如果ADT中使用了beneficent muta**tion,必须要通过“加锁”机制**来保证线程安全。

实现强不可变类的做法有:
①没有mutator(改变器),不提供setter方法;
②所有成员变量都是privatefinal修饰(不允许子类重写父类,finalprivate修饰,或使用工厂方法);
③没有表示泄露(使用防御式编程);
④所有变量都是不可变的。

10.1.5.3、ThreadSafe数据类型(共享线程安全的可变数据)

如果必须要用mutable的数据类型在多线程之间共享数据,要使用线程安全的数据类型。
一般来说,JDK同时提供两个相同功能的类(javadoc中有说明),一个是threadsafe,另一个不是。原因:threadsafe的类一般性能上受影响。
举例:StringBuilder和StringBuffer,前者线程不安全,后者线程安全。

Threadsafe Collections
集合类都是线程不安全的(List,Set,Map,ArrayList,HashMap,HashSet),Java API提供了decorator(wrapper methods,包装器),可以使集合线程安全,且仍然是可变的。这些包装器有效地使集合的每个方法相对于其他方法是原子的(原子操作不会将其内部操作与其他操作的内部操作交错)。

线程安全的包装器(使用decorator设计模式):

/* 父类集合包装器 */
public static <T> Collection<T> synchronizedCollection(Collection<T> c);
/* Set包装器 */
public static <T> Set<T> synchronizedSet(Set<T> s);
/* List包装器 */
public static <T> List<T> synchronizedList(List<T> list);
/* Map包装器 */
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);
/* SortedSet包装器 */
public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s);
/* SortedMap包装器 */
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m);

关于Iterator
即使在线程安全的集合类上,使用iterator(或for循环)也是不安全的,除非使用lock机制。

原子操作一定能防止竞争?
即使原子操作,仍然可能发生竞争。
举例1:存在竞争

    if( !list.isEmpty()){
        String s = list.get(0);
    }

list变成synchronized ,则执行isEmpty()get() 是原子的,但两个操作合在一起不再是原子的,其他线程可能在isEmpty()get() 操作之间删除元素,会发生错误。
举例2:不存在竞争

    Map<Integer,Boolean> cache = Collections.synchronizedMap(new HashMap<>());
    if (cache.containsKey(x))
        return cache.get(x);
    boolean answer = BigInteger.valueOf(x).isProbablePrime(100);
    cache.put(x, answer);

containsKey()get()put()是原子的,不会改变map的表示不变量;
首先containsKey()get()的竞争是没有害的,如果包含则返回,不会修改不变量;
containsKey()put()存在竞争,但是,如果一个线程调用了put(),另一个线程也调用了put()操作,由于map的相同key值会自动覆盖,所以是线程安全的。(自证threadsafe)

Problem
分析哪些是线程安全数据类型

    //线程安全数据类型, 多线程安全, 没有竞争
    private final String buildingName;     
    //线程安全数据类型,     
    private int numberOfFloors;                      
    private final int[] occupancyPerFloor;       
    //线程安全数据类型, 多线程安全, 没有竞争
    private final List<String> companyNames = Collections.synchronizedList(new ArrayList<>());     
    private final Set<String> roomNumbers = new HashSet<>(); 
    //线程安全数据类型, 多线程安全, 
    private final Set<String> floorplan = Collections.synchronizedSet(roomNumbers);            
10.1.5.4、Locks and Synchronization(最复杂也最有价值的threadsafe策略)

比较
前三种策略的核心思想:
避免共享 -> 即使共享,也只能读/不可写(immutable) -> 即使可写(mutable),共享的可写数据应自己具备在多线程之间协调的能力,即”使用线程安全的mutable ADT”。
本策略:
使用锁机制,获得对数据的独家mutation权,其他线程被阻塞,不得访问。

锁的两种操作:
①acquire:允许线程获得锁的所有权
如果一个线程试图获取当前由另一个线程拥有的锁,它将阻塞,直到另一个线程释放锁。在这一点上,它将与任何其他尝试获取锁的线程竞争。 一次只能有一个线程拥有该锁。
②release:放弃锁的所有权,允许另一个线程获得它的所有权。
如果另一个线程(如线程2)持有锁l,线程1上的获取(l)将会阻塞。它等待的事件是线程2执行释放(l)。 此时,如果线程1可以获取l,则它继续运行其代码,并拥有锁的所有权。 另一个线程(如线程3)也可能在获取(l)时被阻塞。线程1或3将采取锁定并继续。另一个将继续阻塞,再次等待释放(l)。

具体操作:
(1)synchronized 对象或方法
synchronized对象
Lock保护共享数据

    synchronized(list){  //线程阻塞,直到锁释放
        //获得锁
        for(Obj obj : list){...}
        //释放锁
    }

上述,synchronized块内执行是原子执行的,当该线程执行完该块后,其余线程才执行该块。

监视者设计模式(Monitor)
在ADT内的每个方法中使用synchronized(this) ,ADT所有方法都是互斥访问。

synchronized方法
synchronized的方法,多个线程执行它时不允许interleave,也就是说”按原子的串行方式执行”。(一个方法只能被一个线程执行,除非该方法执行完后,其他线程才能执行该方法)

    public synchronized void method(){...}

对比synchronized对象,需要显式的给出lock,且不一定非要是thissynchronized方法,可提供更细粒度的并发控制。

Problem1
What is true while A is in a synchronized (list) { ... } block?
true– It owns the lock on list
false– It does not own the lock on list
false– No other thread can use observers of list
false– No other thread can use mutators of list
true– No other thread can acquire the lock on list
false– No other thread can acquire locks on elements in list
说明:
A只获得了list的锁,锁住了该部分,该部分始终只有一个线程执行;
但在其他地方,其他线程可以使用list的相关操作以及list中的元素;
但若是在另一个方法里面也有synchronized(list){...},则A获得list的锁后,这个方法的该部分也被锁住,其他线程无法执行。

Problem2
假设sharedListCollections.synchronizedList()返回的一个List对象,则下面操作需要synchronized(sharedList){...}的有?
– call isEmpty()(原子操作,不需要)
– call add()(原子操作,不需要)
– iterate over the list(需要,避免其他线程在该线程遍历时修改list)
– call isEmpty(), if it returns false,call remove(0)(需要,避免在isEmpty()remove(0)之间,其他线程执行remove(0)操作,会出现空指针异常)

Hint:
任何共享的mutable变量/对象必须被lock所保护;
涉及到多个mutable变量的时候,它们必须被同一个lock所保护。

(2)同步机制(synchronized)给性能带来极大影响
Synchronized不是灵丹妙药,你的程序需要严格遵守设计原则,先尝试其他办法,实在做不到再考虑lock。

(3)Liveness: deadlock(死锁), starvation(饥饿) and livelock(活锁)
死锁:多个线程竞争lock,相互等待对方释放lock;
饥饿:因为其他线程lock时间太长,一个线程长时间无法获取其所需的资源访问权(lock),导致无法往下进行;

(4)wait(), notify(), and notifyAll()
wait():释放锁,将当前线程加入等待序列等待
o.notify(): 唤醒等待序列中的线程o
notifyAll(): 唤醒等待序列中的所有线程

10.1.6、撰写线程安全策略

以注释的形式撰写线程安全策略
在代码中以注释的形式增加说明:该ADT采取了什么设计决策来保证线程安全。
采取了四种方法中的哪一种?
如果是后两种,还需考虑对数据的访问都是原子的,不存在interleaving?
举例:

/**
* Rep invariant:
*     ...
* Abstraction function:
*     ...
* Safe from represent exposure:
*     ...
* Thread safety argument:
*     这个类是线程安全的,因为它是不可变的:
*     - a 是 final修饰
*     - a 指向一个可变的数组,但这个数组只在这个类中出现,
*         没有和其他对象共享,也没有泄露给客户端
*     - text 所有对text的引用都在方法1内,而方法1使用synchronized锁住
*/
10.2、消息传递和图形用户接口 (GUI)

多线程之间基于消息传递的实现机制、如何保证threadsafe
BlockingQueue接口作为阻塞操作队列
put(e)
take()
这里写图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值