java核心技术卷1::并发

本文深入探讨了Java中的并发编程,包括多线程的创建与中断,线程状态,同步机制如锁对象、条件对象、synchronized关键字,以及volatile变量和final域的作用。此外,还介绍了线程局部变量、阻塞队列、Callable和Future,以及执行器(ExecutorService)的使用,强调了线程池在并发编程中的重要性。
摘要由CSDN通过智能技术生成

并发

多线程

// Thread 类
static void sleep(long millis) // 休眠给定的毫秒数
// 需要捕获 sleep 方法可能抛出的异常 InterruptedException。 当异常发生时,run方法将结束执行。
在一个单独的线程中执行一个任务的过程:
  • 将任务代码移到实现了 Runnable 接口的类的 run 方法中。
    public interface Runnable{
    	void run(); }
    
    由于 Runnable 是一个函数式接口,可以用 lambda 表达式建立一个实例:
    Runnable r = () -> { task code };
    
  • 由 Runnable 创建一个 Thread 对象:
    Thread t = new Thread(r);
    
  • 启动线程:
    t.start();
    

中断线程

没有强制线程终止的方法。然而,interrupt 方法可以用来请求终止线程。
当对一个线程调用 interrupt 方法时,线程的中断状态将被置位,这是每一个线程都具有的 boolean 标志。每个线程都应该不时地检查这个标志,以判断线程是否被中断。
要想弄清中断状态是否被置位,首先调用静态的 Thread.currentThread 方法获得当前线程,然后调用 isInterrupted 方法:
while (!Thread.currentThread().isInterrupted() && more work to do) {
	do more work ;}
但是,如果线程被阻塞,就无法检测中断状态。这是产生 InterruptedException 异常的地方。
当在一个被阻塞的线程(调用 sleep 或 wait)上调用 interrupt 方法时,阻塞调用将会被异常中断。
// 线程将简单地将中断作为一个终止的请求。
Runnable r = () -> {
	try {
		...
		while (!Thread.currentThread().isInterrupted() && more work to do) {
			do more work
		}
	} catch (InterruptedException e) {
		// thread was interruped during sleep or wait
	} finally {
		cleanup, if required
	}
	// exiting the run method terminates the thread
};
避免在每次工作迭代之后都调用 sleep 方法(或者其他的可中断方法),isTnterrupted 检测将会失去用处。
Thread.interrupted(); // 检测当前线程是否被中断,同时清除中断状态,置false。
Thread.currentThread().isInterrupted(); // 检测对象线程是否被中断,不改变中断状态,中断时为ture。
Thread.currentThread().interrupt(); // 向线程发送中断请求。
Thread.currentThread(); // 返回代表当前执行线程的 Thread 对象。

线程状态

New(新建),Runnable(可运行),Blocked(被阻塞),
Waiting(等待),Timed waiting(计时等待),Terminated(被终止)
  • 新建线程

    当用 new 操作符创建一个新线程时,该线程处于新创建状态时,程序还没有开始线程中的代码。

  • 可运行线程

    一旦调用 start 方法,线程处于 runnable 状态。一个可运行的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。

  • 被阻塞线程和等待线程

    当线程处于被阻塞或等待状态时,它暂时不活动。它不运行任何代码且消耗最少的资源。直到线程调度器重新激活它。细节取决于它是怎样达到非活动状态的:

    • 当一个线程试图获取一个内部的对象锁,而该锁被其他线程持有,则该线程进入阻塞状态。当所有其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞状态。
    • 当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。
    • 有几个方法有一个超时参数,调用它们导致线程进入计时等待状态,这一状态将一直保持到超时期满或者接收到适当的通知。
  • 被终止的线程
    • 因为 run 方法正常退出而自然死亡。
    • 因为一个没有捕获的异常终止了 run 方法而意外死亡。

线程属性

  • 线程优先级
    void setPriority(int newPriority) // 设置优先级,在 MIN 和 MAX 之间。
    static int MIN_PRIORITY // 最小优先级,为1
    static int MAX_PRIORITY // 最大优先级,为10
    static int NORM_PRIOTIY // 默认优先级,为5
    static void yield() // 导致当前执行线程处于让步状态,如果存在同等优先级的线程,当前线程将在之后调度
    
  • 守护线程
    t.setDaemon(boolean isDaemon) // 线程启动之前调用。
    

    守护线程的唯一用途是为其他线程提供服务。当只剩下守护线程,虚拟机就退出了。而守护线程应该永远不去访问固有资源,因为它随时会中断。

  • 未捕获异常处理器

    线程的 run 方法不能抛出任何受查异常,而非受查异常会导致线程终止。但在线程死亡之前,异常被传递到一个用于未捕获异常的处理器。

    该处理器必须属于一个实现 Thread.UncaughtExceptionHandler 接口的类。

    void uncaughtException(Thread t, Throwable e)
    
    • 可以用 setUncaughtExceptionHandler 方法为任何线程安装一个处理器。

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

    • 替换处理器可以使用日志 API 发送未捕获异常的报告到日志文件。

    • 如果不为独立的线程安装处理器,此时的处理器就是该线程的 ThreadGroup 对象。

      ThreadGroup 类实现该接口,它的 uncaughtException 方法被调用。

      • 如果该线程组有父线程组,那么父线程组的未捕获处理器方法被调用。
      • 否则,如果默认处理器非空,则调用。
      • 否则,如果 Throwable 是 ThreadDeath 的一个实例,什么都不做。
      • 否则,线程的名字以及 Throwable 的栈轨迹被输出到 System.err 上。

同步

当两个或两个以上的线程需要共享对同一数据的存取,会发生竞争条件。
锁对象
// ReetrantLock 锁对象保护代码块基本结构:
myLock.lock(); // 获取锁
try {
	critical section 
} finally { // 确保锁一定会被释放,否则其他线程将永远阻塞
	myLock.unlock(); // 释放锁
}

这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过 lock 语句。当其他线程调用 lock 时,它们被阻塞,直到第一个线程释放锁对象。


条件对象

通常,线程进入临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。

假设,线程存在一个不满足运行条件的情况,需要另一个线程改变情况,但此时第一个正在阻塞中,尚未释放锁,其他线程无法对其进行操作。此时,就可以使用条件对象。

  • 一个锁对象可以有一个或多个相关的条件对象。newCondition 方法获取一个条件对象。
  • 在不满足运行条件是,调用条件对象的 await 方法,线程将阻塞并放弃锁。
  • 一旦一个线程调用 await 方法,他进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞,直到另一个线程调用同一条件上的 signalAll 方法为止。
  • 这一调用重新激活因为这一条件而等待的所有线程。当这些线程从等待集中移除时,它们再次成为可运行的,调度器将再次激活他们。
  • 同时,它们将试图重新进入该对象,一旦锁成为可用的,它们中的某个将从 await 调用返回,获得该锁并从被阻塞的地方继续执行。
  • 此时,线程应该再次测试该条件,signalAll 方法仅仅是通知正在等待的线程:此时有可能已经满足条件,值得再次去检测该条件。
// 对 await 的调用应该在如下形式的循环体中
while(!(ok to proceed))
	condition.await();
该何时调用 signalAll 呢?在对象的状态有利于等待线程的方向改变时调用 signalAll。
调用 signalAll 不会立即激活一个等待线程。它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法之后,通过竞争实现对对象的访问。
调用 signal, 将随机解除等待集中某个线程的阻塞状态。这将更高效。

  • 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码
  • 锁可以管理试图进入被保护代码段的线程
  • 锁可以拥有一个或多个相关的条件对象
  • 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程

synchronized 关键字

// 内部对象锁将保护整个方法
public synchronized void method() { method body; }
// 等价于
public void method() { 
	Lock.lock(); 
	try { method body; }
	finally { Lock.unlock(); }
	}
// 内部对象锁只有一个相关条件,方法都在 Object 对象中
wait() // 添加一个线程到等待集中
notifyAll() // 解除等待线程的阻塞状态

将静态方法声明为 sychronized 时,该方法获得相关的类对象的内部锁。而没有其他线程可以调用同一个类的这个或任何其他的同步静态方法。

内部锁和条件的局限:

  • 不能中断一个正在试图获得锁的线程。
  • 试图获得锁时不能设定超时。
  • 每个锁仅有单一的条件,可能是不够的。

同步阻塞

通过进入一个同步阻塞,也可以获得锁

// 客户端锁定
synchronized (obj) {
	critical section}
// 于是它获得 obj 的锁

监视器

  • 监视器是只包含私有域的类
  • 每个监视器类的对象有一个相关的锁
  • 使用该锁对所有的方法进行加锁,方法调用开始时自动获得,并且当方法返回时自动释放该锁。
  • 该锁可以有任意多个相关条件

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; }

volatile 变量不能提供原子性,不能保证读取、翻转和写入不被中断。

final变量

将一个域声明为 final 时,其他线程会在构造函数完成构造后才看到这个变量。
如果不使用,就不能保证其他线程看到的是变量更新后的值。

原子性

原子性即一条语句只包含对变量的读取,修改,写入,三者之一的操作。非原子性的操作可能在其中一步正在进行的时候,线程中断。

原子方式操作可以保证即使是多个线程并发地访问同一个实例,也会计算并返回正确的值。
// AtomicInteger 类
public static AtomicLong largest = new AtomicLong();
incrementAndGet // 原子方式整数自增
decrementAndGet // 原子方式整数自减

compareAndGet // 原子方式复杂更新
do {
	oldValue = largest.get();
	newValue = Math.max(oldValue, observed);
} while (!largest.compareAndSet(oldValue, newValue));

如果另一个线程也在更新 largest,就可能阻止这个线程更细。这样,compareAndSet 会返回 false,而不会设置新值。
// 不需要循环的方式
largest.updateAndGet(x -> Math(x, observed)); // lambda 更新
largest.accumulateAndGet(observed, Math::max); // 二元操作符更新
largest.getAndUpdate(); // 返回原值
largest.getAndAccumulate(); // 返回原值

// 解决大量线程访问相同原子值导致性能下降的问题。
--- LongAdder 自增
final LongAdder adder = new LongAdder(); // 可能存在大量竞争时
for (...)
	pool.submit(() -> {
		while (...) {
			...
			if (...) adder.increment(); // 计数器自增
			// adder.add(value); // 增加一个量
		}
	});
...
long total = adder.sum(); // 获取总和
--- LongAccumulator 任意累加
LongAccumulator adder = new LongAccmulator(Long::sum, 0);
adder.accumulate(value);


线程局部变量

// 为每个线程构造一个实例,静态变量线程共享,可能会被并发的访问破坏。
public static final ThreadLocal<SimpleDateFormat> dateFormat = 
	ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
String dateStamp = dateFormat.get().format(new Date()); // 访问具体的格式化方法。

在一个给定线程中首次调用 get 时,会调用 initialValue 方法。在此之后,get 方法会返回属于当前线程的那个实例。
同时,如果多个线程需要等待一个共享的随机数生成器,这将会很低效。
// 线程独立生成器
int random = ThreadLocalRandom.current().nextInt(upperBound);


锁测试与超时

线程在调用 lock 方法来获得另一个线程所持有的锁的时候,很可能发生阻塞。
// tryLock 方法试图申请一个锁,在成功获得锁后返回 true,否则,立即返回 false,而且线程可以立即离开去做其他事情。
if (myLock.tryLock()) {
	try {...}
	finally {myLock.unlock();}
} else ....
// 可以调用 tryLock 时,使用超时参数:
if (myLock.tryLock(100, TimeUnit.MILLISECONDS)) ...
// 调用带有超时参数的 tryLock,那么如果线程在等待期间被中断,将抛出 InterruptedException 异常。
// 允许程序打破死锁
lockInterruptibly // 相当于超时设为无限的 tryLock 方法
// 等待一个条件时
myCondition.await(100, TimeUnit.MILLISECONDS) ...
// 如果一个线程被另一个线程通过调用 signalAll 或 signal 激活,或者超时时限已达到,或者线程被中断,那么 await 方法将返回。


读 / 写锁

--- ReentrantReadWriteLock 读者线程与写者线程共享访问互斥
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private Lock readLock = rwl.readLock(); // 读锁
private Lock writeLock = rwl.writeLock(); // 写锁
// 对所有的获取方法加读锁:
public double getTotalBalance() {
    readLock.lock();
    try {...}
    finally { readLock.unlock(); }
}
// 对所有的修改方法加写锁:
public double transfer(...) {
    writeLock.lock();
    try {...}
    finally { writeLock.unlock(); }
}


阻塞队列 (指令非同步)

对于许多线程问题,可以通过使用一个或多个队列以优雅且安全的方式将其形式化。
  • 当试图向队列添加元素而队列已满,或是想从队列移出元素而队列为空时,阻塞队列导致线程阻塞。
  • 工作者线程可以周期性的将中间结果存储在阻塞队列中,其他的工作者线程移出中间结构并进一步加以修改。
  • 生产者线程向队列添加修改指令,消费者线程阻塞直到从队列中取出指令,操作对象修改。
阻塞队列方法分为三类,这取决于当队列为满或空时它们的响应方式:
  • 如果将队列当做线程管理工具来使用,将要用到 put 和 take 方法;
  • 当试图向满的队列中添加或从空的队列中移出元素时,add、remove 和 element 操作抛出异常。
  • 在一个多线程程序中,队列会在任何时候空或满,因此,一定要使用 offer、poll 和 peek 方法作为代替。
阻塞队列变种:
  • LinkedBlockingQueue,容量可以无上界
  • LinkedbLockingDeque,双端版本
  • ArrayBlockingQueue,定量版本
  • PriorityBlockingQueue,带优先级的队列,无上限,如果队列为空,取元素操作将阻塞。
  • DelayQueue,包含 Delayed 接口的对象 ,getDelay 返回对象残留延迟,负值即延迟结束。元素只有在延迟用完的情况下才能从队列移出。还必须实现 compareTo 方法,队列使用该方法对元素排序。
  • LinkedTransferQueue,实现了TransferQueue接口,允许生产者线程等待,直到消费者准备就绪可以接收一个元素。如果生产者调用 tranfer(item) 方法,这个调用会阻塞。

Callable 和 Future

Callable 与 Runable (无参无返回值)类似,但是有返回值:
public interface Callable<V> {
	V call() throws Exception;}

类型参数是返回值的类型
Future 保存异步计算的结果。可以启动一个计算,将 Future 对象交给某个线程,然后忘掉他。Future 对象的所有者在结果计算好之后就可以获得它。
public interface Future<V> {
	V get() throws ...; // 调用被阻塞,直到计算完成。中断抛出异常。计算完成立即返回。
	V get(long timeout, TimeUnit unit) throws ...; // 超时或中断抛出异常
	void cancel(boolean mayInterrupt); // 取消计算
	boolean isCancelled(); // 取消状态 
	boolean isDone; // 计算进行状态
}

// FututeTask 包装器可将 Callable 转换成 Future 和 Runnable,它同时实现二者的接口。
Callable<Integer> myComputation = ...;
FutureTask<Integer> task = new FutureTasl<Integer>(myComputation);
Thread t = new Thread(task); // Runnable
t.start();
...
Integer result = task.get(); // Future


执行器

使用线程池的理由:
  • 构建一个新的线程需要与操作系统交互,如果程序中创建了大量的生命期很短的线程,就应该使用线程池。一个线程池中包含许多准备运行的空闲线程。将 Runnable 对象交给线程池,就会有一个线程调用 run 方法。当 run 方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。
  • 减少并发线程的数目。创建大量线程会大大降低性能甚至使虚拟机奔溃。如果有一个会创建许多线程的算法,应该使用一个线程数“固定的”线程池以限制并发线程的总数。
执行器(Executor)类有许多静态工厂方法来构建线程池:
  • newCachedThreadPool,必要时创建新线程;空闲线程会被保留 60s。

    对于每个任务,如果有空闲线程可用,立即让它执行任务,如果没有可用的空闲线程,则创建一个新线程。

  • newFixedThreadPool,该池包含固定数量的线程;空闲线程会一直被保留。

    如果提交的任务数多于空闲的线程数,那么把得不到服务的任务放置到队列中。当其他任务完成以后再运行它们。

  • newSingleThreadExecutor,只有一个线程的“池”,该线程顺序执行每一个提交的任务。

    由一个线程执行提交的任务,一个接一个。

以上三个方法将返回实现了 ExecutorService 接口的 ThreadPoolExecutor 类的对象。
// 用下面方法之一将一个 Runnable 对象或 Callable 对象提交给 ExecutorService。
// 该池会在方便的时候尽早执行提交的任务。提交的同时会得到一个 Future 对象,可以用来查询该任务的状态。
Future<?> submit(Runnable task)  // 可调用 isDone、cancel 或 isCancelled。但 get 方法在完成是只是返回null
Future<T> submit(Runnable task, T result) // get 方法在完成时返回指定的 result 对象。
Future<T> submit(Callable<T> task) // 返回的 Future 对象将在计算结果准备好的时候得到他。

  • newScheduledThreadPool,用于预定执行而构建的固定线程池。
  • newSingleThreadScheduledExecutor,用于预定执行而构建的单线程池。

    ScheduledExecutorService接口具有为预定执行或重复执行任务而设计的方法,

当用完一个线程池的时候,调用 shutdown,将启动该池的关闭序列,不再接受新的任务。所有任务完成后,线程池中的线程死亡。
调用shutdownNow,该池取消尚未开始的所有任务并试图中断正在运行的线程。
在使用连接池时应该做的事:
  • 调用 Executors 类中静态的方法 newCachedThreadPool 或 newFixedThreadPool。
  • 调用 sumbit 提交 Runnable 或 Callable 对象。
  • 如果想要取消一个任务,或如果提交 Callable 对象,那就要保存好返回的 Future 对象。
  • 当不在提交任何任务时,调用 shutdown。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值