一.线程之间的等待唤醒机制
1.用法
1.定义一个资源
2.要有一个生产线程
3.要有一个消费线程
4.测试类
2.等待唤醒
作为生产者来说,我们生产了资源,等待,通知消费线程来消费
作为消费者来说,我们消费了资源,等待,通知生产线程来生产
3.常用方法
(1)wait方法
void wait ()
在其他线程调用此对象的 notify () 方法或 notifyAll () 方法前,导致当前线程等待。
void wait (long timeout)
在其他线程调用此对象的 notify () 方法或 notifyAll () 方法,或者超过指定的时间量前,导致当前线程等待。
注意
一旦等待就必须示释放锁
从哪里等待就从哪里醒来
(2)notify方法
void notify ()
唤醒在此对象监视器上等待的单个线程。
void notifyAll ()
唤醒在此对象监视器上等待的所有线程。
4.面试题:sleep()方法和wait()方法的区别
(1)共同点:
都可以让线程处于阻塞状态
(2)不同点:
sleep()方法
必须设置休眠量
线程阻塞后不释放锁(重要区别)
wait()方法
方法可以设置时间量,也可以不设置
线程阻塞后释放锁(重要区别)
二.内存可见性问题
1.问题描述
要了解内存可见性问题,得先了解一下JVM的内存模型
Java内存模型
Java内存模型规定了所有的变量都存储在主内存中。每条线程中还有自己的工作内存,
线程的工作内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。
线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。
不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
2.解决办法
(1)volatile关键字保证可见性
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,
当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
注意:
volatile只可以确保内存可见性,但是不能保证原子性(不可再分割性)
(2)通过synchronized和Lock也能够保证可见性
synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,
并且在释放锁之前会将对变量的修改刷新到主存当中,因此可以保证可见性。
虽然用synchronized可以解决但是效率低,一般不推荐使用
3.原子性问题
可以用CAS算法解决:比较并交换
把普通变量换为原子变量
AtomicInteger num=new AtomicInteger(1);
对于几种常见的锁
我们经常将synchronized称为悲观锁,将volatile、CAS算法:乐观锁,因为他们没有互斥性
三.线程的状态转换图及常见执行情况
新建状态:线程被创建出来
就绪状态:具有CPU的执行资格,但是不具有CPU的执行权
运行状态:具有CPU的执行资格,也具有CPU的执行权
阻塞状态:不具有CPU的执行资格,也不具有CPU的执行权
死亡状态:不具有CPU的执行资格,也不具有CPU的执行权
四.采用匿名内部类来开启线程
1.开启线程的俩种方式
方式一:
new Thread(){
@Override
public void run() {
System.out.println("线程执行了");
}
}.start();
方式二:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程执行了1");
}
}).start();
五.线程池和定时器
1.线程池
(1)概述
程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互,而使用线程池可以很好的提高性能,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池。
线程池是装有一定线程对象的一个容器,线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用。
在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池
(2)用法
JDK5新增了一个Executors工厂类来产生线程池,有如下几个方法
创建ExecutorService 线程池(三个方法)
ExecutorService executorService = Executors.newCachedThreadPool();//这个线程中创建几个就有几个线程对象
ExecutorService executorService = Executors.newSingleThreadExecutor();//这个线程池里面,只有一个线程对象
ExecutorService executorService = Executors.newFixedThreadPool(3);//这个线程里面,提前创建三个线程对象
executorService.submit(new MyRunnable());
Future<Integer> f = executorService.submit(myCallable);
Integer integer = f.get();//获取线程执行完的结果
executorService.shutdown();//关闭线程池
2.定时器Timer
(1)概述
一种工具,线程用其安排以后在后台线程中执行的任务。可安排任务执行一次,或者定期重复执行。
(2)用法
public void schedule (TimerTask task,long delay):
延迟多少毫秒之后,让定时任务执行
timer.cancel()
取消定时器
延迟2000毫秒之后让定时任务执行
几个简单的定时器案例
普通定时器
Timer timer = new Timer();
MyTimerTask myTimerTask = new MyTimerTask(timer);
timer.schedule(myTimerTask,2000);
public class MyTimerTask extends TimerTask {
Timer timer;
public MyTimerTask(Timer timer) {
this.timer=timer;
}
@Override
public void run() {
System.out.println("~~~~砰!爆炸了");
timer.cancel();
}
}
连环定时
Timer timer = new Timer();
TimerTask tt= new TimerTask() {
@Override
public void run() {
System.out.println("砰!爆炸了!");
}
};
timer.schedule(tt,10,1000);//连续十次,每次间隔一千毫秒
//tt.cancel(); //取消定时任务
在指定日期执行定时任务
Timer timer = new Timer();
timer.schedule(new TimerTask(){
@Override
public void run() {
System.out.println("删库跑路");
}
},new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2019-07-31 00:00:00"));
六.设计模式
1.概述
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。
使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性以及代码的结构更加清晰.
2.分类
(1)简单的工厂模式
又叫静态工厂方法模式,它定义一个具体的工厂类负责创建一些类的实例
优点
使用静态工厂模式的优点是实现责任的分割,该模式的核心是工厂类,工厂类含有必要的选择逻辑,可以决定什么时候创建哪一个产品的实例,而客户端则免去直接创建产品的责任,而仅仅是消费产品。也就是说静态工厂模式在不改变客户端代码的情况可以动态的增加产品。
明确了类的职责。
缺点
这个静态工厂类负责所有对象的创建,如果有新的对象增加,或者某些对象的创建方式不同,就需要不断的修改工厂类,不利于后期的维护
(2)工厂方法模式
工厂方法模式中抽象工厂类负责定义创建对象的接口,具体对象的创建工作由继承抽象工厂的具体类实现。
优点
客户端不需要在负责对象的创建,从而明确了各个类的职责,如果有新的对象增加,只需要增加一个具体的类和具体的工厂类即可,不影响已有的代码,后期维护容易,增强了系统的扩展性
缺点:
需要额外的编写代码,增加了工作量
(3)单例模式
懒汉式
public static void main(String[] args) {
Student student = Student.getStudent();
Student student1 = Student.getStudent();
System.out.println(student==student1);
}
private static Student student=null;
private Student() {
}
public synchronized static Student getStudent(){//加上synchronized 避免多线程环境下,有可能出现new了多次
if(student==null){
student=new Student();
}
return student;
}
饿汉式
public static void main(String[] args) {
Teacher teacher = Teacher.getTeacher();
Teacher teacher1 = Teacher.getTeacher();
System.out.println(teacher==teacher1);
}
private static Teacher teacher=new Teacher();
private Teacher() {}
public static Teacher getTeacher(){
return teacher;
}
(4)模版设计模式
模版方法模式就是定义一个算法的骨架,而将具体的算法延迟到子类中来实现
优点
使用模版方法模式,在定义算法骨架的同时,可以很灵活的实现具体的算法,满足用户灵活多变的需求
缺点
如果算法骨架有修改的话,则需要修改抽象类
(5)装饰模式
装饰模式就是使用被装饰类的一个子类的实例,在客户端将这个子类的实例交给装饰类。是继承的替代方案
单例设计模式
优点
使用装饰模式,可以提供比继承更灵活的扩展对象的功能,它可以动态的添加对象的功能,并且可以随意的组合这些功能。
缺点:
正因为可以随意组合,所以就可能出现一些不合理的逻辑。
(3)观察者模式
以猎头公司为案例
大致步骤:
1.定义一个猎头公司类
2.首先两个成员变量,一个装求职者,一个装工作岗位
3.添加求职者的方法
4.添加工作岗位
5.通知求职者
6.发布求职信息的方法
7.注销求职者的方法
8.定义工作类和求职者类
9.在执行类中添加求职者姓名和工作岗位
代码案例:
猎头公司类
ArrayList<WokerMan> mans=new ArrayList<>();
ArrayList<Job> jobs = new ArrayList<>();
public void addMan(WokerMan wokerMan){
mans.add(wokerMan);
}
public void addJob(Job job){
jobs.add(job);
sendJobToMan(job,mans);//通知求职者
}
private void sendJobToMan(Job job, ArrayList<WokerMan> mans) {
for (WokerMan man : mans) {
System.out.println(man.name+"你好,有一份"+job.jobName+"薪资"+job.sal+"欢迎你前去面试");
}
}
public void removeMan(WokerMan man){
mans.remove(man); //注销求职者的方法
}
工作类
public String jobName;
public double sal;
public Job(String jobName, double sal) {
this.jobName = jobName;
this.sal = sal;
}
求职者类
public String name;
public WokerMan(String name) {
this.name = name;
}
执行类
public static void main(String[] args) {
WokerMan man1 = new WokerMan("张三");
WokerMan man2 = new WokerMan("李四");
WokerMan man3 = new WokerMan("王五");
Hunter hunter = new Hunter();
hunter.addMan(man1);
hunter.addMan(man2);
hunter.addMan(man3);
Job java = new Job("Java开发工程师", 10000);
Job php = new Job("PHP开发工程师", 20000);
Job web = new Job("WEB开发工程师", 8000);
hunter.addJob(java);
hunter.addJob(php);
hunter.removeMan(man1); //注销一个求职者
hunter.addJob(web);
}