java多线程

Contents

  1. 1. 线程
    1. 1.1. 什么是线程?
    2. 1.2. 多进程与多线程的区别
    3. 1.3. 定义线程
    4. 1.4. 中断线程(java.lang.Thread)
    5. 1.5. 线程状态(6种状态)
    6. 1.6. 线程属性
    7. 1.7. 实现线程安全的方法
    8. 1.8. 用于管理线程,提高线程执行效率:
    9. 1.9. java.utils.concurrent
  2. 2. 同步
    1. 2.1. 锁和条件型
    2. 2.2. 监视器(monitor)
    3. 2.3. volatile关键字
    4. 2.4. final变量
  3. 3. 阻塞队列(blocking queue)
  4. 4. 线程安全的集合
  5. 5. 执行器(线程池)
    1. 5.1. Callable
    2. 5.2. Future
    3. 5.3. 执行器(Executor)
  6. 6. 同步器
    1. 6.1. 同步器
    2. 6.2. 分类
  7. 7. 参考资料

多线程是java的一个很重要的高级特性,本文主要归纳了线程的相关概念,包括线程的定义,与进程的区别,线程的状态和属性。同时介绍了线程中很重要的一个知识点——如何实现线程安全:1使线程同步、使用阻塞队列或线程安全的集合。除此之外,还介绍了用于管理线程,提高线程执行效率的执行器(线程池)和同步器。
本文是在看了《Think in JAVA》、《JAVA核心技术卷》以及相关的一些博客后的总结笔记,纯属个人理解。若有错误,望指出。文末会贴出参考资料的链接。

线程

什么是线程?

一个程序同时执行多个任务,每一个任务称为一个线程。

多进程与多线程的区别

每一个进程拥有自己的一整套变量,而线程则共享数据,共享变量使线程之间的通信比进程之间的通信更有效、更容易。

定义线程

  1. 定义一个实现Runable接口的类
    class MyRunable implements Runable{
     public void run(){
         task code; 
     }
    }
    
  2. 创建类对象
    Runable r=new MyRunable();
    
  3. 由Runable创建一个Thread对象
    Thread t=new Thread(r);
    
  4. 启动线程
    t.start()
    
    也可以通过构建一个Thread类的子类定义一个线程
    class MyThread extends Thread{
      public void run(){
           task code
      }
    }
    
    然后创建子类的实例对象,调用start()方法。
    不建议用这种方法,建议用线程池
    不要调用Thread类或Runable对象的run方法。直接调用run方法,只会执行同一个线程中的任务,而不会启动新线程。应调用Thread.start方法。

中断线程(java.lang.Thread)

  1. void interrupt方法:请求终止线程,线程的中断状态将被置位为true。如果线程被阻塞(线程调用sleep或wait),就无法检测中断状态。会抛出Interrupt Exception异常。(存在不能被中断的阻塞I/O调用)
  2. static boolean interrupted():一个静态方法,它检测当前的线程是否被中断,并且会清除该线程的中断状态,置为false。
  3. boolean isInterrupted():一个实例方法,测试线程是否被中断。不会改变中断状态。
  4. static Thread currentThread():返回当前执行线程的Thread对象。

线程状态(6种状态)


New(新创建):程序还没有开始运行线程中的代码。
Runable(可运行):不始终运行,采用时间片机制。在桌面以及服务器操作系统使用抢占式调度,手机等小型设备使用协作式调度。
Blocked(被阻塞):当一个线程视图获取一个内部的对象锁,而该锁被其他线程持有,则进入阻塞状态。
Waiting(等待):当线程等待另一个线程通知调度器一个条件时,进入等待状态。
Timed waiting(计时等待):调用含有超时参数的方法,会导致线程进入计时等待。
Terminated(被终止):
(1) 因为run方法正常退出而自然死亡。
(2) 因为一个没有捕获的异常终止了run方法而意外死亡。

线程属性

包括线程优先级、守护线程、处理未捕获异常的处理器、线程组
1. 线程优先级:高度依赖于系统
setpriority(int newPriority):设置线程优先级
MIN_PRIORITY:1;
NORM_PRIORITY:5(默认优先级);
MAX_PRIORITY:10
yield():导致当前执行线程处于让步状态。
2. 守护线程:
thread.setDaemon(true):为其他线程提供服务,如计时线程。当只剩下守护线程时,虚拟机就退出了。
3. 未捕获异常处理器
线程的run方法不能抛出任何被检测的异常。可以用setUncaughtExceptionHandler方法为线程安装一个处理器,来处理未捕获异常。处理器需实现Thread.UncaughtExceptionHandler接口。
4. 线程组:
一个可以统一管理的线程集合,不建议使用。

实现线程安全的方法

(个人总结,不保证正确性):

  1. 使线程同步、同步的方法有:显式地声明锁和条件;使用synchronized关键字(隐式声明锁);监视器(不需要程序员去考虑锁的问题);volatile关键字(提供免锁机制);final变量(实现内存可见性);
  2. 阻塞队列(java.utils.concurrent包中的机制)
  3. 线程安全的集合

用于管理线程,提高线程执行效率:

  1. 执行器(线程池):
  2. 同步器(预置功能):信号量、CountDownLatch、cyclicBarrier、交换器、同步队列

java.utils.concurrent

concurrent包的实现:

  1. 首先,声明共享变量为volatile;
  2. 然后,使用CAS的原子条件更新来实现线程之间的同步;
  3. 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

concurrent包的实现示意图如下:

AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类)


同步

线程同步的工具:
(1)锁和条件型(非面向对象);
(2)监视器

锁和条件型

两种机制防止代码块受并发访问的干扰:

  • Lock和Condition对象;
  • synchronized关键字;
    1. 锁对象
    (1) 用ReentrantLock保护代码块的基本结构
    ReentrantLock mylock=new ReentrantLock();
    mylock.lock();
    try{
       critical section
    }finally{
       mylock.unlock;
    }
    
    这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程得到锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,则会被阻塞,直到第一个线程释放锁对象。
    (2) void lock():获取这个锁
    void unlock():释放锁
    ReentranLock():构建一个可以被用来保护临界区的可重入锁
    ReentrantLock(boolean fair):构建一个带有公平策略的锁。一个公平锁偏爱等待时间最长的线程。会降低性能。默认非公平
    2. 条件对象
    线程进入临界区,并满足一个条件后它才能执行。可以用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。
    Condition mycond;
    ReentrantLock mylock=new ReentrantLock();
    mylock.lock();
    try{
       mycond=mylock.newCondition();
       while(条件)
            mycond.await();  //进入该条件的等待集中,处于阻塞状态
       ......
       mycond.signalAll();
    }finally{
       mylock.unlock;
    }
    
    (1) 等待获得锁的线程调用await()的线程在本质上是不同。调用await()的线程进入该条件的等待集,即使锁可用,该线程也不能立即解除阻塞。直到另一个线程调用同一条件上signalAll方法。所以这个线程的激活要依靠其他线程,如果没有其他线程重新激活,则会导致死锁现象。
    (2) 死锁:所有的线程都进入阻塞状态。没有任何线程可以解除其他线程的阻塞,程序挂起。
    (3) siganlAll不会立即激活一个等待线程,它仅仅解除所有等待线程的阻塞,这些线程退出同步方法后,通过竞争实现对对象的访问。
    (4) siganl与siganlAll的区别:siganl随机解除等待集中某个线程的阻塞状态。siganlAll是解除所有等待线程。signal更加有效也更危险。更容易导致死锁现象。即当它随机选择的线程发现自己仍然不满足条件对象,再次被阻塞。
    3. 总结锁和条件
  • 锁是用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
  • 锁可以管理试图进入被保护代码段的线程(公平锁和非公平锁。
  • 锁可以拥有一个或多个条件对象。
  • 条件对象可以管理进入临界区但不能运行的线程。
    4. synchronized关键字
    java中的每一个对象都有一个对象锁。
    (1) 当一个方法用synchronized关键声明,该对象的锁将保护整个方法。
    public synchronized  void method(){
      method body
    }
    
    等价于
    public void method{
      this.intrinisclock.lock();
      try{
      method body
      }
      finally{ this.intrinisclock.unlock();  }
    }
    
    (2) 内部对象锁只有一个相关条件:
    wait方法添加一个线程到等待集:wait()=instrinsicCondition.await()
    notifyAll方法解除所有等待线程的阻塞状态:notifyAll() =instrinsicCondition.signalAll()
    notify方法随机选择一个等待线程解除其阻塞状态:notify() =instrinsicCondition.signal()
    (3) 内部锁和条件的局限
    a. 不能中断一个正在试图获得锁的线程
    b. 试图获得锁时不能设定超时
    c. 每个锁仅有单一的条件
    5. Lock+Condition对象还是Synchronized同步方法?
    (1) Lock+Condition 对象和ynchronized同步方法最好不使用,可以使用concurrent包中的机制。
    (2) 尽量使用synchronized而不是Lock/Condition结构。

    监视器(monitor)

    1. 监视器的特性:(不需要程序员考虑如何加锁)
    (1) 监视器是只包含私有域的类
    (2) 每个监视器类的对象有一个相关的锁
    (3) 使用该锁对所有的方法进行加锁,调用方法时自动获得锁,方法返回时自动释放锁。
    (4) 该锁可以有任意多个相关条件
    2. 用synchronized关键字声明方法的Java对象类似于监视器,但又有不同之处:
    (1) 域不要求必须是private
    (2) 方法不要求必须是synchronized
    (3) 内部锁对客户是可用的

    volatile关键字

  1. volatile关键字为实例域的同步访问提供了一种免锁机制。
  2. volatile与synchronized关键字的区别间java内存管理。
    具体内容见博文java内存模型

final变量

对于final域,编译器和处理器要遵守两个重排序规则:

  1. 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  2. 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

阻塞队列(blocking queue)

使用队列可以安全地从一个线程向另一个线程传递数据。
(1) 阻塞队列在协调多个线程之间的合作上是一个有用的工具。
(2) 当试图向队列添加元素而队列已满,或是想从队列移出元素而队列为空的时候,阻塞队列导致线程阻塞。队列会自动平衡负载
(3) 阻塞队列方法分三类

(4) 阻塞队列的变种
a. ArrayBlockingQueue:带有指定的容量和公平性设置(可选、等待时长为指标 )的阻塞队列。该队列用循环数组实现。
b. LinkedBlockingQueue:无上限(或指定容量)的阻塞队列,用链表实现。
c. LinkedBlockingDeque:无上限(或指定容量)的双向阻塞队列,用链表实现。
d. DelayQueue:一个包含Delayed元素的无界的阻塞时间有限的阻塞队列。只有那些延迟已经超过时间的元素才可以从队列中移出。
e. PriorityBlockingQueue:带优先级的队列,按照优先级顺序被移出。用堆实现。


线程安全的集合

  1. java.util.concurrent包提供了高效的映射表、有序集和队列:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentLinkedQueue。。这些集合允许并发地访问数据结构的不同部分来使竞争极小化。
  2. 集合返回弱一致性(weakly consisteut)的迭代器。这意味着迭代器不一定能反映出它们被构造之后的所有的修改。
  3. 写数组的拷贝:CopyOnWriteArrayList和CopyOnWriteArraySet是线程安全地集合,其中所有的修改线程对底层数组进行复制。
  4. Vector(线程安全)——》ArrayList(非线程安全)
    Hashtable(线程安全)——》HashMap(非线程安全)
  5. 任何集合类可通过使用同步包装器变成线程安全的
    List<E> synchArrayList=Collections.synchronizedList(new ArrayList<E>());  
    Map<K,V>synchHashMap=Collections.synchronizedMap(new HashMap<K,V>());
    
  6. 最好使用java.util.concurrent包中定义的集合,不使用同步包装器。
    例外:对于经常被修改的数组列表,同步的ArrayList可以胜过CopyOnWriteArrayList。

执行器(线程池)

Callable

Runable封装一个异步运行的任务,可以把它想象成为一个没有参数和返回值的异步方法。Callable与Runnable类似,但是有返回值。

public interface Callable<V>{
     V call() throws Exception;
}

Future

保存异步计算的结果。可以启动一个计算,将Future对象交给某个线程,然后不用管它,Future对象的所有者在结果计算好之后就可以获得它

public interface Future<V>{
     V get() throws...;
     V get(long timeout,TimeUnit) throws...;
     void cancel(boolean mayInterrupt);
     boolean isCancelled();
     boolean isDone();
}

执行器(Executor)

  1. 若程序中创建了大量的生命期很短的线程,应该使用线程池
  2. 一个线程池中包含许多准备运行的空闲线程,将某个Runnable对象交给线程池,就会有一个线程调用run方法,运行完run方法,线程不会死亡,而是在池中准备为下一个请求提供服务。
  3. 使用线程池的好处:减少并发线程的数目,线程数过多会降低性能甚至使虚拟机奔溃
  4. 执行器类有许多静态工厂方法用来构建线程池。
  5. shutdown:当用完一个线程池时,调用shutdown,该方法启动该池的关闭序列。被关闭的执行器不再接受新任务,当所有任务完成后,线程池的线程死亡。
  6. shutdownNow:取消尚未开始的所有任务并试图中断正在运行的线程。
  7. 使用连接池的过程
    a.调用Executors类中静态的方法newCachedThreadPool或newFixedThreadPool
    b.调用submit提交Runable或Callable对象
    c.取消一个任务或提交Callable对象,要保存好返回的Future对象。
    d.当不再提交任何任务时,调用shutdown

同步器

同步器

帮助人们管理相互合作的线程集,为线程之间的“共用集结点模式”提供“预置功能”

分类


参考资料

书本:
《Think in Java》
JAVA核心技术卷
网址:
http://www.mamicode.com/info-detail-517008.html
http://lavasoft.blog.51cto.com/62575/27069/