发表时间:2022-03-25来源:网络
我们使用多线程模拟车站卖票,车站的票数一开始都是确定好的,多个窗口使用多个线程并发执行来模拟;
方式1:自定义类继承Thread类
public class MyTest1 { public static void main(String[] args) { sellTickets th1 = new sellTickets("线程-A"); sellTickets th2 = new sellTickets("线程-B"); sellTickets th3 = new sellTickets("线程-C"); th1.start(); th2.start(); th3.start(); } } public class sellTickets extends Thread { private static int tickets = 100; public sellTickets(String name) { super(name); } @Override public void run() { while (true) { if (tickets >= 1) { try { /*售票时网络是不能实时传输的, 总是存在延迟的情况,所以,在出售一张票以后, 需要一点时间的延迟, 这里增加的这行代码是为了更好的演示多线程的随机性与模拟网络延迟*/ Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "卖出一张票,还剩:" + (tickets--) + "张票"); } } } }实验结果:
方式2:自定义类实现Runnable接口
实验结果:
方式3:自定义类实现Callable接口
实验结果:
1、假设当th1线程这时候获得了CPU的执行权,这个时候假设票数=1了,因为我们在run()里面睡眠了50ms,在这50ms内,很有可能其他线程获得了CPU的执行权,
2、假设这时候th2获得了执行权,他也进入了while循环,判断此时的票数还是1,因此进入if循环,
3、假设这时候th1睡醒了,th2进入睡眠,th1执行了卖票语句,这时候票数变为0,
4、th2线程睡醒之后,也执行了卖票语句,这时候票数就变成了负数,或者同样情况下可能出现的0票;

主要目标:
定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样底层细节。此处的变量与Java编程时所说的变量不一样,指包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,后者是线程私有的,不会被共享。
规则:
所有的变量都存储在主内存中,每个线程还有自己的工作内存,线程的工作内存中保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量;
read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用;
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中;
由于线程的原子性造成的;原子性指的是不可分割性,线程中对于卖票的操作不是原子性操作,首先,我们需要将三个线程都共享的数据票数从主存中读取到每个线程的工作内存,然后在每个线程的工作内存中进行修改,修改完再把这个数据写回到主存当中;
1、在这个过程当中,假设线程th1先获得CPU执行权,他读取到了票数4,
2、然后在自己的工作内存当中修改了票数为3,还没来得及刷回主存当中,
3、线程th2就抢走了CPU执行权,读取到了票数4,
4、它也改了票数为3,
5、这时候th1又抢回了CPU执行权,将票数3刷回主存,因此票数显示3;
6、这个时候th2抢走了cpu执行权,票数也显示了3;
最终造成显示相同票数的原因;
1、是不是一个多线程环境;
2、多个线程有没有共享数据;
3、有没有多条语句在操作共享变量;
其中一次运行结果:
再也没有出现数据安全问题;
这里要注意:三个线程必须使用同一个锁对象,因为只有这样,才能保证某一时间段内只有一个线程可以持有这把锁,其中一个线程抢到了CPU执行权,锁住了门,其他线程只能等在门外,如果三个对象各自使用一把锁,就没有任何意义了;
1、什么是synchronized?
synchronized关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象是对多个线程可见的,那么对该对象的所有读写都将通过同步的方式来进行;
2、synchronized包括哪两个JVM重要的指令?
monitor enter和 monitor exit
3、synchronized锁的是什么?
普通同步方法 ---------------> 锁的是当前实例对象; 静态同步方法---------------> 锁的是当前类的Class对象; 同步方法块 ---------------> 锁的是synchonized括号里配置的对象;运行结果:
某一次运行结果:
避免了数据安全问题,这个时候sychronized默认持有锁对象是当前类的字节码文件对象,它属于类锁;
1、每个java对象都可以用做一个实现同步的锁,这些锁成为内置锁;
2、线程进入同步代码块或同步方法的时候会自动获得该锁,在退出同步代码块或同步方法时会释放该锁;
3、获得内置锁的唯一途径就是进入这个锁的保护的同步代码块或方法;
4、Java内置锁是一个互斥锁,这就是意味着最多只有一个线程能够获得该锁,当线程A尝试去获得线程B持有的内置锁时,线程A必须等待或者阻塞,直到线程B释放这个锁,如果B线程不释放这个锁,那么A线程将永远等待下去;
5、Java的对象锁和类锁:Java的对象锁和类锁在锁的概念上基本上和内置锁是一致的,但是,两个锁实际是有很大的区别的,对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。
6、我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象, 所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。
7、但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。
8、 一个类的对象锁和另一个类的对象锁是没有关联的,当一个线程获得A类的对象锁时,它同时也可以获得B类的对象锁。
某一次执行结果:
我想应该是这样的:首先对程序来讲同步的部分很影响运行效率,而一个方法通常是先创建一些局部变量,再对这些变量做一些操作,如运算,显示等等;而同步所覆盖的代码越多,对效率的影响就越严重。因此我们通常尽量缩小其影响范围;
同步的出现解决了多线程的安全问题;
当某个线程进入同步方法获得对象锁,那么其他线程访问这里对象的同步方法时,必须等待或者阻塞,这对高并发的系统是致命的,这很容易导致系统的崩溃。如果某个线程在同步方法里面发生了死循环,那么它就永远不会释放这个对象锁,那么其他线程就要永远的等待。这是一个致命的问题;
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,比较抽象,为了更清晰的表达如何加锁和释放锁,JDK 1.5以后,并发包新增Lock接口来实现锁功能;
1、概述:
我们一般使用的是Lock锁的是是实现类:ReentrantLock
这个是 JDK @since 1.5 添加的一种颗粒度更小的锁,它完全可以替代 synchronized 关键字来实现它的所有功能,而且 ReentrantLock锁的灵活度要远远大于前者;
从类结构图看出,ReentrantLock 实现了 Lock 接口,ReentrantLock 只是 Lock 接口的一个实现而已。
java.util.concurrent.locks.Lock
它们都是java.util.concurrent 包里面的内容(俗称 JUC、并发包),也都是 JDK 1.5 开始加入的;
2、为什么叫重入锁
ReentrantLock,我们把它拆开来看就明了了,Re-Entrant-Lock:即表示可重新反复进入的锁,但仅限于当前线程;
public void m() { lock.lock(); lock.lock(); try { // ... method body } finally { lock.unlock() lock.unlock() } }如示例代码所示,当前线程可以反复加锁,但也需要释放同样加锁次数的锁,即重入了多少次,就要释放多少次,不然也会导入锁不被释放;
试想一下,如果不设计成可重入锁,那自己如果反复给自己加锁,不是会把自己加死锁了吗?
3、重入锁最重要的几个方法
(1)lock()
获取锁,有以下三种情况:
锁空闲:直接获取锁并返回,同时设置锁持有者数量为:1;
当前线程持有锁:直接获取锁并返回,同时锁持有者数量递增1;
其他线程持有锁:当前线程会休眠等待,直至获取锁为止;
(2)lockInterruptibly()
获取锁,逻辑和 lock() 方法一样,但这个方法在获取锁过程中能响应中断。
(3)tryLock()
从关键字字面理解,这是在尝试获取锁,获取成功返回:true,获取失败返回:false, 这个方法不会等待,有以下三种情况:
锁空闲:直接获取锁并返回:true,同时设置锁持有者数量为:1;
当前线程持有锁:直接获取锁并返回:true,同时锁持有者数量递增1;
其他线程持有锁:获取锁失败,返回:false;
(4)tryLock(long timeout, TimeUnit unit)
逻辑和 tryLock() 差不多,只是这个方法是带时间的。
(5)unlock()
释放锁,每次锁持有者数量递减 1,直到 0 为止。所以,现在知道为什么 lock 多少次,就要对应 unlock 多少次了吧。
(6)newCondition
返回一个这个锁的 Condition 实例,可以实现 synchronized 关键字类似 wait/ notify 实现多线程通信的功能,不过这个比 wait/ notify 要更灵活,更强大!
大概用法如下:
class X { private final ReentrantLock lock = new ReentrantLock(); // ... public void m() { lock.lock(); // block until condition holds try { // ... method body } finally { lock.unlock() } } }加锁和释放锁都在方法里面进行,可以自由控制,比 synchronized 更灵活,更方便。但要注意的是,释放锁操作必须在 finally 里面,不然如果出现异常导致锁不能被正常释放,进而会卡死后续所有访问该锁的线程。
synchronized 是重入锁吗?
4、synchronized 是重入锁吗
你可能会说不是,因为 ReentrantLock 既然是重入锁,根据推理,相反,那 synchronized 肯定就不是重入锁,那你就错了,答案是:yes,看下面的例子:
public synchronized void operation(){ add(); } public synchronized void add(){ }operation 方法调用了 add 方法,两个方法都是用 synchronized 修饰的,add() 方法可以成功获取当前线程 operation() 方法已经获取到的锁,说明 synchronized 就是可重入锁。
5、卖票案例演示:
import java.util.concurrent.locks.ReentrantLock; public class MyTest1 { public static void main(String[] args) { myLockThread th1 = new myLockThread("线程-1"); myLockThread th2 = new myLockThread("线程-2"); myLockThread th3 = new myLockThread("线程-3"); th1.start(); th2.start(); th3.start(); } } class myLockThread extends Thread{ public static ReentrantLock lock=new ReentrantLock(); public static int tickets=100; public myLockThread(String s) { super(s); } @Override public void run() { while (true){ lock.lock(); if(tickets>=1){ try { Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第:" + (tickets--) + " 张票"); } lock.unlock(); } } }某一次执行结果:
之前在学习集合与自定义类的时候,提到有些类是属于线程安全效率低,而有些则是线程不安全效率高,这么说的原因是什么?
1、ArraryList 与 Vector 集合
前者是线程不安全,效率高;
后者是线程安全,效率低;
2、StringBuilder 与 StringBuffer 类
前者是线程不安全,效率高;
后者是线程安全,效率低;
由此可见,这些线程安全的类是因为他们的成员方法都加了关键字sychronized,保证了一段时间内只能有一个线程来执行这个方法里面的逻辑,也就保证了数据安全;
指的是两个或两个以上的线程,因为同时抢占CPU资源而出现的相互等待现象;
------------------------------------------------------------------------
代码演示:
某一次执行结果:
另外一次执行结果:
出现了死锁现象,为什么会出现这样的现象:
1、true线程最先抢占了CPU资源,持有了A锁;
2、flase线程紧接着抢占了CPU资源,持有了B锁;
3、true线程等待false线程执行完毕释放B锁,false线程等待true线程执行完毕释放A锁,但是他们都处于互相等待状态,谁也不让谁,就出现了死锁现象;
4、这就相当于:中国人和美国人一起吃饭,中国人使用的筷子,美国人使用的刀和叉;中国人获取到了美国人的刀,美国人获取到了中国人的一根筷子,谁也不让谁,谁都吃不成饭;
在实际的软件开发过程中,经常会碰到如下场景:
某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等); 产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。
可能会有这样的疑问:这个缓冲区有什么用?为什么不让生产者直接调用消费者的某个函数,直接把数据传递过去?搞出这么一个缓冲区干什么?其实这里面是大有讲究的,大概有如下一些好处;
1、解耦:
假设生产者和消费者分别是两个类,如果让生产者直接调用消费者的某个方法,那么生产者对于消费者就会产生依赖(也就是耦合),将来如果消费者的代码发生变化,可能会影响到生产者; 而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也就相应降低了; 接着上述的例子,如果不存在包子(也就是缓冲区),你必须得把信直接交给邮递员。有同学会说,直接给邮递员不是挺简单的嘛?其实不简单,你必须得认识谁是邮递员,才能把信给他(光凭身上穿的制服,万一有人假冒,就惨了)。这就产生和你和邮递员之间的依赖(相当于生产者和消费者的强耦合)。万一哪天邮递员换人了,你还要重新认识一下(相当于消费者变化导致修改生产者代码)。而邮筒相对来说比较固定,你依赖它的成本就比较低(相当于和缓冲区之间的弱耦合)。2、支持并发(concurrency)
生产者直接调用消费者的某个方法,还有另一个弊端;由于函数调用是同步的(或者叫阻塞的),在消费者的方法没有返回之前,生产者只好一直等在那边。万一消费者处理数据很慢,生产者就会白白糟蹋大好时光。 使用了生产者/消费者模式之后,生产者和消费者可以是两个独立的并发主体(常见并发类型有进程和线程两种)。生产者把制造出来的数据往缓冲区一丢,就可以再去生产下一个数据,基本上不用依赖消费者的处理速度;其实当初这个模式,主要就是用来处理并发问题的;从寄信的例子来看。如果没有邮筒,你得拿着信傻站在路口等邮递员过来收(相当于生产者阻塞);又或者邮递员得挨家挨户问,谁要寄信(相当于消费者轮询),不管是哪种方法,都挺土的。3、支持忙闲不均
缓冲区还有另一个好处。如果制造数据的速度时快时慢,缓冲区的好处就体现出来了。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中,等生产者的制造速度慢下来,消费者再慢慢处理掉。 为了充分复用,我们再拿寄信的例子来说事。假设邮递员一次只能带走1000封信。万一某次碰上情人节(也可能是圣诞节)送贺卡,需要寄出去的信超过1000封,这时候邮筒这个缓冲区就派上用场了。邮递员把来不及带走的信暂存在邮筒中,等下次过来时再拿走。1、我们使用Student类模拟缓存,使用getThread类模拟消费者,使用setThread类来模拟生产者;
2、首先需要明确,生产者消费者模型希望达到的效果是:生产一个学生对象,消费一个学生对象,也就是set线程的run()需要增加学生信息(比如设置 [“张三”,23]),get线程的run()需要获取学生信息 [获取到 “张三”,23这个学生对象],但是学生对象是new出来的,两个线程之间没办法共享这个数据,怎么才能共享呢?我们可以给两个线程提供一个有参构造,在测试类里面就创建好学生对象,然后将一个学生对象传递给两个线程,这样可以实现学生对象的资源共享;
某一次运行结果:
另外一次运行结果:
3、上面的运行结果可以看出,程序出现了数据安全的问题,本来希望达到生产一个消费一个的效果没有出现,明明生产的是李四–24,张三–23,却出现了李四–23,张三–24的现象;因为线程一旦开启,就会具有随机性,有可能还没生产,就消费了,也有可能生产到一半,就被消费了,造成名字与年龄不匹配;
4、怎么解决数据安全的问题呢?可以给生产资源和消费资源的代码加sychronized锁,锁住这部分代码,保证在没有生产好资源的时候,不能消费资源;并且需要注意的是,我们生产和消费线程必须使用的同一个锁住,这样才能保证一段时间内只能执行一个线程的任务,这时候,他们共享资源学生对象可以充当这个锁对象;
5、但是这样写代码还是不行,因为我们生产者一次只能生产一个资源,但是由于线程的随机性,有可能生产了一个资源,但是却在重复消费这个资源;
6、我们怎么解决上面的问题呢?在生产线程里面,我们希望生产完成一个资源,消费一个资源;
作为生产者来说,我生产一个资源,就通知消费线程来消费这个资源;作为消费线程来说,我消了资源,我就等着,并且通知生产线程来生产;
这时候就需要用到等待——唤醒机制;
Object 类中:
void wait ()——在其他线程调用此对象的 notify () 方法或 notifyAll () 方法前,导致当前线程等待;
void wait (long timeout)——在其他线程调用此对象的 notify () 方法或 notifyAll () 方法,或者超过指定的时间量前,导致当前线程等待;
void notify ()——唤醒在此对象监视器上等待的单个线程;
void notifyAll ()——唤醒在此对象监视器上等待的所有线程;
notify等于说将等待队列中的一个线程移动到同步队列中,而notifyAll是将等待队列中的所有线程全部移动到同步队列中。7、考虑如何判断是否已经生产好了学生对象,因为不能出现还没生产就消费了的现象,可以给学生对象加一个布尔类型的值作为标记,如果还没有生产或者是生产之后被消费了该学生资源,将标记置为false,并使得当前线程处于等待状态;如果资源已经生产好了,就去通知消费,也就是唤醒;这里使用共享资源Student资源让线程等待或者唤醒;
-----生产者: public class setThread extends Thread { int i = 1; Student student; public setThread(Student student) { this.student = student; } @Override public void run() { while (true) { synchronized (student) { if (student.flag) { //有资源,生产者线程就等待 try { //th1 student.wait();//线程一旦等待,就要立马释放锁,在哪里等待,被唤醒后,就从哪里开始执行 } catch (InterruptedException e) { e.printStackTrace(); } } //没有资源生产资源 if (i % 2 == 0) { //生产资源 student.name = "张三"; student.age = 23; } else { //生产资源 th1 student.name = "李四"; student.age = 24; } //有了资源通知消费线程去消费线程 //修改标记 student.flag = true; student.notify(); //通知 唤醒之后,线程还得再次争抢时间片 th1 } i++; } } } -----消费者: public class getThread extends Thread { private Student student; public getThread(Student student) { this.student = student; } @Override public void run() { //消费资源 while (true) { synchronized (student) { if (!student.flag) { //false //消费线程没有资源,就等着 try { student.wait(); //一旦等待,就会释放锁,在哪里等待,被唤醒后,就从这里执行 } catch (InterruptedException e) { e.printStackTrace(); } } //有了资源就消费 System.out.println(student.name + "===" + student.age); //消费了就没有了 //通知生成者线程去生产 student.flag = false;//修改标记 student.notify(); } } } } ----缓存: public class Student { public int age; //定义一个标记,用来表示是否有资源。 false 表示没有资源, true表示有资源。 public boolean flag = false; public String name; } -----测试类: public class MyTest1 { public static void main(String[] args) { Student student = new Student(); setThread th1 = new setThread(student); getThread th2 = new getThread(student); th1.start(); th2.start(); } }某一次运行结果:
执行结果:
由于这个写回主存的时间具有不确定性,因此具有随机性,但是主程序很可能很早就读取了这个成员变量的值,造成了数据安全的问题;
为了避免这个错误,我们可以给程序加一个sychronized关键字,保证程序顺利执行;
为什么可以顺利执行的原因:
Java程序在编为字节码文件的时候,代码的执行顺序有可能和写的顺序不一样,也就是不知道什么时候flag会写回主存,所以造成了上面的代码出现卡死; 上面的程序加锁应该给读数据、写数据都要加锁; Sychronized关键字还有一个作用是 确保变量的内存可见性,写数据的时候读数据线程去申请同一个锁,它的工作内存会被设置为无效,然后读线程会重新从主存中加载它要访问的变量到它的工作内存中; 因此就保证了读完之后再写上去程序顺利执行; public class MyRunnable implements Runnable { public boolean flag = false; public boolean getFlag() { return this.flag; } @Override public void run() { synchronized (ObjectUtils.objA){ System.out.println("线程进来执行了!"); flag = true; } } } public class MyTest { public static void main(String[] args) { MyRunnable runnable = new MyRunnable(); Thread th = new Thread(runnable); th.start(); while (true){ synchronized (ObjectUtils.objA){ if(runnable.getFlag()){ System.out.println("进来执行了!"); break; } } } //线程进来执行了! //进来执行了! } } 加入sychronized关键字也可以,但是这样一来程序可以顺利执行,但是这样造成效率一定会降低,有没有更好的办法?我们可以引入Volatile关键字;1、Volatile关键字是轻量级的,会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值,它保证了 内存的可见性问题;
2、 而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性;
代码示例:
-----测试类: public class MyTest { public static void main(String[] args) { MyRunnable runnable = new MyRunnable(); Thread th = new Thread(runnable); th.start(); while (true) { if (runnable.getFlag()) { System.out.println("进来执行了!"); break; } } //线程进来执行了! //进来执行了! } } ----自定义任务类: public class MyRunnable implements Runnable { public volatile boolean flag = false; public boolean getFlag() { return this.flag; } @Override public void run() { System.out.println("线程进来执行了!"); flag = true; } }3、另外,通过 synchronized 和 Lock 也能够保证可见性,synchronized 和 Lock 能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中,因此可以保证可见性;
4、volatile 关键字:相较于 synchronized 是一种较为轻量级的同步策略;
5、volatile 变量,用来确保将变量的更新操作通知到其他线程;
6、可以将 volatile 看做一个轻量级的锁,但是又与锁有些不同:
1、首先要了解的是,volatile可以保证可见性和顺序性,这些都很好理解,那么它为什么不能保证原子性呢?
2、首先需要了解的是,Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作,但是像j = i或者i++这样的操作都不是原子操作,因为他们都进行了多次原子操作,比如先读取i的值,再将i的值赋值给j,两个原子操作加起来就不是原子操作了;
3、举例:
①线程A首先得到了 i 的初始值100,但是还没来得及修改,就阻塞了;
②这时线程B开始了,它也得到了 i 的值,由于 i 的值未被修改,即使是被volatile 修饰,主存的变量还没变化,那么线程B得到的值也是100;
③之后对其进行加1操作,得到101后,将新值写入到缓存中,再刷入主存中;
④根据可见性的原则,这个主存的值可以被其他线程可见;
⑤问题来了,线程A已经读取到了 i 的值为100,也就是说读取的这个原子操作已经结束了,所以这个可见性来的有点晚,线程A阻塞结束后,继续将100这个值加1,得到101,再将值写到缓存,最后刷入主存;
所以即便是volatile具有可见性,也不能保证对它修饰的变量具有原子性。
某一次执行结果:
1、CAS算法概述
CAS (Compare-And-Swap) 是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问; CAS 是一种无锁的非阻塞算法的实现;2、CAS算法原理
CAS 包含了 3 个操作数:需要读写的内存值 V、进行比较的值 A、拟写入的新值 B; 当且仅当 V 的值等于 A 时, CAS 通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作; jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronouse同步锁的一种乐观锁; JDK 5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。3、图解:

上面的图可知,两个线程其实就是在比较谁将新值刷回主存更快,你读到的不一定是此时内存中的值;
假如有三个线程,都读取到了主存中的100作为预估值,并进行修改操作,修改为101,但是线程1刷回主存很快,将101写入主存,其他两个线程将预估值和主存中的值一对比,对不上,因此刷回失败;就是在比较谁刷的快;
4、应用
java.util.concurrent.atomic 包下提供了一些原子操作的常用类:
我们只需要把变量对应的类型换为上面的类型就好了,这些类型里面比如AtomicInteger 提供的自加、自减操作都可以确保原子性操作;
5、代码示例:
运行结果:
使用匿名内部类更简单的开启线程:
—方式1:
—方式2:
public class MyTest1 { public static void main(String[] args) { Thread th = new Thread(new Runnable() { @Override public void run() { System.out.println("这是一个线程任务"); } }); th.start(); //这是一个线程任务 } }1、程序启动一个新线程成本是比较高的,因为它涉及到要与操作系统进行交互;
2、而使用线程池可以很好的提高性能,他本质就是一个存储一定数量线程对象的容器,尤其是当程序中要创建大量生存期很短的线程时,更应该考虑使用线程池;
3、线程池里的每一个线程代码结束后,并不会死亡,而是再次回到线程池中成为空闲状态,等待下一个对象来使用;
4、在JDK5之前,我们必须手动实现自己的线程池,从JDK5开始,Java内置支持线程池;
1、 JDK5新增了一个Executors工厂类来产生线程池,有如下几个方法
public static ExecutorService newCachedThreadPool():
根据任务的数量来创建线程对应的线程个数
public static ExecutorService newFixedThreadPool(int nThreads):
固定初始化几个线程
public static ExecutorService newSingleThreadExecutor():
初始化一个线程的线程池
2、这些方法的返回值是ExecutorService对象,该对象表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程。它提供了如下方法:

线程池返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()来获取返回值,get()方法会阻塞当前线程直到任务完成;get(long timeout,TimeUnit unit)可以设置超时时间;
3、使用步骤:
创建线程池对象
创建Runnable实例
提交Runnable实例
关闭线程池
4、为什么使用线程池
几乎所有需要异步或者并发执行任务的程序都可以使用线程池;降低系统消耗:重复利用已经创建的线程降低线程创建和销毁造成的资源消耗。
提高响应速度:当任务到达时,任务不需要等到线程创建就可以立即执行。
提供线程可以管理性:可以通过设置合理分配、调优、监控。
5、线程池工作流程
(1)判断核心线程池里的线程是否都有在执行任务,否->创建一个新工作线程来执行任务,是->走下个流程。
(2)判断工作队列是否已满,否->新任务存储在这个工作队列里,是->走下个流程。
(3)判断线程池里的线程是否都在工作状态,否->创建一个新的工作线程来执行任务,是->走下个流程。
(4)按照设置的策略来处理无法执行的任务。
6、代码示例:
import java.util.concurrent.*; public class MyTest { public static void main(String[] args) throws ExecutionException, InterruptedException { //创建一个线程池对象 ExecutorService pool = Executors.newCachedThreadPool(); //提交任务,传递一个任务类的参数 Future?> future = pool.submit(new Runnable() { @Override public void run() { System.out.println("这是一个Runnable里面的任务"); } }); //创建一个任务类的参数 CallableInteger> callable = new CallableInteger>() { @Override public Integer call() throws Exception { System.out.println("这是一个Callable里面的任务"); return 20; } }; //提交任务 FutureInteger> submit = pool.submit(callable); //获取任务的返回值 System.out.println("Callable里面的任务的返回值:" + submit.get()); //关闭线程池 pool.shutdown(); /*这是一个Runnable里面的任务 这是一个Callable里面的任务 Callable里面的任务的返回值:20*/ } }Timer:
public Timer()
public void schedule(TimerTask task, long delay):
--------------------安排在指定的时间执行指定的任务
public void schedule(TimerTask task,long delay,long period);
--------------------task - 所要安排的任务。
--------------------delay - 执行任务前的延迟时间,单位是毫秒。
--------------------period - 执行各后续任务之间的时间间隔,单位是毫秒。
public void schedule(TimerTask task, Date time):
public void schedule(TimerTask task, Date firstTime, long period):
TimerTask:定时任务
public abstract void run()
public boolean cancel()
代码示例1:
import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Timer; import java.util.TimerTask; public class MyTest1 { public static void main(String[] args) throws ParseException { //创建一个定时器 Timer timer = new Timer(); //创建一个定时任务 TimerTask task = new TimerTask() { @Override public void run() { System.out.println("这是一个定时任务!"); } }; //定时执行一个任务 timer.schedule(task,new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2020-06-11 0:06:30")); //这是一个定时任务! } }代码示例2:
import java.text.ParseException; import java.util.Timer; import java.util.TimerTask; public class MyTest2 { public static void main(String[] args) throws ParseException { //创建一个定时器对象 Timer timer = new Timer(); //将定时器传递过去,方便在任务结束后关闭定时器 MyTimerTsk task = new MyTimerTsk(timer); //第一次执行是在当前系统时间后的2s后,以后每隔1s之后执行一次任务 //等3秒第一次执行任务,以后间隔1秒重复执行定时任务 timer.schedule(task, 3000,1000); } } class MyTimerTsk extends TimerTask { private Timer timer; public MyTimerTsk(Timer timer) { this.timer = timer; } @Override public void run() { System.out.println("砰~爆炸了!"); //任务执行完了,取消定时器 //timer.cancel();可以选择执行一次任务后就关闭定时器 } }——优化代码的第一步
小明写的图片加载框架中将各个功能拆分,将Image’Loader一分为二,ImageLoader只负责图片的加载逻辑,ImageCache只负责图片的缓存逻辑;这样ImageLoader的代码量少了,逻辑也清晰了——让程序更稳定、更灵活
软件中的对象(类、模块、函数)应该对扩展是开放的,但是对修改是封闭的。而遵守开闭原则的重要手段应该时通过抽象…;开闭原则指导我们,当软件需要变化时,应该“尽量”通过扩展的方式来实现变化,而不是修改已有的代码; 并不是绝对的,现实场景中会有修改源码的情况。 小明的代码通过依赖注入(set方法)的方式设置缓存类型,比如内存缓存、sd卡缓存、双缓存;ImageLoader中声明Cache的接口类,set方法设置的对象都是Cache的子类;也就是多态;这样修改后,如果后期有其他缓存方式需要修改,完全不需要对源码修改,只需要添加一个新的Cache的实现类就可以,也就是对扩展开放,对修改是封闭的。——构建扩展性更好的系统
依赖继承、多态这两大特性,简单的说就是任何使用父类的地方都可以使用子类代替; 上面小明通过提取Cache接口,使用依赖注入的方法设置缓存类型,传入Cache的子类(内存、sd卡、双缓存)的这种方式就是里氏替换原则。——让项目拥有变化的能力
是指一种特定的解耦形式,是指高层次的模块不依赖于低层次的模块的实现细节的目的,依赖模块被颠倒了。 上面小明通过提取Cache接口,使用依赖注入的方法设置缓存类型,传入Cache的子类(内存、sd卡、双缓存)的这种方式就是里氏替换原则;并且这样的话不会像修改前期那样,在ImageLoader中直接创建ImageCache对象,ImageLoader过度依赖低层次模块(ImageCache);将其抽取成接口,使用依赖注入的方式,这样就是依赖倒置原则。——系统有更高的灵活性
定义:不要强制破坏程序实现不想实现的原则; 目的:系统解开耦合,从而容易重构、更改和重新部署 小明设计的ImageLoader中的ImageCache就是接口隔离原则的运用,ImageLoader只需要知道该缓存对象有存、取缓存图片的接口即可,其他一概不管,这就使得缓存功能的具体实现对ImageLoader隐藏。这就是用最小化接口隔离了实现类的细节,也促使我们将庞大的接口拆分到更细粒度的接口当中,这使得我们的系统具有更低的耦合性,更高的灵活性。——更好的扩展性
定义:一个类应该对自己需要耦合或者调用的类知道的最少,类的内部如何实现与调用者或者依赖者没关系,调用者或者依赖着只需要知道它需要的方法即可,其他的一概不用管。类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另外一个类的影响也越大。——高内聚,低耦合 案例:房屋、中介、租客;改前 租客通过中介获取房屋资源,自己筛选符合条件的房屋;修改后租客直接调用中介的找房方法(传输价格和空间条件),中间根据条件找到合适的房屋返给租客就ok;创建型模式(创建对象的): 单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式;
行为型模式(对象的功能): 适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式;
结构型模式(对象的组成): 模版方法模式、命令模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式、访问者模式。
1、懒汉式
public class MyTest { public static void main(String[] args) { Student student = Student.getStudent(); Student student1 = Student.getStudent(); System.out.println(student == student1); //true } } class Student { public static Student stu = null; private Student() { } //多线程环境下能保证这个方法的一定是创建一个对象吗? //加上synchronized保证多线程环境下的使用也是单列的 public synchronized static Student getStudent() { if (stu == null) { stu = new Student(); } return stu; } }2、饿汉式
public class MyTest1 { public static void main(String[] args) { Teacher tea1 = Teacher.getTeacher(); Teacher tea2 = Teacher.getTeacher(); System.out.println(tea1 == tea2); //true } } class Teacher { public static Teacher tea = new Teacher(); private Teacher() { } public static Teacher getTeacher() { return tea; } }实际开发中我们使用饿汉式;Java中有一个类,Runtime,他就使用了饿汉式;
这个类可以执行一些DOS命令;
需求: 计算一个for循环执行的时间
模版设计模式概述:就是定义一个算法的骨架,而将具体的算法延迟到子类中来实现; 优点: 使用模版方法模式,在定义算法骨架的同时,可以很灵活的实现具体的算法,满足用户灵活多变的需求; 缺点: 如果算法骨架有修改的话,则需要修改抽象类; abstract class CalcClass { public void calc() { //算法骨架 long start = System.currentTimeMillis(); calcMethod(); long end = System.currentTimeMillis(); System.out.println("耗时:" + (end - start) + "毫秒"); } public abstract void calcMethod(); } class CalcFor extends CalcClass { @Override public void calcMethod() { for (int i = 0; i 5; i++) { System.out.println("abc"); } } } class CopyFile extends CalcClass { @Override public void calcMethod() { System.out.println("复制文件"); } } -----测试类: public class MyTest3 { public static void main(String[] args) { CalcClass js = new CalcFor(); js.calc(); js = new CopyFile(); js.calc(); /*abc abc abc abc abc 耗时:1毫秒 复制文件 耗时:0毫秒*/ } }岗位类 求职者 猎头(注册方法,注销方法,发布方法)
import java.util.ArrayList; public class Hunter { //定义两个集合,用来装求职者,和工作岗位 private ArrayListJobSeeker> jobSeekers = new ArrayList>(); private ArrayListJob> jobs = new ArrayList>(); //注册工作岗位 public void addJob(Job job) { jobs.add(job); //工作岗位过来之后,通知求职者 notifyJobSeeker(job); } private void notifyJobSeeker(Job job) { for (JobSeeker jobSeeker : jobSeekers) { System.out.println(jobSeeker.getName() + "你好!有一份工作:" + job.getJobName() + "薪资:" + job.getSal() + "欢迎你前去面试"); } } //注册求助者 public void addJobSeeker(JobSeeker jobSeeker) { jobSeekers.add(jobSeeker); } //注销 public void removeJobSeeker(JobSeeker jobSeeker) { jobSeekers.remove(jobSeeker); } } public class Job { private String jobName; private double sal; public Job() { } public Job(String jobName, double sal) { this.jobName = jobName; this.sal = sal; } public String getJobName() { return jobName; } public void setJobName(String jobName) { this.jobName = jobName; } public double getSal() { return sal; } public void setSal(double sal) { this.sal = sal; } } public class JobSeeker { private String name; public JobSeeker() { } public JobSeeker(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } } public class MyTest5 { public static void main(String[] args) { //观察者 = 订阅者 + 发布者 JobSeeker zs = new JobSeeker("张三"); JobSeeker ls = new JobSeeker("李四"); JobSeeker ww = new JobSeeker("王五"); //在猎头出注册 Hunter hunter = new Hunter(); hunter.addJobSeeker(zs); hunter.addJobSeeker(ls); hunter.addJobSeeker(ww); Job job = new Job("Java开发工程师", 8000); hunter.addJob(job); System.out.println("====================="); Job job2 = new Job("前端开发工程师", 18000); hunter.addJob(job2); System.out.println("========================"); //注销 hunter.removeJobSeeker(ww); Job job3 = new Job("运维工程师", 5000); hunter.addJob(job3); } }上一篇:Java程序是如何运行的呢?
下一篇:Java并行程序基础总结
皓盘云建最新版下载v9.0 安卓版
53.38MB |商务办公
ris云客移动销售系统最新版下载v1.1.25 安卓手机版
42.71M |商务办公
粤语翻译帮app下载v1.1.1 安卓版
60.01MB |生活服务
人生笔记app官方版下载v1.19.4 安卓版
125.88MB |系统工具
萝卜笔记app下载v1.1.6 安卓版
46.29MB |生活服务
贯联商户端app下载v6.1.8 安卓版
12.54MB |商务办公
jotmo笔记app下载v2.30.0 安卓版
50.06MB |系统工具
鑫钜出行共享汽车app下载v1.5.2
44.7M |生活服务
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-03-26
2022-02-15
2022-02-14