Java线程(1):Java 并发学习总览

JAVA并发涵盖一下知识块


基础知识、并发理论(JMM)、并发关键字、Lock体系、并发容器、线程池(Executor体系)、原子操作类、并发工具、并发实践。如下图所示

 

 

 

 

基础知识

包含并发编程的优缺点、线程的状态和一些基本操作

 

 

 

 

并发理论(JMM)

包含JMM内存模型、重排序、happens-before规则

 

 

 

 

并发关键字:

包含synchronized、volatile、final、三大性质

 

 

 

 

Lock体系:

包含Lock与synchronized的比较、AQS、AQS源码解析、ReentrantLock、ReentrantReadWriteLock、Condition机制、LockSupport

 

 

 

 

并发容器:

包含concurrentHashMap、CopyOnWriteArrayList、ThreadLocal、BlockingQueue、ConcurrentLinkedQueue

 

 

 

 

线程池(Executor体系):

包含ThreadPoolExecutor、ScheduledThreadPoolExecutor、FutureTask

 

 

 

 

原子操作类:

包含实现原理、原子更新基本类型、原子更新数组类型、原子更新引用类型、原子更新字段类型

 

 

 

 

并发工具:

包含倒计时器CountDownLatch、循环栅栏CyclicBarrier、CountDownLatch和CyclicBarrier的比较、资源访问控制Semaphore、数据交换Exchanger

 

 

 

 

并发实践:

包含生产者-消费者问题

 

 

前言

  使用并发的一个重要原因是提高执行效率。由于I/O等情况阻塞,单个任务并不能充分利用CPU时间。所以在单处理器的机器上也应该使用并发。

为了实现并发,操作系统层面提供了多进程。但是进程的数量和开销都有限制,并且多个进程之间的数据共享比较麻烦。另一种比较轻量的并发实现是使用线程,一个进程可以包含多个线程。线程在进程中没有数量限制, 数据共享相对简单。线程的支持跟语言是有关系的。Java语言中支持多线程。

Java中的多线程是抢占式的。这意味着一个任务随时可能中断并切换到其它任务。所以我们需要在代码中足够的谨慎,防范好这种切换带来的副作用。

 

基础

 

 

Java 1.5之后,不再推荐直接使用Thread对象作为任务的入口。推荐使用Executor管理Thread对象。Executor是线程与任务之间的的一个中间层,它屏蔽了线程的生命周期,不再需要显式的管理线程。并且ThreadPoolExecutor实现了此接口,我们可以通过它来利用线程池的优点。

线程池涉及到的类有:ExecutorExecutorServiceThreadExecutorPoolExecutorsExecutors.newFixedThreadPool()Executors.newCachedThreadPool()Executors.newSingleThreadExecutor()Executors.newScheduledThreadPool()

1. Executor 只有一个方法,execute(Runnable command)来提交一个任务
2. ExecutorService 提供了管理异步任务的方法,也可以产生一个Future对象来跟踪一个异步任务。

主要的方法如下:

  1. submit 可以提交一个任务
  2. shutdown 可以拒绝接受新任务
  3. shutdownNow 可以拒绝新任务并向正在执行的任务发出中断信号
  4. invokeXXX 批量执行任务
3. ThreadPoolExecutor线程池的具体实现类。线程池的好处在于提高效率,能避免频繁申请/回收线程带来的开销。

它的使用方法复杂一些,构造线程池的可选参数有:

  1. corePoolSize : int 工作的Worker的数量。
  2. maximumPoolSize : int 线程池中持有的Worker的最大数量
  3. keepAliveTime : long 当超过Workder的数量corePoolSize的时候,如果没有新的任务提交,超过corePoolSizeWorker的最长等待时间。超过这个时间之后,一部分Worker将被回收。
  4. unit : TimeUnit keepAliveTime的单位
  5. workQueue : BlockingQueue 缓存任务的队列,这个队列只缓存提交的Runnable任务。
  6. threadFactory : ThreadFactory 产生线程的”工厂”
  7. handler : RejectedExecutionHandler 当一个任务被提交的时候,如果所有Worker都在工作并且超过了缓存队列的容量的时候。会交给这个Handler处理。Java中提供了几种默认的实现,AbortPolicyCallerRunsPolicyDiscardOldestPolicyDiscardPolicy。 这里的Worker可以理解为一个线程。

这里之前想不通,觉得线程不可能重新利用绑定新任务。看了下源码发现原来确实不是重新绑定任务。每一个Worker的核心部分只是一个循环,不断从缓存队列中取任务执行。这样达到了重用的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
final void runWorker(Worker w) {
Runnable task = w.firstTask;
// ...
try {
while(task != null || (task=getTask())!=null) {
try{
task.run();
} catch(Exception e){
}
// ...
}
} finally {
// ...
}
// ...
}

 

4. Executors类提供了几种默认线程池的实现方式。
  1. newCachedThreadPool() 工作线程的数量没有上限(Integer的最大值), 有需要就创建新线程。
  2. newFixedThreadPool() 预先一次分配固定数量的线程,之后不再需要创建新线程。
  3. newSingleThreadExecutor() 只有一个线程的线程池。如果提交了多个任务,那么这些任务将排队,每个任务都在上一个任务执行完之后执行。所有任务都是按照它们的提交顺序执行的。

sleep(long) 当前线程 中止一段时间。它不会释放锁。Java1.5之后提供了更加灵活的版本。

TimeUnit 可以指定睡眠的时间单位。

  1. 优先级 绝大多数情况下我们都应该使用默认的优先级。不同的虚拟机中对应的优先级级别的总数,一般用三个就可以了MAX_PRIORITYNORM_PRIORITYMIN_PRIORITY
  2. 让步 Thread.yield()建议相同优先级的其它线程先运行,但是不保证一定运行其它线程。
  3. 后台线程 一个进程中的所有非后台线程都终止的时候整个进程也就终止,同时杀死所有后台线程。与优先级没有什么关系。
  4. join() 线程A持有线程T,当在线程T调用T.join()之后,A会阻塞,直到T的任务结束。可以加一个超时参数,这样在超时之后线程A可以放弃等待继续执行任务。
  5. 捕获异常 不能跨线程捕获异常。比如说不能在main线程中添加try-catch块来捕获其它线程中抛出的异常。每一个Thread对象都可以设置一个UncaughtExceptionHandler对象来处理本线程中抛出的异常。线程池中可以通过参数ThreadFactory来为每一个线程设置一个UncaughtExceptionHandler对象。

访问共享资源

在处理并发的时候,将变量设置为private非常的重要,这可以防止其它线程直接访问变量。

synchronized修饰方法在不加参数情况下,使用对象本身作为锁。静态方法使用Class对象作为锁。同一个任务可以多次获得对象锁。

  1. 显式锁 Lock,相比synchronized更加灵活。但是需要的代码更多,编写出错的可能性也更高。只有在解决特殊问题或者提高效率的时候才用它。
  2. 原子性 原子操作就是永远不会被线程切换中断的操作。很多看似原子的操作都是非原子的,比如说long,double是由两个byte表示的,它们的所有操作都是非原子的。所以,涉及到并发异常的地方都加上同步吧。除非你对虚拟机十分的了解。
  3. volatile 这个关键字的作用在于防止多线程环境下读取变量的脏数据。这个关键字在c语言中也有,作用是相同的。
  4. 原子类 AtomicXXX类,它们能够保证对数据的操作是满足原子性的。这些类可以用来优化多线程的执行效率,减少锁的使用。然而,使用难度还是比较高的。
  5. 临界区 synchronized关键字的用法。不是修饰整个方法,而是修饰一个代码块。它的作用在于尽量利用并发的效率,减少同步控制的区域。
  6. ThreadLocal 这个概念与同步的概念不同。它是给每一个线程都创建一个变量的副本,并保持副本之间相互独立,互不干扰。所以各个线程操作自己的副本,不会产生冲突。

终结任务

这里我讲一下自己当前的理解。

一个线程不是可以随便中断的。即使我们给线程设置了中断状态,它也还是可以获得CPU时间片的。只有因为sleep()方法而阻塞的线程可以立即收到InterruptedException异常,所以在sleep()中断任务的情况下可以直接使用try-catch跳出任务。其它情况下,均需要通过判断线程状态来判断是否需要跳出任务(Thread.interrupted()方法)。

synchronized关键字修饰的代码不会在收到中断信号后立即中断。ReentrantLock锁控制的同步代码可以通过InterruptException中断。

Thread.interrupted方法调用一次之后会立即清空中断状态。可以自己用变量保存状态。

线程协作

wait/notifyAll wait/notifyAllObject类中的方法。调用wait/notifyAll方法的对象是互斥对象。因为Java中所有的Object都可以做互斥量(synchronized关键字的参数),所以wait/notify方法是在Object类中的。

waitsleep 不同在于sleep方法是Thread类中的方法,调用它的时候不会释放锁;wait方法是Object类中的方法,调用它的时候会释放锁。

调用wait方法之前,当前线程必须持有这段逻辑的锁。否则会抛出异常,不能继续执行。

wait方法可以将当前线程放入等待集合中,并释放当前线程持有的锁。此后,该线程不会接收到CPU的调度,并进入休眠状态。有四种情况肯能打破这种状态:

  1. 有其它线程在此互斥对象上调用了notify方法,并且刚好选中了这个线程被唤醒;
  2. 有其它线程在此互斥对象上调用了notifyAll方法;
  3. 其它线程向此线程发出了中断信号;
  4. 等待时间超过了参数设置的时间;

线程一旦被唤醒之后,它会像正常线程一样等待之前持有的锁。直到恢复到wait方法调用之前的状态。

还有一种不常见的情况,spurious wakeup(虚假唤醒)。就是在没有notifynotifyAllinterrupt的时候线程自动醒来。查了一些资料并没有弄清楚是为什么。不过为了防止这种现象,我们要在wait的条件上加一层循环。

当一个线程调用wait方法之后,其它线程调用该线程的interrupt方法。该线程会唤醒,并尝试恢复之前的状态。当状态恢复之后,该线程会抛出一个异常。

  • notify 唤醒一个等待此对象的线程。
  • notifyAll 唤醒所有等待此对象的线程。

错失的信号

当两个线程使用notify/wait或者notifyAll/wait进行协作的时候,不恰当的使用它们可能会导致一些信号丢失。例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
T1:
synchronized(shareMonitor){
// set up condition for T2
shareMonitor.notify();
}

T2:
while(someCondition){
// Point 1
synchronized(shareMonitor){
shareMonitor.wait();
}
}

 

信号丢失是这样发生的:

T2执行到Point1的时候,线程调度器将工作线程从T2切换到T1T1完成T2条件的设置工作之后,线程调度器将工作线程从T1切换回T2。虽然T2线程等待的条件已经满足,但还是会被挂起。

解决的方法比较简单:

1
2
3
4
5
6
T2:
synchronized(sharedMonitor) {
while(someCondition) {
sharedMonitor.wait();
}
}

 

将竞争条件放到while循环的外面即可。在进入while循环之后,在没有调用wait方法释放锁之前,将不会进入到T1线程造成信号丢失。

  1. notify/notifyAll 前面已经提过这两个方法的区别。notify是随机唤醒一个等待此锁的线程,notifyAll是唤醒所有等待此锁的线程。
  2. Condition 他是concurrent类库中显式的挂起/唤醒任务的工具。它是真正的锁Lock对象产生的一个对象。其实用法跟wait/notify是一致的。await()挂起任务,signalAll()唤醒任务。
  3. 生产者消费者队列 Java中提供了一种非常简便的容器,BlockingQueue。已经帮你写好了阻塞式的队列。除了BlockingQueue,使用PipedWriter/PipedReader也可以方便的在线程之间传递数据。

 

posted @ 2019-07-31 16:15  qxwang  阅读(27)  评论(0)    收藏  举报