3G基础知识
我们如今已经了解到,除非使用锁或volatile修饰符,否则无法从多个线程安全地读取一个域。
但是还有一种情况可以安全的访问一个共享域,即这个域声明为final时。
final Map《String,Double》 accounts = new HashMap();
这样子,其他线程会在构造函数完成构造之后才看到这个accounts变量。
如果不使用final,就不能保证其他线程看到的是accounts更新后的值,它们可能都只是看到null,而不是新构造的HashMap。
当然,对这个映射表的访问是安全的,但是并不意味对他的操作是安全的!如果多个线程在读写这个映射表,仍然要进行同步。
假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为volatile(他就是一个简单赋值,而不是读取 增加某个数 再赋值 ,所以他是原子操作! 是 x = 1 而不是 x = x +1)。
但是我们还有更好的原子操作的表示方法,请看:
在java.util.concurrent.atomic包中有很多类使用了高效的机器指令(而不是锁)来保证其他操作的原子性。
例如:AtommicInteger类提供了方法incrementAndGet 和 decrementAndGet,它们分别以原子方式将一个整数自增或自减。例如,可以安全地生成一个数值序列,如下所示:
public static AtomicLong nextNumber = new AtomicLong();
// In some thread.。.
long id = nextNumber.incrementAndGet();
incrementAndGet方法以原子方式将将AtomicLong自增,并返回自增后的值。可以保证,即使多个线程访问一个实例,也会计算并返回正确的值。
有很多方法可以以原子方式设置和增减值,不过,如果希望完成更复杂的更新,就必须使用compareAndSet方法。例如,假设希望跟踪不同线程观察最大值。下面的代码应该是这样的:
do{
oldValue = largest.get();
newValue = Math.max(oldValue,observed);
}while(!largest.compareAndSet(oldValue,newValue))
这样子的话,如果另一个线程也在更新largest,就可能阻止这个线程更新。这样一来,compareAndSet就会返回false,而不是设置新值。这种情况下,循环会再次尝试,读取更新后的值,并尝试修改。最终,他会成功地用新值替换原来的值。
accumulateAndGet方法利用一个二元操作符来合并原子值和所提供的参数。
还有getAndUpate和getAndAccumulate。
如果有大量的线程要访问相同的原子值,性能会大大下降,因为乐观更新需要太多次重试。Java 8 针对这点提供了LongAdder 和 LongAccumulator类来解决这个问题。LongAdder包括多个变量(加数),其综合为当前值。可以有多个线程更新不同的加数,线程个数增加时会自动提供新的加数。代码如下:
final LongAdder adder = new LongAdder();
for(...)
pool.submit(()-》{
while(...){
...
if(...)adder.increment();
}
});
...
long total = adder.sum());
LongAccumulator将这种思想推广到任意的累加操作中。在构造器中,可以提供这个操作以及它的零元素。要加入新的值,可以调用accumulate。调用get来获得当前值。
锁和条件不能解决多线程中的所有问题比如,死锁(考虑下哲学家问题)。
遗憾的是java中并没有能完全避免死锁的方法,但是我们可以通过自己的设计和良好的习惯来避免死锁。
这里就可能需要用到测试锁了:线程在调用lock方法获得另一个线程持有的锁的时候,很可能发生阻塞,甚至发生死锁。trylock方法试图去申请一个锁,在成功获得锁后会返回一个true,否则,立即返回false,而且线程可以立即离开去做其他事情。
if(mylock.trylock()){
// 现在已经上锁
try{...}
finally{ mylock.unlock();}
}
else
// do something else
可以调用tryLock时,使用超时参数,像这样:if(mylock.tryLock(100,TimeUnit.MILLISECONDS))
TimUnit是个枚举类,可取的值包括SECONDS,MILLISECONDS,MICROSECONDS和NANOSECONDS。
有趣的地方在这里,如果一个线程调用带有超时参数的tryLock,同时调用后如果线程在等待期间被中断,将抛出一个InterruptedException异常。这是一个非常有用的特性,因为允许程序打破死锁!
而调用lockInterruptibly方法,就相当于一个超时设为无限的tryLock方法。
在等待条件对象时候也可以提供一个超时:
myCondition.await(100,TimeUnit.MILLISECONDS)
前面几节中,我们讨论了在线程间共享变量的风险。有时可能要避免共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例。例如,SimpleDateFormat类不是线程安全的。假设有一个静态变量:
public static final SimpleDateFormat dataFormat = new SimpleDateFormat(“yyyy-MM-dd”);
如果两个线程都执行以下操作:
String dateStamp = dateFormat.format(new Date());
结果很可能会混乱,因为dateFormat使用内部数据结构可能会被并发的访问破坏。当然可以使用同步,但开销很大;或者也可以在需要时构造一个局部SimoleDateFormat对象,不过这也太浪费了。
这时候,我们就可以为每一个线程构造一个实例,如下:
public static final ThreadLocal《SimpleDateFormat》 dateFormat =
ThreadLocal.withInitial(()-》new SimpleDateFormat(“yyyy-MM-dd”));
要是说具体的格式化方法,可以调用:
String dateStamp = dateFormat.get().format(new Date());
在一个给定线程中首次调用get时,会调用initialValue方法。在此之后,get方法会返回属于当前线程的那个实例。
Part 读写锁:
java.until.concurrent.locks包定义了两个锁类,我们已经讨论的ReentrantLock类和ReentranReandWriteLock类。如果很多线程从一个数据结构读取数据而很少线程修改其中数据的话,后者是十分有用的。在这种情况下,允许读者线程共享访问是合适的,当然,写者线程依然必须是互斥访问。
下面是读写锁的必要步骤:
(1)构造一个ReentranReandWriteLock对象;
private ReetrantReadWriteLock rwl = new ReentrantReanWriteLock();
(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();}
}
全部0条评论
快来发表一下你的评论吧 !