java writelock 死锁_Java学习day094 并发(四)(同步(三):死锁、线程局部变量、锁测试与超时、读/写锁、为什么弃用 stop 和 suspend 方法)...

使用的教材是java核心技术卷1,我将跟着这本书的章节同时配合视频资源来进行学习基础java知识。

day094   并发(四)(同步(三):死锁、线程局部变量、锁测试与超时、读/写锁、为什么弃用 stop 和 suspend 方法)

1.死锁

锁和条件不能解决多线程中的所有问题。考虑下面的情况:

账户1:$200

账户2:$300

线程1:从账户1转移$300到账户2

线程2:从账户2转移$400到账户1

如图所示,线程1和线程2都被阻塞了。因为账户1以及账户2中的余额都不足以进行转账,两个线程都无法执行下去。有可能会因为每一个线程要等待更多的钱款存人而导致所有线程都被阻塞。这样的状态称为死锁(deadlock)。在这个程序里,死锁不会发生,原因很简单。每一次转账至多$1000。因为有100个账户,而且所有账户的总金额是$100000,在任意时刻,至少有一个账户的余额髙于$1000。从该账户取钱的线程可以继续运行。但是,如果修改run方法,把每次转账至多$1000的限制去掉,死锁很快就会发生。试试看。将NACCOUNTS设为10。每次交易的金额上限设置为2*INITIAL_BALANCE,然后运行该程序。程序将运行一段时间后就会挂起。

9b8808231713eaee3d0dcfc44922f8a1.png

导致死锁的另一种途径是让第i个线程负责向第i个账户存钱,而不是从第i个账户取钱。这样一来,有可能将所有的线程都集中到一个账户上,每一个线程都试图从这个账户中取出大于该账户余额的钱。试试看。在SynchBankTest程序中,转用TransferRunnable类的run方法。在调用transfer时,交换fromAccount和toAccount。运行该程序并查看它为什么会立即死锁。

还有一种很容易导致死锁的情况:在SynchBankTest程序中,将signalAll方法转换为signal,会发现该程序最终会挂起(将NACCOUNTS设为10可以更快地看到结果)。signalAll通知所有等待增加资金的线程,与此不同的是signa丨方法仅仅对一个线程解锁。如果该线程不能继续运行,所有的线程可能都被阻塞。考虑下面这个会发生死锁的例子。

账户1:$1990

所有其他账户:每一个$990

线程1:从账户1转移$995到账户2

所有其他线程:从他们的账户转移$995到另一个账户

显然,除了线程1,所有的线程都被阻塞,因为他们的账户中没有足够的余额。

线程1继续执行,运行后出现如下状况:

账户1:$995

账户2:$1985

所有其他账户:每个$990

然后,线程1调用signal。signal方法随机选择一个线程为它解锁。假定它选择了线程3。该线程被唤醒,发现在它的账户里没有足够的金额,它再次调用await。但是,线程1仍在运行,将随机地产生一个新的交易,例如,

线程1:从账户1转移$997到账户2

现在,线程1也调用await,所有的线程都被阻塞。系统死锁。问题的起因在于调用signal。它仅仅为一个线程解锁,而且,它很可能选择一个不能继续运行的线程(在我们的例子中,线程2必须把钱从账户2中取出)。遗憾的是,Java编程语言中没有任何东西可以避免或打破这种死锁现象。必须仔细设计程序,以确保不会出现死锁。

2.线程局部变量

我们学习了在线程间共享变量的风险。有时可能要避免共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例。例如,SimpleDateFormat类不是线程安全的。假设有一个静态变量:

public static final SimpleDateFormat dateFormat = new SimpleDateForniat("yyyy-MM-dd");

如果两个线程都执行以下操作:

String dateStamp = dateFormat.format(new Date());

结果可能很混乱,因为dateFormat使用的内部数据结构可能会被并发的访问所破坏。当然可以使用同步,但开销很大;或者也可以在需要时构造一个局部SimpleDateFormat对象,不过这也太浪费了。要为每个线程构造一个实例,可以使用以下代码:

public static final ThreadLocaldateFormat = ThreadLocal.withInital(() - > new SimpleDateFormat("yyy-MM-dd");

要访问具体的格式化方法,可以调用:

String dateStamp = dateFormat.get().format(new Date());

在一个给定线程中首次调用get时,会调用initialValue方法。在此之后,get方法会返回属于当前线程的那个实例。在多个线程中生成随机数也存在类似的问题。java..util.Random类是线程安全的。但是如果多个线程需要等待一个共享的随机数生成器,这会很低效。

可以使用ThreadLocal辅助类为各个线程提供一个单独的生成器,不过JavaSE7还另外提供了一个便利类。只需要做以下调用:

int random=ThreadLocalRandom.current().nextlnt(upperBound):

ThreadLocalRandom.current()调用会返回特定于当前线程的Random类实例。

bccf7622591c8b3f0ad825906f6bc797.png

3.锁测试与超时

线程在调用lock方法来获得另一个线程所持有的锁的时候,很可能发生阻塞。应该更加谨慎地申请锁。tryLock方法试图申请一个锁,在成功获得锁后返回true,否则,立即返回false,而且线程可以立即离开去做其他事情。

if (myLock.tryLock())

{

//now the thread owns the lock

try { . . . }

finally { myLock.unlockO; }

}

else

//do something else

可以调用 tryLock 时,使用超时参数,像这样:

if (myLock.tryLock(100, TineUnit.MILLISECONDS)) ...

TimeUnit是一枚举类型,可以取的值包括SECONDS、MILLISECONDS,MICROSECONDS和NANOSECONDS。lock方法不能被中断。如果一个线程在等待获得一个锁时被中断,中断线程在获得锁之前一直处于阻塞状态。如果出现死锁,那么,lock方法就无法终止。

然而,如果调用带有用超时参数的tryLock,那么如果线程在等待期间被中断,将抛出InterruptedException异常。这是一个非常有用的特性,因为允许程序打破死锁。也可以调用locklnterruptibly方法。它就相当于一个超时设为无限的tryLock方法。在等待一个条件时,也可以提供一个超时:

myCondition.await(100,TineUniBILLISECONDS))

如果一个线程被另一个线程通过调用signalAU或signal或者超时时限已达到,或者线程被中断,那么await方法将返回。如果等待的线程被中断,await方法将抛出一个InterruptedException异常。在你希望出现这种情况时线程继续等待(可能不太合理),可以使用awaitUninterruptibly方法代替await。

c9518d1756ee1148270c23a32b27edb6.png

d5d2c0d234814f7b9e7c4d92fc0cefca.png

4.读/写锁

java.util.concurrent.locks包定义了两个锁类,我们已经讨论的ReentrantLock类和ReentrantReadWriteLock类。如果很多线程从一个数据结构读取数据而很少线程修改其中数据的话,后者是十分有用的。在这种情况下,允许对读者线程共享访问是合适的。当然,写者线程依然必须是互斥访问的。

下面是使用读/写锁的必要步骤:

1)构造一个ReentrantReadWriteLock对象:

private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

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

}

115a802e83e14c272498fc5f9c0be53d.png

5.为什么弃用 stop 和 suspend 方法

初始的Java版本定义了一个stop方法用来终止一个线程,以及一个suspend方法用来阻塞一个线程直至另一个线程调用resume。stop和suspend方法有一些共同点:都试图控制一个给定线程的行为。

stop、suspend和resume方法已经弃用。stop方法天生就不安全,经验证明suspend方法会经常导致死锁。我们将看到这些方法的问题所在,以及怎样避免这些问题的出现。

首先来看看stop方法,该方法终止所有未结束的方法,包括run方法。当线程被终止,立即释放被它锁住的所有对象的锁。这会导致对象处于不一致的状态。例如’假定TransferThread在从一个账户向另一个账户转账的过程中被终止,钱款已经转出,却没有转人目标账户,现在银行对象就被破坏了。因为锁已经被释放,这种破坏会被其他尚未停止的线程观察到。

当线程要终止另一个线程时,无法知道什么时候调用stop方法是安全的,什么时候导致对象被破坏。因此,该方法被弃用了。在希望停止线程的时候应该中断线程,被中断的线程会在安全的时候停止。

接下来,看看suspend方法有什么问题。与stop不同,suspend不会破坏对象。但是,如果用suspend挂起一个持有一个锁的线程,那么,该锁在恢复之前是不可用的。如果调用suspend方法的线程试图获得同一个锁,那么程序死锁:被挂起的线程等着被恢复,而将其挂起的线程等待获得锁。

在图形用户界面中经常出现这种情况。假定我们有一个图形化的银行模拟程序。Pause按钮用来挂起转账线程,而Resume按钮用来恢复线程。

pauseButton.addActionListener(event ->{

for (int i = 0;i {

for (int i = 0;i < threads.length;i++)

threads[i].resume();

});

假设有一个paintComponent方法,通过调用getBalances方法获得一个余额数组,从而为每一个账户绘制图表。就像之前所看到的,按钮动作和重绘动作出现在同一个线程中—事件分配线程(eventdispatchthread)。

考虑下面的情况:

1)某个转账线程获得bank对象的锁。

2)用户点击Pause按钮。

3)所有转账线程被挂起;其中之一仍然持有bank对象上的锁。

4)因为某种原因,该账户图表需要重新绘制。

5)paintComponent方法调用getBalances方法。

6)该方法试图获得bank对象的锁。

现在程序被冻结了。

事件分配线程不能继续运行,因为锁由一个被挂起的线程所持有。因此,用户不能点击Resume按钮,并且这些线程无法恢复。

如果想安全地挂起线程,引人一个变量suspendRequested并在run方法的某个安全的地方测试它,安全的地方是指该线程没有封锁其他线程需要的对象的地方。当该线程发现suspendRequested变量已经设置,将会保持等待状态直到它再次获得为止。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值