Java并发学习(二)

一、start和run的区别

在这里插入图片描述

  • start方法是创建一个新的子线程并启动(调用run方法)
  • run方法只是Thread的一个普通方法的调用

二、线程的状态

1. 新建(New): 创建后尚未启动的线程的状态

2. 运行(Runnable):包含Running和Ready

3. 无限期等待(Waiting): 不会被分配CPU执行时间,需要被唤醒

  • 没有设置Timeout参数的Object.wait()方法
  • 没有设置Timeout参数的Thread.join()方法
  • LockSupport.part()

4. 限期等待(timed waiting):在一定时间后会由系统自动唤醒

  • Thread.sleep()
  • 没有设置Timeout参数的Object.wait() 方法
  • 没有设置Timeout参数的Thread.join() 方法
  • LockSupport.parkNanos() 方法
  • LockSupport.parkUntil() 方法

5. 阻塞(Blocked): 等待获取排他锁

  • 就是在竞争锁的时候,被阻塞了(例如因Lock 或者synchronize 关键字产生的状态)

6. 终止(terminate):终止状态

在这里插入图片描述

三、wait/notify/notifyAll

这几个方法光知道是个啥,但理解的还不够深入,这里做一下总结。

1. wait/notify/notifyAll

在聊上面这个常见的方法之前,有必要先知道什么是管程。

当我们对临界区进行实现的时候,往往都是通过PV操作来实现的,但让程序员手动去做PV操作,很容易发生死锁。 所以为了方便编程,减少死锁出现的可能,我们希望能有一种数据结构或是软件模块来专门为我们提供对“临界区”的实现,这就是管程了~(但单单就说管程就是对临界区的实现是不准确的,继续往下看)

但仅仅是实现临界区还是不够的,比如,当线程A获取到锁了之后,进入了临界区,这个时候因为一些外部条件X, 而导致无法进行下去,这个时候就需要等待这个外部条件X的发生… 而假设这个外部条件X的发生是需要另一个线程B进入到当前的这个“临界区”中才能触发,而因为线程A已经处于临界区中了,所以线程B需要等待线程A退出临界区才能继续执行。。 于是。。就变成了线程A在等线程B,线程B在等线程A,死锁出现了。。

因此,解决临界区中的线程同步问题,也是管程需要实现的。

一个解决方案就是,在临界区中的线程A一旦发现自己想要的外部条件没有发生,而不能够继续进行下去了的时候,就主动释放掉当前获取的这个临界区的锁,然后让其他线程进入到这个临界区来触发这个“外部条件X”的发生。。 等到这个外部条件X发生了之后,再通知线程A(之前因这个条件而释放掉锁的那个线程)重新去竞争锁,继续执行临界区…

这个方法流程是不是很熟悉? 没错,这不就是wait和notify嘛。。

因此,管程的实现主要就是:

  • 临界区的实现
  • monitor 对象及锁的实现
  • 条件变量以及定义在 monitor 对象上的 wait,signal 操作的实现

然后就可以了解下Java对管程的实现了。

a. 对临界区的实现

Synchronized的同步块, ReentrantLock在lock和unlock期间的那段代码… 都是对临界区的实现…

在Java中,每个对象都有两个池,锁(monitor)池和等待池

锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.

b. 条件变量以及定义在 monitor 对象上的 wait,signal 操作的实现

对于Synchronized,只实现了wait和signal操作…
如果想使用更细粒度的条件变量,来控制临界区内线程的同步,那么可以使用ReentrantLock来做…

ReentrantLock提供了Condition变量,作为条件变量,对应的方法是 condition.await() 和 condition.signal()

锁池和等待池

在Java中,每个对象都有两个池,锁(monitor)池和等待池

锁池: 假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

等待池: 假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池。

wait原理:

如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

必读:
sleep()和wait()方法与对象锁、锁池、等待池

Thread.yield()和Thread.sleep(0)

推荐阅读

2. wait和park的区别

我们在编程的时候必须能保证wait方法比notify方法先执行。如果notify方法比wait方法晚执行的话,就会导致因wait方法进入休眠的线程接收不到唤醒通知的问题。

而park、unpark则不会有这个问题,我们可以先调用unpark方法释放一个许可证,这样后面线程调用park方法时,发现已经许可证了,就可以直接获取许可证而不用进入休眠状态了。

LockSupport.park() 的实现原理是通过二元信号量做的阻塞,要注意的是,这个信号量最多只能加到1。我们也可以理解成获取释放许可证的场景。unpark()方法会释放一个许可证,park()方法则是获取许可证,如果当前没有许可证,则进入休眠状态,知道许可证被释放了才被唤醒。无论执行多少次unpark()方法,也最多只会有一个许可证。

另外,和wait方法不同,执行park进入休眠后并不会释放持有的锁。
并且,调用wait方法需要已经获取到锁,而park则不需要

四、wait的局限,以及Condition的出场

使用wait的一个前提就是在sync的同步块里,而这又导致了在同步块里的条件变量只有一个,尽管可以通过共享变量的方式来实现“需要多个条件变量的场合”,但这样不仅实现的复杂度高,而且也不是很高效。因此,为了让在同步块中使用更多样的条件变量(即对某一资源或者某一个事件的等待),ReentrantLock就提供了Condition这一个神器,一个Lock可以new出多个Condition,即多个等待队列。

所以,await/signal, 可以看成强化版的 wait/notify

参考链接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章