守护线程
总结
今天主要学习了多线程间的通信问题。
1. 多线程间通信
既多个线程在操作同一个资源,但是操作的动作不同。
Figure 1
如图,input和output在同时操作同一个资源,但是他们所做的的动作并不同。
2. 解决多线程通信安全问题
当多个线程不同方法操作同一资源时,会出现数据错乱问题,这就涉及到了
多线程间通信的安全问题。
出现安全问题要用同步,加锁,但是这里不能直接将run方法同步,这样会导致其成为单线程。
Figure2
要解决此问题,需要将synchronized定义在run方法内部,只需要将线程操作代码同步即可。示例中(代码省略)有input、output线程,因此需要将其各自内部的操作共同资源的代码同步。(虽然他们(线程操作代码)在两个run方法里面,但是它们在操作同一个资源)。
要用同步解决这类问题,必须满足两个前提:
l 明确是否是多线程;
l 明确是否使用同一个锁。
1、
Output类中的run方法里有操作共同资源的代码,因此要对其进行同步
Figure3
Input类中的run方法里也有操作共同资源的代码,因此也要进行同步。
Figure4
但是,运行后发现仍然出现数据错乱。
此时需要考虑是否使用同一个锁
2、
使用唯一对象。
可以是某唯一的对象,XXX.class(较牵强)
建议使用这里共同操作的唯一资源,——r
Figure5
3、数据私有化(private),只对外提供方法
Figure6
3. 等待唤醒机制
Figure7
操作线程的方法:wait、notify、notifyAll——都在Object类中。
监视器(也就是锁),只有同步才会有锁,所以这些方法都用于同步当中。
因为要对持有监视器(锁)的线程操作。所以要使用在同步中(只有同步才具有锁)。
由于这些方法在操作同步线程时,都必须要标识他们所操作线程持有的锁,只有同一个锁上的被等待线程,才可以被同一个锁上notify唤醒;不可以对不同锁中的线程进行唤醒,所以这些操作线程的方法要定义在Object类中。
也就是说,等待和唤醒必须是同一个锁。
锁可以是任意对象,可以被任意对象调用的方法定义在Object类中(为什么wait、notify、notifyAll都定义在Object类中)。
线程运行时,内存中会建立一个线程池,等待线程都存放在其中,notify唤醒线程池中的线程。notify通常唤醒第一个被等待的线程。
4. 生产者消费者例子
Figure8
if只判断一次;while,每次醒来都判断
While—–
Notifyall—-
为什么要定义while判断标记?
让被唤醒的线程再一次判断标记。
为什么要定义notifyAll()?
为了唤醒对方线程。如果只用notify(),容易出现只唤醒本方线程的情况,导致程序中的所有线程都等待——wait().
Figure9
JDK升级后提供了新的包(packet):Java.util.concurrent.locks(使用Lock之前一定要导入此包)
提供了多线程升级解决方案。未升级之前,一个锁只对应一个wait、notify。
将同步synchronized替换成实现Lock。
将Object中的wait、notify、notifyAll替换成了condition对象。该对象可以通过Lock锁进行获取。
Public interface Lock—->Lock提供了比使用synchronized方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的Condition对象。
condition将Object监视器方法(wait/notify/notifyAll)分解成截然不同的对象,以便通过将这些对象与任意的Lock实现组合使用,为每个对象提供了多个等待。
Lock替代了synchronized方法和语句的使用。
Condition替代了Object监视器方法的使用
Figure10
privateLock lock = new ReentrantLock();
private Condition con = lock.newCondition();
Reentrant:可重入;函数可以由多于一个线程并发使用,而不必担心数据错误。可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据。
ReentrantLock(重入锁):是一种递归无阻塞的同步机制。
Figure11
获取锁lock.lock();————void lock()____获取锁,如果锁不可用,出于线程调度目的,将禁用当前线程,并且在获得锁之前,该线程将一直出于休眠状态。
释放锁lock.unlock();————void unlock()____
Figure12
condition.await();等待————void await()
condition.signal();唤醒————void signal()
Void signalAll()唤醒所有等待线程
释放锁的动作一定要执行(finally中的内容一定会执行)
finally { } 里存放的一般是释放资源动作
Figure13
Figure14
和notifyAll的道理一样,也要使用signalAll()唤醒(且唤醒包含对方)。仅仅是将原来的代码wait、notifyAll替换为了lock、signalAll。
新特性好处:一个锁上可以有多个相关的Condition对象。
升级后,可以定义两个对象。就可以在set()、out()两个方法中调用这两个对象的await、signal方法,来彼此交替唤醒。
Figure15
PrivateCondition condition_pro = lock.newCondition();
Private Conditioncondition_con = lock newCondition();
Public void set(Stringname)throws InterruptedException
{
Lock.lock();
Try{
While(flag)
Condition_pro.await();(生产者等待)
——-;
Flag = true;
Condition_con.signal();(唤醒消费者)
}
Finally{
Lock.unlock();
}
}
Public void out()
{
Lock.lock();
Try{
While(!flag)(先判断是否满足标记){
Condition_con.await();
——–;
Flag = false;
Condition_pro.signal();
}
}
Finally{
Lock.unlock();
}
}
互相唤醒(只唤醒对方)。
5. 停止线程
Stop方法已经过时。有bug,但是没有在api中删除,因为有老程序员在用。
l 如何停止线程:run方法结束(唯一的方法)run方法结束,线程就结束。
开启多线程运行,运行代码通常是循环结构。控制住循环,就可以让run方法结束,线程也就结束(最终原理)。
l 当线程处于了冻结(中断)状态,就不会读取到标记,线程就不会结束。(中断不是停止)
Public void interrupt();
Interrupt方法将处于冻结状态的线程强制恢复到运行状态中来,
强制清除冻结状态
Figure 16
当没有指定的方式让冻结的线程恢复到运行状态时,这时需要对冻结进行清除。
强制让线程恢复到运行状态中来,这样就可以操作标记线程结束。
Thread类提供该方法interrupt();——sleep、wait、join都能被中断。
6. 守护线程
Public final void setDaemon(boolean on)
将该线程标记为守护线程或用户线程,当正在运行的线程都是守护线程时,jvm退出。(该方法必须在启动线程前调用)
Figure 17
两个线程都是守护线程时:
可以看到java虚拟机退出。
Figure 18
当所有的前台线程都结束后,后台自动结束运行。