1

1、多线程介绍

多线程优点

  1. 资源利用率好

  2. 程序设计简单

  3. 服务器响应更快

多线程缺点

  1. 设计更复杂

  2. 上下文切换的开销

  3. 增加资源消耗
    线程需要内存维护本地的堆栈,同时需要操作系统资源管理线程。

2、并发模型

并发系统可以有多种并发模型,不同的并发模型在处理任务时,线程间的协作和交互的方式也不同。

并行工作者

委托者将任务分配到不同的现场去执行,每个工作者完成整个任务。工作者们并行运作在不同的线程上,甚至可能在不同的CPU上。如图所示:
image_1bbo07gk5l631v4rb0n1vmu6389.png-13.8kB

优点:很容易理解和使用。
缺点:

  • 共享状态会很复杂
    image_1bbo0g2541apr12q2eaa1fbie0cm.png-29.7kB

共享的工作者经常需要访问一些共享数据,无论是内存中的或者共享的数据库中的。

在并行工作者模型中,线程需要以某种方式存取共享数据,以确保某个线程的修改能够对其他线程可见。线程需要避免竟态,死锁以及很多其他共享状态的并发性问题。

  • 无状态的工作者
    共享状态能够被系统中得其他线程修改。所以工作者在每次需要的时候必须重读状态,以确保每次都能访问到最新的副本,不管共享状态是保存在内存中的还是在外部数据库中。工作者无法在内部保存这个状态(但是每次需要的时候可以重读)称为无状态的。

每次都重读需要的数据,将会导致速度变慢,特别是状态保存在外部数据库中的时候。

  • 任务顺序是不确定的
    作业执行顺序是不确定的。无法保证哪个作业最先或者最后被执行。

流水线模式

类似于工厂中生产线上的工人们那样组织工作者。每个工作者只负责作业中的部分工作。当完成了自己的这部分工作时工作者会将作业转发给下一个工作者。每个工作者在自己的线程中运行,并且不会和其他工作者共享状态。有时也被成为无共享并行模型
image_1bbo0tfnh1dva6pk12jb8kjkcg13.png-6.7kB

通常使用非阻塞的IO来设计使用流水线并发模型的系统。非阻塞IO就是,一旦某个工作者开始一个IO操作的时候(比如读取文件或从网络连接中读取数据),这个工作者不会一直等待IO操作的结束。IO操作速度很慢,所以等待IO操作结束很浪费CPU时间。此时CPU可以做一些其他事情。当IO操作完成的时候,IO操作的结果(比如读出的数据或者数据写完的状态)被传递给下一个工作者。

image_1bbo143c1101e96r1jrrfuc1e0j1g.png-8.2kB

在实际过程中,可能会是这样:
image_1bbo18uoof41gqh1l6d1kv82111t.png-14.2kB

也可能是这样:
image_1bbo19kef1cuni78l031e9e6gi2a.png-14.9kB

当然还会有更复杂的设计,……

缺点: 代码编写复杂,追踪某个作业到底被什么代码执行难度较大。
优点:

  • 无需共享的状态

工作者之间无需共享状态,无需考虑所有因并发访问共享对象而产生的并发性问题,基本上是一个单线程的实现。

  • 有状态的工作者

当工作者知道了没有其他线程可以修改它们的数据,工作者可以变成有状态的。对于有状态,是指,可以在内存中保存它们需要操作的数据,只需在最后将更改写回到外部存储系统。因此,有状态的工作者通常比无状态的工作者具有更高的性能。

  • 较好的硬件整合(Hardware Conformity)

当能确定代码只在单线程模式下执行的时候,通常能够创建更优化的数据结构和算法。单线程有状态的工作者能够在内存中缓存数据,访问缓存的数据变得更快。

  • 合理的作业顺序

基于流水线并发模型实现的并发系统,在某种程度上是有可能保证作业的顺序的。作业的有序性使得它更容易地推出系统在某个特定时间点的状态。更进一步,你可以将所有到达的作业写入到日志中去。一旦这个系统的某一部分挂掉了,该日志就可以用来重头开始重建系统当时的状态。按照特定的顺序将作业写入日志,并按这个顺序作为有保障的作业顺序。

Actors

在Actor模型中每个工作者被称为actor。Actor之间可以直接异步地发送和处理消息。Actor可以被用来实现一个或多个像前文描述的那样的作业处理流水线。下图给出了Actor模型:
image_1bbo1f6vs1m7a1v3a5o0pufdq42n.png-9.8kB

Channels

工作者之间不直接进行通信。相反,它们在不同的通道中发布自己的消息(事件)。其他工作者们可以在这些通道上监听消息,发送者无需知道谁在监听。下图给出了Channel模型:

image_1bbo1h1te7s11ca61qps1f8l2e834.png-14.1kB

channel模型对于来说似乎更加灵活。一个工作者无需知道谁在后面的流水线上处理作业。只需知道作业(或消息等)需要转发给哪个通道。通道上的监听者可以随意订阅或者取消订阅,并不会影响向这个通道发送消息的工作者。这使得工作者之间具有松散的耦合。

3、实现多线程方式

多线程实现方法有两种:

  • 继承Thread类

public class MyThread extends Thread {
   public void run(){
     System.out.println("MyThread running");
   }
}

//调用
MyThread myThread = new MyThread();
myTread.start();
  • 实现Runnble接口

public class MyRunnable implements Runnable {
   public void run(){
    System.out.println("MyRunnable running");
   }
}

//调用
Thread thread = new Thread(new MyRunnable());
thread.start();

实现Runnble接口比Thread类的优势:

  • 可以避免Java单继承带来的局限

  • 增强程序健壮性,能够被多个线程共享,代码和数据是独立的

  • 适合多个相同程序代码的线程区处理同一资源

Thread中,start和run的区别:run是在当前线程运行,start是开辟新的线程运行!所以一般情况下使用的是start!
执行完run()方法后,或在run()方法中return,线程便自然消亡。

线程中断

当一个线程运行时,另一个线程可以调用对应的 Thread 对象的 interrupt()方法来中断它,该方法只是在目标线程中设置一个标志,表示它已经被中断,并立即返回。这里需要注意的是,如果只是单纯的调用 interrupt()方法,线程并没有实际被中断,会继续往下执行。

sleep()方法的实现检查到休眠线程被中断,它会相当友好地终止线程,并抛出 InterruptedException 异常。

public class SleepInterrupt extends Object implements Runnable{  
    public void run(){  
        try{  
            System.out.println("in run() - about to sleep for 20 seconds");  
            Thread.sleep(20000);  
            System.out.println("in run() - woke up");  
        }catch(InterruptedException e){  
            System.out.println("in run() - interrupted while sleeping");  
            //处理完中断异常后,返回到run()方法人口,  
            //如果没有return,线程不会实际被中断,它会继续打印下面的信息  
            return;    
        }  
        System.out.println("in run() - leaving normally");  
    }  

    public static void main(String[] args) {  
        SleepInterrupt si = new SleepInterrupt();  
        Thread t = new Thread(si);  
        t.start();  
        //主线程休眠2秒,从而确保刚才启动的线程有机会执行一段时间  
        try {  
            Thread.sleep(2000);   
        }catch(InterruptedException e){  
            e.printStackTrace();  
        }  
        System.out.println("in main() - interrupting other thread");  
        //中断线程t  
        t.interrupt();  
        System.out.println("in main() - leaving");  
    }  
} 

如果将 catch 块中的 return 语句注释掉,则线程在抛出异常后,会继续往下执行,而不会被中断,从而会打印出leaving normally信息。

待决中断

另外一种情况,如果线程在调用 sleep()方法前被中断,那么该中断称为待决中断,它会在刚调用 sleep()方法时,立即抛出 InterruptedException 异常。

public class PendingInterrupt extends Object {  
    public static void main(String[] args){  
        //如果输入了参数,则在mian线程中中断当前线程(亦即main线程)  
        if( args.length > 0 ){  
            Thread.currentThread().interrupt();  
        }   
        //获取当前时间  
        long startTime = System.currentTimeMillis();  
        try{  
            Thread.sleep(2000);  
            System.out.println("was NOT interrupted");  
        }catch(InterruptedException x){  
            System.out.println("was interrupted");  
        }  
        //计算中间代码执行的时间  
        System.out.println("elapsedTime=" + ( System.currentTimeMillis() - startTime));  
    }  
}

这种模式下,main 线程中断它自身。除了将中断标志(它是 Thread 的内部标志)设置为 true 外,没有其他任何影响。线程被中断了,但 main 线程仍然运行,main 线程继续监视实时时钟,并进入 try 块,一旦调用 sleep()方法,它就会注意到待决中断的存在,并抛出 InterruptException。

中断状态判断

  • isInterrupted()方法判断是否中断

  • Thread.interrupted()方法判断中断状态

join & yield

join 方法用线程对象调用,如果在一个线程 A 中调用另一个线程 B 的 join 方法,线程 A 将会等待线程 B 执行完毕后再执行。

yield 可以直接用 Thread 类调用,yield 让出 CPU 执行权给同等级的线程,如果没有相同级别的线程在等待 CPU 的执行权,则该线程继续执行。

守护线程

Java有两类线程:UserThread(用户线程)、Daemon Thread(守护线程)。
用户线程在前台,守护线程在后台运行,为其他前台线程提供服务。当所有前台线程都退出时,守护线程就会退出。如果有前台线程仍然存活,守护线程就不会退出。
守护线程并非只有虚拟机内部提供,用户可以使用Thread.setDaemon(true)方法设置为当前线程为守护线程。

  • setDaemon(true)必须在调用的线程的start()方法之前设置,否则会抛出异常。

  • 在守护线程中产生的新线程也是守护线程

线程阻塞

线程在以下四种状态下会产生阻塞:

  1. 执行Thread.sleep()

  2. 当线程遇见wait()语句,它会一直阻塞到接到通知notify()

  3. 线程阻塞与不同的I/O的方式有多种。例:InputStreamread方法,一直阻塞到从流中读取一个字节的数据为知。

  4. 线程阻塞等待获取某个对象锁的访问权限。

4、线程安全

定义:当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么这个类就是线程安全的!

竞态条件 & 临界区

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。
导致竞态条件发生的代码区称作:临界区。

下例中add()方法就是一个临界区,它会产生竞态条件。在临界区中使用适当的同步就可以避免竞态条件。

public class Counter {
    protected long count = 0;
    public void add(long value){
        this.count = this.count + value;   
    }
}

数据安全

线程逃逸规则:如果一个资源的创建,使用,销毁都在同一个线程内完成,且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。

属性 描述 是否线程安全
局部变量 在栈中,不会被线程共享 线程安全
局部对象 引用所指的对象都存在共享堆中,对象不会被其它方法获得,也不会被非局部变量引用到 线程安全
对象成员 多个线程执行读操作,或者每个线程的对象都相互独立 线程安全
局部对象 对象会被其它方法获得,或者被全局变量引用到 线程非安全
对象成员 存储在堆上。若多个线程同时更新同一个对象的同一个成员 线程非安全

线程安全

当多个线程同时访问同一个资源,并且其中的一个或者多个线程对这个资源进行了写操作,才会产生竞态条件。多个线程同时读同一个资源不会产生竞态条件。

我们可以通过创建不可变的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全,如下所示:

public class ImmutableValue{
    private int value = 0;

    public ImmutableValue(int value){
        this.value = value;
    }

    public int getValue(){
        return this.value;
    }
}

如果非要对ImmutableValue进行操作的话,可以创建新的实例进行隔离:

public class ImmutableValue{
    private int value = 0;

    public ImmutableValue(int value){
        this.value = value;
    }

    public int getValue(){
        return this.value;
    }

    //创建一个新的实例
    public ImmutableValue add(int valueToAdd){
        return new ImmutableValue(this.value + valueToAdd);
    }
}

ImmutableValue可以看做是线程安全的,但是如果别的类引用了ImmutableValue,就不能保证线程安全了。如下所示:

public void Calculator{
    private ImmutableValue currentValue = null;

    public ImmutableValue getValue(){
        return currentValue;
    }

    public void setValue(ImmutableValue newValue){
        this.currentValue = newValue;
    }

    public void add(int newValue){
        this.currentValue = this.currentValue.add(newValue);
    }
}

即使Calculator类内部使用了一个不可变对象,但Calculator类本身还是可变的,因此Calculator类不是线程安全的。换句话说:ImmutableValue类是线程安全的,但使用它的类不是。

5、同步(synchronized)

当多个线程访问某个状态变量,并且有线程执行写入操作时,必须采用同步机制来协同这些线程对变量的访问。

Java的主要同步机制有:

  1. synchronized关键字

  2. volatile类型变量

  3. 显示锁

  4. 原子变量

无论是同步方法,还是同步块都是只针对同一个对象的多线程而言的,只有同一个对象产生的多线程,才会考虑到同步方法或者是同步块。

实例方法

Java实例方法同步是同步在对象上。这样,每个方法同步都同步在方法所属的实例。只有一个线程能够在实例方法同步块中运行。如果有多个实例存在,那么一个线程一次可以在一个实例同步块中执行操作。一个实例一个线程。

 public synchronized void add(int value){
    this.count += value;
 }

静态方法同步

静态方法的同步是指同步在该方法所在的类对象上。因为在Java虚拟机中一个类只能对应一个类对象,所以同时只允许一个线程执行同一个类中的静态同步方法。

对于不同类中的静态同步方法,一个线程可以执行每个类中的静态同步方法而无需等待。不管类中的那个静态同步方法是否被调用,一个类只能由一个线程同时执行。

public static synchronized void add(int value){
    count += value;
}

实例方法中的同步块

有时你不需要同步整个方法,而是同步方法中的一部分。

public void add(int value){
    synchronized(this){
       this.count += value;
    }
}

示例使用Java同步块构造器来标记一块代码是同步的。该代码在执行时和同步方法一样。在上例中,使用了“this”,即为调用add方法的实例本身。在同步构造器中用括号括起来的对象叫做监视器对象。

静态方法中的同步块

和上面类似,下面是两个静态方法同步的例子。这些方法同步在该方法所属的类对象上。

public class MyClass {
    public static synchronized void log1(String msg1, String msg2){
       log.writeln(msg1);
       log.writeln(msg2);
    }

    public static void log2(String msg1, String msg2){
       synchronized(MyClass.class){
          log.writeln(msg1);
          log.writeln(msg2);
       }
    }
  }

这两个方法不允许同时被线程访问。
如果第二个同步块不是同步在MyClass.class这个对象上。那么这两个方法可以同时被线程访问。

6、线程通信

线程通信的目标是使线程间能够互相发送信号。另一方面,线程通信使线程能够等待其他线程的信号。

通过共享对象通信

线程间发送信号的一个简单方式是在共享对象的变量里设置信号值。

public class MySignal{
  protected boolean hasDataToProcess = false;

  public synchronized boolean hasDataToProcess(){
    return this.hasDataToProcess;
  }

  public synchronized void setHasDataToProcess(boolean hasData){
    this.hasDataToProcess = hasData;
  }
}

线程A在一个同步块里设置boolean型成员变量hasDataToProcess为true,线程B也在同步块里读取hasDataToProcess这个成员变量。
线程A和B必须获得指向一个MySignal共享实例的引用,以便进行通信。如果它们持有的引用指向不同的MySingal实例,那么彼此将不能检测到对方的信号。

忙等待(Busy Wait)

线程B运行在一个循环里,等待线程A的一个可执行的信号。

protected MySignal sharedSignal = ...

...
while(!sharedSignal.hasDataToProcess()){
   //do nothing... busy waiting
}

wait(),notify()和notifyAll()

除非忙等待的时间特别短,否则会浪费CPU资源。合理的做法:让等待线程进入睡眠或者非运行状态,直到它接收到它等待的信号。

java.lang.Object 类定义了三个方法,wait()、notify()和notifyAll()来实现这个等待机制。

一个线程一旦调用了任意对象的wait()方法,就会变为非运行状态,直到另一个线程调用了同一个对象的notify()方法。

为了调用wait()或者notify(),线程必须先获得那个对象的锁。也就是说,线程必须在同步块里调用wait()或者notify()。

在wait()/notify()机制中,不要使用全局对象,字符串常量等。应该使用对应唯一的对象

public class MonitorObject{
}

public class MyWaitNotify{

  MonitorObject myMonitorObject = new MonitorObject();

  public void doWait(){
    synchronized(myMonitorObject){
      try{
        myMonitorObject.wait();
      } catch(InterruptedException e){...}
    }
  }

  public void doNotify(){
    synchronized(myMonitorObject){
      myMonitorObject.notify();
    }
  }
}

不管是等待线程还是唤醒线程都在同步块里调用wait()和notify()。这是强制性的!一个线程如果没有持有对象锁,将不能调用wait(),notify()或者notifyAll()。否则,会抛出IllegalMonitorStateException异常。

一旦线程调用了wait()方法,它就释放了所持有的监视器对象上的锁。这将允许其他线程也可以调用wait()或者notify()。

被唤醒的线程必须重新获得监视器对象的锁,才可以退出wait()的方法调用,因为wait方法调用运行在同步块里面。如果多个线程被notifyAll()唤醒,那么在同一时刻将只有一个线程可以退出wait()方法,因为每个线程在退出wait()前必须获得监视器对象的锁。

丢失信号

notify()和notifyAll()方法不会保存调用它们的方法,如果方法被调用时,没有线程处于等待状态。通知信号过后便丢弃了。因此,如果一个线程先于被通知线程调用wait()前调用了notify(),等待的线程将错过这个信号。在某些情况下,这可能使线程错过了唤醒信号,永远在等待不再醒来。

为了避免丢失信号,必须把它们保存在信号类里。在MyWaitNotify的例子中,通知信号应被存储在MyWaitNotify实例的一个成员变量里。

public class MyWaitNotify2{

  MonitorObject myMonitorObject = new MonitorObject();
  boolean wasSignalled = false;

  public void doWait(){
    synchronized(myMonitorObject){
      if(!wasSignalled){
        try{
          myMonitorObject.wait();
         } catch(InterruptedException e){...}
      }
      //clear signal and continue running.
      wasSignalled = false;
    }
  }

  public void doNotify(){
    synchronized(myMonitorObject){
      wasSignalled = true;
      myMonitorObject.notify();
    }
  }
}

在上述例子中,doNotify()方法在调用notify()前把wasSignalled变量设为true。同时,留意doWait()方法在调用wait()前会检查wasSignalled变量。

为了避免信号丢失,用一个变量来保存是否被通知过。在notify前,设置自己已经被通知过。在wait后,设置自己没有被通知过,需要等待通知。。

假唤醒

线程有可能在没有调用过notify()和notifyAll()的情况下醒来。这就是所谓的假唤醒(spurious wakeups)。等待线程即使没有收到正确的信号,也能够执行后续的操作。

为了防止假唤醒,保存信号的成员变量将在一个while循环里接受检查,而不是在if表达式里。这样的一个while循环叫做自旋锁。

public class MyWaitNotify3{

  MonitorObject myMonitorObject = new MonitorObject();
  boolean wasSignalled = false;

  public void doWait(){
    synchronized(myMonitorObject){
      while(!wasSignalled){
        try{
          myMonitorObject.wait();
         } catch(InterruptedException e){...}
      }
      //clear signal and continue running.
      wasSignalled = false;
    }
  }

  public void doNotify(){
    synchronized(myMonitorObject){
      wasSignalled = true;
      myMonitorObject.notify();
    }
  }
}

如果等待线程没有收到信号就唤醒,wasSignalled变量将变为false,while循环会再执行一次,促使醒来的线程回到等待状态。

目前的JVM实现自旋会消耗CPU,如果长时间不调用doNotify方法,doWait方法会一直自旋,CPU会消耗太大。

7、TheadLocal

ThreadLocal类创建的变量只被同一个线程进行读和写操作。因此,尽管有两个线程同时执行一段相同的代码,而且这段代码又有一个指向同一个ThreadLocal变量的引用,但是这两个线程依然不能看到彼此的ThreadLocal变量域。

//创建一个ThreadLocal变量:每个线程仅需要实例化一次即可。
//每个线程只能看到私有的ThreadLocal实例,不同的线程在给ThreadLocal对象设置不同的值,也不能看到彼此的修改。
private ThreadLocal myThreadLocal = new ThreadLocal();

//设置、获取数据
myThreadLocal.set("A thread local value");
String threadLocalValue = (String) myThreadLocal.get();

//创建泛型对象
private ThreadLocal myThreadLocal1 = new ThreadLocal<String>();

myThreadLocal1.set("Hello ThreadLocal");
String threadLocalValues = myThreadLocal.get();

InheritableThreadLocal类是ThreadLocal的子类。为了解决ThreadLocal实例内部每个线程都只能看到自己的私有值,所以InheritableThreadLocal允许一个线程创建的所有子线程访问其父线程的值。


引用

1、并发编程网-Java并发性和多线程
2、兰亭风雨专栏


流云
323 声望15 粉丝

下一篇 »
Java并发