目录
一.概念
多进程编程已经可以解决并发编程的问题,但创建一个进程,消耗会比较大。(资源分配和回收)线程的出现让创建、销毁、调度的速度更快一些。
线程:轻量级进程
一个进程可以包含一个线程,也可以包含多个线程。只有第一个线程启动的时候开销比较大。同一个进程的多个线程之间,共用了进程的同一份资源(主要是内存和文件表述符)、
同一内存:线程1new的对象,在线程2,3,4里面都可以使用
同一文件描述符:线程1打开的文件在线程2,3,4里都可以使用
操作系统实际调度时,是以线程为单位进行调度的
出现安全问题:
线程模型,资源共享,多线程争抢同一资源容易触发。
进程模型,天然资源隔离,不容触发,进行进程间通信的时候,多进程访问同一个资源可能会出现问题。
二.创建线程(Thread)
Java中创建线程的写法有很多种
(1)继承Thread,重写run()
package thread;
import static java.lang.Thread.sleep;
class MyThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println("hello Thread");
try {
Thread.sleep(1000);//使打印慢一些方便观察
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t=new MyThread();
t.start();
//start里面没有调用run(),start是创建了一个线程,由新的线程来执行run()方法
//主线程main()调用t.start(),创建出一个新的线程,新的线程调用run()
//如果run()方法执行完毕,新的线程自然销毁
while (true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
运行结果:
观察打印结果:操作系统调度线程的时候是“抢占式执行”,哪个线程先执行,哪个线程后执行不确定,取决于操作系统调度器的具体实现策略。
改成单线程:
打印结果:
观察两次结果可知:没有调用start()就不能开启一个新的线程。
run()和start()的区别:
start()是真正创建了一个线程(从系统这里创建的),线程是独立的执行流。
run()只是描述了线程要做的事情,直接在main中调用run(),此时没有创建新线程,全程是main线程在执行。
可以用jdk自带的工具jconsole查看执行的进程,这里是本机的路径
当前线程有许多,main和Thread0是我们在idea中创建的。其他的是jvm自带的线程。
(2)实现Runnable接口
package thread;
//Runnable作用是描述一个要执行的任务,run()方法就是任务的执行细节
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("hello thread");
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
Runnable runnable=new MyRunnable();
//这只是描述了一个任务
Thread t=new Thread(runnable);//把任务交给线程来执行
t.start();
}
}
优点:
解耦合:目的就是为了让线程和线程要干的活之间分离开
未来如果要代码,不用多线程,使用多进程或者线程池,或者协程,此时代码改动比较小。
(3)使用匿名内部类,继承Thread
//使用匿名内部类来创建线程
public class ThreadDemo3 {
public static void main(String[] args) {
Thread t=new Thread(){
@Override
public void run() {
System.out.println("hello");
}
};
t.start();
}
}
1)创建了一个Thread子类(子类没有名字)所以才叫做”匿名“
2)创建了子类的实例,并让t引用指向该实例
(4)使用匿名内部类,实现Runable
public class ThreadDemo4 {
public static void main(String[] args) {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
t.start();
}
}
这个代码本质和2相同,只不过是把实现Runnable任务交给匿名内部类的语法
此处是创建了一个类,实现Runnable,同时创建了类的实例,并且传给Thread的构造方法。
(5)使用Lambda表达式
推荐写法,也是最简单的写法
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t=new Thread(()->{
System.out.println("hello");
});
t.start();
}
}
把任务用lambda表达式来描述,直接把lambda传给Thread构造方法。
lambda就是一个匿名函数(没有名字的函数),用一次就销毁。
三.Thread常见的类
(1)常见的构造方法
Thread(String name)
public class ThreadDemo6 {
public static void main(String[] args) {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println("hello");
}
}
},"myThread");//创建的线程名字为myThread
t.start();
}
}
(2)Thread的常见属性
代码里面手动创建的线程,默认都是前台的,包括main默认也是前台的,其他的jvm自带的线程都是后台的。
isAlive()是在判断当前系统里面这个线程是不是真的有了:
在正真调用start()之前,调用isAlive就是false;调用start()之后,调用isAlive就是true。
如果内核里线程把run()执行完了,此时线程销毁,pcb随之释放,但是Thread这个对象不一定被释放,此时也是false。
xxx.start()会让内核创建一个PCB(),此时这个PCB才表示一个真正的线程。
如果t的run()还没有跑,isAlive()就是false;
如果t的run()正在跑,isAlive()就是true;
如果t的run()跑完了,isAlive()就是false;
注意:当调用了start()后,先执行run()还是下面的while()是不确定的,随机的。
(3)中断线程
中断的意思是,不是让线程立即就停止,而是通知线程应该要停止了。是否真的停止,取决于线程这里具体代码的写法。
1.使用标志位来控制线程是否要停止。
例:使用whlie(flag)来控制线程结束,当flag=false时,线程结束。
public class ThreadDemo8 {
private static boolean flag=true;
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
while (flag){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
Thread.sleep(3000);
//在主线程里就可以随时通过flag变量的取值,来操作t线程是否结束
flag=false;
}
}
2.使用Thread自带的标志位进行判定
Thread.currentThread()是Thread类的静态方法,通过这个方法可以获取到当前进程。(类似于this)
interrupt会做两件事:
1.把线程内部的标志位(boolea)给设置成true
2.如果在进行sleep,就会触发异常,把sleep唤醒(但sleep唤醒时还会做一件事,把刚才设置的标志位再次设置称为flase),sleep完循环还会继续。
线程忽略终止请求:
public class ThreadDemo9 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{//在t.run()中被使用,此处获取的就是t线程
while (!Thread.currentThread().isInterrupted()){
System.out.println("hello Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(3000);
t.interrupt();//终止线程(终止t线程)
}
}
运行结果:
调用interrupt,只是通知终止,不是线程一定要服从终止 。就像上面线程可以忽略终止操作。
线程立即响应终止请求:加上break
运行结果:
线程稍后进行终止
(4)等待一个线程
线程是一个随机调动的过程。等待线程就是控制两个线程的结束顺序。
xxx.join()
如果开始执行join的时候,t线程已经结束了,join不会阻塞,就会立即返回。
(5)获取当前线程的引用
在哪个线程中调用就能获取到哪个线程的实例。
(6)休眠当前线程
让线程休眠,本质上就是让这个线程不参加调度了(不去CPU上执行了)
四.线程的状态
状态是根据当前线程调度的情况来描述的。
1.NEW 创建了Thread对象,但是还没有调用start(内核里还没有创建对应PCB)
2.TERMINATE 表示内核中的PCB已经执行完毕了,但是Thread对象还在。
3.RUNNABLE 可以运行的(正在CPU上执行的;在就绪队列里,随时可以去CPU上执行的)
4.WAITING
5.TIMED_WAITING
6.BLOCKED(4,5,6都是表示线程PCB正在阻塞队列中)
(1)观察线程的所有状态
(2)线程状态转移
public class ThreadDemo11 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
for (int i = 0; i < 100000; i++) {
//这个循环体内不做操作,也不sleep
}
});
//启动之前,获取t的状态,就是new的状态
System.out.println("start之前:"+t.getState());
t.start();
System.out.println("执行中的状态:"+t.getState());
t.join();
//线程执行完成之后就是terminated状态
System.out.println("t结束之后:"+t.getState());
}
}
五.线程安全
多线程的抢占式执行带来的随机性。
举例:
class Counter{
public int count=0;
public void add(){
count++;
}
}
public class ThreadDemo12 {
public static void main(String[] args) {
Counter counter=new Counter();
//添加两个线程,两个线程分别针对counter调用5w次的add方法
Thread t1=new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
//启动线程
t1.start();
t2.start();
//等待两个线程结束
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印count的值
System.out.println("count="+counter.count);
}
}
结果:不是我们想象中要得到的10w,因为两个线程的并发执行顺序有很多种情况。
理想中的执行顺序:两次调用count++,依次增加两次
举例一个其他的情况:
(1) 出现线程安全问题的原因
1.根本原因:抢占式执行,随机调度
2.代码结构:多个线程同时修改同一个变量(一个线程修改一个变量,没事;多个线程读取同一个变量,没事;多个线程修改不同的变量,也没事。)
3.原子性:非原子的,出现问题概率非常高
原子:不可拆分的基本单位(例如上述的count++非原子性,这里可以拆分为load,add,save三个操作)
针对线程安全问题,解决的最主要的手段就是从原子性入手,把非原子性的操作变成原子性的操作。(加锁)
4.内存可见性问题:一个线程对共享变量值的修改,可以被其他线程看到。
5.指令重排序(本质上是编译器优化的bug)
编译器认为我们写的代码不够好,在保证逻辑不变的情况下,自动调整代码的执行顺序,从而加快程序的执行效率。
(2)synchronized加锁
synchronized使用方法
1.修饰方法
1)普通修饰方法
2)修饰静态方法
2.修饰代码块
1.普通修饰方法
修饰方法:进入方法就加锁,离开方法就解锁
对上面的代码的add()方法加锁synchronized
加了synchronized之后,进入方法就会加锁,出了方法就会解锁。如果两个线程同时尝试加锁,此时一个能获取锁成功,另一个只能阻塞等待(blocked),一直阻塞到刚才的线程释放锁,当前线程才能加锁成功。
加锁本质是把并发变成串行。
一旦加锁后,代码执行速度一定会大打折扣。虽然加锁后,算的慢了,但还是比单线程快,加锁只是针对count++加锁了,除count++之外,还有for循环的代码,for循环代码是可以并发执行的,只是count++串行执行了。
修饰普通方法,锁对象就是this;修饰静态方法,锁对象就是类对象。(xxx.class);修饰代码块,显示/手动指定锁对象。
修饰静态方法和修饰一般方法同理。
2.修饰代码块
this这里可以指定任意对象,不一定非是this。进入代码块就加锁,出了代码块就解锁。
这样加完锁后,可以得到结果10_0000
若两个线程一个加锁一个不加锁,就没有锁竞争了。例如写两个add方法,一个加锁,一个不加锁。代码执行和一开始都没有加锁是一样的。
3. synchronized特性
1)互斥:某个线程执行到某个对象的synchronized时,其他线程如果也执行到同一个对象synchronized就会阻塞等待
2)刷新内存(目前还存在争议)
3)可重入:synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。
一个线程针对同一个对象加锁两次,是否会有问题,如果没有问题,就叫可重入的。
锁对象是this,只要有线程调用add,进入add方法的时候,就会先加锁,紧接着又遇到了代码块,再次尝试加锁。此处是特殊情况,第二个线程和第一个线程本质是同一个线程 。
如果允许上述操作,这个锁是可以重入的。
如果不允许上述操作(第二次加锁会阻塞等待),就是不可重入的。(死锁)
在Java中为了避免死锁的出现,把synchronized设定成可重入的了。
(3)死锁
1.一个线程一把锁,连续加锁两次,如果锁是不可重入锁,就会死锁。
2.两个线程两把锁,t1和t2各自先针对锁A和锁B加锁,再尝试获取对方的锁。
举例:
//乐乐手上有可乐,文手上有芬达。乐乐给文说:你把芬达给我,我才给你可乐
//文对乐乐说:你把可乐给我,我才给你芬达
//这两个线程相互阻塞
public class ThreadDemo13 {
public static void main(String[] args) {
Object kele=new Object();
Object fata=new Object();
Thread lele=new Thread(()->{
synchronized (kele){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (fata){
System.out.println("lele把可乐和芬达都拿到了");
}
}
});
Thread wen=new Thread(()->{
synchronized (fata){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (kele){
System.out.println("wen把可乐和芬达都拿到了");
}
}
});
lele.start();
wen.start();
}
}
造成死锁:循环等待
线程1尝试获取到锁A和锁B,线程2尝试获取到锁B和锁A。线程1在获取B的时候等待线程2释放B;同时线程2在获取A的时候等待线程1释放A。
避免的方法:给锁编号,然后指定一个固定的顺序(比如从小到大)来加锁。任意线程加多把锁的时候,都让线程遵守上述顺序,此时循环等待自然破除。
针对死锁这样的问题要借助像jconsole这样的工具来进行定位,分析代码在哪里出现死锁。
(5)Java标准库中的线程安全类
如果多个线程操作同一个集合类,就需要考虑到线程安全。
(6)volatile关键字
和内存可见性相关
import java.util.Scanner;
class MyCounter{
public int flag=0;
}
public class ThreadDemo14 {
public static void main(String[] args) {
MyCounter myCounter=new MyCounter();
Thread t1=new Thread(()->{
while (myCounter.flag==0){
}
System.out.println("t1循环结束");
});
Thread t2=new Thread(()->{
Scanner scanner=new Scanner(System.in);
System.out.println("请输入一个整数:");
myCounter.flag=scanner.nextInt();
});
t1.start();
t2.start();
}
}
这个情况就是内存可见性问题,这是一个线程不安全问题。
t1线程,汇编原理解释有两部:
1.load 把内存中flag的值读取到寄存器里。
2.cmp 把寄存器里的值和0比较,根据比较结果,决定下一步往哪里执行
上述循环,速度极快。在t2正真修改前,load的结果都是一样的,并且load和cmp相比速度慢非常多。编译器认为没有什么变化,jvm编译器优化,不再重复load,只读一次。而实际上是有t2进行修改的,但是jvm/编译器对于这种多线程的情况,判断可能存在误差。
内存可见性问题:一个线程对一个变量进行读取操作,同时另一个线程针对这个变量进行修改,此时读到的值,不一定是修改后的值,读线程没有感知到线程的变化。
volatile:告诉编译器,这个变量是“异变的”,每次都要重新读取这个变量的内存内容,不知道什么时候就变了,不能再进行激进的优化了。
六.wait和notify
(1)wait
某个线程调用wait方法,就会进入阻塞(无论通过哪个对象wait)此时就处在WATING
wait的工作顺序:
1.先释放锁
2.进行阻塞等待
3.收到通知后,重新尝试获取锁,并且在获取锁后,继续往下执行。
wait要搭配synchroniced使用
wait()无参数版本:死等
wait(xxx)有参数版本:指定了等待的最大时间
(2)notify:通知
public class ThreadDemo17 {
public static void main(String[] args) throws InterruptedException {
Object object=new Object();
Thread t1=new Thread(()->{
System.out.println("t1:wait之前");
try {
synchronized (object) {
object.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1:wait之后");
});
Thread t2=new Thread(()->{
System.out.println("t2:notify之前");
synchronized (object) {
object.notify();
}
System.out.println("t2:notify之后");
});
t1.start();
//此处的sleep 500大概率会让t1先执行wait
//极端情况下,电脑特别卡,可能t2先执行notify
Thread.sleep(500);
t2.start();
}
}
notifyAll和notify相似,多个线程在wait的时候,notify随机唤醒一个,notifyAll所有线程都唤醒,这些线程再一起竞争锁。
例:有三个线程,分别打印ABC,设计使打印出的顺序为ABC
public class ThreadDemo18 {
public static void main(String[] args) throws InterruptedException {
Object locker1=new Object();
Object locker2=new Object();
Thread t1=new Thread(()->{
System.out.println("A");
synchronized (locker1){
locker1.notify();
}
});
Thread t2=new Thread(()->{
synchronized (locker1){
try {
locker1.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("B");
synchronized (locker2){
locker2.notify();
}
});
Thread t3=new Thread(()->{
synchronized (locker2){
try {
locker2.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("C");
});
t2.start();
t3.start();
Thread.sleep(100);
//保证t2,t3的wait都执行了,再来启动t1
t1.start();
}
}
七.多线程代码案例
(1)单例模式
单例模式---->单个实例(对象)
在有些场景中,有的特定的类,只能创建出一个实例,不该创建出多个实例。使用了单例模式后,就很难创建出多个实例。当创建多个实例的时候会编译报错。
1.饿汉模式
在类加载阶段,就急切的把实例创建出来了
//饿汉模式的单例模式实现
//此时保证Singleton这个类只能创建出一个实例
class Singleton{
//创建实例
private static Singleton instance=new Singleton();
//如果需要使用这个唯一实例,同一通过Singleton.getInstance()方式来获取
public static Singleton getInstance(){
return instance;
}
//为了避免Singleton类不小心被复制多份
//构造方法设为private,在类外面,就不能通过new创建这个Singleton的实例了
private Singleton(){
}
}
public class ThreadDemo19 {
public static void main(String[] args) {
Singleton s=Singleton.getInstance();
Singleton s2=Singleton.getInstance();
// Singleton s3=new Singleton();编译报错,不能new多个实例
System.out.println(s==s2);
}
}
2.懒汉模式
class SingletonLazy{
private static SingletonLazy instance=null;
public static SingletonLazy getInstance(){
if(instance==null){
instance=new SingletonLazy();
}
return instance;
}
private SingletonLazy(){}
}
public class ThreadDemo20 {
public static void main(String[] args) {
SingletonLazy s=SingletonLazy.getInstance();
SingletonLazy s2=SingletonLazy.getInstance();
System.out.println(s==s2);
}
}
3.单例模式的线程安全模式(多线程)
class SingletonLazy {
//volatile解决的是内存可见性和指令重排序问题,new的过程存在多步指令的问题
private volatile static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
//第一次new前需要加锁,后面的比较是读操作不存在修改不需要加锁
if (instance == null) {//if:判断是否要加锁
//load-cmp-new 加锁限制程序的运行顺序,保证多线程安全
//加锁后,t2load的就是t1修改后的值,这样就触发不了if条件,也不再创建新的对象
synchronized (SingletonLazy.class) {
if (instance == null) {//if:是否创建对象
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {}
}
public class ThreadDemo20 {
public static void main(String[] args) {
SingletonLazy s=SingletonLazy.getInstance();
SingletonLazy s2=SingletonLazy.getInstance();
System.out.println(s==s2);
}
}
(2)阻塞队列
阻塞队列也是特殊的队列,虽然也是先进先出的,但是带有特殊功能。
如果队列为空,执行出队列操作,就会阻塞,阻塞到另一个线程往队列里面添加元素(队列为空)为止;如果队列满了,执行入队列操作,也会阻塞,阻塞到另一个线程从队列取走元素位置。(队列不满)
消息队列:特殊的队列,相当于在阻塞队列的基础上,加上了消息类型,按照指定的类型进行先进先出
1.生产者消费者模型
1)实现了发送方和接收方之间的“解耦”。(解耦:降低耦合的过程)
开发中典型的场景:服务器之间的相互调用
针对上述场景,使用生产者消费者模型,可以有效降低耦合。
此时A和B之间的耦合就降低了很多,A和B都不知道彼此,AB代码都与彼此无关,AB都只知道队列,此时新增一个C对于A毫无影响。
2)生产者消费者模型,第二个好处是可以做到“削峰填谷”,保证系统的稳定性。(比如“热搜”。多个客户端会发来大量请求,客户端的大量请求会在阻塞队列中按顺序依次执行,不会对服务器产生大的冲击)
queue提供的方法有三个:
入队列offer ;出队列poll;取队首元素peek
阻塞队列提供的方法主要是两个:两个方法都带有阻塞功能
入队列put;出队列take
简单了解阻塞队列的用法:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class ThreadDemo21 {
public static void main(String[] args) {
BlockingQueue<Integer> blockingQueue=new LinkedBlockingQueue<>();
//创建两个线程来作为生产者和消费者
Thread customer=new Thread(()->{
while (true){
try {
Integer result=blockingQueue.take();
System.out.println("消费元素:"+result);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
Thread producer=new Thread(()->{
int count=0;
while(true){
try {
blockingQueue.put(count);
System.out.println("生产元素:"+count);
count++;
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
}
}
自己版本的阻塞队列
//自己写的阻塞队列
// 此处不考虑泛型,直接用int表示元素类型
class MyBlockingQueue {
private int[] item = new int[1000];
private int head = 0;
private int tail = 0;
private int size = 0;
//入队列
public void put(int value) throws InterruptedException {
synchronized (this) {
while (size == item.length) {
// return;
//队列满了,此时要产生阻塞
this.wait();
}
item[tail] = value;
tail++;
// 对tail到末尾的处理
// (1) tail=tail % item.length;
if (tail >= item.length) {
tail = 0;
}
size++;
//这个notify唤醒take中的wait
this.notify();
}
}
//出队列
public Integer take() throws InterruptedException {
int result=0;
synchronized (this) {
while (size == 0) {
//队列为空,也需要阻塞
this.wait();
}
result = item[head];
head++;
if (head >= item.length) {
head = 0;
}
size--;
//唤醒put中的wait
this.notify();
}
return result;
}
}
public class ThreadDemo22 {
public static void main(String[] args) throws InterruptedException {
MyBlockingQueue queue=new MyBlockingQueue();
Thread customer=new Thread(()->{
while (true){
try {
int result=queue.take();
System.out.println("消费:"+result);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
Thread producer=new Thread(()->{
int count=0;
while(true){
try {
System.out.println("生产:"+count);
queue.put(count);
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
}
}
(3)定时器
类似于定闹钟,平时的闹钟有两种风格:
1.指定特定时刻,提醒
2.指定特定时间段后提醒
这里的定时器,不是提醒,而是执行一个实现准备好的方法/代码
schedule();这个方法的效果是给定计时器,注册一个任务,任务不会立即执行,而是在指定时间进行执行。
此方法的写法:
自己写一个定时器:
1.让被注册的任务能够在指定时间被执行
2.一个定时器可以执行n个任务,n个任务会按照最初约定的时间按顺序执行
定时器里面的核心:
1.有一个扫描线程,负责判定时间/执行任务
2.还有一个数据结构,来保存所有被注册的任务(优先级队列是个好的选择)
此时扫描线程只需要扫一下队首元素即可,不必遍历整个队列。
此处的优先级队列会在多线程环境下使用(调用schedule是一个线程,扫描是另一个线程)
Java中提供的阻塞优先级队列:
PriorityBlockingQueue
import java.util.concurrent.PriorityBlockingQueue;
//使用这个类来表示一个定时器中的任务
class MyTask implements Comparable<MyTask>{
//要执行的任务内容
private Runnable runnable;
//任务在啥时候执行(使用毫秒时间戳表示)
private long time;
public MyTask(Runnable runnable, long time) {
this.runnable = runnable;
this.time = time;
}
//获取当前任务的时间
public long getTime() {
return time;
}
//执行任务
public void run(){
runnable.run();
}
@Override
public int compareTo(MyTask o) {
//返回小于0,大于0,0
//this比o小,返回小于0
//this比o大,返回大于0
//this和o相同,返回0
//当前要实现的效果,是队首元素是时间最小的任务
return (int) (this.time-o.time);
}
}
//自己设计的简单的计时器
class MyTimer{
//扫描线程
private Thread t=null;
//有一个阻塞优先级队列,来保存任务
private PriorityBlockingQueue<MyTask> queue=new PriorityBlockingQueue<>();
public MyTimer() {
t=new Thread(()->{
while(true){
try {
//取出队首元素。检查看队首元素的任务是否到时间了
//如果时间没到就把任务塞回到队列里面
//如果时间到了,就把任务进行执行
synchronized (this){
MyTask myTask=queue.take();
long curTime=System.currentTimeMillis();
if(curTime< myTask.getTime()){
//还没到点,先不必执行
//现在是13点,取出来的任务是14点执行
queue.put(myTask);
synchronized (this) {
this.wait(myTask.getTime() - curTime);
}
}else {
//时间到了,执行任务
myTask.run();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
//有一个schedule方法来注册任务
//第一个参数是任务内容,第二个参数是任务在多少毫秒之后执行
public void schedule(Runnable runnable,long after){
//注意这里时间要换算
MyTask task=new MyTask(runnable,System.currentTimeMillis()+after);
queue.put(task);
synchronized (this) {
this.notify();
}
}
}
public class ThreadDemo24 {
public static void main(String[] args) {
MyTimer myTimer=new MyTimer();
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("任务1");
}
},1000);
myTimer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("任务2");
}
},2000);
}
}
(4)线程池
使用线程池,来降低创建/销毁线程的开销(字符串常量池,数据库连接池.....)
事先把需要使用的线程创建好,放到“池”中,后面要使用的时候,直接从池中获取,如果用完了,也还给池。(比创建销毁更加高效)
创建一个线程池,池子里线程数目是10个。这个操作,使用某个类的某个静态方法,直接构造出一个对象来(相当于是new操作给隐藏到这样的方法后面了)
像这样的方法,就称为“工厂方法”,提供这个工厂方法的类,就称为“工厂类 ”。此处这个代码就使用了“工厂模式”这种设计模式。
工厂模式:使用普通的方法代替构造方法去创建对象。
此处要注意:当前是往线程池里面放了1000个任务,1000个任务就是这10个线程平均分配,但这里并非是严格的平均。(每个线程都执行完一个任务后,再立即取下一个任务.....由于每个任务执行时间都差不多,因此每个线程做的任务数量就差不多)
线程池里面至少有两个大的部分:
1.阻塞队列,保存任务
2.若干个工作线程
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class MyThreadPool{
//阻塞队列,此处不涉及时间,只有任务
private BlockingQueue<Runnable> queue=new LinkedBlockingQueue<>();
//n表示线程的数量
public MyThreadPool(int n){
//在这里创建出n个线程
for (int i = 0; i <n; i++) {
Thread t=new Thread(()->{
while(true){
try {
Runnable runnable= queue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
//注册任务给线程池
public void submit(Runnable runnable){
try {
queue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//自己实现一个线程池
public class ThreadDemo26 {
public static void main(String[] args) {
MyThreadPool pool=new MyThreadPool(10);
for (int i = 0; i < 1000; i++) {
int n=i;
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello"+n);
}
});
}
}
}