温故而知新---陆续学习陆续更新中,你有更好的记忆和学习方法,请在评论区提出来大家一起交流,认真看完,一行行代码备注看完看懂,保证学会,学不会找我。
前言:
代码学完发现很简单,很多初学者搞不明白什么这个那个,其实是没有把概念记住,没有那么多为什么,为什么就这样做,就像小时候学数学,有很多的公式,刚开始学,我们只要死记公式,然后多练习,练习多了就不需要公式了,有时口算都能出来,到现在也不需要去理解公式为何是这样,不需要去深究它。后面你精通后在去专研它也可以。
比如在我的笔记里,里面有很多固定写法,你只需要记住就行,比如有class开头你就知道后面肯定是类对象,有new就是新建一个对象,List这样的你就知道是集合、有<>是泛型的概念等等,多多照着做,一百个人写一个相同的代码都可能有100个为什么这样写可以,这样写不可以,我能不能这样写的念头。
师傅领进门,修行看个人,同一套剑法,同一个师傅,100个学生的等级功力都不一样,更可况后面选择的职业和方向都不一样的,要想厉害就要多磨剑,多练,不然久不练剑法招式会忘光,代码页一样,你天天敲,敲个一年半载,其实来回敲的都是那些代码,换汤不换药,都一个套路,程序员你一个星期不写代码,你都不熟练甚至都不知道了,更可况你一个新入门的新手。
Java基本入门基础已经写完,现在要讲些进阶教程(标红的都是需要掌握的知识),这里让你快速复习这些进阶知识的概念,快速回忆:
- 线程/并发(加锁、死锁)
- 枚举
- IO流(文件操作)
- 集合(容器)
- 泛型
- 反射
- 网络通信
- 注解
- JVM虚拟机(类加载)
- 动态与函数式编程
- 异步/同步
线程
我们原来做的都是属于单线程,就是一步步的执行,那多个线程是什么,就是说你在执行这个的时候,另个线程也在同步执行,就看谁先抢占资源。
单线程:逐行(一行行)去执行,如:先写一个类,在写一个方法,然后写内容。
程序:就是执行的代码;
进程:执行中的程序;
线程:轻量级的进程;(线程本身不能单独运行,必须放在一个进程中才能执行)
线程模型:一般是五种状态,要记住;
1.新建状态:线程被创建后处于新建状态;
2.就绪状态:(1)新建状态的线程调用start()进入就绪状态;(2)阻塞状态的线程解除之后进入就绪状态;
3.运行状态:处于就绪状态的线程获得CPU资源,该线程就进入了运行状态;
4.阻塞状态:是一个正运行的线程,因为一些原因让出CPU资源暂时中止而进入的状态;
5.终止状态:(1)正常终止;(2)强制终止,stop、destroy;(3)异常中止:当线程中产生了异常,线程终止;
一、创建线程Thread:
这个也比较简单,跟我们继承类一样,需要继承,但是继承在java只能继承一个,使用extends来继承Thread线程;
固定写法:
很明显,跟我们继承差不多,就是继承了多线程Thread类(也就是说别人写好了多线程这个类给你用)
class 类名 extends Thread{//Thread具有线程功能
@Override //重写run方法
public void run(){
//相关代码
}
}
例子:
如何让它具备线程功能:
步骤1.A a = new A();//要先new一个对象出来,继承的new方法:MyThread thread = new MyThread();
步骤2.A.start();//调用,对象.start();
怎么调用?写个测试类:
public static void main (Strung[] eras){
MyThread thread = new MyThread();//步骤1:new一个thread的对象
thread.run();//我们一般调用继承就是这么调用的,但是这样子会失去多线程的功能;要等这个执行完,才执行后面的语句;
thread.start();//步骤2:调用方法,这才是正确调用多线程的方式,对象.start();可以先执行后面的语句,在执行这句话(谁抢占到资源就执行谁,这就是多线程的)
system.out。ptint(“后面的语句,用来测试最后才输出还是同步输出”);
}
二、创建Runnable接口
实现起来也比较简单,接口可以实现多个,使用implements关键字来继承Runnable线程和其他;
固定写法:
Class A implements Runnable{
@Override //重写run方法
public void run(){
//相关代码
}
}
如何给它线程的功能,怎么调用:
步骤1.A a = new A();//要先new一个对象出来,接口new对象的方法:MyThread mt = new MyThread();
步骤2.Thread t = new Thread(A);//这个才是让它具备主线程的方法
或者 Thread t1 = new Thread(A,“线程1”);//重载方法,给这个线程取个名字(这样在执行好判断是哪个线程在执行),
步骤3.A.start();//调用,对象.start();
例子:
如何具备线程功能,如何调用,
//主线程,main函数、测试。
public static void main (Strung[] eras){
MyThread mt = new MyThread();//步骤1.new一个对象,接口new对象的方式,实现MyThread这个类,没有实现线程功能
Thread t = new Thread(mt);//步骤2.创建线程功能,Thread才具备线程功能,跟继承写法差不多
t.start();//步骤3.调用线程的方法
//主线程在执行其他代码
for(){
try{
Thread.sleep(100);//休眠
}catch(InterruptedException e){
eprintStackTrace();
}
}
System.out。println(“主线程”+i);
}
三、Thread和Runnable的比较:
从表面看一个是属于继承Thread(extends Tread 这个在java中只能单继承),一个属于接口Runnable(implements Runnable这个能够实现多个接口,灵活,但是需要依赖第一种的写法),这就是继承和接口的概念;
这两个用哪个都可以;
多线程的应用(实战):
举例来讲解:
随便写两个线程,执行下,看他们能否并发执行;
先新建一个线程1:MyRunnable1,输出+号
再新建一个线程1:MyRunnable2,输出*号
写个main主函数测试方法测试一下:
输出的结果 是+和*杂乱无章,不按顺序输出,每次点输出结果都不一样,都是由计算机判断谁抢到资源谁就先输出;
根据线程的五种状态运行,很符合;
总结:java对于线程启动后唯一能保证的是每个线程都被启动并结束,但对于哪个先执行,什么时候执行,是没有保证的。
线程的优先级(查询or改变线程优先级)
既然输出的规则杂乱无章,没有规律可循,那么优先级是否能影响它们的输出呢?
在java中的优先级是从1-10,默认5,最高时10,最低是1;
查询优先级:对象.getprioity();
上面的例子,我们先查询它们的优先级(main函数里面写):
输出,查询出来的默认优先级都是5
我们来改变优先级(main函数里面写):
最后根据上面的例子,执行的+和*号,输出结果我们发现t1的高优先级输出的概率高,也就是t1先输出,可不是每次都这样,多运行几次,有时就先出t2优先级低的。
总结:java中优先级高的线程有更大的可能获得CPU(优先执行的概率大而已),但并不是优先级高的总是执行,也不是优先级低的不执行;
调度线程的三个方法(常用):
一、休眠方法sleep();
通过这个方法让线程睡一会,睡醒之后,在进入就绪状态,等等调度,休眠多久,转入毫秒,纳秒,如:sleep(毫秒数);
案例:顺序输出0~9,我们每输出一个数字就休眠1秒,即sleep(1000);每打印一个数就休息一秒
二、暂停方法yield();
这个暂停方法是用于释放资源的;
案例:有两个线程a和b,假如a先获得资源,a先执行,a执行完后,就释放资源a.yield,这是a就释放资源给大家抢(注意:这里有个坑:a其实也假如抢资源,它自己释放,它自己也可以抢,有可能也被a再次抢到资源,记住这个知识点),也就是说,当a释放资源后,a、b两个加入随机抢资源的
案例:创建两个资源线程试试
先写第一个资源线程试试(用接口和继承都可以),线程1 MyRunnable1,(a线程),输出200个+号,然后释放资源Thread.yield();
在写第二个线程2 MyRunnable2(与线程1差不多,名字不一样而已,b线程),输出200个*号,
最后写个main主函数,测试类试试:
输出结果:输出线程1和线程2的+和*号是随机出现的,因为线程1和线程2的资源释放后也再次参与抢,一起竞争;
有些资料这么理解,是错误的理解,误人子弟,他们认为输出结果是++++++******,这样交替输出的,这是因为他们理解释放完后其他线程去抢,所以是交替的。---大家要注意这个坑
这个方法会经常被误用,所以用的时候要小心;
三、挂起方法join();
这个方法是用于插队的,让线程插队先执行;
案例:
也是写两个线程输出+和*号,这里略,可以参考上面的,不重要,理解就行;
直接写main主函数测试:
这里解释下,我们在第五行的时候i==5输出+号(就是第五行的时候强行插入*号),即mt.join();的用法;
线程同步
问题的由来(为何需要同步方法):
说到同步,我们先来说个例子,在办公室只有一台打印机、有几个人需要打印(如图),这种情况我们知道是多线程状态,无论哪个人先抢到资源,我们知道线程中,执行的线程都是随机的,会出现各种意想不到的情况,无法决定线程的输出顺序。比如:老王先抢到资源,但是他没有打印完30页,资源又被老李抢去了,就打印老李的,然后又随机,可想而知,打印机打印出来的内容都是穿插每个人的内容,还要挑出来多麻烦。
我们能不能这样,谁先抢到资源,然后先执行完他的,在执行第二个抢到资源的那位,比如:老王先抢到资源,打印机把老王的30页先打印出来先,然后再释放给下一位,如老李抢到第二个资源,就接着打印20页完....这样的场景才是真实的场景。
代码例子:
1.先新建一个打印机类,具备打印机功能print类
Thread.sleep();加这个是为了被打印那么快,为了看看效果而已。
2.在写人,如老王、老李的类具有线程功能,因为模拟抢占打印机(报错是里面没有好有重写run方法,写进去就可以 ,然后快捷键出属性的构造方法)
最后写个main方法测试下:
这就是几个人同时用打印机,打印出来时这样的结果,不符合实际运用,我们下节课讲到锁;
这就是我们今天讲的同步资源,我们可以采取加锁(Synchronized)的概念,老王先抢到资源,那我就先给老王加锁,等老王打印完后再释放,以此类推!
解决方法:加锁(Synchronized)---就是同步方法
为什么需要同步,如:小明、小红打印出来的都是随机的,应该是一个人打印完,在打另一个人的。
固定写法(两种方法):
1. 同步方法:在方法中加锁,当线程同步的时候,会获得同步方法所属对象的锁,一旦获得对象的锁,则其他线程不能再执行被锁对象的其他同步方法。只有在同步方法执行完毕后释放了锁,其他线程才能执行。
例如:老王先抢到资源,给老王加了锁,其他人就不能抢资源了,等老王打印后,释放锁释放资源,第二个老李抢到,又给老李加锁,老李才执行第二个人得打印工作,以此类推!
Synchronized 方法声明{
}
2.同步块方法:同步块是在run方法中加锁。
//同步块是在run方法中加锁
Synchronized(资源对象){//相当于单线程了,资源对象时哪个需要资源,如老王还是老李
//需要进行同步的方法
}
例:还是上面的案例:在run()方法中加锁
结果:输出的结果很明显,先打印完一个人得,在执行另一个人得
同步方法加锁和同步块加锁,同步方法是在类对象中加锁,这个加锁代表类对象里的都已经都加了锁,同步块加锁是锁住资源对象。
写两个类对象method1()和method2()加锁试试,这里就不试了,理解就行。
死锁
有了锁的概念,我们会出现什么问题呢?比如:a和b线程,a持有锁A,在等待锁B,而b持有锁B,在等待锁A,这样a和b陷入了等待,谁都不获得资源就不释放,两者就这么僵持下去,最后谁都不执行。这就是死锁的概念。
也就是,你不给我资源A,我就不给你资源B,两个对持。最好是一手交钱一手交货才好。
代码例子就省略了,理解这个意思就行,当你给资源加锁,发现不执行,那么就是进入了死锁里了。
看下面的输出,线程123都相互在等待资源,而这些资源也都没有被释放,就这么等下去了,右上角红色图标说明程序还在执行当中,没有结束,这样就是进了死锁里面了。
生产者/消费者设计模式
生产者生成东西放仓库,消费者从仓库中取,生产仓库满了,生产者就不生产了,消费者取仓库空了,就不取了。生产者每生产东西就通知消费者,消费取东西也会通知生产者