Java线程汇总

一、多线程介绍

多线程优势

  1. 资源利用率好java

  2. 程序设计简单算法

  3. 服务器响应更快数据库

多线程缺点

  1. 设计更复杂编程

  2. 上下文切换的开销缓存

  3. 增长资源消耗
    线程须要内存维护本地的堆栈,同时须要操做系统资源管理线程。安全

二、并发模型

并发系统能够有多种并发模型,不一样的并发模型在处理任务时,线程间的协做和交互的方式也不一样。服务器

并行工做者

委托者将任务分配到不一样的现场去执行,每一个工做者完成整个任务。工做者们并行运做在不一样的线程上,甚至可能在不一样的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模型对于来讲彷佛更加灵活。一个工做者无需知道谁在后面的流水线上处理做业。只需知道做业(或消息等)须要转发给哪一个通道。通道上的监听者能够随意订阅或者取消订阅,并不会影响向这个通道发送消息的工做者。这使得工做者之间具备松散的耦合。

三、实现多线程方式

多线程实现方法有两种:

  • 继承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. 线程阻塞等待获取某个对象锁的访问权限。

四、线程安全

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

竞态条件 & 临界区

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

下例中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类是线程安全的,但使用它的类不是。

五、同步(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这个对象上。那么这两个方法能够同时被线程访问。

六、线程通讯

线程通讯的目标是使线程间可以互相发送信号。另外一方面,线程通讯使线程可以等待其余线程的信号。

经过共享对象通讯

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

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会消耗太大。

七、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容许一个线程建立的全部子线程访问其父线程的值。


引用

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

相关文章
相关标签/搜索