Book(5) Java高并发程序设计

160 阅读8分钟
基础概念
  • 同步和异步: 同步方法一旦调用,必须要等到方法调用返回后才能继续后续操作。而异步方法通常会在另一个线程中“真实”的执行,整个过程不会阻塞调用者的工作。

  • 并发和并行: 并行是真正意义上的同时执行,而对于并发来说,系统可能在不同任务之间来回切换,对于外部观察者来说,任务是在“同时执行”的。

  • 临界区: 表示一种公共资源或者共享数据,可以被多个线程使用。但是每一次只有一个线程使用它,一旦临界区资源被占用,其他线程就必须等待。

  • 阻塞和非阻塞: 如果一个线程占用了临界区的资源,那么其他所有线程就必须等待,等待会导致线程挂起,这种情况就是阻塞。如果占用资源的线程一直不愿意释放资源,那么其他阻塞在这个临界区的线程都不能工作。非阻塞则相反,任何一个线程都不会妨碍其他线程执行,所有线程都不断尝试继续执行。

  • 死锁、饥饿和活锁: 都是属于多线程活跃性的问题。死锁是指线程之间相互占用了彼此的资源,互相导致其他线程得不到足够资源而无法继续执行。 饥饿是指一个活着多个线程因为种种原因无法获得所需的资源,可能是优先级太低,一直被其他线程抢占了资源等。

  • 活锁: 资源来回在不同线程之间跳动,但是没有一个线程拿到了足够的资源继续执行。

并发级别

阻塞:一个线程是阻塞的,那么在其他资源释放之前,当前线程无法执行。 无饥饿:所有线程都有机会公平获得资源,不存在低优先级的线程一直得不到资源 无障碍:彼此都可以访问临界资源,一旦发生冲突就回滚。 无锁:所有线程都可以对临界区资源进行访问,如果有冲突会回滚,但是能保证总有一个线程能在有限步内完成操作。 无等待:要求所有线程必须在有限步内完成操作。

两个并行的重要定律
  • Amdahl定律: 加速比 = 优化钱系统耗时/优化后系统耗时 CPU的数量越多,串行比例越低,优化效果越好
  • Gustafson定律 加速比 = n - F(n-1) F表示串行比例,串行比例越低,优化效果越好。
Java内存模型(JMM)
  • 原子性:表示一个操作是不可中断的,即使多个线程一起执行的时候,一个操作一旦开始,就不糊被其他线程干扰。
  • 可见行:是指一个线程修改了一个共享变量的值时,其他线程能否立即找到这个修改。
  • 有序性:串行语义的有序,但是不同线程的指令可能会不同顺序执行,但是不会影响最终执行结果。
哪些指令不能重排?Happen-Before原则
  • 程序顺序原则:一个线程内保证语义的串行性。
  • volatile规则:volatile变量的写先于读发生,这保证了volatile变量的可见行
  • 锁规则:解锁必然发生在加锁前。
  • 传递性:a 先于 b, b 先于 c,那么a 先于c
  • 线程的start()方法先于它的每一个动作。
  • 线程所有操作先于线程的终结。
  • 线程的中断先于被中断线程的代码
  • 对象的构造函数的执行,结束 先于finalize方法
线程的基本操作
  • 新建线程: Thread t1 = new Thread(); t1.start();

  • 终止线程: t1.stop();一个被废弃的方法,可能会引起数据一致性问题。

  • 等待(wait)和通知(notify) 属于Object类的方法,任何对象都可以调用这两个方法。A线程调用了.wait() 方法后,必须要等到其他方法调用.notify()方法后才能继续执行。如果多个线程都在wait。那么notify会随机选择一个唤醒。如果调用notifyAll() 会将所有的线程都唤起。但是notify和wait都必须包含在synchronized关键词中。也就是获得对象的监视器。

  • 挂起(suspend)和继续执行(resume)线程 不推荐使用suspend是因为suspend方法在导致线程暂停访问的同时,并不会释放任何资源。直到其他线程执行了resume方法,被挂起的线程才能继续执行。如果resume方法在suspend方法之前意外的执行了。那么被挂起的线程就很难有机会被唤醒了,但是依然占据资源。

  • join和yeild 一个线程执行了yeild后,会无限等待,知道另一个线程执行了join

  • volatile和Java内存模型(JMM) voletile用于保持变量的可见行,但是无法保证符合操作的变量的原子性。 synchronized关键字 通过synchronized可以解决volitile无法保证符合操作的原子性的问题。 它的作用时保证线程之间的同步,对同步代码进行加锁,使得每一次都只有一个线程进入同步块,从而保证线程的安全性。 可以作用于指定加锁对象,直接作用于实例方法或者直接作用于静态方法。

  • 并发下的ArrayList 如果两个线程同时向arraylist添加大量内容,有可能会正确执行。也有可能会跑出错误,因为两个线程可能同时对一个位置进行赋值导致的。可以使用Vector代替ArrayList。

  • HashMap 如果两个线程同时向HashMap进行put操作。那么可能会有3种情况 1,正确执行 2,执行结束,实际数量比预期小 3,程序永远无法执行结束。多线程冲突导致链表成环。 可以用ConcurrenctHashMap代替HashMap

  • 可重入锁ReentrantLock .lock() 加锁 .unlock() 解锁 .lockInterruptibly() 获得锁,但是优先响应中断 .tryLock() 限时等待

  • Condition接口 可以利用Condition对象,让线程在合适的时间等待,或者在某一个特定的时刻得到通知继续执行。 .await() 让当前线程等待,并释放当前锁,当其他线程中使用signal方法或者signalAll方法时,线程就会重新获得锁并继续执行。线程被中断时,可以跳出等待。 .awaitUninterruptibly() .signal() 唤醒一个等待中的线程 .sianglAll() 唤醒所有等待中的线程

  • 信号量Semaphore 可以限制同时有多个线程可以获得锁。 .acquire() 尝试后的一个许可的准入 .acquireUninterruptibly() 尝试获得一个许可的准入,并且不相应中断 boolean .tryAcquire() 尝试获取一个许可的准入,成功返回true,失败返回false 并不会进行等待。 boolean .tryAcquire(long timeout) .release() 线程访问资源后释放一个许可

  • ReadWriteLock 读写锁 如果所有线程对临界区都是读,那么就不会阻塞

  • CountDownLatch 倒计时器 等待所有前置任务都完工后,开始执行

  • CyclicBarrier 循环栅栏 相当于循环计数的倒计时器

  • LockSupport工具 park() unpark() park如果执行在unpark之后,还可以获得许可,但是最多保留一个许可。

线程池

在juc包中,ThreadPoolExceutor 表示一个线程池,Executor类则扮演了线程池工厂的角色,通过Executors可以取得一个拥有特定功能的线程池。ThreadPoolExecutor类实现了Executor接口,任何Runnable的对象都可以被ThreadPoolExecutor线程池调度。 Executor矿机爱提供了各种类型的线程池,主要有以下工厂方法:

public static ExecutorService newFixedThreadPool(int nThreads)

返回一个固定线程数量的线程池。新的任务提交时,线程池中若有空闲的线程,则立即执行,若没有,新的任务就会被暂时存放在一个任务队列中,待有线程空闲时,便处理任务队列中的任务

public static ExecutorService newSingleThreadExecutor()

该方法返回一个只有一个线程的线程池。若多余一个任务被提交到改线程池,任务就会被保存在一个任务队列中,待线程空闲时,按先入顺序一次执行

public static ExecutorService newCachedThreadPool()

返回一个根据实际情况调整线程数量的线程池,线程池的数量不稳定,但若有空闲线程可以反复使用,则优先使用可复用的线程,若所有线程都在工作,又有新的任务提交,则创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

public static ScheduledExecutorService newSingleThreadScheduledExecutor()

返回一个ScheduledExecutorServer对象,线程池大小为1, ScheduledExecutorService接口在ExecutorService上有扩展了在给定时间执行任务的功能,比如在某个固定的时间之后执行,或者周期性的执行

public static ScheduleExecutorService newScheduledThreadPool(int corePoolSize)

该方法也返回一个ScheduledExecutorService对象,但是可以执行线程数量。 newScheduledThreadPool() 返回一个ScheduledExecutorService对象,可以根据时间对线程进行调度。 它有两个方法 scheduleAtFixedRate() 每隔一定时间执行一次。 scheduleAtFixedDelay() 每次任务之间间隔固定时长。

有助于提高锁的几点建议
  • 减少持锁时间
  • 减少持锁力度
  • 使用读写锁来替换独占锁
  • 锁分离
  • 锁粗化