多线程

来源:互联网 发布:矩阵的奇异值如何计算 编辑:程序博客网 时间:2024/05/23 10:51

一、线程的创建

1、继承Thread类创建线程

2、实现Runnable接口创建线程,一般使用匿名内部类

     Thread   thread=new Thread(new Runnable(){

            public void run(){

                      方法体

            }

     })

3、使用Callable接口和Future接口,call方法可以返回值,可以抛出异常

     使用FutureTask类作为Thread类的target,FutureTask类封装了Callable实现类,一般使用Lambda表达式

二、线程的生命周期

新建:new关键字创建线程之后

就绪:调用strat()方法之后

运行:获得cpu、执行run()方法之后

阻塞:sleep、阻塞式IO、等待同步锁、等待通知、suspend之后

死亡:run、call、stop之后,抛出未捕获的Exception或Error后

三、线程同步

1、同步代码块,obj作为同步监视器

      synchronized(obj){

              同步代码快

       }

2、同步方法

      以synchronized关键字修饰方法,this作为同步监视器

3、同步锁

ReadWriteLock ->ReentrantReadWriteLock

       维护了一对相关的,一个用于只读操作,另一个用于写入操作。读取锁可以由多个 线程同时保持,允许对共享资源并发访问,写入锁是独占的。所有 ReadWriteLock 实现都必须保证 writeLock 操作的内存同步效果,也要保持与相关readLock 的联系。也就是说,成功获取读锁的线程会看到写入锁所做的所有更新。

      与互斥锁相比,读-写锁允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程),读-写锁利用了这一点。从理论上讲,与互斥锁相比,使用读-写锁所允许的并发性增强将带来更大的性能提高。

      与互斥锁相比,使用读-写锁能否提升性能则取决于读写操作期间读取数据相对于修改数据的频率,以及数据的争用——即在同一时间试图对该数据执行读取或写入操作的线程数。例如,某个最初用数据填充并且之后不经常对其进行修改的 collection,因为经常对其进行搜索(比如搜索某种目录),所以这样的 collection 是使用读-写锁的理想候选者。但是,如果数据更新变得频繁,数据在大部分时间都被独占锁,这时,就算存在并发性增强,也是微不足道的。更进一步地说,如果读取操作所用时间太短,则读-写锁实现(它本身就比互斥锁复杂)的开销将成为主要的执行成本。

Lock->ReentrantLock

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

  synchronized 方法或语句的使用强制所有锁获取和释放均要出现在一个块结构中:当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的词法范围内释放所有锁。

    虽然 synchronized 方法和语句的范围机制使得使用监视器锁编程方便了很多,而且还帮助避免了很多涉及到锁的常见编程错误,但有时也需要以更为灵活的方式使用锁。例如,某些遍历并发访问的数据结果的算法要求使用 "hand-over-hand" 或 "chain locking":获取节点 A 的锁,然后再获取节点 B 的锁,然后释放 A 并获取 C,然后释放 B 并获取 D,依此类推。Lock 接口的实现允许锁在不同的作用范围内获取和释放,并允许以任何顺序获取和释放多个锁,从而支持使用这种技术。  

(注)实现互斥同步的三种方法

1、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。 
2、互斥量:为协调共同对一个共享资源的单独访问而设计的。 
3、信号量:为控制一个具有有限数量用户资源而设计。 

临界区

保证在某一时刻只有一个线程能访问数据的简便办法。在任意时刻只允许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么 在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。 
临界区包含两个操作原语: 
EnterCriticalSection() 进入临界区 ;LeaveCriticalSection() 离开临界区 
EnterCriticalSection() 语句执行后代码将进入临界区以后无论发生什么,必须确保与之匹配的 LeaveCriticalSection()都能够被执行到。否则临界区保护的共享资源将永远不会被释放。虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。 
临界区提供一个 CCriticalSection类,使用该类进行线程同步处理是非常简单的。只需在线程函数中用CCriticalSection类成员函数Lock()和UnLock()标定出被保护代码片段即可。Lock()后代 码用到的资源自动被视为临界区内的资源被保护。UnLock后别的线程才能访问这些资源。

互斥量

互斥量跟临界区很相似,只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。互斥量比临界区复杂。因为使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。 

信号量

信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源 ,这与操作系统中的PV操作相同。它指出了同时访问共享 资源的线程 最大数目。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用CreateSemaphore()创建信号量 时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数 就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目, 不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可 用资源计数加1。在任何时候当前可用资源计数决不可能大于最大资源计数。 


四、线程通信

1、利用Object类的wait()、notify()、notifyAll()方法来控制线程调度

wait:导致当前线程等待

notify:唤醒在此同步监视器上等待的单个线程

notifyAll:唤醒在此同步监视器上等待的所有线程

2、使用condition控制线程通信

当使用Lock对象来保持同步时,Java提供了一个Condition类来保持协调,condition类提供了如下三个方法:

await():导致当前线程等待

singal():唤醒在此Lock对象上等待的单个线程

singalAll():唤醒在此Lock对象上等待的所有线程

3、使用阻塞队列控制线程通信

BlockingQueue接口:当生产者试图向BlockingQueue中放入元素时,如队列已满,则该线程阻塞;当消费者试图从BlockingQueue中取出元素时,如果队列已空,线程阻塞。

put(E e):向BlockingQueue中放入元素时,如队列已满,则该线程阻塞

take(E e):从BlockingQueue中取出元素时,如果队列已空,线程阻塞

有五个实现类:

ArrayBlockingQueue:基于数组实现的队列

LinkedBlockingQueue:基于链表的队列

PriorityBlockingQueue:基于堆的队列

SynchronousQueue:同步队列,对该队列存取操作必须交替执行

五、线程组和未处理的异常

1、ThreadGroup表示线程组,可以对一批线程进行分类管理

      Thread类提供如下构造器来设置新创建的线程属于哪个线程组:Thread(ThreadGroup group,String name)

2、如果线程执行过程中抛出了一个未处理得异常,JVM在结束改线程之前会自动查找是否有对应的处理器对象(UncaughtExceptionHandler),如果找到该处理器对象,则会调用该对象的uncaughtException(Thread t,Throwable e)方法来处理该异常

每个线程所属的线程组将会作为默认的异常处理器

六、线程池

使用线程池可以帮助我们实现线程复用,减少频繁创建和销毁线程

1、ThreadPoolExecutor是线程池的核心类

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)

corePoolSize:核心线程数量,线程池刚创建时,线程数量为0,当每次执行execute添加新的任务时会在线程池创建一个新的线程,直到线程

数量达到corePoolSize为止

maximumPoolSize:最大线程数量,当workQueue队列已满,放不下新的任务,再通过execute添加新的任务则线程池会再创建新的线程,

线程数量大于corePoolSize但不会超过maximumPoolSize,如果超过maximumPoolSize,那么会抛出异常,如

RejectedExecutionException

keepAliveTime、unit:当线程池中线程数量大于workQueue,如果一个线程的空闲时间大于keepAliveTime,则该线程会被销毁。

unit则是keepAliveTime的时间单位

workQueue:阻塞队列,当线程池正在运行的线程数量已经达到corePoolSize,那么再通过execute添加新的任务则会被加到workQueue队列

中,在队列中排队等待执行,而不会立即执行

总结一下线程池添加任务的整个流程:

  • 线程池刚刚创建是,线程数量为0;
  • 执行execute添加新的任务时会在线程池创建一个新的线程;
  • 当线程数量达到corePoolSize时,再添加新任务则会将任务放到workQueue队列;
  • 当队列已满放不下新的任务,再添加新任务则会继续创建新线程,但线程数量不超过maximumPoolSize;
  • 当线程数量达到maximumPoolSize时,再添加新任务则会抛出异常。
2、通过Executors工厂类来产生线程池

3、使用线程池来执行线程任务流程:

  • 调用Executors类的静态工厂方法创建一个ExecutorService对象,该对象代表一个线程池
  • 创建Runnable或者Callable的实例
  • 调用ExecutorService对象的submit()方法来提交unnable或者Callable的实例
  • 不想提交任何任务时,调用ExecutorService对象的shutdown()方法来关闭线程池
4、使用ForkJoinPool来充分利用多CPU、多核CPU的优势

七、线程相关类

1、ThreadLocal类

代表一个线程局部变量,为每一个使用该变量的线程提供一个变量副本,使得每一个线程可以独立

地改变自己的副本,不会和其他线程副本产生冲突,达到隔离多个线程的数据共享目的。

下面来看看ThreadLocal的实现原理(jdk1.5源码) 

Java代码  收藏代码
  1. public class ThreadLocal<T> {  
  2.     /** 
  3.      * ThreadLocals rely on per-thread hash maps attached to each thread 
  4.      * (Thread.threadLocals and inheritableThreadLocals).  The ThreadLocal 
  5.      * objects act as keys, searched via threadLocalHashCode.  This is a 
  6.      * custom hash code (useful only within ThreadLocalMaps) that eliminates 
  7.      * collisions in the common case where consecutively constructed 
  8.      * ThreadLocals are used by the same threads, while remaining well-behaved 
  9.      * in less common cases. 
  10.      */  
  11.     private final int threadLocalHashCode = nextHashCode();  
  12.   
  13.     /** 
  14.      * The next hash code to be given out. Accessed only by like-named method. 
  15.      */  
  16.     private static int nextHashCode = 0;  
  17.   
  18.     /** 
  19.      * The difference between successively generated hash codes - turns 
  20.      * implicit sequential thread-local IDs into near-optimally spread 
  21.      * multiplicative hash values for power-of-two-sized tables. 
  22.      */  
  23.     private static final int HASH_INCREMENT = 0x61c88647;  
  24.   
  25.     /** 
  26.      * Compute the next hash code. The static synchronization used here 
  27.      * should not be a performance bottleneck. When ThreadLocals are 
  28.      * generated in different threads at a fast enough rate to regularly 
  29.      * contend on this lock, memory contention is by far a more serious 
  30.      * problem than lock contention. 
  31.      */  
  32.     private static synchronized int nextHashCode() {  
  33.         int h = nextHashCode;  
  34.         nextHashCode = h + HASH_INCREMENT;  
  35.         return h;  
  36.     }  
  37.   
  38.     /** 
  39.      * Creates a thread local variable. 
  40.      */  
  41.     public ThreadLocal() {  
  42.     }  
  43.   
  44.     /** 
  45.      * Returns the value in the current thread's copy of this thread-local 
  46.      * variable.  Creates and initializes the copy if this is the first time 
  47.      * the thread has called this method. 
  48.      * 
  49.      * @return the current thread's value of this thread-local 
  50.      */  
  51.     public T get() {  
  52.         Thread t = Thread.currentThread();  
  53.         ThreadLocalMap map = getMap(t);  
  54.         if (map != null)  
  55.             return (T)map.get(this);  
  56.   
  57.         // Maps are constructed lazily.  if the map for this thread  
  58.         // doesn't exist, create it, with this ThreadLocal and its  
  59.         // initial value as its only entry.  
  60.         T value = initialValue();  
  61.         createMap(t, value);  
  62.         return value;  
  63.     }  
  64.   
  65.     /** 
  66.      * Sets the current thread's copy of this thread-local variable 
  67.      * to the specified value.  Many applications will have no need for 
  68.      * this functionality, relying solely on the {@link #initialValue} 
  69.      * method to set the values of thread-locals. 
  70.      * 
  71.      * @param value the value to be stored in the current threads' copy of 
  72.      *        this thread-local. 
  73.      */  
  74.     public void set(T value) {  
  75.         Thread t = Thread.currentThread();  
  76.         ThreadLocalMap map = getMap(t);  
  77.         if (map != null)  
  78.             map.set(this, value);  
  79.         else  
  80.             createMap(t, value);  
  81.     }  
  82.   
  83.     /** 
  84.      * Get the map associated with a ThreadLocal. Overridden in 
  85.      * InheritableThreadLocal. 
  86.      * 
  87.      * @param  t the current thread 
  88.      * @return the map 
  89.      */  
  90.     ThreadLocalMap getMap(Thread t) {  
  91.         return t.threadLocals;  
  92.     }  
  93.   
  94.     /** 
  95.      * Create the map associated with a ThreadLocal. Overridden in 
  96.      * InheritableThreadLocal. 
  97.      * 
  98.      * @param t the current thread 
  99.      * @param firstValue value for the initial entry of the map 
  100.      * @param map the map to store. 
  101.      */  
  102.     void createMap(Thread t, T firstValue) {  
  103.         t.threadLocals = new ThreadLocalMap(this, firstValue);  
  104.     }  
  105.   
  106.     .......  
  107.   
  108.     /** 
  109.      * ThreadLocalMap is a customized hash map suitable only for 
  110.      * maintaining thread local values. No operations are exported 
  111.      * outside of the ThreadLocal class. The class is package private to 
  112.      * allow declaration of fields in class Thread.  To help deal with 
  113.      * very large and long-lived usages, the hash table entries use 
  114.      * WeakReferences for keys. However, since reference queues are not 
  115.      * used, stale entries are guaranteed to be removed only when 
  116.      * the table starts running out of space. 
  117.      */  
  118.     static class ThreadLocalMap {  
  119.   
  120.     ........  
  121.   
  122.     }  
  123.   
  124. }  
可以看到,每个线程都有自己的ThreadLocalMap,ThreadLocal实例作为key,要封装的变量作为value,而每个ThreadLocal实例都有唯一

的threadLocalHashCode值作为标识,这样就达到了各个线程之间变量的独立

2、包装线程不安全的集合

3、concurrent包

  3.1CopyOnWriteArrayList、CopyOnWriteArrayListSet

问题:为什么List有了可以包装成线程安全的集合类以及Vector还要concurrent包中的安全类?

这与迭代和并发修改之间的交互有关,使用java.util包下的Collection作为集合对象时,如果该集合对象创建迭代器

集合元素发生改变,会引发ConcurrentModificationException异常。而CopyOnWriteArrayList采用了复制底层

数组、创建副本的方式来实现写操作。CopyOnWriteArraySet底层封装了CopyOnWriteArrayList,所以实现机制与

CopyOnWriteArrayList类似

3.2 ConcurrentHashMap

相比于HashTable、SynchronizedMap包装类,ConcurrentHashMap大大提高了并发性。

Hashtable 和 synchronizedMap 所采取的获得同步的简单方法(同步 Hashtable 中或者同步的 Map 封装器对象

中的每个方法)有两个主要的不足。首先,这种方法对于可伸缩性是一种障碍,因为一次只能有一个线程可以访问

hash 表。同时,这样仍不足以提供真正的线程安全性,许多公用的混合操作仍然需要额外的同步。虽然诸如 get()

和 put() 之类的简单操作可以在不需要额外同步的情况下安全地完成,但还是有一些公用的操作序列,例如迭代或

put-if-absent(空则放入),需要外部的同步,以避免数据争用。

Hashtable 和 Collections.synchronizedMap 通过同步每个方法获得线程安全。这意味着当一个线程执行一个

Map 方法时,无论其他线程要对 Map进行什么样操作,都不能执行,直到第一个线程结束才可以。对比来说,

ConcurrentHashMap 允许多个读取几乎总是并发执行,读和写操作通常并发执行,多个同时写入经常并发执行。

结果是当多个线程需要访问同一 Map 时,可以获得更高的并发性。

ConcurrentHashMap底层结构

允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash表的不同部分进行的修

改。ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段包含一个HashEntry类型的数组。

只要多个修改操作发生在不同的段上,它们就可以并发进行。读操作是不需要锁定的,原因是:段中有一个统计段

中数据的成员变量,是volatile类型的,保证了每次读取时能够知道对段的最新修改。同时存储在HashEntry中的数

据类型也是volatile类型的,使得读取的值是最新修改的值。写操作除了需要写之前要加lock()/unlock()之外,其他

与HashMap基本相同

3.3 ConcurrentLinkedQueue

快速、线程安全的、无阻塞 FIFO 队列

原创粉丝点击