关注个人公号:Android开发吹牛皮。互相学习java
有了并发咱们能够同时作不少事情,可是,两个或者多个线程互相干扰的问题也存在。若是不防范这种冲突,就可能出现两个线程同时访问一个银行帐户,向同一个打印机打印,改变同一个值等问题。npm
单个线程每次只能作一件事情。由于只有一个实体因此永远不用担忧两我的在同一个地方停车的问题。可是多线程会在同时访问一个资源。缓存
咱们先作一个实验,多个任务。一个任务产生一个偶数,其余的任务检验偶数的有效性。安全
public abstract class IntGenerator {
//为了表示可见性,使用 volatile 修饰
private volatile boolean canceled = false;
public abstract int next();
public void cancel(){
canceled = true;
}
//查看该对象是否已经被撤销
public boolean isCanceled() {
return canceled;
}
}
复制代码
任何 IntGenerator 均可以用下面的 EvenChecker 类来测试:bash
public class EvenChecker implements Runnable{
private IntGenerator generator;
private final int id;
protected EvenChecker(IntGenerator generator, int id) {
super();
this.generator = generator;
this.id = id;
}
@Override
public void run() {
while (!generator.isCanceled()) {
int val = generator.next();
if (val %2 !=0) {
System.out.println("不是偶数");
generator.cancel();
}
}
}
public static void test(IntGenerator gp,int count) {
ExecutorService service = Executors.newCachedThreadPool();
for (int i = 0; i < count; i++) {
service.execute(new EvenChecker(gp, i));
}
service.shutdown();
}
public static void test(IntGenerator gp) {
test(gp,10);
}
}
复制代码
上面的示例中 generator.cancel()
撤销的不是任务自己,而是 IntGenerator 对象是否能够被撤销的条件。必须仔细考虑并发系统失败的全部可能途径,例如,一个任务不能依赖于另外一个任务。由于任务关闭的顺序没法获得保证。这里,经过使任务依赖于非任务对象,咱们能够消除潜在的竞争条件。多线程
EvenChecker 老是读取和测试 IntGenerator 的返回值。若是 isCanceled() 返回值为 true,则 run() 返回,这将告知 test() 中的 Executor 该任务完成了。任何 EvenChecker 任务均可以在与其关联的 IntGenerator 上调用 cancel(),这将致使全部其余使用该 IntGenerator 的 EvenChecker 获得关闭。并发
第一个 IntGenerator 有一个能够产生一些列偶数值的 next() 方法:dom
public class EvenGenerator extends IntGenerator{
private int currentEvenValue = 0;
@Override
public int next() {
++currentEvenValue;
++currentEvenValue;
return currentEvenValue;
}
public static void main(String[] args) {
EvenChecker.test(new EvenGenerator());
}
}
复制代码
执行结果:jvm
1537不是偶数
1541不是偶数
1539不是偶数
复制代码
一个任务可能在另外一个任务执行第一个递增操做以后,可是没有执行第二个递增操做以前,调用 next() 方法。这将使这个值处于不恰当状态。为了证实这是可能发生的,text() 方法建立了一组 EvenChecker 对象,以用来连续的读取并输出同一个 EvenGenerator,并检测每一个数值是否都是偶数。若是不是就报错终止。ide
这个程序最终会失败终止,由于每一个 EvenChecker 任务在 EvenGenerator 处于不恰当的状态时,仍可以访问其中的信息。可是根据不一样的操做系统和实现细节这个问题在循环屡次以后也可能不会被探测到。有一点很重要,那就是递增程序自身也须要多个步骤,而且在递增过程当中任务可能被挂起。也就是说递增在 Java 中不是原子性操做。所以,若是不保护任务,即便单一的递增也不是安全的。
前面的示例展现使用线程的一个基本问题:你永远不知道一个线程什么时候在运行。对于并发操做,你须要某种方式来防止两个任务访问相同的资源,至少在关键阶段不能出现这种状况。防止这种冲突的方法是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这个资源,使其余任务在其被解锁前没法访问他,而在其解锁之时,另外一个任务就能够锁定并使用它,以此类推。
基本上全部的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案。这意味着在给定时刻只容许一个任务访问共享资源。一般这种是经过在代码前面加上一句锁语句来实现的,这就使得在一段时间内只有一个任务能够运行这段代码。由于锁语句产生一种相互排斥的效果,这种机制称为互斥量。
另外当一个锁被解锁的时候,咱们并不能肯定下一个使用锁的任务,由于线程调度机制并非肯定性的。能够经过 yield() 和 setPriorit() 来给线程调度器提供建议。
Java 以提供关键字 synchronized 的形式,为防止资源冲突提供了内在支持。当任务要执行被 synchronized 关键字保护的代码片断的时候,它将检查锁是否可用,而后获取锁,执行代码,释放锁。共享资源通常是以对象像是存在于内存片断,能够是文件、输入输出端口。要控制对共享资源的访问,得先把它包装进一个对象。而后把全部要访问这个资源的方法标记为 synchronized。
下面是声明 synchronized 方法的方式:
synchronized void f(){};
synchronized void g(){};
复制代码
全部对象都自动含有单一的锁(监视器)。当在对象上调用其任意 synchronized 方法的时候,此对象被加锁,这时这个对象上的其余 synchronized 方法只有等到前一个方法调用完毕并释放了锁以后才能被调用。对于某个特定对象来讲,其全部 synchronized 方法共享同一个锁,这能够被用来防止多个任务同时访问被编码为对象内存。
注意:使用并发时将对象设置为 private 是很是重要的,不然,synchronized 关键字就不能防止其余的任务直接访问域,这样就会产生冲突。
针对每一个类也有一个锁,因此 synchronized static 方法能够在类的范围内防止对 static 数据的并发访问。
该何时同步呢?
若是你正在写一个变量,它可能接下来被另外一个线程读取,或者正在读取一个上一次被另外一个线程写过的变量,那么你必须使用同步,而且,读写线程都必须用相同的监视器锁同步。
复制代码
同步控制 EvenGenerator
经过在 EvenGenerator 中加入 synchronized 关键字,能够防止不但愿的线程访问:
public class EvenGenerator extends IntGenerator{
private int currentEvenValue = 0;
@Override
public synchronized int next() {
++currentEvenValue;
Thread.yield();
++currentEvenValue;
return currentEvenValue;
}
public static void main(String[] args) {
EvenChecker.test(new EvenGenerator());
}
}
复制代码
对 Thread.yield() 的调用被插入到两个线程之间,以提升奇数的可能性。由于互斥能够防止多个任务同时进入临界区,因此上面不会产生任何的失败。第一个进入 next() 的任务得到锁,任何其余试图获取锁的任务都将被阻塞,直到第一个任务释放锁。
使用显示的 Lock 对象
Java SE5 的类库中还包含定义在 java.util.concurrent.locks 中的显示的互斥机制。Lock 对象必须被显示地建立、锁定和释放。所以,它与内建的锁形式相比,代码缺少有雅性。可是对于解决某些类型的问题时更加的灵活。
下面用显示的 Lock 重写上面的代码:
public class EvenGenerator extends IntGenerator{
private int currentEvenValue = 0;
//建立锁
private Lock lock = new ReentrantLock();
@Override
public int next() {
//锁定
lock.lock();
try {
++currentEvenValue;
Thread.yield();
++currentEvenValue;
return currentEvenValue;
}finally {
//计算完毕后释放锁
lock.unlock();
}
}
public static void main(String[] args) {
EvenChecker.test(new EvenGenerator());
}
}
复制代码
当你在使用 lock 对象时,示例的惯用法很重要:对 unlock() 方法的调用必须放在 try-finlly 语句中。注意,return 语句必须在 try 子句中出现,以确保 unlock() 不会过早的发生,从而将数据暴露在第二个任务。尽管 try-finlly 子句比 synchronized 关键字要多,但显示的 lock 的优势也是显而易见的。若是在使用 synchronized 关键字时某些事物失败了,那么就会抛出一个异常。可是咱们并无机会去处理,以维护系统良好的状态。显示的 lock 对象,你就可使用 finlly 子句维护系统的正确状态。大致上咱们使用 synchronized 的状况更多,只有遇到解决特殊问题时才是用显示的 lock 对象。
示例:使用 synchronized 关键字不能尝试着获取锁且获取锁会失败,或者尝试着获取一段时间而后放弃它。
public class AttemptLocking {
private ReentrantLock lock = new ReentrantLock();
public void untimed() {
//尝试获取锁
boolean captured = lock.tryLock();
try {
System.out.println("tryLock(): " + captured);
} finally {
if(captured)
lock.unlock();
}
}
public void timed() {
boolean captured = false;
try {
//尝试2秒后失败
captured = lock.tryLock(2, TimeUnit.SECONDS);
} catch(InterruptedException e) {
throw new RuntimeException(e);
}
try {
System.out.println("tryLock(2, TimeUnit.SECONDS): " +
captured);
} finally {
if(captured)
lock.unlock();
}
}
public static void main(String[] args) {
final AttemptLocking al = new AttemptLocking();
al.untimed(); // True -- lock is available
al.timed(); // True -- lock is available
// Now create a separate task to grab the lock:
new Thread() {
{ setDaemon(true); }
public void run() {
al.lock.lock();
System.out.println("acquired");
}
}.start();
Thread.yield(); // Give the 2nd task a chance
al.untimed(); // False -- lock grabbed by task
al.timed(); // False -- lock grabbed by task
}
}
复制代码
执行结果:
tryLock(): true
tryLock(2, TimeUnit.SECONDS): true
tryLock(): true
tryLock(2, TimeUnit.SECONDS): true
acquired
复制代码
ReentrantLock 容许咱们尝试着获取锁可是最终未获取锁,这样若是其余人已经获取了锁,那么你就能够决定离开作一些其余的事情,而不是一直等待这个锁被释放。显示的 Lock 对象在加锁和释放锁方面,相对于内建的 synchronized 锁来讲,还赋予了你更细粒度的控制力。
在 Java 线程中,经常咱们会认为原子操做不须要进行同步控制。原子操做是不能被线程调度机制中断的。这样的想法是错误的,依赖于原子性是危险的。原子性在 Java 的类库中已经实现了一些更加巧妙的构建。原子性能够应用于除了 long 和 double 以外的全部基本类型之上的 “简单操做”。可是 jvm 会把 64 位的 long 和 double 操做当作两个分离的 32 位的操做来执行,这就产生了一个读取和写入操做之间产生上下文切换,从而致使了不一样的任务产生不正确结果的可能性。可是若是咱们使用 volatile 关键字就会得到原子性 (在 Java SE5 以前一直未能正确工做)。
所以,原子操做可由线程机制来保证其不可中断,可是即使这样,这也是一种简化的机制。有时看起来很安全的原子性操做实际上也可能不安全。
在多核处理器上,可视性问题远比原子性问题多得多。一个任务作出的修改可能对其余任务是不可见的。由于每一个任务都会暂时把信息存储在缓存中。同步机制强制在处理器中一个任务作出的修改必须是可见的。volatile 关键字确保了这种可视性。一个任务修改了对这个修饰对象的操做,那么其余的任务读写操做都能看到这个修改。即便是用了缓存也能被看到,由于 volatile 会被当即写入主存。而读写操做就发生在主存中。同步也会致使向主存中刷新,因此若是一个对象是 synchronized 保护的那么久没必要使用 volatile 修饰。使用 volatile 而不是 synchronized 的惟一安全的状况是类中只有一个可变的域。咱们的第一选择应该是 synchronized 关键字,这是最安全的方式。
对域中的值作赋值和返回操做一般都是原子性的。可是递增和递减并非:
public class Atomicity {
int i;
void f(){
i++;
}
void g(){
i +=3;
}
}
复制代码
咱们看编译后的文件:
void f();
0 aload_0 [this]
1 dup
2 getfield concurrency.Atomicity.i : int [17]
5 iconst_1
6 iadd
7 putfield concurrency.Atomicity.i : int [17]
// Method descriptor #8 ()V
// Stack: 3, Locals: 1
void g();
0 aload_0 [this]
1 dup
2 getfield concurrency.Atomicity.i : int [17]
5 iconst_3
6 iadd
7 putfield concurrency.Atomicity.i : int [17]
}
复制代码
每一个指令都产生了一个 get 和 put ,他么之间还有一些其余的指令。所以在获取和修改之间,另外一个任务可能会修改这个域。因此,这些操做不是原子性的:
咱们再看下面这个例子是否符合上面的描述:
public class AtomicityTest implements Runnable {
private int i = 0;
public int getValue() {
return i;
}
private synchronized void evenIncrement() {
i++;
i++;
}
public void run() {
while(true)
evenIncrement();
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
AtomicityTest at = new AtomicityTest();
exec.execute(at);
while(true) {
int val = at.getValue();
if(val % 2 != 0) {
System.out.println(val);
System.exit(0);
}
}
}
}
复制代码
测试结果:
1
复制代码
改程序找到奇数并终止。尽管 return i 是原子性操做,可是缺乏同步使得其数值能够在不稳定的中间状态时被读取。还有因为 i 不是 volatile 的也存在可视性的问题。getValue() 和 evenIncrement() 必须都是 synchronized 的。对于基本类型的读取和赋值操做被认为是安全的原子性操做。可是当对象处于不稳定状态时,仍旧颇有可能使用原子性操做获得访问。最明智的作法是遵循同步的规则。
Java SE5 中引入了诸如 AtomicInteger、AtomicLong、AtomicReference 等等特殊的原子性变量类,他们提供下面形式的原子性条件更新操做:
boolean compareAndSet(expectedValue,updateValue);
复制代码
这些类被调整为可使用在现代处理器上,而且是机器级别的原子性,所以在使用他们时不须要担忧。常规来讲不多使用他们,可是对于性能调优来讲,他们就大有用武之地了。
示例,重写上面的实例:
public class AtomicIntegerTest implements Runnable{
private AtomicInteger ger = new AtomicInteger(0);
public int getValue() {
return ger.get();
}
private void eventIncrement() {
ger.addAndGet(2);
}
@Override
public void run() {
while (true) {
eventIncrement();
}
}
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
AtomicIntegerTest aIntegerTest = new AtomicIntegerTest();
exec.execute(aIntegerTest);
while (true) {
int val = aIntegerTest.getValue();
if (val % 2 !=0) {
System.out.println(val);
System.exit(0);
}
}
}
}
复制代码
Atomic 类被设计为构建 Java.util.concurrent 中的类,所以只有在特殊状况下才在代码中使用他们。上面的例子没有使用任何加锁机制也能获得很好的同步。可是一般依赖于锁对咱们来讲更安全一点。
有时咱们须要防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法。经过这种方式分离出来的代码被称为临界区,也是使用 synchronized 关键字修饰。语法是:synchronized 被用来指定某个对象,此对象的锁被用来对括号内的代码进行同步控制:
synchronized (syncObject){
//被同步控制的代码块
}
复制代码
这被称之为同步代码块;在进入此段代码以前,必须获得 syncObject 对象的锁。若是其余线程已经获得锁,那么就得等到锁被释放以后,才能进入临界区。经过使用同步控制块,而不是整个方法进行同步控制,可使多个任务访问对象的时间性获得显著提升。
下面的例子比较了两种同步控制方法:
public class Pair {
private int x, y;
public Pair(int x, int y) {
this.x = x;
this.y = y;
}
public Pair() { this(0, 0); }
public int getX() { return x; }
public int getY() { return y; }
//递增操做是非线程安全的
public void incrementX() {
x++;
}
public void incrementY() {
y++;
}
public String toString() {
return "x: " + x + ", y: " + y;
}
public class PairValuesNotEqualException extends RuntimeException {
public PairValuesNotEqualException() {
super("Pair values not equal: " + Pair.this);
}
}
// Arbitrary invariant -- both variables must be equal:
public void checkState() {
if(x != y)
throw new PairValuesNotEqualException();
}
}
复制代码
模板类:
public abstract class PairManager {
//线程安全的
AtomicInteger checkCounter = new AtomicInteger(0);
protected Pair p = new Pair();
//集合也是线程安全的
private List<Pair> storage =Collections.synchronizedList(new ArrayList<Pair>());
//方法是线程安全的
public synchronized Pair getPair() {
// Make a copy to keep the original safe:
return new Pair(p.getX(), p.getY());
}
// 每次添加一次间隔 50毫秒
protected void store(Pair p) {
storage.add(p);
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch(InterruptedException ignore) {
}
}
public abstract void increment();
}
复制代码
实现模板:
public class PairManager1 extends PairManager{
//在方法体上修饰代表方法是同步控制的
@Override
public synchronized void increment() {
// 递增和递减是非线程安全的
p.incrementX();
p.incrementY();
store(getPair());
}
}
public class PairManager2 extends PairManager{
@Override
public void increment() {
Pair temp;
//同步代码块,计算完毕以后赋值
synchronized(this) {
p.incrementX();
p.incrementY();
temp = getPair();
}
store(temp);
}
}
复制代码
建立两个线程:
public class PairManipulator implements Runnable {
private PairManager pm;
public PairManipulator(PairManager pm) {
this.pm = pm;
}
public void run() {
while(true)
pm.increment();
}
public String toString() {
return "Pair: " + pm.getPair() +
" checkCounter = " + pm.checkCounter.get();
}
}
public class PairChecker implements Runnable{
private PairManager pm;
public PairChecker(PairManager pm) {
this.pm = pm;
}
public void run() {
while(true) {
pm.checkCounter.incrementAndGet();
pm.getPair().checkState();
}
}
}
复制代码
测试类:
public class CriticalSection {
static void testApproaches(PairManager pman1, PairManager pman2) {
ExecutorService exec = Executors.newCachedThreadPool();
PairManipulator
pm1 = new PairManipulator(pman1),
pm2 = new PairManipulator(pman2);
PairChecker
pcheck1 = new PairChecker(pman1),
pcheck2 = new PairChecker(pman2);
exec.execute(pm1);
exec.execute(pm2);
exec.execute(pcheck1);
exec.execute(pcheck2);
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch(InterruptedException e) {
System.out.println("Sleep interrupted");
}
System.out.println("pm1: " + pm1 + "\npm2: " + pm2);
System.exit(0);
}
public static void main(String[] args) {
PairManager
pman1 = new PairManager1(),
pman2 = new PairManager2();
testApproaches(pman1, pman2);
}
}
复制代码
最后的测试结果:
pm1: Pair: x: 11, y: 11 checkCounter = 2183
pm2: Pair: x: 12, y: 12 checkCounter = 24600386
复制代码
尽管每次运行的结果可能会不一样,但通常状况下 PairChecker 的检查频率 PairManager1 比 PairManager2 少。后者采用同步代码块进行控制,因此对象不加锁的时间更长。使得其余线程可以更多的访问。
synchronized 块必须给定一个在其上同步的对象,而且合理的方式是,使用其方法正在被调用的当前对象:synchronized(this),在这种方式中若是得到了 synchronized 块上的锁,那么该对象其余的 synchronized 方法和临界区就不能被调用了。
有时必须在另一个对象上同步,可是若是你这样作,就必须确保全部相关的任务都是在同一个对象上同步的。
下面的例子演示了两个任务能够同时进入同一个对象,只要这个对象上的方法是在不一样的锁上同步的便可:
class DualSynch {
private Object syncObject = new Object();
public synchronized void f() {
for(int i = 0; i < 5; i++) {
print("f()");
Thread.yield();
}
}
public void g() {
synchronized(syncObject) {
for(int i = 0; i < 5; i++) {
print("g()");
Thread.yield();
}
}
}
}
public class SyncObject {
public static void main(String[] args) {
final DualSynch ds = new DualSynch();
new Thread() {
public void run() {
ds.f();
}
}.start();
ds.g();
}
}
复制代码
执行结果:
g()
f()
g()
f()...
复制代码
其中 f() 是在 this 上同步的,而 g() 是在一个 syncObject 上同步的 synchronized 块。所以,这两个同步是相互独立的。经过在 main() 中的方法调用能够看到,这两个方法并无阻塞。
防止任务在共享资源上产生冲突的第二种方式是根除对变量内存的共享。线程本地存储是一种自动化机制,可使用相同变量的每一个不一样的线程建立不一样的存储。所以,若是你有5个线程,那么线程会在本地生成 5 个不一样的存储块。它们使得你能够将状态和线程关联起来。
建立和管理线程本地存储能够由 java.lang.ThreadLocal 类来实现:
public class Accessor implements Runnable{
private final int id;
protected Accessor(int id) {
super();
this.id = id;
}
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
ThreadLocalVariableHolder.increment();
System.out.println(this);
Thread.yield();
}
}
@Override
public String toString() {
// TODO Auto-generated method stub
return "#"+id+":"+ThreadLocalVariableHolder.get();
}
}
复制代码
线程本地存储:
public class ThreadLocalVariableHolder {
private static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){
private Random dRandom = new Random(47);
protected synchronized Integer initialValue(){
return dRandom.nextInt(10000);
}
};
public static void increment() {
value.set(value.get()+1);
}
public static int get() {
return value.get();
}
public static void main(String[] args) throws Exception{
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
executorService.execute(new Accessor(i));
}
TimeUnit.SECONDS.sleep(3);
executorService.shutdown();
}
}
复制代码
测试结果:
#0:712564
#0:712565
#0:712566
#0:712567
#0:712568/...
复制代码
ThreadLocal 对象一般当作静态存储域。建立 ThreadLocal 方法时只能经过 get() 和 set() 方法来访问内容,其中,get() 方法返回与对象相关联的副本,而 set() 将会将参数插入到为其线程存储的对象中,并返回存储中原有对象。运行这个程序的时候会发现每一个单独的线程都分配了本身的存储,由于他们每一个都要跟踪本身的计数值。