java线程学习(二):线程池

本章继续深入学习线程-线程池

线程池的基本概念

JDK5之前,都是程序员自己手动创建线程池,JDK5之后,内置线程池技术。

什么是线程池呢?

       可以简单理解成容纳多个线程的容器,其中的线程是可以反复使用的,省去了频繁创建对象的操作。

为什么使用线程池呢?

       在java中,如果每个请求到达就创建一个新线程,开销是相当大的。在实际使用中,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个jvm里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足。

      线程池主要用来解决线程生命周期开销问题和资源不足问题。通过对多个任务重复使用线程,线程创建的开销就被分摊到了多个任务上了,而且由于在请求到达时线程已经存在,所以消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使用应用程序响应更快。另外,通过适当的调整线程中的线程数目可以防止出现资源不足的情况。


线程池的使用

一、线程池的创建

摘记API

static ExecutorService newFixedThreadPool(int nThreads)  创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。
static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) 创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程,在需要时使用提供的 ThreadFactory 创建新线程。
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)   创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)  创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
   



扫描二维码关注公众号,回复: 2275752 查看本文章




二、线程安全

如果有多个线程在同时运行,而且这些线程可能会同时执行这段代码,程序每次运行的结果和单线程运行的结果是一样的,而且其他的变量值也是跟预期一样的,就是线程安全的。

只有多条线程在使用共享数据的时候会出现线程安全问题。

为什么会出现线程安全问题:

做一个经典模拟,电影院三个窗口卖100张电影票

public class TicketsDemo {

  public static void main(String[] args) {
    Runnable tick = new Tickets();
    Thread t1 = new Thread(tick,"窗口1");
    Thread t2 = new Thread(tick,"窗口2");
    Thread t3 = new Thread(tick,"窗口3");
    t1.start();
    t2.start();
    t3.start();
  }

  public static class Tickets implements Runnable {
    // 总共有100张票
    private Integer tickets = 100;

    @Override
    public void run() {
      while (true) {
        // 电影票售空之前 进入循环
        if (tickets > 0) {
          //------------------------------------流程 1
          try {
            // 模拟服务器卡顿,增加CPU切换执行概率.
            Thread.sleep(100);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          //------------------------------------流程 2
          // 输出那个窗口买到第几张票
          System.out.println(Thread.currentThread().getName() + " 购买到 " + tickets--);
        }
      }
    }
  }
}

console->


发现不仅买到了第0张,还买到了第-1张,这在程序当中肯定是不允许出现的。那么我们来分析一下,出现0和负数的原因,


当CPU执行线程一到流程①位置时候,已经进行了电影票判断,然后休眠的时候,线程二拿到了CUP执行权,同样线程二执行到流程①位置的时候,判断过电影票数,到休眠状态到达,这个时候CPU执行权被线程三抢到,等CPU执行线程三后,重新回来接着执行线程一的时候,CPU会接着上次的线程一休眠后流程②的位置开始往下执行,不会再去判断票数是否售空,此时票数再减减,接着回到线程二的休眠结束后流程②的位置往下执行,这样一来票数继续减减,那么出现买到票数0和负数的情况也就出现了。

如何解决线程安全问题:

1、使用关键字synchronized:

public class TicketsDemo {

  public static void main(String[] args) {
    Runnable tick = new Tickets();
    Thread t1 = new Thread(tick,"窗口1");
    Thread t2 = new Thread(tick,"窗口2");
    Thread t3 = new Thread(tick,"窗口3");
    t1.start();
    t2.start();
    t3.start();
  }

  public static class Tickets implements Runnable {
    // 总共有100张票
    private Integer tickets = 100;
    // 创建Obj
    private Object obj = new Object();
    @Override
    public void run() {
      while (true) {
        // 电影票售空之前 进入循环
        synchronized (obj) {
          if (tickets > 0) {
            //------------------------------------流程 1
            try {
              // 模拟服务器卡顿,增加CPU切换执行概率.
              Thread.sleep(100);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
            //------------------------------------流程 2
            // 输出那个窗口买到第几张票
            System.out.println(Thread.currentThread().getName() + " 购买到 " + tickets--);
          }
        }
      }
    }
  }
}

console->


多次测试 并没有出现0或者负数的情况。那么成功解决线程安全问题,但是每一次都得使用这种语法,无疑会写很多代码,有没有办法简化这一过程呢,答案当然是有的,我们将需要同步的提出来,在方法前加上 synchronized 关键字变成同步方法。

 public class TicketsDemo {

  public static void main(String[] args) {
    Runnable tick = new Tickets();
    Thread t1 = new Thread(tick, "窗口1");
    Thread t2 = new Thread(tick, "窗口2");
    Thread t3 = new Thread(tick, "窗口3");
    t1.start();
    t2.start();
    t3.start();
  }

  public static class Tickets implements Runnable {

    // 总共有100张票
    private Integer tickets = 100;
  
    @Override
    public void run() {
      while (true) {
        // 电影票售空之前 进入循环
        pay();
      }
    }
    private synchronized void pay() {
      if (tickets > 0) {
        try {
          // 模拟服务器卡顿,增加CPU切换执行概率.
          Thread.sleep(100);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        // 输出那个窗口买到第几张票
        System.out.println(Thread.currentThread().getName() + " 购买到 " + tickets--);
      }
    }
  }
}
console输出一样没有0或负数,成功解决安全问题。

仔细观察输出会发现,速度明显比不使用synchronized要慢的多得多,所以解决线程安全的同时,舍去的是效率。

值得注意的是synchronized不具有继承性,如果父类有一个带有synchronized的方法,子类继承并重写了这个方法,但是同步不能继承,所以你还需要在子类方法中添加synchronized关键字。

synchronized的缺陷:线程进入同步方法要获取锁,但是执行完成之后在哪里释放的锁我们并不知道。上述代码持有锁进入同步代码,如果sleep出现异常,不出同步方法,那么锁就永远不会被释放。那么JDK1.5之后为我们提供了LOCK接口,下面我们一起来看一下这个接口。

2、使用Lock接口:

摘自API

java.util.concurrent.locks 接口 Lock

Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。下面是Lock接口为我们提供的方法。

 void lock() 获取锁。
 void lockInterruptibly() 如果当前线程未被中断,则获取锁。
 Condition newCondition() 返回绑定到此 Lock 实例的新 Condition 实例。
 boolean tryLock()  仅在调用时锁为空闲状态才获取该锁。
 boolean tryLock(long time, TimeUnit unit) 如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁。
 void unlock() 释放锁。
有了lock() 方法和 unlock()方法,那么在哪里获得的锁,在哪里释放的锁我们可以很清晰的看出来,我们还能将unlock() 方法写在finally() 中,出现异常也能顺利释放锁。比synchronize灵活,用法更加广泛。

使用同样的售票代码,这次我们使用Lock接口进行改造。

public class TicketsDemo {

  public static void main(String[] args) {
    Runnable tick = new Tickets();
    Thread t1 = new Thread(tick, "窗口1");
    Thread t2 = new Thread(tick, "窗口2");
    Thread t3 = new Thread(tick, "窗口3");
    t1.start();
    t2.start();
    t3.start();
  }

  public static class Tickets implements Runnable {

    // 总共有100张票
    private Integer tickets = 100;
    //  创建Lock实现类对象
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
      while (true) {
        // 获得锁
        lock.lock();
        if (tickets > 0) {
          try {
            // 模拟服务器卡顿,增加CPU切换执行概率.
            Thread.sleep(100);
            // 输出那个窗口买到第几张票
            System.out.println(Thread.currentThread().getName() + " 购买到 " + tickets--);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }finally {
            // 释放锁
            lock.unlock();
          }

        }
      }
    }
  }
}

查看console,重复多次也没有出现0或者负数,那么这个方法也是可行的,注意的是使用synchronized是自动释放锁,使用lock的时候,我们需要手动调用unlock()方法手动释放锁,为了保证锁一定会被释放,我们将unlock() 放入 finally,互斥代码放入try 。

JDK为我们提供了三个Lock接口的实现 。

ReentrantLock,    ReentrantReadWriteLock.ReadLock,    ReentrantReadWriteLock.WriteLock

根据功能分类可以分成:

  • 排他锁:在同一时刻只允许一个线程进行访问,其他线程等待;
  • 读写锁:在同一时刻允许多个读线程访问,但是当写线程访问,所有的写线程和读线程均被阻塞。读写锁维护了一个读锁加一个写锁,通过读写锁分离的模式来保证线程安全,性能高于一般的排他锁。

上面使用到的是Lock的实现类ReentrantLock,下面着重讲解一下读写锁。

读写锁的特性:

  1. 公平性选择:支持公平和非公平(默认)两种获取锁的方式,非公平锁的吞吐量优于公平锁。
  2. 可重入:允许读锁可写锁可重入。写锁可以获得读锁,读锁不能获得写锁。 
  3. 锁降级:允许写锁降低为读锁。
  4. 中断锁的获取:在读锁和写锁的获取过程中支持中断 。
 * @since 1.5
 * @author Doug Lea
 */
public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    private static final long serialVersionUID = -6992448646407690164L;
    /** Inner class providing readlock */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;
    /** Performs all synchronization mechanics */
    final Sync sync;

从源码中可以看到,ReentrantReadWriteLock类 实现了ReadWriteLock接口。并且定义了三个变量,一个读锁对象,一个写锁对象,一个实现同步。

而ReadWriteLock接口中只定义了两个抽象方法,一个读锁,一个写锁。

 * @since 1.5
 * @author Doug Lea
 */
public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}











三、死锁









猜你喜欢

转载自blog.csdn.net/chenhao_c_h/article/details/80810578