Java - 线程
线程定义(先脑补这个):线程,是一个程序里面不同的执行路径。
有人说进程是可以执行的,从严格意义来讲,这个句是错误的。
进程只是一个静态的概念,QUE:什么叫进程?ANS:机器上的一个class文件、一个exe文件,这叫做一个进程。
大家都知道这个程序的执行过程,首先第一步:要把这个程序的代码放到内存代码区里面,放进去之后开始执行了吗?没有吧,OK,这个时候说明一个进程已经产生了,但是没有执行,因为它本身不能动。
那么平常说的进程执行了是指的什么呢?指的是进程里面主线程开始执行了。
在我们机器上面实际上运行的都是线程。
在同一个时间点,一个CPU(单核)只能够支持一个线程在运行,只不过CPU比较牛,它的计算速度非常快,我们人已经感觉不到间断了,看上去就好像多个线程同时在运行一样。
Java中每一个Thread对象代表一个线程。
补充说明:主线程可以在其它线程终止之前终止(各线程独立<包括主线程>,线程之间互不影响!)
想要在程序中开辟一个新路径,实际上分两步执行:
第一步:你首先要创建一个自己的线程对象(两种方式:1、实现Runnable接口。2、继承Thread类,Thread类本身也实现了Runnable接口。)
第二步:new Thread(自己的线程对象)对象并调用其start()方法启动该线程(需要注意的是:如果是继承的Thread类,那么本身就是一个Thread了,就不需要再new一个Thread进行封装,直接调用本身的start即可)。
补充说明:两种创建线程的方式,不推荐用继承,推荐能用接口的尽量用接口,因为它比较灵活,你实现这个接口后还可以继承其它类、实现其他接口。
规定(自己总结的):为了不产生说法与理解上的冲突,以后说实现了Runnable接口或者继承了Thread类的类叫线程运行类,它的实例叫线程运行对象,Thread类叫线程类(明确的说叫线程处理类),它的实例叫线程对象(明确的说叫线程处理对象)。
补充说明:如上线程状态转换图可以看出,当某线程调用start()方法只是说明该线程进入就绪状态,想要运行,还需等待CPU调度,调用start()方法并不代表立马就能进行运行状态。
sleep()方法例子:
join()方法例子:
yield()方法例子:
补充说明:主线程默认优先级是5。
synchronized(Java关键字,同步锁定):
1、 synchronized ( Object obj ) { … },这种写法用来声明某个对象的同步锁定块,圆括号内只能为引用数据类型。
2、 synchronized还可以用来修饰某个类中的动态属性(方法),表示该方法声明为同步锁定,例:public synchronized void getXXX() { … }或者public void getXXX() { synchronized ( this ) { … } }。
3、 类的成员变量不能被synchronized修饰,例:class T { public synchronized Xxx xxx; }
4、 当某对象被同步锁定时(注意:这里明确规定必须是同一对象,不是说equals的就一样,必须内存地址一样。),在同步锁定执行期内,该对象的同步内容(也就是被synchronized修饰的东西)被当前线程独享,此期间其它线程无法访问该对象中的同步内容,但依然可以访问该对象中的非同步内容(包括对这些内容进行同步锁定,比如说:B对象中有个引用数据类型的成员变量A<该引用A无论为静态还是动态的,只是要不是B的引用,都一样>,当B对象被同步锁定时并不代表也同步锁定了A,也就说其他线程也可以在B的同步锁定执行期来同步锁定A)
5、对象在同步锁定执行期内,可以被当前线程同步锁定多次(这里只是为了说明线程会正常执行,并不会出现死锁,因为这样编程无意义)。
上面这个例子程序中的Timer类的add方法需要上锁,Java关键字:Synchronized,写法如下:
死锁的原理:当多个线程在同一时间段内都需要同时锁住同一多个资源(两个或两个以上)才能完成执行时,会出现死锁。
比喻:你和你哥关系好,你们用同一套牙具(牙刷、牙膏、牙杯,现假设同时拿到这三样东西才能完成刷牙):有一天早上,你们同时起床去刷牙,假设你先拿到(程序里叫锁定)牙刷和牙膏,你哥先拿到牙杯,你在等拿到牙杯就可以开始刷牙,你哥在等拿到牙刷和牙膏就可以开始刷牙,现在东西只有一份,你们都死拿着自己已经拿到的东西不放,都在那等,所以谁也刷不成牙(程序里就叫死锁)。
与下面这个是连着一起的
解决死锁办法(有很多种办法):
1、把锁的粒度加粗。
以上例子程序说明一个问题:
如果一个类里即有同步锁定方法,也有非同步锁定方法,这时你只能保证同步锁定方法在该对象在同步锁定执行期被当前线程独享,你并不能确保非同步锁定方法在此期间被其它线程访问(这就有可能会影响到你已经声明了同步锁定方法)。所以为了数据的前后一致,考虑把这个类中的某个方法声明为同步锁定时,你应该也该考虑该类中其它方法是否也会存在不同步,需要一并考虑,若存在,则都需要声明为同步锁定。
同步锁定方法必须等该对象解锁后才能调用。
同步锁定方法被调用时,如果调用无效(也就说该同步锁定方法正处于同步锁定执行期),则当前线程原地等待此同步锁定方法解锁(此时线程处于阻塞状态),解锁后当前线程才会继续往下执行!
同步锁定方法在执行过程中可以随意调用本对象中的同步锁定方法(包括自己,这就说明同步锁定方法是可以递归的),也可以调用其它类的同步锁定方法,前提是该同步方法所在资源是解锁状态)!!!
理解方法(暂不予以采用,感觉有些不妥):
QUE:怎样去理解锁呢?ANS:把一个类想像成一个仓库,该仓库中许多资源,有些资源比较严密,仓库管理员都给它们上了锁并且放置在同一禁区内,该禁区的大门也上了一把锁,大门上挂着一把钥匙,这是唯一能开启禁区大门与所有资源锁的钥匙,想要进入禁区用资源,必须得先有这把钥匙,进入禁区后,禁区大门自动关闭(别人无法进入:也就说,在同一时间段,禁区内只允许有一人在使用资源,其他要用资源的人在禁区大门外排队等候),此时,您可以使用该仓库(包括禁区)的所有资源(也就说同步方法可以调用本资源中的任意一个同步方法,包括自己),也可以使用其它仓库的资源,如果是禁区的(也就是其它资源的同步方法),前提是你能拿到这个仓库的钥匙(也就说前提是该资源是解锁状态的),出禁区时该钥匙不能带走,必须归还原位,供下一位进入禁区用资源。
经典问题:生产者与消费者的问题(例如,一些人做馒头,都放到同一个篮子里,一些人都从这个篮子里拿馒头吃),如下图:
程序代码:
名词解释:
监视器、监听室(指的同一个东西,但推荐用监听室说法,因为容易理解一点):设想每一类对象都共享一个监听室,在监听室内的所有对象,都在监听该监听室所属对象(也就是该监听室的主人)发出某种通知(比如说状态改变)。
Object类中有三个方法:
1、public final void wait()throws InterruptedException:说的不是让当前对象wait,而是让当前正在访问这个对象的线程wait(也就是去该对象的监听室内等待)。
2、public final void wait(long timeout) throws InterruptedException:当前正在访问这个对象的线程进入该对象的监听室内等待,若等待时间超过timeout毫秒数(不是说一定要等timeout毫秒才能被唤醒,说的是也可以在此之前被唤醒),则自动被唤醒。
3、public final void wait(long timeout, int nanos) throws InterruptedException:与wait(long timeout)相似,只不过时间控制更精确,nanos参数为微毫秒(纳秒。
4、public final void notify():唤醒一个正在此对象的监听室内等待的线程,线程被唤醒后要做的第一件事应该就是唤醒其它线程。
5、public final void notifyAll():唤醒所有在此对象的监听室内等待的线程,线程被唤醒后要做的第一件事应该就是唤醒其它线程。
6、这一点是自己想的,还未被证实,但觉得应该是如此的:若监听室内可能同时存在多个等待的线程,强烈推荐用notifyAll(),否则用notify()可能会出现死锁(因为notify()只是唤醒一个线程,若当前线程是最后一个能执行notify()的线程(也就说该线程执行完本次notify()就终止了),又当被它唤醒的这个线程重新判断等待条件被再次满足时,则此被唤醒的线程将再次进入此对象的监听室内等待,而又因为在它进入监听室之前是目前为此最后一个不在监听室内的线程,所以会有短暂的死锁现象,直到再有一个能执行此对象notify()方法的新线程来通知(否则将一直死锁),并且被唤醒的还要是无需wait()的线程也就是重新判断等待条件时不满足,否则同上理,再次死锁。),此时notifyAll()也会会比nitofy()执行速度要快,运行时系统能更快的找到监听室内无需wait()的线程,就看这一点都强烈推荐。
注意:
1、 只有锁定了当前对象的线程才能调用此对象的wait()方法,否则你别谈wait(),你都没锁定我这个对象,凭什么叫我wait(),你一调wait(),立马报错(提示:编译是不会出错的)。
2、 this.wait()方法特定的执行条件对于所有可能进入该对象的监听室内等待的线程来说应该存在对立关系,若有满足条件的线程A,就一定要有不满足的线程B,否则也可能会有短暂死锁现象。
3、this.wait()和Thread.sleep()的巨大区别:当某对象调用this.wait()方法后,当前线程将解除该对象的同步锁定(注意:此处只解除该对象的同步锁定,并不解除当前线程已锁定的其它对象的同步锁定),并且进入该对象的监听室内等待;而Thread.sleep(),该对象如果是锁定的,则不会解锁(睡觉也锁定这对象不放),直到sleep完毕后,再等同步锁定块执行完毕,该对象才会解锁。
4、this.wait()方法一般是和this.notify()方法应是一一对应的,满足特定条件就wait(),每顺利执行一次我就notify()一次(也就说每顺利执行一次,我就通知在当前对象的监听室内等待的线程一次),所以wait()后一般要写notify()方法,否则若某线程在进入该对象的监听室内等待之前是最后一个在监听室外的线程,那么此对象的监听室内所有等待的线程就都一直wait()在这里了,因为此时大家都在等其它线程通知,所以此时所有在此对象的监听室内等待的线程都将无法被唤醒,这也是个死锁现象(不过也是短暂的),直到再有一个能执行此对象notify()方法的新线程来通知并成功唤醒,即可解锁)。
5、notify()是唤醒一个在该对象的监听室内等待的线程(至于是哪个线程,由运行时系统决定,程序无法控制),notifyAll()则是唤醒所有在该对象的监听室内等待的线程(全部一拥而起)。
6、当某线程被唤醒,继续执行时,是接在wiat()方法后的代码继续往下执行,而不会执行wiat()方法之前的代码,所以wait()方法的特定执行条件不应用if( ... ) { …wait();… }判断,而应用while( … ) { …wait();… },这样的话,当线程再次被唤醒继续执行时,就会再次判断wait()方法的特定条件,一旦满足,将继续wait(),这样就避免了直接跨过等待条件判断,从而确保程序不出现非预计的异常情况。