Java并发性和多线程介绍

 

java并发性和多线程介绍:

  • 单个程序内运行多个线程,多任务并发运行

多线程优点:

  • 高效运行,多组件并行。读->操作->写;
  • 程序设计的简单性,遇到多问题,多开线程就好;
  • 快速响应,异步式设计;

多线程代价:

  • 实现负载,虽然开线程没什么难度,但是修改共享模块的时候,会有争用的可能性;
  • 上下文切换所带来的开销。CPU进行上下文切换代价蛮大,运行线程更换需要上下文切换,为了效率,尽量避免;
  • 资源消耗,虽然线程不占用资源,但是管理需要资源,自身实例化也需要相应的堆栈内存;

创建线程:

  • 创建线程子类:
    1、继承java Thread类

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

    调用的话直接:

      MyThread myThread = new MyThread();
      myThread.start();

    也可以直接创建匿名子类:

      Thread thread = new Thread(){
          public void run(){
              System.out.println("Thread running");
          }
      };
      thread.start();

    2、实现Runnable接口

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

    调用:

      Thread thread = new Thread(new MyRunnable);
      thread.start();

    实现Runnable的匿名接口:

      Runnable myRunnable = new Runnable(){
          public void run(){
              System.out.println("Runnable running");
          }
      };
      Thread thread = new Thread(myRunnable);
      thread.start();

    这些东西在Thread的api文档中都可以找到,更详细的可以去查API。

  • 实现方式的优劣
    就两种都存在且存在到现在来看,应该是属于各有优势的。就JVM优化方式来看,实现Runnable接口的线程更加出色一点(线程池的管理机制)。当然具体的需要考虑实现语境。

竞态条件和临界区

  • 竞态条件:当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件
  • 临界区:导致竞态条件发生的代码区称作临界区
  • 通常在临界区中使用同步阻塞就可以避免发生竞态条件

线程安全与共享资源

  • 线程安全:允许被多个线程同时执行的代码称作线程安全的代码。线程安全的代码不包含竞态条件。

  • 局部变量:一般意义上的局部变量是线程安全的,即使有对象引用。只要没有方法将当前线程操作对象传输给其它线程,代码都可认为是线程安全的。

    NotThreadSafe sharedInstance = new NotThreadSafe();
    new Thread(new MyThred(sharedInstance)).start();
    new Thread(new MyThred(sharedInstance)).start();
    
    class MyThread implements Runnable{
      NotThreadSafe instance = null;
      public MyThread(NotThreadSafe instance){
          this.instance = instance;
      }
      public void run(){
          //对instance进行修改操作;
      }
    }

    这里的sharedInstance相对于两个线程实例来说就非局部变量,另外线程的运行也进行了修改操作。所以这里会造成竞态条件。有个修改方案就是:

    new Thread(new MyThread(new NotThreadSafe())).start();
    new Thread(new MyThread(new NotThreadSafe())).start();

    这里给每个线程实例化了一个NotThreadSafe类(现在其实算是ThreadSafe类了),线程运行的操作也是针对自有的instance进行操作,自然就不会有竞态条件问题了。

  • 线程控制逃逸原则

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

    资源可以是对象,数组,文件,数据库连接,套接字等等。

线程安全及不可变性

  • 可以通过创建不可变性的共享对象来保证对象在线程间共享时不会被修改,从而实现线程安全;

    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成员变量value赋值没有set方法,这意味着一旦类实例化后,value不可变。而add方法是以加法操作的结果作为一个新的ImmutableValue类实例返回,而不是对他自己的value变量进行操作。

  • 引用不是线程安全的!即使一个类是线程安全的,但是用这个类的类则不一定:

    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使用了一个不可变的量ImmutableValue,但是它自身存在set方法,本身可变所以不是线程安全的。 当然修改方案则是将setValue()&add()方法设置为同步方法就好。

Java同步块

  • Java同步块关键字:synchronized

  • 实例方法

    public synchronized void add(int value){
      this.count += value;
    }
  • 静态方法
    public static synchronized void add(int value){
      count += value;
    }
  • 实例方法中的同步块

    public class MyClass {
    
     public synchronized void log1(String msg1, String msg2){
        log.writeln(msg1);
        log.writeln(msg2);
     }
    
     public void log2(String msg1, String msg2){
        synchronized(this){
           log.writeln(msg1);
           log.writeln(msg2);
        }
     }
    }
  • 静态方法中的同步块

    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);
         }
      }
    }

    同步块部分代码执行时,多线程调动会造成阻塞。以此避免竞态条件。

线程通信

  • 共享对象通信
  • 忙等待
  • wait(), notify() & notifyAll()
  • 丢失的信号
  • 假唤醒
  • 多线程等待相同信号
  • 常量字符串或全局对象不要调用wait()
    前两个是用户定义的一种锁机制,增加一个布尔型锁变量,有线程使用就加锁,用完解锁。
    第三个是系统自带的锁机制,然后接下来的几项都是对系统自带的改良。
    此处不详加叙述,多次练习后自有体会。

死锁

  • 死锁概念:两个线程针对两个资源,线程A锁住资源A,申请B;线程B锁住资源B,申请A。这就会产生死锁,和操作系统死锁产生类似。都是资源争用。

  • 多线程死锁:多个线程争用的后果

  • 数据库死锁:

    Transaction 1, request 1, locks record 1 for update
    Transaction 2, request 1, locks record 2 for update
    Transaction 1, request 2, tries to lock record 2 for update.
    Transaction 2, request 2, tries to lock record 1 for update.

避免死锁

  • 加锁顺序:对资源排序,按照规定顺序进行加锁。可以很有效的避免死锁,但是需要预知所有可能会用到的锁,但总有时候是无法预知的。(这句话没理解)

  • 加锁时限:对资源加锁定时间,然后超时自动释放资源。synchronized关键字没有设置超时时间。可以使用java5以后 concurrency包中的工具,自己写一个计时器也不是什么难事。

  • 死锁检测:通过map或者group的数据结构将其记下。

死锁检测到后,有两个方案,一个是释放所有锁,回退,随机等待然后重试(类似于CSMA/CD协议);另一个是给线程配置优先级,如果优先级低的先释放资源。直到死锁解除。

饥饿和公平

  • 饥饿:一个线程因为CPU时间全部被其他线程抢走而得不到CPU运行时间,这种状态被称之为“饥饿”

  • 解决饥饿的方案被称之为“公平性” – 即所有线程均能公平地获得运行机会

Java饥饿的原因

  • 高优先级线程吞噬所有的低优先级的CPU时间
    这个其实不难理解,优先级高的线程总在运作中,长时占用。机器如果没有自身调节机制,低优先级线程只能饥饿。

  • 线程被永久堵塞在一个等待进入同步块的状态
    同上,优先级高的长期霸占。

  • 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的wait方法)
    多个线程都处于wait()方法执行,对其调用notify()也是随机的,可能会有倒霉蛋线程永远唤不醒。

Java公平性方案

  • 使用锁替代同步块
    之前有提到用synchronized关键字封装的程序块,是运行线程独占的。下面是一个Lock类的实现:

    public class Lock{
      private boolean isLocked     = false;
      private Thread lockingThread = null;
    
      public synchronized void lock() throws InterruptedException{
          while(isLocked){
              wait();
          }
          isLocked = true;
          lockingThread = Thread.currentThread();
      }
    
      public synchronized void unlock(){
          if(this.lockingThread != Thread.currentThread()){
              throw new IllegalMonitorStateException(
                "Calling thread has not locked this lock");
          }
          isLocked = false;
          lockingThread = null;
          notify();
      }
    }

    在看一个应用代码实现:

    public class Synchronizer{
      Lock lock = new Lock();
      public void doSynchronized() throws InterruptedException{
          this.lock.lock();
          //critical section, do a lot of work which takes a long time
          this.lock.unlock();
      }
    }

    这里的Lock类实现加锁是通过wait()方法,这样带来的好处,就是减少了synchronized的轮询开销。(synchronized锁住时候,等待线程会定期查看对象是否解锁。而wait/notify是异步机制,wait之后,就等notify信号。没有轮询)
    但是就公平性而言,这个跟wait/notify与synchronized差不多。

  • 公平锁
    这个设计其实就是一个队列的实现,每个申请锁的线程都会扔进一个等待队列里面。当对象解锁以后,队列的第一个线程被允许获取对象锁。其他的继续在等待队列。下面给出代码实现:

    public class FairLock {
      private boolean           isLocked       = false;
      private Thread            lockingThread  = null;
      private List<QueueObject> waitingThreads =
              new ArrayList<QueueObject>();
    
      public void lock() throws InterruptedException{
          QueueObject queueObject           = new QueueObject();
          boolean     isLockedForThisThread = true;
    
          synchronized(this){
              waitingThreads.add(queueObject);
          }
          while(isLockedForThisThread){
            synchronized(this){
              isLockedForThisThread =
                  isLocked || waitingThreads.get(0) != queueObject;
              if(!isLockedForThisThread){
                isLocked = true;
                 waitingThreads.remove(queueObject);
                 lockingThread = Thread.currentThread();
                 return;
               }
            }
            try{
              queueObject.doWait();
            }catch(InterruptedException e){
              synchronized(this) { waitingThreads.remove(queueObject); }
              throw e;
            }
          }
      }
    
      public synchronized void unlock(){
          if(this.lockingThread != Thread.currentThread()){
            throw new IllegalMonitorStateException(
              "Calling thread has not locked this lock");
          }
          isLocked      = false;
          lockingThread = null;
          if(waitingThreads.size() > 0){
            waitingThreads.get(0).doNotify();
          }
    }
    }
    public class QueueObject {
      private boolean isNotified = false;
    
      public synchronized void doWait() throws InterruptedException {
          while(!isNotified){
              this.wait();
          }
          this.isNotified = false;
      }
    
      public synchronized void doNotify() {
          this.isNotified = true;
          this.notify();
      }
    
      public boolean equals(Object o) {
          return this == o;
      }
    }

    代码详解此处略去,参考备注

  • 性能
    从代码角度来看,FairLock要比Lock复杂很多。执行起来自然相对较慢。如何取舍,根据需求来好了。

嵌套管程锁死

首先说一下管程:管程 (英语:Monitors,也称为监视器)是对多个工作线程实现互斥访问共享资源的对象或模块。这些共享资源一般是硬件设备或一群变量。管程实现了在一个时间点,最多只有一个线程在执行它的某个子程序。与那些通过修改数据结构实现互斥访问的并发程序设计相比,管程很大程度上简化了程序设计。
然后说嵌套管程锁死: 这货是个和死锁类似的概念,都是多个线程阻塞相互等待。不同的是死锁,都是等待其他线程释放锁。而嵌套管程锁死是线程A持有锁a,然后等待线程B发信号,而线程B需要锁a才能给线程A发信号。这就是差不多的定义。
参考代码

Slipped Conditions

Slipped Conditions就是从一个线程检查某一个特定条件期间,这个条件已经被其他线程改变,导致错误操作发生。给段代码:

 public class Lock {
    private boolean isLocked = true;

    public void lock(){
      synchronized(this){
        while(isLocked){
          try{
            this.wait();
          } catch(InterruptedException e){
            //do nothing, keep waiting
          }
        }
      }
      synchronized(this){
        isLocked = true;
      }
    }
    public synchronized void unlock(){
      isLocked = false;
      this.notify();
    }
 }

我们假设有线程A,线程B几乎同时进入lock(),线程A稍微快点儿,但也就一点点。当线程A刚检测完isLocked为flase的时候。线程B进入检测,这时候线程A已经到了isLocked = true;改之前的一瞬间,B完成检测。啊哦,线程A,线程B都认为自己拿到了锁。悲惨的故事就这么发生了。你又能怎么办呢?
有一个参考方案是把整个方法加synchronized关键字。就是检测和加锁放到一个synchronized中。

更详细的例子笔记这儿不写了,插一句:
代码质量的高下有两个东西决定:一个是悟性,就是天资;还有一个就是训练。长时间的训练或者不错的天资都能避免笨蛋代码,优秀的代码也许就是需要一点悟性了。所以普遍意义(不谈天资)上来讲代码质量却撇开代码量的积累,这有点耍流氓的味道了。

Java中的锁

自从java5以后,java.util.concurrent就附带了锁的实现,关于细节觉得看API更有效一点。有个要说的是在可能抛异常的操作中建议加上finally并调用unlock(),否则可能会造成调用该资源的其他线程一直阻塞。

Java中的读写锁

读写锁也是java5以后,concurrent包已经实现。
读写锁机制是:读读共存,读写不共存,写写不共存。
其他的操作可以参见API。
额外介绍的是锁的重入的概念,这本来是上一节可以介绍的,但是懒,下面有要用,这里说一下概念:
重入是指在已经获取一个锁的情况下,运行过程中对另一个锁的申请。synchronized关键字可以实现重入,给段参考代码:

 public class Reentrant{
    public synchronized outer(){
        inner();
    }

    public synchronized inner(){
        //do something
    }
 }

而当我们用Lock类(之前写的那个简单版本)的时候:

 public class Reentrant2{
    Lock lock = new Lock();

    public outer(){
        lock.lock();
        inner();
        lock.unlock();
    }

    public synchronized inner(){
        lock.lock();
        //do something
        lock.unlock();
    }
 }

联系之前的代码,调用inner()方法的时候会发生什么?
很有意思是吧。这差不多就是重入的概念了,解释到此结束。

重入锁死

重入的概念上一节已经有介绍,这里聊一下重入锁死:
重入锁死与死锁和嵌套管程锁死非常相似。具体体现就是在上一节写的那段代码,执行outer()方法时,内部调用inner()方法。线程会无限期的wait()下去,因为它完全不知道自己拿了自己的锁。怎么办咧?下面这段代码可以感受一下:

 public class Lock{
    boolean isLocked = false;
    Thread  lockedBy = null;
    int lockedCount = 0;

    public synchronized void lock()
        throws InterruptedException{
        Thread callingThread =
            Thread.currentThread();
        while(isLocked && lockedBy != callingThread){
            wait();
        }
        isLocked = true;
        lockedCount++;
        lockedBy = callingThread;
    }

    public synchronized void unlock(){
        if(Thread.curentThread() ==
            this.lockedBy){
            lockedCount--;

            if(lockedCount == 0){
                isLocked = false;
                notify();
            }
        }
    }
    ...
 }

避免死锁通常有两种方案:
1、编写代码时避免再次获取已经持有的锁;
2、使用可重入锁

信号量

Semaphore(信号量) 是一个线程同步结构,用于在线程间传递信号,以避免出现信号丢失,或者像锁一样用于保护一个关键区域。

下面的代码给出一个简单的信号量的实现:

 public class Semaphore {
    private boolean signal = false;

    public synchronized void take() {
        this.signal = true;
        this.notify();
    }
    public synchronized void release() throws InterruptedException{
        while(!this.signal) wait();
        this.signal = false;
    }
 }

长得很像锁是吧?我也觉得像。其实这么写的Semaphore跟锁真心没差,主要有两个方法:take()以及release()。take()的角色类似于notify()就是发信息给线程提醒它别睡了,可以干活儿了。而release()就类似于wait()了。跟闹铃差不多,接受take()发出来的消息,起床干活儿。

信号量还可以当成计数器用,代码:

 public class CountingSemaphore {
    private int signals = 0;

    public synchronized void take() {
        this.signals++;
        this.notify();
    }
    public synchronized void release() throws InterruptedException{
        while(this.signals == 0) wait();
        this.signals--;
    }
 }

然后价格bound属性就是有上限的计数器:

 public class BoundedSemaphore {
    private int signals = 0;
    private int bound   = 0;

    public BoundedSemaphore(int upperBound){
        this.bound = upperBound;
    }

    public synchronized void take() throws InterruptedException{
        while(this.signals == bound) wait();
        this.signals++;
        this.notify();
    }

    public synchronized void release() throws InterruptedException{
        while(this.signals == 0) wait();
        this.signals--;
        this.notify();
    }
 }

当bound置为1的时候,这货就是一个锁了。
有没有一种快被玩坏了的赶脚。。。恩,这就是信号量。

阻塞队列

试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来,如从队列中移除一个或者多个元素,或者完全清空队列,下图展示了如何通过阻塞队列来合作: Alt text
阻塞队列在java5以后的concurrent包中已经实现,不多说。

线程池

线程池是装线程的池子。(说了句废话,囧Orz)
主要是在一些应用上需要限制同一时刻运行的线程数目,另外开线程是有开销的哦,亲。特别是在服务器应用这种并发量吓死人的应用环境。所以就做了一个池子来装线程。奏是酱紫。
这货也是java5以后放进来的,在concurrent包中。(java5是一个革命版本)下面给个简单实现:

 public class ThreadPool {
    private BlockingQueue taskQueue = null;
    private List<PoolThread> threads = new ArrayList<PoolThread>();
    private boolean isStopped = false;
    public ThreadPool(int noOfThreads, int maxNoOfTasks) {
        taskQueue = new BlockingQueue(maxNoOfTasks);
        for (int i=0; i<noOfThreads; i++) {
          threads.add(new PoolThread(taskQueue));
        }
        for (PoolThread thread : threads) {
          thread.start();
        }
    }
    public void synchronized execute(Runnable task) {
        if(this.isStopped) throw
      new IllegalStateException("ThreadPool is stopped");
      this.taskQueue.enqueue(task);
    }
    public synchronized boolean stop() {
        this.isStopped = true;
        for (PoolThread thread : threads) {
          thread.stop();
        }
    }
}
 public class PoolThread extends Thread {
     private BlockingQueue<Runnable> taskQueue = null;
     private boolean                 isStopped = false;

     public PoolThread(BlockingQueue<Runnable> queue) {
        taskQueue = queue;
     }

     public void run() {
         while (!isStopped()) {
            try {
                Runnable runnable =taskQueue.take();
                runnable.run();
            } catch(Exception e) {
                // 写日志或者报告异常,
                // 但保持线程池运行.
            }
         }
     }

     public synchronized void toStop() {
        isStopped = true;
        this.interrupt(); // 打断池中线程的 dequeue() 调用.
     }

     public synchronized boolean isStopped() {
        return isStopped;
     }
 }

Talk is cheap, show me the code.
其实是扯淡,我懒得一行行解释了。

剖析同步器

同步器(锁,信号量,阻塞队列等)功能上都有些区别,内部设计实现却差不多(看代码应该能看出来~~~)。同步器大部分用来保护临界区代码用的。一般设计有以下特征:

  • 状态:Lock是布尔型的isLocked变量;信号量是整型的signals;阻塞队列是元素列表和容量。
  • 访问条件:就是看状态喽,我觉得蛮容易理解的。
  • 状态变化:准备用和用完都有状态变化,怎么变看之前的记录就好了。
  • 通知策略:这个是通知谁的问题,通常三种:1、等待中的随机线程;2、指定线程;3、所有线程。
  • Test-and-Set方法:先测试是否有权限,有就set,没有接着test。(说的有点简单,但是道理差不多就是这样子)
  • Set方法:修改状态,通知线程。没了。

EOF

posted @ 2013-09-23 21:16  沉小风  阅读(1623)  评论(3编辑  收藏  举报
based on lovelazycat