什么是线程
多线程——类似于计算机多任务,与多进程相比,多线程的特点是共享数据(会产生一些风险,但使得线程之间的通信要比进程之间的更容易)、比进程更加轻量级。创建撤销的开销要远小于对进程的开销。
先来看看一个使用两个线程的简单程序,它可以在银行账户之间完成资金转账:
package threads;
import java.util.*;
/**
* A bank with a number of bank accounts
*/
public class Bank
{
private final double [] accouts;
/**
*Constructs the bank
*@param n the number of accouts
*@param initialBalance the initial balance for each accout
*/
public Bank(int n,double initialBalance)
{
accounts = new double[n];
Arrays.fill(accounts,initialBalance);
}
/**
*Transfers money from one account to another
*@param from the account to transfer from
*@param to the account to transfer to
*@param amount the amount to transfer
*/
public void transfer(int form,int to,double amount)
{
if(accounts[from]<amont)return;
System.out.print(Thread.currentThread());
accounts[from]-=amount;
System.out.printf(" %10.2f from %d to %d",amount,from,to);
accouts[to]+=amount;
System.out.printf(
" Total Balance:%10.2f%n",getTotalBalance());
}
/**
*Gets the sum of all account balances
*@return the total balance
*/
public double getTotalBalance()
{
double sum = 0;
for(double a:accounts)
sum+=a;
return sum;
}
/**
*Gets the number of accounts in the bank
*@return the number of accounts
*/
public int size()
{
return accounts.length;
}
}
transfer方法将一定金额从一个账户转移到另一个账户。
下面是使用一个单独的线程来运行一个任务的简单过程:
- 将执行这个任务的代码方在一个类的run方法中,这个类要实现Runnable接口:
public interface Runnable
{
void run();
}
这是一个函数式接口,所以可以使用一个lambda表达式创建一个实例:
Runnable r = ()->{ code};
- 从这个Runnable创造一个Thread对象:
var t = new Thread (r);
- 启动线程:
t.start();
为了启动单独的线程来完成转账,我们只需要将转账代码放在一个Runnable的run方法里,然后启动一个线程:
Runable r = ()->{
try
{
for(int 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();
对于给定的步骤数,这个线程会转账一个随机金额,然后休眠一个随机的延迟时间。
两个线程会同时进行任务,一般会交错的输出结果。
另一种定义线程的方法是建立Thread的一个子类:
class MyThread extends Thread
{
public void run()
{
code
}
}
然后可以构建子类的对象并调用start方法,不过现在不再推荐这么做。应当把要并行的任务与运行机制解耦合、如果多个任务,为每个任务分别创建一个单独的线程开销会很大,此时应当使用一个线程池(见后文)
如下同时进行0->1,2->3的账户转账:
package threads;
/**
*@version 1.0
*@author 乐乐想学会Java
*/
public class ThreadTest
{
public static final int DELAY = 10;
public static final int STEPS = 100;
public static final double MAX_AMOUNT = 1000;
public static void main(String[]args)
{
var bank = new Bank(4,100000);
Runnable task1=()->
{
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)
{
}
};
Runnable task2=()->
{
try
{
for(int i=0;i<STEPS;i++)
{
double amount = MAX_AMOUNT * Math.random();
bank.transfer(2,3,amount);
Thread.sleep((int)(DELAY * Math.random()));
}
}
catch(InterruptedException e)
{
}
};
new Thread(task1).start();
new Thread(task2).start();
}
}
@!警告:不要调用Runnable或Thread类的run方法!因为它们没有地洞新的线程,必须要调用Thread.start()方法,这会创建一个执行对应run方法的线程!
——————————————————————————
线程状态:
线程拥有的六种状态:
- New 新建
- Runnable 可运行
- Blocked 阻塞
- Waiting 等待
- Timed waiting 计时等待
- Terminated 终止
要获取一个线程的当前状态使用getState方法
——————————————————————————
java.lang.Thread
方法 | 功能 |
---|---|
Thread(Runnnable target) | 构造一个新线程,调用指定目标的run()方法 |
void start() | 启动这个线程,从而调用run()方法。这个方法会立即返回。新线程会并发运行 |
void run() | 调用相关Runnable的run方法 |
static void sleep(long millis) | 休眠指定的毫秒数 |
———————————————————————————
状态分析:
- 新建:当使用new操作符创建一个线程时,如new Thread(r),这个线程还没开始运行。这意味着它的状态处于新建。当一个线程处于新建状态时,程序还没有开始运行线程中的代码,而是先做基础准备。
- 可运行线程:字面意思
- 阻塞和等待线程:当线程处于阻塞或等待状态时,它暂时是不活动的。它不运行任何代码。而且消耗最少的资源,此时需要线程调度器重新激活这个线程,具体细节取决于它是如何达到非活动状态的。
1 .当一个线程试图获取一个内部的对象锁(而不是java.util.concurrect库中的Lock),而这个锁目前被另一个对象占着,那么这个线程就会被阻塞,当所有的其他线程都释放了这个锁,并且线程调度器允许线程持有这个锁时,它将变成非阻塞状态。
2 .当线程等待另一个线程通知调度器出现一个条件时,这个线程会进入等待状态。我们会在12.4.4节讨论条件。调用Object.wait 方法或者
Thread.join方法,或者是等待java、util.concurrent库中的Lock或者Condition时,就会出现这种情况,类似于阻塞状态。
3 .有几个方法有超时参数,调用这些方法会使得进程进入计时等待状态。这一状态将一直保持到超时期慢或者接受开始通知。
简要概括就是阻塞需求锁,等待等通知,计时等定时或通知。
- 终止线程:线程将因为以下两个原因之一被终止:
1 run方法自然退出,线程自然终止
2 应为一个没有被补货的异常终止了run方法,使线程意外终止。
—————————————————————————————
线程属性
中断线程
在java早期版本中stop方法用于终止一个线程,但是现在已经被废
弃,现在没有任何方法可以用于强制终止一个进程。
但是interrupt方法可以用来请求终止一个线程
当对一个线程调用interrupt方法时,就会设置线程的“中断状态”。这是每个线程都会有的boolean标示,每个线程都会不时检查这个标志判断线程是否被中断。
对于中断状态的检查,首先调用静态Thread.currentThread方法获取当前线程,再采用isIntertupted方法:
while(!Thread.currentThread().isInterrupted()//&&more work
)
{
//do more
}
但是一个被阻塞的线程无法检查中断状态。所以一般在一个被sleep或者wait调用的阻塞的线程上调用interrupt方法时,那个阻塞调用将会被一个InterruptedException异常中断。中断的线程不一定要被终止。不过很普遍的情况是我们将中断解释为一个终止请求。这种线程的run方法形式如下:
Runnable r = ()->{
try
{
...
while (!Thread.currentThread().isInterrupted()
//&&more work to do
)
{
//do more
}
}
catch(InterruptedException e)
{
//thread was intertupted during sleep or wait
}
finally
{
//cleanup,if required
}
//exiting the run method terminates the thread
};
如果在每次工作迭代后都使用sleep(或者其他可中断的)方法,
isInterrupted检查既没有必要也没有用处。如果设置了中断状态,而此时调用了sleep方法,它不会休眠,而是清楚中断状态并且抛
出InterruptedException。因此,如果循环调用了sleep,不要检测中断状态,而是抛出InterruptedException异常!如下:
Runnable r = ()->{
try
{
...
while ( )
{
//do more
Thread.sleep(delay);
}
}
catch(IntertuptedException e)
{
//Thread was interrupted during sleep
}
finally
{
//cleanup,if required
}
//exiting the run method terminates the thread
};
区分:
interrupted | isInterrupted |
---|---|
静态方法,检查当前线程是否被中断 ,调用interrupted方法会清楚该线程的中断状态。 | 实例方法,可以用来检查是否有线程被中断。调用这个方法不会改变中断状态 |
如果在catch子句中想不出有什么可以做的工作,仍有两种合理的选择:
- 在catch子句中调用Thread.currentThread().intertupt()来设置中断状态,这样一来使用者可以检测中断状态。
void mySubTask()
{
...
try {sleep(delay);}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
}
}
- 或者更好的选择是:用throws IntertuptedException 标记方法,去掉try语句块。这样一来调用者(或者最终的run方法)就可以捕获这个异常。
void mySubTask() throws InterruptedException
{
...
sleep(delay);
...
}
守护线程
可以通过调用
t.setDaemon(true);
将一个线程转换为守护线程——为其他线程服务的“辅助”,比如计时器线程,清空过时缓存的线程都是守护线程
- 当只剩下守护线程时,虚拟机就退出,因为剩下的守护线程不是程序运行下去的意义,没必要继续运行程序了。
**线程名:**默认情况下,线程按照设置好的规范安排容易记的名字。
但是也可以通过setName方法为线程设置任何名字
var t = new Thread(runnable);
t.setName("Web crawler");
——————————————————————————
异常捕获处理器:
ru方法不能抛出任何检查型的异常,但是非检查型异常可能会让程序终止,这种情况下线程会死亡。
不过对于可以传播的异常,并没有设置任何catch子句,事实上,在线程死亡前,异常会传递到一个专门用来处理未捕获异常的处理器
-
这个处理器必须属于一个实现了Thread.UncaughtExceptionHander接口的类。该接口·只有一个方法:
void uncaughtException(Thread d ,Throwable e) -
可以用setUncaughtExceptionHander的方法为任何线程安装一个处理器,也可以用Thread类的静态方法setDefaultUncaughtExceprionHandler为所有线程安装一个默认的处理器。如果没有安装默认处理器,为null。如果没有为单个线程安装处理器,处理器就是该线程的ThreadGroup对象
线程组:是可以一起管理的线程的集合。默认情况下创建的所有线程都属于同一个线程组。但是也可以建立其他组。但是现在一般有更好的处理线程集合的特性。
p563线程优先级:了解即可
同步
竞态条件: 大多数的多线程应用中,两个或者两个以上的线程需要共享对同一数据的存取。如果两个线程存取同一个对象,并且每个线程分别调用了一个修改该对象状态的方法,会发生什么呢,事实上最终这两个线程操作会相互覆盖。取决于访问的次序,可能导致对象被破坏,这种情况通常叫做竞态条件
为了避免多线程破坏共享程序,必须学习如何同步存取。
还是以银行为例:我们安排随机地选择从哪个源账户转到哪个目标账户。下面我们来看看Bank类transfer方法的代码:
public void transfer(int from,int to,double amount)
//CAUTION:unsafe when called from multiple threads
//multiple:数量多的
{
System.out.print(Thread.currentThread());
accounts[from] -=amount;
System.out.printf(" %10.2f from %d to %d ",amount,from,to);
accounts[to] +=amount;
System.ou.printf("Total Balance :%10.2f%n",getTotalBalance());
}
下面是Runnable实例的代码,。run方法不断从一个给定的银行账号取钱。在每次迭代中,run方法选择一个随机的目标账户和一个随机金额,调用bank对象的transfer方法,然后休眠:
Runnable r = ()->{
try
{
while(true)
{
int toAccount = (int) (bank.size() * Math.random());
double amount = MAX_AMOUNT *Math.random();
bank.transfer(fromAccount,toAccount,amount);
Thread.sleep((int)(DELAY * Math.random()));
}
}
catch(InterruptedException e)
{
}
};
由于设置了死循环,这个程序永远不会结束,只能ctrl+c手动结束程序。在一段时间的调用后,我们发现一个明显的问题:银行内部转账然而银行总金额发生了变动。
详解:
假如有两个或者多个线程试图同时更新同一个账户对象的时候,就会出现上述问题。假如两个线程同时执行指令:
accounts[to] += amount;
问题在于这不是原子操作。这个指令可能的处理步骤:
- 将accounts【to】加载到寄存器
- 增加amount
- 将结果写回 accounts【to】
现在假设一个线程执行12,然后它的运行权被抢占,第二个线程被唤醒,更新account数组的一个元素,然后第一个线程被唤醒执行3,然后结果是第二个线程进行的更新被完全抹去。这样一来总金额就不再正确了。
真正的问题在于transfer方法可能在执行中间被中断。如果能确保线程失去控制前方法已经运行完成,那么银行账户对象的状态就不会被破坏!
——————————————————————————
锁对象
有两种机制可以防止并发访问代码块。Java语言提供了一个synchronized关键字来达到这一目的。java5中提供的ReentrantLock类也能达到目的
- synchronized关键字会自动提供一个锁以及相关的“条件”
- 用ReentrantLock保护代码块的基本结构如下:
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语句放在finally中尤为重要,能避免抛出异常时整个线程永久阻塞,无法释放锁对象
- 解锁方法不是close,使用try - with- resource是行不通的。
下面使用一个锁来保护Bank类的transfer方法:
public class Bank
{
private var bankLock = new ReentrantLock();
...
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);
accounts[to] +=amount;
System.ou.printf("Total Balance :%10.2f%n",getTotalBalance());
}
finally
{
bankLock.unlock();
}
}
}
假设一个线程调用了transfer,但是在执行结束前被强占。再假设第二个线程也调用transfer,由于第二个线程不能获得锁,会在调用lock方法时被阻塞,然后暂停。必须等待第一个线程执行完transfer方法释放锁的时候才能开始运行。
- 注意每个Bank对象都有自己的ReentrantLock对象。如果两个线程试图访问同一个Bank对象,每个线程那么所可以用来保证串行化访问。不过,如果两个线程访问不同的Bank对象,每个线程会获得不同的锁对象,两个线程都不会阻塞。本该如此,因为线程在操作不同的Bank实例中,线程不会相互影响。
- 这个锁称为重入(reentrant)锁,因为线程可以反复获得已有的锁。锁有一个持有计数来跟踪对lock方法的嵌套调用。线程每一次使用lock后都要调用unlock来释放锁。每有一个线程获取一个重入锁的时候计数就加一,释放的时候则是减一。
——————————————————————————
条件对象:
锁名.newCondition();
构建与这个线程相对应的条件对象
通常,线程进入临界区以后却发现只有满足某个条件才能执行。可以使用一个条件对象来管理那些已经获得一个锁却不能开始有效工作的线程。由于历史原因,条件对象也被叫做条件变量。
优化前面的模拟银行:如果一个账户没有足够的资金转账,我们不希望从这样的账户转出资金。注意不能使用以下类似的代码:
if(bank.getBalance(from)>=amount)
bank.transfer(from,to,amount);
在测试后,transfer调用前有可能中断,很多情况下执行transfer时的账户余额可能已经不满足要求了。为此可以用一个锁来保护这个测试转账操作:
public void transfer(int from,int to,int amount)
{
bankLock.lock();
try
{
while (accounts[from]<amount)
{
//wait
}
//tansfer funds
...
}
finally
{
bankLock.unlock();
}
}
现在当账户中没有足够的资金时,我们应该等待其他的线程向账户中储存了资金,但是我们刚刚获取了这个锁并没有释放,因此别的线程没有存款的机会
这种情况下需要引入条件对象:可以用newCondition方法来获取一个条件对象。习惯上我们会给每个条件对象一个合适的名字来反映其对应的条件。例如此处我们就建立一个“资金充足”条件。
class Bank
{
private Condition sufficientFunds;
...
public Bank()
{
...
sufficientFunds = bankLock.newCondition();
}
}
如果transfer方法发现资金不足,就会调用:
sufficientFunds.await();
> 这里是引用
这会暂停当前线程,并放弃锁。这样就允许另一个线程执行。我们希望它能够增加账户余额。
调用await方法的线程会进入这个条件的“等待集”,用于区别开没有获得过锁的线程。当锁可以用时,该线程并不会立即变为运行状态。事实上暂停会一直保持到另一个线程在同一条件上调用signalAll方法
另一个线程完成转账时,应该调用:
sufficientFunds.signalAll();
这个调用会重新激活等待这个条件的所有线程,它们从等待集移除时,再次成为可运行线程,调度器最终将它们激活。同时它们会尝试重新进入该对象。一旦锁可用,它们中的某个线程将从await调用返回,得到这个锁 ,并从之前暂停的地方继续执行。 此时线程应当再次测试条件:signalAll()只是通知等待中的线程:有可能出现了能满足的条件,值得再次进行检查。
通常await调用应当被放在如下形式的循环中:
while((!OK to proceed))
condition.await( );
最终需要有某个线程调用signalAll方法,当一个线程调用await后就没有办法自我激活了。它们寄希望于其他线程,如果没有其他线程激活等待集。它们就永远不再运行了。这就导致了令人不快的死锁现象。,如果所有的其他线程都被阻塞,最后一个线程调用了await方法但是没有先接触另外线程的阻塞,那么程序就被永远挂起,没有线程能激活等待了。
————————————————————————————
[ˈsɪŋkrənaɪzd]:同步
synchronized 关键字
前面得到的锁和条件的要点:
- 锁可以用来保护代码片段,一次只能有一个线程执行被保护的代码
- 锁可以管理试图进入被保护代码的线程
- 一个锁可以有一个或多个关联的条件对象
- 每个条件对象管理那些已经进入被保护代码段但是不能执行的线程
锁和条件可以保持java程序的稳定执行。不过大多数情况下可以直接用java自带的一种机制。从java1.0开始,java中的每一个对象都有一个内部锁,如果对象声明时有synchronized关键字,那么对象的锁将保护整个方法:要调用这个方法,线程必须获得内部对象锁。
换句话说:
public synchronized void method()
{
method body;
}
等价于:
public void method()
{
this.intrinsicLock.lock();
try
{
method body;
}
finally {this. intrinsicLock.unlock();}
}
- 内部对象锁只有一个关联条件
- wait方法将一个线程加入等待集中,notifyAll /notify方法可以解除等待线程的阻塞。换句话说,调用wait或notifyAll等价于:
intrinsicCondition.await();
intrinsicCondition.signalAll();
wait、notifyAll、notify是Object类的final方法。Condition中方法必须命名为await、signalAll和signal以免和那些方法发生冲突。
例如可以用java如下方法实现Bank类:
class Bank
{
private double[] accounts;
public synchronized void transfer(int from,int to,int amount)
throws IntertuptedException
{
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
}
}
使用synchronized关键字显而易见的能得到更简洁的代码。
主要机制是每个对象都有一个内部锁。并且锁有一个内部条件。这个锁会管理试图进入synchronized方法的线程。这个条件可以管理调用了wait的线程。
内部锁的限制:
- 不能中断一个正尝试获得锁的线程
- 不能指定尝试获得锁时的超时时间
- 每个锁仅有一个条件实际使用上是不够的
选择使用哪种方法:
- synchronized关键字在能用的时候优先,可以简洁代码
- Lock/Conditon提供的特殊能力需要的用
- 更加一般的情况下,可以试着不用同步。后文中介绍了可以用util包下concurrent包的某种机制(阻塞队列)
synchronized的使用案例——模拟银行
package java.util.*;
public class Bank
{
private final double[] accounts;
public Bank(int n,double initialBalance)
{
accounts = new double[n];
Arrays.fill(accounts,initialBalance);
}
public synchronized void transfer(int from,int to,double amount)throws InterruptedException
{
while(accounts[from]<amount)
wait();
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.printg("%10.2f from %d to %d",amount,from,to);
accounts[to] += amount;
System.out.printf("Total Balance:% 10.2f%n",getTotalBalance());
notifyAll();
}
public synchronized double getTotalBalance()
{
double sum = 0;
for(double a:accounts)
sum+=a;
return sum;
}
public accounts.length;
}
————————————————————————————
同步块
正如前文所讨论的,每一个java对象有一个锁。线程可以通过调用同步方法获得锁。还有另一种机制可以获得锁:进入一个同步块。当一个线程进入如下形式的同步块时:
synchronized(obj)
{
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)
{
accounts[from] -= amount;
accounts[to]+=amount;
}
System.out.println(...);
}
}
客户端锁定:
使用一个对象的锁来实现额外的原子操作:
以Vector类为例子:
这是一个列表,它的方法是同步的。现在假设将银行余额存储于
Vector< Double >中,下面是transfer方法的一个原生实现:
public void transfer(Vector<Double>accounts, int from,
int to, int amount)
{
accounts.set(from,accounts.get(from)-amount);
accounts.set(to,accounts.get(to)+amount);
System.out.println(...);
}
Vector的get和set方法是同步的,但是,在外部transfer的处理中,它完全可能被抢占。然后就会出现竞态条件。
不过我们可以尝试截获这个锁:
public void transfer (Vector<Double>accounts ,int from,
int to,int amount)
{
synchronized (accounts)
{
accounts.set(from,accounts.get(from)-amount);
accounts.get(to,accounts.get(to)+amount );
}
System.out.println(...);
}
这个方法是确实可行的,但是完全依赖于这样的事实:Vector类会对自己的所有更改方法使用内部锁。但是这个并非在所有版本中都明确规定的,所以客户端锁定是非常脆弱的,很可能因为一次版本更新而崩溃
————————————————————————————
监视器
锁和条件是实现线程同步的强大工具,但是严格来说,它们不是很面向对象。对于不考虑显式锁就能保证多线程安全性的方法,提出了最好的解决方案之一——监视器。
它一般具有以下特性:
- 监视器是只包含私有字段的类
- 监视器类的所有字段拥有一个关联的锁
- 所有方法由这个锁锁定。换句话说,如果客户端调用obj.method(),那么obj对象的锁在方法开始时自动获得,方法返回时自动释放锁。由于所有的字段都是私有。这样可以确保一个线程处理字段的时候字段不会被其他任何线程访问。
- 锁可以有任意多个关联的条件。
java对象有以下三个重要的方面不同于监视器,这削弱了线程的安全性:
- 字段不要求是private
- 方法不要求是synchronized
- 内部锁对客户是可用的
(并不是什么很有利的特性)
—————————————————————————————
volatile字段
有时候如果只是为了读写一两个字段而使用同步,所带来的开销可能有些划不来。但是由于一些计算机特性又不得不加以限制。
volatile关键字为实例字段的同步提供了一种免锁机制,若声明一个字段为volatile,那么编译器和虚拟机就知道这个字段可能被另一个线程并发更新。
案例:假设一个对象有个Boolean标记done,它的值由一个线程设置,而另一个查询,在前面说过可以使用锁:
private boolean done;
public synchronized boolean isDone{return done;}
public synchronized void setDone(){done = true;}
或许使用内部对象锁不是个好主意。如果另一个线程已经对该对象加锁。isDone和setDone方法可能会阻塞。如果这不是个问题,可以只为这个变量使用单独的锁。但是,这会很麻烦。
在这种情况下,将字段声明为volatile就很合适:
private volatile boolean done;
public boolean isDone(){return done;}
public void setDone(){done = true;}
利用final字段
前面了解到,除非使用锁或者volatile修饰符,否则无法从多个线程安全地读取一个字段。
另一种情况可以安全的访问一个共享字段:
final var accounts - new Hashmap<String ,double>()
使用final保证了构造器构造完后accounts才能被访问。
可以大概达到一些需求,但是实际上对这个映射的操作并不是线程安全的。如果有多个线程要同时进行访问,仍然需要同步。
————————————————————————
原子性
- 原子性就是指该操作是不可再分的。不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。
- 非原子性:也就是整个过程中会出现线程调度器中断操作的现象 类似"a ++"这样的操作不具有原子性,因为它可能要经过以下两个步骤:
(1)取出 a 的值
(2)计算 a+1
——————————————————————
死锁
有可能因为所有的线程都在等待一个条件的满足而成为死锁。所有的线程在这种状态下都被堵塞。
——————————————————————
线程局部变量
线程之间有时候可能要避免共享变量以消除可能出现的风险。
————————————————————
废除stop和suspend方法
stop的一个很大问题是终止线程的时候会立即释放被锁定的所有对象的锁,都可能导致对象处于不一致的状态。比如钱被取出没有转进对方账户。这是致命的失误。
suspend方法不会破坏对象。但是要是用它来挂起一个持有锁的线程,那么在线程恢复运行前锁是不可用的。如果调用suspend方法试图获得同一个锁,那么程序死锁:被挂起的线程等着被恢复,而将其挂起的线程等待获得锁。
——————————————————————
线程安全的集合
线程安全的数据结构实现可以在多个线程并发的访问下减少出现的混乱。也不用考虑用锁来保护共享的数据结构
阻塞队列:
使用一个或多个队列来处理:生产者向队列插入元素。消费者线程获取元素,使用队列可以安全的在线程间传递数据。
比如银行转账,转账线程将指令对象插入一个队列,而不是直接访问银行对象。另一个线程从队列中取出指令完成转账。只有这个线程可以访问银行对象的内部。因此不需要同步。
当企图向队列添加元素但检测到队列已满,或者想移除元素发现队列为空的情况,阻塞队列会阻塞线程,在协调多个线程之间的合作时,阻塞队列是一个有用的工具。工作线程可以周期性的将中间结果储存在阻塞队列中,其他工作线程移除中间结果并进一步修改。队列会自动平衡负载。如果1比2慢。2在瞪待结果时会阻塞。
p590开始介绍了阻塞队列的基本实现。
高效的映射、集和队列:
并发集视图
并行数组算法
——————————————————————————
任务和线程池
线程的构造开销是较大的,因为涉及到操作系统的交互。如果程序中创建了大量生命周期短的线程,那么不应该将每个任务映射到一个单独的线程,而应该用线程池,其中包含许多准备运行的线程,为线程池提供Runnable,就会有一个调用run方法,当run退出时,这个线程不会死亡,而是留在池中为下一个请求提供服务。
——————————————————————————————
进程
有时候可能需要同时执行另一个程序,可以利用进程机制操作(p628起)
核心技术1结束