Skip to content

Latest commit

 

History

History
236 lines (124 loc) · 11.4 KB

File metadata and controls

236 lines (124 loc) · 11.4 KB

Java并发

背景知识

  1. Java所使用的并发系统会共享内存I/O等资源,因此编写多线程程序的基本困难在于协调不同线程所驱动的任务之间对这些共享资源的使用,使得这些任务不会被同时被多个任务访问.

  2. Java线程的机制是抢占式的,这表示调度机制会周期性地中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使每个线程都会分配到数量合理的时间去驱动他的任务.

  3. 线程机制是一种建立透明的,可扩展的程序方法,为机器添加一个CPU就能很容易的加快程序运行速度.

基本的线程机制:

  • 一些方法说明:

    • Thread.yield(): 对线程调度器的一种建议,表示可以切换给其他任务执行一段时间.
    • Thread.sleep()(旧方法)/TimeUnit.MILLISECONDS.sleep(): 使当前任务中止执行一段时间(线程阻塞).
    • threadObj.join(): 挂起当前线程,等待threadObj线程执行结束才恢复.(可被interrupt()中断)
    • Thread.currentThread(): 获得驱动该任务的Thread对象的引用.

建立一个线程的方式:

  • 建立实现Runnable接口的任务并传入Thread构造器.
  • 直接继承Thread.

使用Executor:

- Executor(执行器)在客户端和任务执行之间提供了一个中介层,这个中介层将代替客户端执行任务,而无需程序员显示地管理线程的生命周期.
- 任务对象知道如何运行具体的任务,而ExecutorService(具有服务生命周期Executor)知道如何构建恰当的上下文来执行Runnable对象.

Executor其实就是线程对象管理池,代替我们管理线程的生命周期,ExecutorService提供各种Executor.

  • 几种不同的Executor:

    1. FixedThreadPool: 使用有限(自己设定)的线程集来执行所提交的任务.(预先进行了线程的分配,驱动任务的时候就直接使用)

    2. CacheThreadPool: 在程序执行的过程中创建与所需数量相同的线程,然后在Executor回收旧线程时停止创建新线程.(在程序运行的过程中进行线程分配再驱动任务运行)

    3. SingleThreadPool: 就像是线程数量为1的FixedThreadPool.

  • 静态的ExecutorService创建方法可以接受一个ThreadFactory对象(用于定制线程优先级,是否后台,名称),Executor将用这个对象来创建进行.
  • 调用某个ExecutorServiceshutdownNow()时,它会调用所有由它控制的线程的interrupt().

从任务中产生返回值:

  • 实现Callable接口而不是Runnable接口,并且要使用ExecutorService.submit()方法调用.
  • submit()调用会产生Future对象,可以使用isDone()来查询任务是否完成,或者直接用get()获取结果,如果任务未完成,get()将阻塞直至结果就绪.

线程的优先级:

  • 线程的优先级将该线程的重要器传给Executor,Executor将倾向于让优先权最高的任务先执行.

  • JDK有10个优先级,但由于操作系统的优先级多样性.在调整优先级的时候只使用: MAX_PRIORITY,NORM_PRIORITYMIN_PRIORITY三种级别.

后台(守护)线程:

  • 在程序运行的时候在后台提供一种通用服务的线程,并且这种线程不属于程序中不可或缺的部分.当所有非后台线程结束,会杀死进程中的所有后台线程.

  • 一个后台线程创建的任何子线程都将被自动设为后台线程.

  • 后台线程存在不执行finally子句就退出的情况.

捕获异常:

Java多线程程序中,所有线程都不允许抛出未经捕获的异常,所有的异常都要在自己线程内解决.(run()方法没有throws exception)
当是线程仍有可能抛出异常.当此类异常跑抛出时,线程就会终结,而对于主线程和其他线程完全不受影响,且完全感知不到某个线程抛出的异常(也是说完全无法catch到这个异常).

  • 当线程没有处理异常,我们要在线程代码边界之外(run()方法之外)处理这个异常的方法:

    Thread对象提供的setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)方法.通过该方法给某个thread设置一个UncaughtExceptionHandler,可以确保在该线程出现异常时能通过回调UncaughtExceptionHandler接口的public void uncaughtException(Thread t, Throwable e)方法来处理异常,这样的好处是可以在线程代码边界之外(Thread的run()方法之外),有一个地方能处理未捕获异常。

    • 但是要特别明确的是:虽然是在回调方法中处理异常,但这个回调方法在执行时依然还在抛出异常的这个线程中!另外还要特别说明一点:如果线程是通过线程池创建,线程异常发生时UncaughtExceptionHandler接口不一定会立即回调。

终结任务

(待补充)

如何安全地共享资源

各种各样的锁:

隐式锁:

  1. 对象锁:

    所有对象都自动含有单一的锁(monitor),在对象上调用任意synchronized方法的时候,对象都被加锁,此时其他该对象上其他synchronized方法只有等到前一个方法调用完毕并释放锁才能被调用.

  2. 类锁:

    每个类对象都有一个锁,所以synchronized static方法可以在类的范围内防止对static数据的并发访问.

显式锁:

  1. Lock对象:

    Java类库中的显式互斥机制,Lock对象必须被显式地创建,锁定释放.与内建锁相比,代码缺乏优雅性,但对于某些问题的解决更加灵活.

  2. ReentrantLock:

    ReentrantLock允许尝试着获取一个锁,如果其他人已经获取了这个锁,可以选择离开去执行其他事情,而不是一直等待锁的释放.

  • 一个任务可以多次获得对象的锁(锁的计数递增)

原子性与易变性:

  • 原子性可以应用于除long,double以外的所有基本类型之上的 "简单操作".

  • JVM将64位的读取写入当做两个分离的32位操作来执行,产生了在读取写入操作中间发生上下文(线程)切换,导致不同任务看到不同结果的可能性(字撕裂).

  • volatile关键字确保可视性.(修饰的域的修改立即写入主存,不进行任何读写优化)

  • 同步也会导致锁释放前向主存中刷新.

原子类:

  • JavaSE5引入了AutomicInteger,AutomicLong,AutomicReference等特殊的原子性变量类,并提供原子性的更新操作.

临界区:

防止多个线程同时访问方法内部的部分代码,而不是整个方法,这部分代码叫做临界区(也称同步代码块)

  • synchronized创建临界区的方法:

    synchronized用来指定某个对象,此对象的锁被用来对花括号内的代码进行同步

      synchronized(obj){
          // 这部分代码一次只能被一个线程访问
      }
    

线程本地存储:

  • 防止任务在共享资源上发生冲突的第二种方式是根除对变量的共享.
    线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储.通过ThreadLocal实现.

线程之间的协作

wait()与notifyAll():

  • 有两种形式的wait():

    1. 接收毫秒数作为参数,在一段时期内暂停(阻塞):

      1. 在这段时期内锁是释放的
      2. 可以通过notify(),notifyAll()或者指令时间到期,恢复执行.
    2. 不接受任何参数,这时线程将无限执行下去,直至收到notify()notifyAll()消息

  • wait(),notify()notifyAll()都是基类Object的一部分.

  • 只能在同步代码块里调用wait(),notify()notifyAll().

  • 在进行协作时,信号量可能会丢失,从而导致线程无限阻塞.(notify()/notifyAll()wait()之前发生)

notify()与notifyAll()

  • 使用notify()时,众多等待同个锁的任务中只有一个会唤醒,要保证唤醒的是恰当的任务.

  • notifyAll()只唤醒所有等待这个锁的任务.

显式的Lock和Condition对象:

通过在Condition上调用await()来挂起一个任务. 当外部条件变化,某个任务需要执行时,通过调用signal()唤醒一个任务或signalAll()唤醒在这个Condition上被其挂起的任务.

  • 与使用notifyAll()相比,signalAll()是更安全的方式:
    notifyAll()唤醒所有在此对象上的等待synchronized锁的任务;
    signalAll()唤醒被Condition挂起的任务,控制粒度更细.

  • 每个lock()的调用都必须紧跟一个try-finally子句,保证所有情况下都能释放锁.

生产者-消费者与队列:

更高的抽象级别解决线程协作,是使用同步队列,同步队列在任何时刻都只允许一个任务插入或移除元素.

  • LinkedListBlockingQueue: 无界同步队列.
  • ArrayBlockingQueue: 指定尺寸的同步队列.

任务间使用管道(阻塞队列)进行输入/输出:

通过输入/输出在线程间进行通讯,Java输入/输出类库中的对应物是PipedWriter类(允许任务向管道写)和PipedReader类(允许不同任务从同一管道读取).这个"管道"基本上是一个阻塞队列.

死锁

某个任务在等待另一个任务,后者又在等待其他的任务,这样一直下去,直到这个链条上的任务又在等待第一个任务释放锁.任务间循环相互等待,没有哪个线程能继续,称之为死锁.

  • 当一下四个条件全部满足时,死锁就会发生:

  1. 互斥条件.
  2. 至少有一个任务,它持有一个资源且正在等待获取一个被别的任务持有的资源.
  3. 资源不能被抢占.
  4. 有循环等待.
  • Java并没有提供语言层面上的支持来避免死锁.要防止死锁的发生,只需破坏上述条件之一.

新类库中的构件

CountDownLatch:

用来同步一个或多个任务,强制他们等待由其他任务执行的一组操作的完成:

  1. CountDownLatch对象设置一个初始计数值,任何在这个对象上调用await()的方法都将阻塞,直至计数值达到0.
  2. 其他任务在结束其工作时,可以在该对象上调用countDown()来减小这个计数值.
  • CountDownLatch被设计为只能触发一次,计数值不能重置.

CyclicBarrier:

用于使所有并行任务在栅栏处列队,然后一致地向前移动.与CountDownLatch类似,但CyclicBarrier可以多次重用.

  • 可以向CyclicBarrier提供一个Runnable的"栅栏动作",当计数值达到0时自动执行.

DelayQueue:

一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,里面的对象只能到期时才能从队列中取走.
这种队列是有序的,对象头的到期时间最长.如果没有任何到期,那就不会有任何头元素.

PriorityBlockingQueue:

基础的具有阻塞读取操作的优先级队列.

ScheduledExecutor:

ScheduleThreadPoolExecutor提供了任务的定时执行功能,通过使用schedule()(运行一次)或者scheduleAtFixedRate()(每隔规则的时间重复执行),将Runnable对象设置为在将来的某个时刻执行.

Semaphore:

Semaphore(计数信号量)允许n个任务同时访问某个资源.(正常的锁在任何时刻只允许一个任务访问一项资源)

Exchanger:

Exchanger是在两个任务之间交换对象的栅栏.当任务进入栅栏前,任务各自拥有一个对象,离开栅栏时,他们拥有之前对方持有的对象.

性能调优:

(待补充)