什么是线程
1.将执行这个任务的代码放在一个类的run方法中,这个类要实现Runnable接口。Runnable接口非常简单,只有一个方法;
public interface Runnable
{
void run();
}
由于Runnable是一个函数式接口,可以用一个lambda表达式创建一个实例:
Runnable r = () ->{task code};
2.从这个Runnable构造一个Thread对象:
var t = new Thread(r);
3.启动线程:
t.start();
为了建立单独的线程来完成转账,我们只需要把转账代码放在一个Runnable的run方法中,然后启动一个线程:
Runnable r = () ->{
try
{
for (int i = 0;i < STEPS; i++)
{
double amount = MAX_AMOUNT * Math.random();
bank.transfer(0,1,amount);
Thread.sleep((int) (DELAY *Math.random()));
}
}
catch (InterruptedException e)
{
}
};
var t = new Thread(r);
t.start();
对于给定的步骤数,这个线程会转帐一个随机金额,然后休眠一个随机的延迟时间。
竞态条件详解
上一节运行了一个程序,其中有几个线程会更新银行账户余额。一段时间之后,不知不觉地出现了错误,可能有些钱会丢失,也可能几个账号同时有钱进账。当两个线程试图同时更新同一个账户时,就会出现这个问题。假设两个线程同时执行指令
accounts[to] += amount;
问题在于这不是原子操作。这个指令可能如下处理:
1.将accounts[to]加载到寄存器。
2.增加amount。
3.将结果写回accounts[to]。
现在,假定第一个线程执行步骤1和2,然后,它的运行权被抢占。再假设第2个线程被唤醒,更新account数组中的同一个元素。然后,第1个线程被唤醒并完成其第3步。
锁对象
有两种机制可防止并发访问代码块。Java语言提供了一个synchronized关键字来达到这一目的。synchronized关键字会自动提供一个锁以及相关的“条件”,对于大多数需要显式锁的情况,这种机制功能很强大,也很便利。
用ReentrantLock保护代码块的基本结构如下:
myLock.lock();//a ReentrantLock object
try
{
critical section
}
finally
{
my.Lock.unlock();//make sure the lock is unlocked even if an exception is thrown
}
下面使用一个锁来保护Bank类的transfer方法:
public class Bank
{
private var bankLock = new ReentranLock();
...
public void transfer(int from,int to,int amount)
{
bankLock.lock();
try
{
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf("%10.2f from %d to %d",amount ,from, to);
}
finally
{
bankLock.unlock();
}
}
}
假设一个线程调用了transfer,但是在执行结束前被抢占。再假设第二个线程也调用了transfer,由于第二个线程不能获得锁,将在调用lock方法时被阻塞。它会暂停,必须等待第一个线程执行完transfer方法。当第一个线程释放锁时,第二个线程才能开始运行。
条件对象
通常,线程进入临界区后却发现只有满足了某个条件之后它才能执行。可以使用一个条件对象来管理那些已经获得了一个锁却不能做有用工作的线程。
一旦一个线程调用了await方法,他就进入这个条件的等待集(wait set)。直到另一个线程在同一条件上调用signalAll方法。
应该什么时候调用signalAll呢?只要一个对象的状态有变化,而且可能有利于等待的线程,就可以调用signalAll。
例如,当一个账户余额发生改变时,而且可能有利于等待的线程就可调用。
public void transfer(int from,int to,int amount)
{
bankLock.lock();
try
{
while (accounts[from] < amount)
sufficientFunds.await();
//transfer funds
...
sufficientFunds.signalAll();
}
finally
{
bankLock.unlock();
}
}
注意signalAll调用不会立即激活一个等待的线程。它只是解除等待线程的阻塞,使这些线程可以在当前线程释放锁之后竞争访问对象。
synchronized关键字
总结:
锁用来保护代码片段,一次只能有一个线程执行被保护的代码。
锁可以管理试图进入被保护代码段的线程。
一个锁可以有一个或多个相关联的条件对象。
每个条件对象管理那些已经进入被保护代码段但还不能运行的线程。
如果一个方法声明时有synchronized关键字,那么对象的锁将保护整个方法。也就是说,要调用这个方法,线程必须获得内部对象锁。
换句话说,
public synchronized void method()
{
method body
}
等价于
public void method()
{
this.intrinsicLock.lock();
try
{
method body
}
finally { this.intrinsicLock.unlock();}
}
例如,可以用Java如下实现Bank类:
class Bank
{
private double[] accounts;
public synchronized void transfer(int from,int to,int amount)
throws InterruptedException
{
while (accounts[from] <amount)
wait();//wait on intrinsic object lock's single condition
accounts[from] -= amount;
accounts[to] += amount;
notifyAll();//notify all threads waiting on the condition
}
public synchronized double getTotalBalance() {...}
}
同步块
正如刚刚讨论的,每一个Java对象都有一个锁。线程可以通过调用同步方法获得锁。还有另一种机制可以获得锁:即进入一个同步块。
当线程进入如下形式的块时:
synchronized (obj)//this is the syntax for a synchronized
{
critical section
}
它会获得obj的锁。
有时我们会发现一些“专用”锁,例如:
public class Bank
{
private double[] accounts;
private var lock = new Object();
...
public void transfer(int from,int to,int amount)
{
synchronized(lock)//an ad-hoc lock
{
accounts[from] -= amount;
accounts[to] += amount;
}
System.out.println(...);
}
}
监视器概念
锁和条件是实现线程同步的强大工具,但是,严格来讲,它们不是面向对象的。
最成功的解决方案之一是监视器(monitor)。
监视器是包含私有字段的类。
监视器类的每个对象有一个关联的锁。
所有方法由这个锁锁定。换句话说,如果客户端调用obj.method(),那么obj对象的锁在方法调用开始时自动获得,并且当方法返回时,没有其他线程能够访问这些字段。
锁可以有任意多个相关联的条件。
volatile字段
volatile关键字为实例字段的同步访问提供了一种免锁机制。如果声明一个字段为volatile,那么编译器和虚拟机就知道该字段可能被另一个线程并发更新。
final变量
还有一种情况可以安全访问一个共享字段,即这个字段声明为final时。考虑以下声明:
final var accounts = new HashMap<String,Double>();
其他线程会在构造器完成构造之后才看到这个accounts变量。
如果不适用final,就不能保证其他线程看到的是accounts更新后的值,它们可能都只是看到null,而不是新构造的HashMap。
原子性
假设对共享变量除了赋值之外并不做其他操作,那么可以将这些共享变量声明为volatile。
如果有大量线程要访问相同的原子值,性能会大幅下降,因为乐观更新需要太多次重试。LongAdder和LongAccumulator类解决了这个问题。
如果预期可能存在大量竞争,只需要使用LongAdder而不是AtomicLong。方法名稍有区别。要调用increment让计数器自增,或者调用add来增加一个量,另外调用sum来获取总和。
var adder = new LongAdder();
for(...)
pool.submit(()->{
while(...){
...
if(...)adder.increment();
}
});
...
long total = adder.sum();
线程安全的集合
阻塞队列
很多线程问题可以使用一个或多个队列以优雅而安全的方式来描述。生产者线程向队列插入元素,消费者线程则获取元素。使用队列,可以安全地从一个线程向另一个线程传递数据。
当试图向队列添加元素而队列已满,或是想从队列移出元素而队列为空的时候,阻塞队列将导致线程阻塞。在协调多个线程之间的合作时,阻塞队列是一个有用的工具。工作线程可以周期性地将中间结果存储在阻塞队列中。其他工作线程移除中间结果,并进一步进行修改。队列会自动地平衡负载。
高效的映射、集和队列
java.util.concurrent包提供了映射、有序集和队列的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentLinkedQueue。
这些集合使用复杂的算法,通过允许并发地访问数据结构的不同部分尽可能减少竞争。
映射条目的原子更新
ConcurrentHashMap原来的版本只有为数不多的方法可以实现源自更新,这使得编程有些麻烦。
对并发散列映射的批操作
Java API为并发散列映射提供了批操作,即使有其他线程在处理映射,这些操作也能安全地执行。批操作会便利映射,处理遍历过程中找到的元素。这里不会冻结映射的当前快照。除非你恰好知道批操作运行时映射不会被修改,否则就要把结果看作是映射状态的一个近似。
任务和线程池
Callable与Future
Runnable封装一个异步允许的任务,可以把它想象成一个没有参数和返回值的异步方法。Callable与Runnable类似,但是有返回值。Callable接口是一个参数化的类型,只有一个方法call。
public interface Callable<V>
{
v call() throws Exception;
}
类型参数是返回值的类型。
Future保存异步计算的结果。可以启动一个计算,将future对象交给某个线程,然后忘掉它。这个future对象的所有者在结果计算好之后就可以获得结果。
执行器
执行器(Executors)类有许多静态工厂方法,用来构造线程池。
fork-join框架
假设有一个处理任务,它可以很自然地分解为子任务,如下所示:
if (problemSize<threshold)
solve problem directly
else
{
break problem into subproblems
recursively solve each subproblem
combine the results
}
异步计算
可完成Future
当有一个Future对象时,需要调用get来获得值,这个方法会阻塞,直到值可用。CompletableFuture类实现了Future接口,它提供了获得结果的另一种机制。你要注册一个回调,一旦结果可用,就会利用该结果调用这个回调。
CompletableFuture<String> f=...;
f.thenAccept(s -> Process the result string s);
通过这种方式,无须阻塞就可以在结果可用时对结果进行处理。
用户界面回调中的长时间运行任务
在程序中使用线程的理由之一是提高程序的响应性能。当程序需要做某些耗时的工作时,不能在用户界面线程完成这些工作,否则用界面会冻结。应该启动另一个工作线程。
进程
建立一个进程
首先指定你想要执行的命令。可以提供一个List<String>
,或者直接提供命令字符串。
var builder = new ProcessBuilder("gcc","myapp.c");
每个进程都有一个工作目录,用来解析相对目录名。默认情况下,进程的工作目录与虚拟机相同,通常是启动java程序的那个目录。可以用directory方法改变工作目录:
builder = builder.directory(path.toFile());