说明一点:由于本博客的原材料最初是写在Word文档中的,包括一部分代码,所以代码中有
许多的标点符号,比如分号,逗号都是中文的,所以直接复制代码在编译器中会发现许多错误;
但是这些代码都是对博客中的知识点起解释说明作用的,其使用方式都是完全按照黑马官网提供的
教学视频所讲而作的笔记,所以知识点和思想是正确的!
《线程基础篇》
-----------------------------------------------------------------------------------------------------------------------------------
多线程概述:
进程:进程就是一个正在进行着的程序;
线程:就是进程中的一个独立的控制单元,线程在控制着进程的执行;
创建线程的方式:
方式一:继承Thread 类:
步骤:a.定义一个类继承Thread;
b.复写Thread类中的run( )方法:
c.调用线程的start( )----开启线程,并调用执行run( )方法;
方式二:实现Rannable接口:
步骤:a.定义一个类实现Rannable接口;
b.覆盖Rannable接口中的run( )方法;
c.通过Thread类创建线程对象;
d.将Rannable接口的子类对象作为实际参数传递给Thread;
e.调用Thread类的start方法开启线程,调用Runnable接口子类的run方法 。
两种创建方式的区别:
实现方式避免了单继承的局限性,因为在java中是不支持多继承的,当我们定义的类继承Thread类后,
他就不能再去继承其他的类,利用人家的特有成员了,所以扩展性比较差!比如你有一个Student类继承了
Thread类,以后你想把Student类中的一些属性,比如name、age,抽象成Person类,而Student类已经
继承了Thread类,此时就不方便扩展了。如果实现Runnable接口,就可以避免这个限制,因为java支持
一个类实现多个接口。
另外实现Runnable接口比继承Thread类更容易解决资源共享的问题,在Runnable子类中定义的成员
变量可以直接被他的子线程共享,如果使用Thread类,就必须把这个变量声明为static才行,否则每一个子
类中都有一份相同的数据,那就不是访问同一资源了(比如五个售票窗口同时卖200张票的情形!)。
通过查看java源代码我们可以发现:实际上方式一:继承Thread类创建线程在本质上适合方式二相同的,
也就是说;通过继承Thread类创建线程实际上也是通过实现runnable接口来实现功能的。
其实多线程并不能提高程序的运行效率,相反她还会降低程序的运行速度,因为CPU在各个线程之间
进行着快速的切换,而切换的过程这一段时间CPU并未执行任何操作,浪费了时间,也就意味着运行效率
的降低,比如我们分别复制十部电影所需的时间要比一次性复制十部电影所耗的时间我们要多,这就是证明!
但是我们为什么还要用多线程呢?因为利用多线程,我们打开电脑可以同时打游戏,听歌,看电影,聊QQ,
而她降低的运行效率对我们的体验没有任何影响,同时还满足了我们多任务同时运行的要求,何乐而不为呢!
线程安全问题:
对于多线程的运行,CPU实际上在多个线程之间进行着快速的切换,那么就有这样一种情形:多个线程
访问同一个资源,同时这些线程之中有多条执行语句,那么当CPU只执行了某一个线程的一部分代码后,
就切换到了别的线程上面了,等CPU再切换回来,也许所访问的资源已经被消耗光了,但是此时程序不会再
一次去执行其中的控制语句(比如if控制语句),从而使得共享数据发生了错误!
分析了安全隐患的产生原因,我们只要采取相对应的措施,就可以避免问题的发生:
既然在一个线程执行过程中,其他线程突然抢去了CPU的执行权利,可能发生安全隐 患,那么我们可以
不让其他线程抢夺CPU的执行权,不就不会产生安全问题了吗?
那么我们如何不让其他线程来抢夺CPU的执行权呢?java对此提供了专业的解决方式,即通过同步代码块
(除此而外还有同步函数的形式)的方式来保证一个线程在执行的过程中,其他的线程无法第三者插足!
同步代码块格式如下:
synchronized(对象){
需要被同步的代码;
}
在这里对象如同锁,持有锁的线程可以在同步内执行,而没有锁的线程将无法执行;
同步的前提:
1. 必须要有两个或者两个以上的线程;
2. 必须是多个线程使用同一个锁;
同步的好处:解决了线程的安全问题;
同步的弊端:多个线程都需要判断锁,比较消耗资源,同时运行效率下降!
同步函数:被synchronized关键字修饰的函数;----这样就不需要在函数里面去创建同步代码块了,其格式为:
权限修饰符 synchronized返回值函数名(参数列表){}
前面将了对于同步代码块,它的锁是对象,通常用我们手动传入;而同步函数它的锁又是什么呢?
函数需要被对象调用,那么函数都有一个所属的对象的引用,所以同步函数的锁就是调用该函数的对象,
也就是this;
静态同步函数:也就是被static关键字修饰的同步函数;但是静态函数被加载进内存时,内存中可能还并没有,
那么说他所持有的锁是this就不对了。他所持有的锁必须是在内存中已经存在的对象,那么在静态同步函数
加载进内存时,有什么是已经在内存中存在的呢?那就是该类对应的字节码文件---类名.class;所以我们规定
静态同步方法使用的锁是该方法所在类的字节码文件对象!
死锁:我们知道咱们中国吃饭基本上是用筷子,而且必须是两根筷子才可以夹菜,一根筷子是无法夹菜的;
现在有了这样一种情形:两个人吃饭,但是只有两根筷子,二人拿筷子是一根一根的拿,当一个人拿了一根
筷子,还没有等他继续去抢另外一根筷子时,另外一根筷子已经被另外一人抢去了,这时,两人一人一根筷
子,但是两人都很自私,不愿意将筷子送给对方,于是两人都吃不了饭!------对应到java的死锁上面可以把
两个线程换作是那两个人,而筷子就是线程需要抢夺的锁!现在我们明白了死锁产生的原因了,两个线程彼
此都持有了对方想要获得的锁,从而发生了矛盾!
-----所以当我们看见同步代码中还有同步代码这种情形,那么就必须小心有可能发生死锁了!
那么如何避免死锁这种情况的发生呢?
一般我们最好不要写出同步代码嵌套的形式,如果因为开发需求一定要这样写的话,可以在嵌套同步代码
的最外层再给他加上一层单独的锁,它的作用是保证,一个线程在取得了一个锁时,另外的线程不要再去抢夺
另外一个锁了!这样也就可以避免死锁的发生了!对应到吃饭的例子上就是,二人在强筷子之前,打了个赌,
赢的人在一根一根取筷子的时候,另外一个人就不要去抢了,这样就可以保证赢的人一定可以拿到两根筷子。
线程间通信:这种情况是用于多个线程在操作同一资源,但是操作的比较有规律:线程A操作以后,他不会
再去抢夺CPU的执行权,而是停下来,让线程B去执行,线程B执行完了以后,他不会再去抢夺CPU的执行权,
而是停下来让线程C去执行……等所有的线程都执行了一次以后,线程A又才继续权夺CPU执行权继续执行;
线程间通讯就是指一个线程执行结束以后通知其他等待的线程继续执行的情形;
在java中线程间通信的效果是通过等待唤醒机制来实现的;等待唤醒机制主要是由:wait()和notify()
或者notifyAll()这些方法在同步代码中配套使用来完成的,并且这些方法都必须是由锁来调用的!此外,通常在
使用线程间通信时会定义一个标签---flag(boolean型的变量);通过这个变量来控制是否应该等待,或者执行!
具体使用见下方代码(不是完整的,仅是起说明作用):
class Resource {
private String name;
private int count = 1;
private boolean flag = false; //定义了一个标记!
// t1 t2
public synchronized void set(String name) { //所持有的锁是this