07月11, 2020

软件构造复习笔记 - 并发

并发程序的两种模式

  • 共享内存:并发的模块在内存中读写共享数据。
  • 消息传递:并发的模块通过信道连接在一起,并通过信道交换信息。

线程的定义、创建方法

通过继承Thread类定义

Thread类本身的定义为

public class Thread extends Object implements Runnable

可通过继承Thread类来定义线程,并实现其中的run()方法。

例如下面的HelloThread类:

public class HelloThread extends Thread {
    public void run() {
        System.out.println("Hello from a thread!");
    }
    public static void main(String args[]) {
        HelloThread p = new HelloThread();
        p.start();
        //(new HelloThread()).start();//另一种创建线程的方法
    }
}

从Runnable接口构造Thread对象

若使用继承Thread类的方法定义线程,则就无法继承其他的类了,所以实际应用中一般使用实现Runnable接口的方式构造类,然后再将实例传入Thread类的构造器中,构造出一个线程实例。

例如下面的HelloRunnable类:

public class HelloRunnable implements Runnable {
    public void run() {
        System.out.println("Hello from a thread!");
    }
    public static void main(String args[]) {
        (new Thread(new HelloRunnable())).start();
    }
}

注:创建线程的唯一方法是调用Thread实例的start()方法

即使调用了run()方法,虽然程序也会运行,但无法创建线程,只能通过调用start()方法来创建线程。

交错和竞争

  • 原子操作不会导致竞争
  • 单行、单条语句未必是原子的
  • 是否是原子操作由JVM确定

例如课上的例子balance = balance + 1,这条语句在执行时被分解为三步:

  1. 读取balance的值
  2. 运算balance + 1
  3. 将新值赋值给balance

调用方法影响线程之间的interleave关系

sleep()方法

可以使线程进行休眠,不会失去锁的所有权。

interrupt()方法

向线程发送中断信号,但中断与否由线程自己处理。

若线程为t,则相关的方法有:

  • t.interrupt():在其他线程中向t发出中断信号。
  • t.isInterrupted():返回t线程是否被其他线程中断。
  • t.interrupted():返回t线程是否被其他线程中断,并若被中断,则取消它的中断状态。

sleep()方法和interrupt()方法之间的冲突

当线程sleep()时收到中断信号,则会立即结束休眠,并抛出InterruptedException异常;当线程被中断时执行到sleep()语句,也会抛出InterruptedException异常。

join()方法

join()方法用于说明两个线程的先后关系。例如A线程调用B.join(),则是让B线程临时插队,完成B线程任务后再转移回A线程。例如课上的例子中,首先启动了th1线程,然后在主线程中对th1线程调用了join方法,意味着主线程不再继续往下执行,而是要等到th1线程结束再继续执行。后面的th2,th3同理。这是上课时老师没有讲清楚的。

public class JoinExample {
    public static void main(String[] args) {
        Thread th1 = new Thread(new Runnable(){public void run(){System.out.println("th1");}}, "th1");
        Thread th2 = new Thread(new Runnable(){public void run(){System.out.println("th2");}}, "th2");
        Thread th3 = new Thread(new Runnable(){public void run(){System.out.println("th3");}}, "th3");

        th1.start();
        try{
            th1.join();
        }catch(InterruptedException e){}        

        th2.start();
        try{
            th2.join();
        }catch(InterruptedException e){}        

        th3.start();
        try{
            th3.join();
        }catch(InterruptedException e){}
    }
}

线程安全的方法

限制数据共享

线程之间不共享数据,避免使用全局变量(包括静态变量)。

共享不可变数据

  • 使用不可变数据类型和不可变引用,避免多个线程之间的竞争。
  • 不可变数据类型通常是线程安全的。

共享线程安全的可变数据

JDK通常同时提供两个相同功能的类,一个是线程安全的,另一个不是。

  • StringBuffer线程安全,StringBuilder不安全。
  • 集合类都是线程不安全的。
  • 可通过Collections.synchronizedMap/List/Set()方法将集合类变为线程安全的(注:要把参数引用销毁)。
  • 在线程安全的集合类上使用iterator仍是不安全的。
  • 多个操作放在一次仍不安全。
  • 能修改的变量都是线程不安全的。

运用线程锁

锁与同步

锁可理解为将多个操作变为一个原子操作。例如:

synchronized (lock) {
    balance = balance + 1;
}
  • 要保证操作互斥,必须将所有能接触到数据的方法用同一个锁锁住。

以下两种锁的方式等价:

public void test(){
    synchronized (this){
        a = a + 1;
    }
}
public synchronized void test(){
    a = a + 1;
}
  • 静态方法上锁意味着在类的层面上上锁,其他线程无法访问该类的任何实例。

死锁:多个线程竞争锁,相互等待对方释放。

出现原因: 多个线程使用了多个锁且访问锁的顺序不同。

例如:

T1: 
synchronized(a){
    synchronized(b){
        …
    }
}

T2:
synchronized(b){
    synchronized(a){
        …
    }
}

例如上课时讲的:

import java.util.HashSet;
import java.util.Set;

public class Person {

    private final Set<Person> friends = new HashSet<>();
    public static void main(String[] args) {
        Person a = new Person();
        Person b = new Person();
        Thread th1 = new Thread(new Runnable(){public void run(){a.friend(b);a.defriend(b);}}, "th1");
        Thread th2 = new Thread(new Runnable(){public void run(){b.friend(a);b.defriend(a);}}, "th2");
        th1.start();
        th2.start();
    }

    public synchronized void friend(Person that){
        if(friends.add(that)) that.friend(this);
    }


    public synchronized void defriend(Person that){
        if(friends.remove(that)) that.defriend(this);
    }

}

实际运行结果: W20P01.png

解决方法:改变锁的顺序。

效率问题:排序 需提前知道要使用哪些锁

解决方案:多个锁变为一个锁,细粒变为粗粒。

例如使用多个锁的共性特征(如在同一个集合中)作为新的锁。

wait(), notify(), notifyAll()

在某些条件需要满足时,为了避免重复的while()等待,可暂时挂起该线程,待条件满足后再唤醒。

  • wait()操作只能在synchronized中调用,并且调用的实例需与锁相同。
  • o.wait()操作代表将该线程阻塞,并将该线程添加到o对象的等待队列中。
  • o.notify()操作将o对象等待队列中的随机一个线程唤醒。
  • o.notifyAll()操作将o对象等待队列中的所有线程唤醒。

本文链接:http://blog.zireaels.com/post/Java-Concurrency.html

-- EOF --

Comments