Java并发基础知识

初识Java并发

有互相关注的没,交个朋友



一、什么是线程

  多线程程序在较低的层次上扩展了多任务的概念:一个程序同时执行多个任务。通常,每一个任务称为一个线程(thread)。
  多进程与多线程有哪些区别呢?本质的区别在于每个进程拥有自己的一整套变量, 而线程则共享数据。
启动线程:
1 ) 将任务代码移到实现了 Runnable 接口的类的 run方法中。这个接口非常简单,只有 一个方法:

public interface Runnable {
    void run();
}

由于 Runnable 是一个函数式接口,可以用 lambda 表达式建立一个实例:

Runnable r = 0 -> { taskcode}; 

2 ) 由 Runnable 创建一个 Thread 对象:

Thread t = new Thread(r); 

3 ) 启动线程:

t.start();

二、如何中断线程

1 正常运行结束
2 interrupt 方法可以用来请求终止线程

Thread.currentThread().interrupt()

三、认识线程状态

要确定一个线程的当前状态, 可调用 getState 方法。
线程可以有如下 6 种状态:
线程状态关系图

1 New (新创建)
  当用 new 操作符创建一个新线程时,如 new Thread®,该线程还没有开始运行。这意味着它的状态是 new。

2 Runnable (可运行)
  一旦调用 start 方法,线程处于 runnable 状态。一个正在运行中的线程仍然处于可运行状态。
  一个可运行的线程可能正在运行也可能没有运行

3 Blocked (被阻塞)
  当一个线程试图获取一个内部的对象锁,而该 锁被其他线程持有, 则该线程进人阻塞状态。

4 Waiting (等待)
  当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。

5 Timed waiting (计时等待)
  有几个方法有一个超时参数。调用它们导致线程进人计时等待(timed waiting) 状 态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有 Thread.sleep 和 Object.wait、Thread.join、Lock.tryLock 以及 Condition.await 的计时版。

6 Terminated (被终止)
  线程因如下两个原因之一而被终止:
  1)因为run方法正常退出而自然死亡。
  2)因为一个没有捕获的异常终止了run方法而意外死亡。

四、认识线程属性

1 线程优先级
  默认情况下,一个线程继承它的父线程的优先级。可以用 setPriority 方法提高或降低任何一个线程的优先级。

2 守护线程
  可以通过调用 t.setDaemon(true); 将线程转换为守护线程。守护线程的唯一用途是为其他线程提供服务。
  例子:计时线程定时地发送信号给其他线程或清空过时的高速缓存项的线程。当只剩下守护线程时,虚拟机就退出了,由于如果只剩下守护线程,就没必要继续运行程序了。

3 未捕获异常处理器
  在线程死亡之前,异常被传递到一个用于未捕获异常的处理器。
  该处理器必须属于一个实现 Thread.UncaughtExceptionHandler 接口的类。这个接口只有— 个方法。

void uncaughtException(Thread t, Throwable e)

  可以用 setUncaughtExceptionHandler方法为任何线程安装一个处理器。
  也可以用 Thread 类的静态方法 setDefaultUncaughtExceptionHandler 为所有线程安装一个默认的处理器。

五、同步处理

  两个或两个以上的线程需要共享对同一数据的存取会导致并发问题,所以需要做同步处理。

1 锁对象
  代码结构:

myLock.lock(); // a ReentrantLock object 
try {
     critical section
} finally {
     myLock.unlock();// make sure the lock is unlocked even if an exception is thrown 
}

  这一结构确保任何时刻只有一个线程进入。一旦一个线程lock封锁了锁对象,当其他线程调用 lock 时,它们会被阻塞,直到第一个线程unlock释放锁对象。

2 条件对象
java.util.concurrent.locks.Lock类方法

Condition newCondition()    //返回一个与该锁相关的条件对象。

java.util.concurrent.locks.Condition类方法

void await()       //将该线程放到条件的等待集中。
void signalA11()   //解除该条件的等待集中的所有线程的阻塞状态。
void signal ()     //从该条件的等待集中随机地选择一个线程, 解除其阻塞状态。

3 synchronized 关键字
由1和2可知锁和条件对象的作用:
  1)锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
  2)锁可以管理试图进入被保护代码段的线程。
  3)锁可以拥有一个或多个相关的条件对象。
  4)每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。

  Java 中的每一个对象都有一个内部锁。如果一个方法用 synchronized关键字声明,那么对象的锁将保护整个方法。

public synchronized void method() {
    method body 
}

等价于

public void method() {
   this.intrinsidock.lock();
   try {
       method body
   } finally {
       this.intrinsicLock.unlock();
   }
}

  内部对象锁只有一个相关条件。wait 方法添加一个线程到等待集中,notifyAll /notify方 法解除等待线程的阻塞状态。
  调用 wait 或 notityAll 等价条件对象的intrinsicCondition.await(); intrinsicCondition.signalAll()

4 同步阻塞
  线程可以通过调用同步方法获得锁。
  当线程进入如下形式的阻塞:

    synchronized (obj) { // this is the syntax for a synchronized block 
        critical section 
    } 

  它就获得了 Obj 的锁。

5 Volatile域
  volatile关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为 volatile, 那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。
只读写一两个实例域就使用同步会使得性能消耗过大。如下段代码:

private boolean done; 
public synchronized boolean isDone() {
    return done; 
} 
public synchronized void setDone() { 
    done = true; 
}

于是可以使用volatile关键字:

private volatile boolean done; 
public boolean isDone(){
    return done; 
}
public void setDone(){
    done = true;
}

6 原子性
  原子是最小单位,指操作是不可再分的。
  java.util.concurrent.atomic中有一组使用无锁算法实现的原子操作类。
  如:AtomicInteger、AtomicBoolean、AtomicLong 外还有AtomicReference 。它们分别封装了对整数、整数数组、长整型、长整型数组和普通对象的多线程安全操作。
  这些都是居于CAS算法实现的。CAS即:Compare and Swap,是比较并交换的意思。
  如果有大量线程要访问相同的原子值,性能会大幅下降。
  Java SE 8 提供了 LongAdder 和 LongAccumulator类来解决这个问题。
  LongAdder包括多个变量(加数),其总和为当前值。
  LongAccumulator将这种思想推广到任意的累加操作。在构造器中,可以提供这个操作以及它的零元素。要加人新的值, 可以调用accumulate。调用 get 来获得当前值。
  此外DoubleAdder 和 DoubleAccumulator也采用同样的方式处理double值。

7 死锁(deadlock)
  锁和条件不能解决多线程中的所有问题。
  例如:
   账户 1: $200
   账户 2: $300
   线程 1: 从账户 1 转移 $300 到账户 2
   线程 2: 从账户 2 转移 $400 到账户 1
  推理可知线程 1 和线程 2 都会被阻塞。因为账户 1 以及账户 2 中的余额都不足以进行转账,两个线程都无法执行下去。
  因为每一个线程要等待更多的钱款存人而导致所有线程都被阻塞。这样的状态称为死锁。

8 锁测试与超时
java.util.concurrent.locks.Lock类
boolean tryLock()
尝试获得锁而没有发生阻塞;如果成功返回真。这个方法会抢夺可用的锁, 即使该锁 有公平加锁策略, 即便其他线程已经等待很久也是如此。
boolean tryLock(long time, TimeUnit unit)
尝试获得锁,阻塞时间不会超过给定的值;如果成功返回 true。
void lockInterruptibly()
获得锁,但是会不确定地发生阻塞。如果线程被中断,抛出一个 InterruptedException 异常

java.util.concurrent.locks.Condition
boolean await( long time, TimeUnit unit)
进人该条件的等待集, 直到线程从等待集中移出或等待了指定的时间之后才解除阻塞。如果因为等待时间到了而返回就返回 false, 否则返回 true。
void awaitUninterruptibly()
进人该条件的等待集, 直到线程从等待集移出才解除阻塞。如果线程被中断,该方法不会抛出 InterruptedException 异常。

9 读/写锁
下面是使用读 / 写锁的必要步骤:
1 ) 构造一个 ReentrantReadWriteLock 对象:

private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

2 ) 抽取读锁和写锁:

private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock(); 

3 ) 对所有的获取方法加读锁:

public double getTotalBalance(){
   readLock.lock(); 
   try { . . . } 
   finally {
     readLock.unlock(); 
   } 
}

4 ) 对所有的修改方法加写锁:

public void transfer(. . .) { 
    writeLock.lock(); 
    try { . . . } 
    finally {
      writeLock.unlock();
    } 
}

六、阻塞队列

  对于许多线程问题,可以通过使用一个或多个队列的方式将其形式化。生产者线程向队列插人元素,消费者线程则取出它们。
  当试图向队列添加元素而队列已满, 或是想从队列移出元素而队列为空的时候, 阻塞队列(blocking queue) 导致线程阻塞。
在这里插入图片描述
  如果将队列当作线程管理工具来使用, 将要用到上图的 put 和 take 方法。

七、Callable 与 Future

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

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

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

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

  get方法
  第一个 get 方法的调用会被阻塞,直到计算完成。
  如果在计算完成之前调用第二个get方法超时,会拋出一个 TimeoutException 异常。
  如果运行该计算的线程被中断,两个方法都将拋出IntermptedException。
  如果计算已经完成,那么get方法立即返回。

  isDone方法
  如果计算还在进行,isDone方法返回 false;如果完成了, 则返回 true。

  cancel方法
  可以用cancel 方法取消该计算。如果计算还没有开始,它被取消且不再开始。如果计算处于运行之中,那么如果 maylnterrupt 参数为 true, 它就会被中断。

3 FutureTask包装器
  FutureTask 包装器是一种非常便利的机制,可将 Callable 转换成 Future 和 Runnable, 它同时实现二者的接口。
  例子如下:

    Callable<Integer> nyComputation =...;
    FutureTask<Integer> task = new FutureTask<Integer>(myConiputation);
    Thread t = new Thread(task); //it's a Runnable t.start(); 
    Integer result = task.get();//it's a Future

八、执行器

  由于New一个线程需要与操作系统进行交互,所以会消耗性能。如果程序中有大量生命周期很短的线程,应该使用线程池去管理他们。一个线程池中包含许多准备运行的空闲线程。当线程的 run 方法退出时,线程不会结束,而是回收到线程池中准备为下一个请求提供服务。
  执行器(Executor) 类有许多静态方法用来构建线程池:
在这里插入图片描述
1 线程池
创建过程:
  1)调用 Executors 类中静态的方法 newCachedThreadPool 或 newFixedThreadPool创建线程池。
  2)调用 submit 提交 Runnable 或 Callable对象执行线程。
  3)如果想要取消一个任务,或如果提交 Callable 对象,那就可以保存返回的 Future 对象。
  4)当不再提交任何任务时,调用 shutdown或shutdownNow关闭线程池。

2 控制任务组
  使用执行器的实际意义是可以控制一组相关任务。例如,可以在执行器中使用 shutdownNow 方法取消所有的任务。

java.util.concurrent.ExecutorService
T invokeAny(Co11ection<Callable> tasks)
invokeAny方法提交所有对象到一个 Callable 对象的集合中,并返回某个已经完成了的任务的结果。
List<Future> invokeAll(Col 1ection<Callable> tasks )
invokeAll 方法提交所有对象到一个Callable 对象的集合中,并返回一个 Future 对象的列 表,代表所有任务的结果。

上面方法的缺点是如果第一个任务花去了很多时间,则该方法会进行等待。将结果按可获得的顺序保存起来更加有效。因此可以用 ExecutorCompletionService 类。
java.util.concurrent.ExecutorCompletionService
ExecutorCompletionService(Executor e )
构建一个执行器完成服务来收集给定执行器的结果。
Future submit(Cal 1able task )
Future submit(Runnable task, V result )

提交一个任务给底层的执行器。
Future take( )
移除下一个已完成的结果, 如果没有任何已完成的结果可用则阻塞。
Future poll( )
Future poll(1ong time, TimeUnit unit)

移除下一个已完成的结果, 如果没有任何已完成结果可用则返回 null。第二个方法将等待给定的时间。

九、同步器

  为线程之间的共用集结点模式(common rendezvous patterns) 提供的“预置功能”。
在这里插入图片描述

1 信号量
  一个信号量管理许多的许可证(permit)。为了通过信号量,线程通过调用 acquire请求许可。其实如果没有实际的许可对象, 信号量仅维护一个计数。许可的数目是固定的,由此限制了通过的线程数量。

2 倒计时门栓
  一个倒计时门栓(CountDownLatch) 让一个线程集等待直到计数变为 0。倒计时门栓是一次性的。一旦计数为 0,就不能再重用了。
  例如,假定一个线程集需要一些初始的数据来完成工作。工作器线程被启动并等候。另一个线程准备数据。当数据准备好的时候, 调用 countDown, 所有工作器线程就可以继续运行了。

3 障栅
  CyclicBarrier 类实现了一个集结点(rendezvous) 称为障栅(barrier)。如果有不同的线程在计算同一个运算的不同部分时。当所有线程都计算好时,需要把结果组合在一起。当一个线程完成了它的那部分任务后, 就会运行到障栅处等待。一旦所有的线程都到达了这个障栅,障栅就撤销,线程就可以继续运行。

4 交换器
  当两个线程在同一个数据缓冲区的两个实例上工作的时候, 就可以使用交换器 ( Exchanger)
  例如,一个线程向缓冲区填人数据,另一个线程消耗这些数据。当它们都完成以后,相互交换缓冲区。

5 同步队列
  同步队列是一种将生产者与消费者线程配对的机制。当一个线程调用 SynchronousQueue 的 put 方法时,它会阻塞直到另一个线程调用 take方法为止,反之亦然。与 Exchanger 的情 况不同, 数据仅仅沿一个方向传递,从生产者到消费者。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值