一个线程就是一个“执行流”,每个线程之间都可以按照顺序执行自己的代码。多个线程之间同时执行着多分代码
class MyThread extends Thread{
@Override
public void run() {
super.run ();
while(true) {
System.out.println ( "hello thread" );
try {
Thread.sleep ( 1000 );
} catch (InterruptedException e) {
e.printStackTrace ();
}
}
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread ();
t.start ();
while(true){
System.out.println ("hello main");
Thread.sleep ( 1000 );
}
}
}
class MyRunable implements Runnable{
@Override
public void run() {
while(true){
System.out.println ("hello thread");
try {
Thread.sleep ( 1000 );
} catch (InterruptedException e) {
e.printStackTrace ();
}
}
}
}
public class Demo2 {
public static void main(String[] args) {
Runnable runnable = new MyRunable ();
Thread t = new Thread (runnable);
t.start ();
while(true){
System.out.println ("hello main");
try {
Thread.sleep ( 1000 );
} catch (InterruptedException e) {
e.printStackTrace ();
}
}
}
}
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread (){
@Override
public void run() {
super.run ();
while(true){
System.out.println ("hello thread");
try {
Thread.sleep ( 1000 );
} catch (InterruptedException e) {
e.printStackTrace ();
}
}
}
};
t.start ();
while(true){
System.out.println ("hello main");
Thread.sleep ( 1000 );
}
}
}
public class Demo4 {
public static void main(String[] args) throws InterruptedException {
Thread t= new Thread ( new Runnable () {
@Override
public void run() {
while(true) {
System.out.println ( "hello thread" );
try {
Thread.sleep ( 1000 );
} catch (InterruptedException e) {
e.printStackTrace ();
}
}
}
} );
t.start ();
while(true){
System.out.println ("hello main");
Thread.sleep ( 1000 );
}
}
}
public class Demo5 {
public static void main(String[] args) {
Thread t =new Thread (()->{
while(true){
System.out.println ("hello thread");
try {
Thread.sleep ( 1000 );
} catch (InterruptedException e) {
e.printStackTrace ();
}
}
});
t.start ();
while(true){
System.out.println ("hello main");
try {
Thread.sleep ( 1000 );
} catch (InterruptedException e) {
e.printStackTrace ();
}
}
}
}
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
public class Demo6 {
public static void main(String[] args) {
Thread t =new Thread (()->{
while(true){
System.out.println ("hello thread");
try {
Thread.sleep ( 1000 );
} catch (InterruptedException e) {
e.printStackTrace ();
}
}
},"这是新线程");
System.out.println ( t.isDaemon () );
//设置t为后台线程
t.setDaemon ( true );
System.out.println ( t.isDaemon () );
t.start ();
}
}
public class Demo7 {
public static void main(String[] args) throws InterruptedException {
Thread t =new Thread (()->{
System.out.println ("线程开始");
try {
Thread.sleep ( 2000 );
} catch (InterruptedException e) {
e.printStackTrace ();
}
System.out.println ("线程结束");
});
t.start ();
System.out.println (t.isAlive ());
Thread.sleep ( 3000 );
System.out.println (t.isAlive ());
}
}
通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程
就开始运行了。
调用start方法,才真的在操作系统的底层创建出一个线程
start方法内部,是会调用操作系统的API,来在系统内核创建出线程
run方法就只是单纯描述了该线程要执行什么内容(会在start创建好后被自动调用)
在java中,要销毁/终止线程,做法是比较唯一的,就是想办法让run方法尽快执行结束
常见的有以下两种方式:
使用自定义的变量来作为标志位.
public class Demo8 {
private static boolean isQuit =false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread (()->{
while(!isQuit){
System.out.println ("线程工作中");
try {
Thread.sleep ( 1000 );
} catch (InterruptedException e) {
e.printStackTrace ();
}
}
System.out.println ("线程工作完毕");
});
t.start ();
Thread.sleep ( 5000 );
isQuit = true;
System.out.println ("设置isQuit为true");
}
}
缺点
Thread.currentThread().isInterrupted() 代替自定义标志位
public class Demo9 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread (()->{
//Thread类內部,有一个现成的标志位,可以用来判定当前循环是否要结束
while(!Thread.currentThread ().isInterrupted ()){//currentThread () 哪个线程调用这个方法,就返回哪个线程的对象
System.out.println ("线程工作中");
try {
Thread.sleep ( 1000 );//interrupt唤醒线程之后,此时sleep方法抛出异常,同时会自动清除刚才设置的标志位
} catch (InterruptedException e) {
//1.假装没听见,循环继续正常执行
e.printStackTrace ();
//2.加上一个break,表示让线程立即结束
//3.做一些其他工作。,完成之后再结束
break;
//如果没有sleep,就没有上述操作空间
}
}
System.out.println ("线程停止工作");
});
t.start ();
Thread.sleep ( 5000 );
System.out.println ("让t线程终止");
t.interrupt();//把上述Thread对象内部的标志位设为true
}
}
即使线程内部的逻辑出现阻塞(sleep)也是可以使用这个方法唤醒的
正常来说,sleep会休眠到时间到,才会结束,此处给出的interrupt就可以使sleep内部触发一个异常,从而提前被唤醒
让一个线程等待另一个线程执行结束,再继续执行,本质上就是控制线程结束的顺序
join实现线程等待效果
主线程中,调用t.join(),此时就是主线程等待t线程先结束
public class Demo10 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread ( () -> {
for (int i = 0; i < 5; i++) {
System.out.println ( "t线程工作中" );
try {
Thread.sleep ( 1000 );
} catch (InterruptedException e) {
e.printStackTrace ();
}
}
} );
t.start ();
//让主线程等待t线程执行结束
//一旦调用join,主线程就会出发阻塞,此时t线程可以趁机完成后续工作
//一直到阻塞到t执行完毕了,join才会解除阻塞,才能继续执行
System.out.println ("join 等待开始");
t.join ();
System.out.println ("join 等待结束");
}
}
方法 | 说明 |
---|---|
public static Thread currentThread(); | 返回当前线程对象的引用 |
方法 | 说明 |
---|---|
public static void sleep(long millis) throws InterruptedException; | 休眠当前线程 millis毫秒 |
public static void sleep(long millis, int nanos) throwsInterruptedException | 可以更高精度的休眠 |
线程调度是不可控的,sleep方法只能保证实际休眠时间是大于参数设置的休眠时间
public class Demo11 {
public static void main(String[] args) throws InterruptedException {
long beg =System.currentTimeMillis ();
Thread.sleep ( 1000 );
long end =System.currentTimeMillis ();
System.out.println ("时间"+(end-beg)+"ms");
}
}
系统会按照1000ms这个时间来控制休眠
当1000ms过了之后,系统会唤醒这个线程(阻塞->就绪)
但这个线程成了就绪状态,并不意味着立即回到cpu上运行(这中间有一个“调度开销”)
对于windows和linux这样的系统来说,调度开销可能达到ms级别
有些场景对时间精度要求很高,这时往往需要使用“实时操作系统”
线程的状态是一个枚举类型 Thread.State
public class Demo12 {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values ()) {
System.out.println (state);
}
}
}
如果多线程环境下代码运行结果符合我们的预期,即在单线程环境应该的结果,则说明这是线程安全的
public class Demo14 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread (()->{
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread t2 = new Thread (()->{
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start ();
t2.start ();
t1.join ();
t2.join ();
//预期结果应该是100000
System.out.println (count);
}
}
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会产生“锁竞争/锁冲突”后一个线程就会阻塞等待
synchronized(),() 中需要放入一个用来加锁的对象
这个对象是啥不重要,重要的是通过这个对象来区分两个线程是否竞争同一个锁
可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的"锁定" 状态
synchronized的使用方法
法1:
public class Demo14 {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Object locker = new Object ();
Thread t1 = new Thread (()->{
for (int i = 0; i < 50000; i++) {
synchronized (locker){
count++;
}
}
});
Thread t2 = new Thread (()->{
for (int i = 0; i < 50000; i++) {
synchronized (locker){
count++;
}
}
});
t1.start ();
t2.start ();
t1.join ();
t2.join ();
//预期结果应该是100000
System.out.println (count);
}
}
法二:
//synchronized的使用方法
class Counter{
public int count;
synchronized public void increase(){//synchronized修饰实例方法
count++;
}
//上述代码等价于
// public void increase(){
// synchronized (this){
// count++;
// }
// }
synchronized public static void increase1(){//synchronized修饰静态方法
}
public static void increase2(){
synchronized (Counter.class){//类对象
}
}
}
public class Demo15 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter ();
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase ();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase ();
}
});
t1.start ();
t2.start ();
t1.join ();
t2.join ();
System.out.println (counter.count);
}
}
可重入锁,指的是一个线程,连续针对一把锁,加锁两次,不会出现死锁,满足这个要求,就是“可重入”,不满足,就是“不可重入”
//t线程中存在下列代码
synchronized(locker){
synchronized(locker){
..........
}
}
一个线程没有释放锁, 然后又尝试再次加锁.就会出现死锁情况
1.一个线程,针对一把锁,连续加锁两次,如果是不可重入锁,就死锁了
synchronized 同步块对同一线程来说是可重入的,不会出现死锁问题
2.两个线程,两把锁(此时无论是不是可重入锁,都会死锁)
(1)线程t1 获取A锁,线程t2 获取B锁
(2)t1尝试获取B,t2尝试获取A
public class Demo16 {
private static Object locker1 = new Object ();
private static Object locker2 = new Object ();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
synchronized (locker1){
try {
Thread.sleep ( 1000 );
} catch (InterruptedException e) {
e.printStackTrace ();
}
synchronized (locker2){
System.out.println ("t1 加锁成功");
}
}
});
Thread t2 = new Thread(()->{
synchronized (locker2){
try {
Thread.sleep ( 1000 );
} catch (InterruptedException e) {
e.printStackTrace ();
}
synchronized (locker1){
System.out.println ("t2 加锁成功");
}
}
});
t1.start ();
t2.start ();
}
}
3.N个线程,M把锁,更容易出现死锁
5根筷子,5个哲学家
通常情况下,整个系统可以良好运转
但是极端境况下,就会出现问题
比如,同一时刻,五个哲学家都想吃面,同时拿起左手的筷子
此时,五个哲学家发现他们的右手没有筷子,于是只能等待
等待过程中,哲学家们不会放下左手的筷子
解决死锁,核心就是破坏上述必要条件,只要破坏一个,死锁就形成不了
计算机运行的程序/代码,经常要访问数据.这些依赖的数据,往往就会存储在 内存 中,cpu使用这个变量的时候,就会把这个内存中的数据,先读出来,放到cpu的寄存器中
cpu读取内存的这个操作,相对与寄存器读取是非常慢的
cpu进行大部分操作,都是非常快,一旦到读/写内存,此时速度就慢下来了
为了减少上述的问题,提高效率,此时编译器就可能对代码做出优化,把一些本来要读内存的操作,优化成读取寄存器
//内存可见性状况引起的问题
public class Demo17 {
private static int isQuit = 0;
public static void main(String[] args) {
Thread t1 = new Thread (()->{
while(isQuit==0){//1.load指令读取内存中isQuit的值到寄存器中
//2.通过cmp指令比较寄存器的值是否为0,决定是否要继续循环
//
//由于这个循环,循环的速度飞快,短时间内就会产生大量的循环
//此时,编译器/JVM就发现,虽然执行了这么多次load,但是load出来的结果都一样,并且load操作非常费时间(1次lord可以执行上万次cmp)
//所以,编译器只是第一次循环的时候,才读了内存,后续不再读内存了,而是直接从寄存器中,取出isQuit的值了
//由于此时修改isQuit代码的是另一个线程,编译器没有正确的判断
}
System.out.println ("t1退出");
});
t1.start ();
Thread t2 = new Thread (()->{
Scanner sc =new Scanner ( System.in );
System.out.println ("请输入isQuit:");
isQuit = sc.nextInt ();
});
t2.start ();
}
}
加上volatile后就可以解决这个问题了
public class Demo17 {
private volatile static int isQuit = 0;
public static void main(String[] args) {
Thread t1 = new Thread (()->{
while(isQuit==0){//1.load指令读取内存中isQuit的值到寄存器中
//2.通过cmp指令比较寄存器的值是否为0,决定是否要继续循环
//
//由于这个循环,循环的速度飞快,短时间内就会产生大量的循环
//此时,编译器/JVM就发现,虽然执行了这么多次load,但是load出来的结果都一样,并且load操作非常费时间(1次lord可以执行上万次cmp)
//所以,编译器只是第一次循环的时候,才读了内存,后续不再读内存了,而是直接从寄存器中,取出isQuit的值了
//由于此时修改isQuit代码的是另一个线程,编译器没有正确的判断
}
System.out.println ("t1退出");
});
t1.start ();
Thread t2 = new Thread (()->{
Scanner sc =new Scanner ( System.in );
System.out.println ("请输入isQuit:");
isQuit = sc.nextInt ();
});
t2.start ();
}
}
synchronized能保证原子性,volatile保证的是内存可见性
synchronized既能保证原子性, 也能保证内存可见性.
public class Demo18 {
private volatile static int isQuit = 0;
public static void main(String[] args) {
Object object = new Object ();
Thread t1 = new Thread (()->{
synchronized (object) {
while (isQuit == 0) {
}
}
System.out.println ("t1退出");
});
t1.start ();
Thread t2 = new Thread (()->{
Scanner sc =new Scanner ( System.in );
System.out.println ("请输入isQuit:");
isQuit = sc.nextInt ();
});
t2.start ();
}
}
协调执行顺序
join是影响线程结束的先后顺序
相比之下,此处希望线程不结束,也能有先后顺序的控制
wait() / wait(long timeout) 等待,让指定线程进入阻塞状态
notify() / notifyAll() 通知,唤醒对应的阻塞状态线程
wait和notify都是Object的方法,随便定义一个对象对可以用
wait,notify可以避免线程饿死
wait 要搭配 synchronized 来使用. 脱离 synchronized 使用 wait 会直接抛出异常.
wait 做的事情:
wait 结束等待的条件:
public class Demo19 {
public static void main(String[] args) throws InterruptedException {
Object object = new Object ();
synchronized (object){
System.out.println ("wait 之前");
object.wait ();
System.out.println ("wait 之后");
}
}
}
这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就
需要使用到了另外一个方法唤醒的方法notify()
notify 方法是唤醒等待的线程
public class Demo20 {
public static void main(String[] args) {
Object object = new Object ();
Thread t1 = new Thread (()->{
synchronized (object){
System.out.println ("wait 之前");
try {
object.wait ();
} catch (InterruptedException e) {
e.printStackTrace ();
}
System.out.println ("wait 之后");
}
});
Thread t2 = new Thread (()->{
synchronized (object){
System.out.println ("进行通知");
object.notify ();
}
});
t1.start ();
t2.start ();
}
}
notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程
1.wait 需要搭配 synchronized 使用. sleep 不需要
2.wait 是 Object 的方法 sleep 是 Thread 的静态方法.
单例模式是非常经典的的设计模式
单例模式保证某个类在程序中只存在唯一一份实例,而不会创建多个实例
这一点在很多场景上都需要. 比如 JDBC 中的 DataSource 实例就只需要一个
单例模式具体实现方式,分为"饿汉"和"懒汉"两种
类加载时,创建实例
class Singleton{
private static Singleton instance = new Singleton ();//static成员 在Singleton类被加载的时候,就会执行到这里的创建实例操作
private Singleton(){
};
public static Singleton getInstance(){
return instance;
}
}
class SingletonLazy{
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){//首次调用getInstance的时候才会真正创建实例
if(instance==null){
instance = new SingletonLazy ();
}
return instance;
}
}
上述的懒汉模式在多线程中是不安全的
线程安全问题发生在首次创建实例时,如果在多个线程中同时调用getInstance方法,就可能导致创建多个实例
如果多个线程,同时修改同一个变量,此时可能会出现线程安全问题
如果多个线程,同时读取同一个变量,此时不会出现线程安全问题
加上synchronized可以改善这里的线程安全问题
class SingletonLazy{
private static SingletonLazy instance = null;
public synchronized static SingletonLazy getInstance(){
if(instance==null){
instance = new SingletonLazy ();
}
return instance;
}
}
上述懒汉模式-多线程版中,每次调用getInstance方法都需要加锁/解锁,开销比较高, 而懒汉模式的线程不安全只是发生在首次创建实例的时候.因此后续使用的时候, 不必再进行加锁了
class SingletonLazy{
private volatile static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if(instance == null) {//这两个if的执行时机可能会差异很大,执行结果也可能截然相反
//第一个if用来判定是否需要加锁
synchronized (SingletonLazy.class) {
if (instance == null) {//第二个if用来判定是否需要new对象
instance = new SingletonLazy ();
}
}
}
return instance;
}
}
我们来理清一下思路
指令重排序,是编译器为了提高执行效率,在保持代码原有的逻辑的前提下,对代码顺序进行重新编排
指令重排序在多线程下,可能会出现误判
new操作,是可能会触发指令重排序的
new操作分为三步:
第1步是一定先执行的,假设进行了指令重排序,执行顺序变为 1->3->2
当t1执行完 第1步和第3步 时,此时instance就已经是非空了!!!但此时,instance指向的是一个还没初始化的对象
第2步 没开始执行,就在同一时间,t2线程开始执行了!!!
t2判定Instance == null ,条件不成立!!!于是t2直接 return instance
进一步t2线程的代码就可能会访问Instance里面的属性和方法了,这时就容易出现bug了
使用volatile的原因
为了避免"指令重排序"导致读取的instance出现偏差,于是补充上 volatile .
阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.
阻塞队列是一种线程安全的数据结构,带有阻塞特性:
阻塞队列的一个最经典场景就是"生产者消费者模型"
生产者消费者模型就是通过一个容器来解决生产者和消费者的耦合问提
生产者和消费者彼此之间不直接通讯,而是通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不需要找生产者要数据,而是直接从阻塞队列中获取
1)阻塞队列相当于一个缓冲区,平衡的生产者和消费者之间的对数据量处理能力(削峰填谷)
比如在 “秒杀” 场景下, 服务器同一时刻可能会收到大量的支付请求. 如果直接处理这些支付请求,
服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程). 这个时候就可以把这些请求都放
到一个阻塞队列中, 然后再由消费者线程慢慢的来处理每个支付请求.
2)阻塞队列也能使生产者和消费者之间解耦合
比如过年一家人一起包饺子. 一般都是有明确分工, 比如一个人负责擀饺子皮, 其他人负责包. 擀饺
子皮的人就是 “生产者”, 包饺子的人就是 “消费者”.
擀饺子皮的人不关心包饺子的人是谁(能包就行, 无论是手工包, 借助工具, 还是机器包), 包饺子的人
也不关心擀饺子皮的人是谁(有饺子皮就行, 无论是用擀面杖擀的, 还是拿罐头瓶擀, 还是直接从超
市买的).
在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.
public class Deom23 {
public static void main(String[] args) throws InterruptedException {
BlockingDeque<String> queue = new LinkedBlockingDeque<> ();
queue.put("abc");
System.out.println ( queue.take () );
}
}
生产者消费者模型
public class Demo24 {
public static void main(String[] args) throws InterruptedException {
BlockingDeque<String> blockingDeque = new LinkedBlockingDeque<> ();
Thread customer = new Thread (()->{
while(true) {
try {
String num = blockingDeque.take ();
System.out.println ( "消费元素" + num );
} catch (InterruptedException e) {
e.printStackTrace ();
}
}
},"消费者");
customer.start ();
Thread producer = new Thread (()->{
Random random = new Random ();
while(true) {
String num = random.nextInt (100)+"";
try {
blockingDeque.put ( num );
System.out.println ( "生产元素" + num );
Thread.sleep ( 1000 );
} catch (InterruptedException e) {
e.printStackTrace ();
}
}
},"生产者");
producer.start ();
customer.join ();
producer.join ();
}
}
一个队列,要么空,要么满
take 和 put 只有一边能阻塞
如果put阻塞,其他线程继续调用put也会阻塞,只有靠take唤醒
如果take阻塞,其他线程继续调用take也会阻塞,只有靠put唤醒
interrupt方法,是可能中断wait的状态
class MyBlockingQueue{
private String[] data = new String[1000];
private volatile int head = 0;//后续代码,有的进行读和写,加上volatile,避免内存可见性问提
private volatile int tail = 0;
private volatile int size = 0;
public synchronized void put(String elemt) throws InterruptedException {
while(data.length==size) {//当wait返回时,确认一下当前队列满不满,满着就继续执行wait
//队列满了
//如果是队列满,继续插入就会阻塞
this.wait ();//在当前代码中,如果interrupt唤醒了wait,直接整个方法就结束了,因为我们使用throws抛出的异常,这个时候代码是没事的
//但当用try-catch,出现异常,方法不会结束,会继续往下执行,此时会把tail指向的元素覆盖掉.实际上此处的队列还是满的,此时tail指向的元素,并非是无效元素(就是弄丢了一个有效元素)
}
//队列没满
data[tail] = elemt;
tail = (tail+1)%data.length;//如果tail++到达了数组末尾,这时就要他回到开头,环形队列
this.notify ();//这个notify是用来唤醒take中的wait
size++;
}
public synchronized String take() throws InterruptedException {
while(size == 0){
//队列为空
this.wait ();
}
//队列不为空
String ret = data[head];
head = (head+1)%data.length;
this.notify ();//这个notify用来唤醒put中的wait
size--;
return ret;
}
}
public class Demo25 {
public static void main(String[] args) throws InterruptedException {
MyBlockingQueue blockingDeque = new MyBlockingQueue ();
Thread customer = new Thread (()->{
while(true) {
try {
String num = blockingDeque.take ();
System.out.println ( "消费元素" + num );
Thread.sleep ( 1000 );
} catch (InterruptedException e) {
e.printStackTrace ();
}
}
},"消费者");
customer.start ();
Thread producer = new Thread (()->{
Random random = new Random ();
while(true) {
String num = random.nextInt (100)+"";
try {
blockingDeque.put ( num );
System.out.println ( "生产元素" + num );
} catch (InterruptedException e) {
e.printStackTrace ();
}
}
},"生产者");
producer.start ();
customer.join ();
producer.join ();
}
}
public class Demo26 {
public static void main(String[] args) {
Timer timer = new Timer ();
//给定时器安排一个任务,预定在xxx时间去执行
timer.schedule ( new TimerTask () {//此处使用匿名内部类的写法,继承了TimerTask并创建出一个实例
@Override
public void run() {
System.out.println ("执行定时器的任务");//通过run来描述任务的详细情况
}
},3000 );
System.out.println ("程序启动");
}
}
主线程执行schedule方法的时候,就是把这个任务给放到timer对象中了
与此同时,timer里头也包含了一个线程,这个线程叫"扫描线程",一旦时间到,扫描线程就会执行刚才安排的任务
完成了任务之后,整个进程没有结束,因为Timer内部的线程阻止了进程结束
Timer里,是可以安排多个任务的
定时器的构成:
为啥要带优先级?
因为阻塞队列中的任务都有各自执行的时间(delay),最先执行的任务一定是delay最小的,使用带优先级的队列就可以高效的把这个delay最小任务找出来
//通过这个类,描述一个任务
class MyTimerTask implements Comparable<MyTimerTask>{//优先级队列需要提供比较
//要有一个执行的任务
private Runnable runnable;
//有一个任务执行的时间
private long time;
//此处的delay就是schedule方法传入的"相对时间"
public MyTimerTask(Runnable runnable , long delay) {
this.runnable = runnable;
//构造出要执行任务的绝对时间
this.time = System.currentTimeMillis ()+delay;
}
@Override
public int compareTo(MyTimerTask o) {
//队首元素是最小时间的值
return (int) (this.time - o.time);
}
public long getTime() {
return time;
}
public Runnable getRunnable() {
return runnable;
}
}
//创建定时器
class MyTimer{
private PriorityQueue<MyTimerTask> queue = new PriorityQueue<> ();
private Object locker = new Object ();
public void schedule(Runnable runnable,long delay){
synchronized (locker) {
queue.offer ( new MyTimerTask ( runnable , delay ) );
locker.notify ();
}
}
//创建一个扫描线程
public MyTimer() {
Thread t = new Thread (()->{
//扫描线程需要不停的扫描队首元素,看是否到达时间
while(true){
try{
synchronized (locker) {
//使用while的目的是为了在wait被唤醒时,再确认一下条件
while (queue.isEmpty ()) {
//队列为空,阻塞等待
//使用wait进行等待
//这里的wait需要由另外的线程唤醒
//添加新的任务就应该唤醒
locker.wait ();
}
MyTimerTask task = queue.peek();
// 比较一下看当前的队首元素是否可以执行了.
long curTime = System.currentTimeMillis();
if (curTime >= task.getTime()) {
// 当前时间已经达到了任务时间, 就可以执行任务了
task.getRunnable().run();
// 任务执行完了, 就可以从队列中删除了.
queue.poll();
} else {
// 当前时间还没到任务时间, 暂时不执行任务.
// 暂时先啥都不干, 等待下一轮的循环判定了.
locker.wait(task.getTime() - curTime);
}
}
} catch (InterruptedException e) {
e.printStackTrace ();
}
}
});
t.start ();
}
}
public class Demo27 {
public static void main(String[] args) {
MyTimer timer = new MyTimer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("3000");
}
}, 3000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("2000");
}
}, 2000);
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("1000");
}
}, 1000);
System.out.println("程序开始执行");
}
}
线程池最大的好处就是减少每次启动线程,销毁线程的开销
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool ( 10 );
service.submit ( new Runnable () {
@Override
public void run() {
System.out.println ("hello");
}
} );
}
ThreadPoolExecutor 核心方法就两个 (1)构造 (2)任务注册(添加任务)
![在这里插入图片描述](https://img-blog.csdnimg.cn/b41e6b3f8c3745eb92fa2c7fee99fadc.png
corePoolSize 核心线程数
maximumPoolSize 最大线程数
这个线程池里的线程数目可以动态变化的
变化范围是[corePoolSize ,maximumPoolSize]
keepAliveTime 允许线程留存的时间
unit 是留存时间的单位
阻塞队列,用来存放线程池中的任务,可以根据需要灵活设置
需要优先级 就可以设置PriorityBlockingQueue
任务数目恒定 就可以设置ArrayBlockingQueue
此处使用ThreadFactory作为工厂类,由这个类负责创建线程
使用工厂类创建,主要是为了在创建过程中,对线程进行初始化
这是线程池的拒绝策略
一个线程池,能容纳的任务数量是有限的
当持续往线程池里添加任务的时候,一旦已经到达上线,继续再添加,会出现的效果跟选择的拒绝策略有关
直接抛出异常
新添加的任务,让添加任务的线程负责执行
丢弃任务队列中最老的任务
丢弃当前新加的任务
一个线程,执行的代码,主要有两类:
1.CPU密集型: 代码里主要的逻辑实在进行算数运算/逻辑判断
2.IO密集型: 代码里主要进行的是IO操作
假设一个线程的所有代码都是cpu密集型代码,这时,线程池设置的数量不应该超过N(N为cpu逻辑核心数).
假设一个线程的所有代码都是IO密集型代码,这时不吃cpu,线程池设置的数量就可以超过N(N为cpu逻辑核心数).一个核心可以通过调度的方式,来实现并发执行
代码不同,线程池的线程数目设置就不同
无法知道一个代码,具体多少内容是cpu密集型,多少内容是IO密集型
正确做法:使用实验的方式,对程序进行性能测试
测试过程中尝试修改线程数目来确定最优解
线程池对象不是我们直接new的
而是通过一个专门的方法,返回一个线程的对象
通常创建对象,使用new.new关键字会触发类的构造方法.但是,构造方法存在一定的局限性
Executors.newFixedThreadPool(10) 工厂模式(设计模式)
工厂模式是给构造方法填坑的
很多时候,构造一个对象,希望有多种构造方式
多种方式,就需要使用多个版本的构造方法来分别实现
但是构造方法要求方法是类名,不同的构造方法只能 通过重载的方式来区分了(重载=>参数类型/个数 不同)
使用工厂设计模式就能解决这个问题 使用普通方法,代替构造方法完成初始化工作 普通方法就可以使用方法的名字来区分,也就不再收到重载的规则制约了
实践中,一般单独创建一个类,给这个类创建一些静态方法,由这样的静态方法负责构造出对象
class PointFactory{
public static Point makePointByXY(double x, double y){
Point p = new Point ();
p.setLocation ( x,y );
return p;
}
public static Point makePointByRA(double r, double a){
Point p = new Point ();
p.setLocation ( r,a );
return p;
}
}
public class Demo28 {
public static void main(String[] args) {
Point p = PointFactory.makePointByXY ( 10,20 );
}
}
class MyThreadPool{
//任务队列
private BlockingDeque<Runnable> queue = new LinkedBlockingDeque<> ();
//通过这个方法,把任务添加到队列中
public void submit(Runnable runnable) throws InterruptedException {
//此处我们的拒绝策略,相当于第五种策略,阻塞等待(这是下策)
queue.put ( runnable );
}
public MyThreadPool(int n){
//创建出n个线程,负责上述队列中的任务
for (int i = 0; i < n; i++) {
Thread t = new Thread (()->{
try {
Runnable runnable = queue.take ();
runnable.run ();
} catch (InterruptedException e) {
e.printStackTrace ();
}
});
t.start ();
}
}
}
public class Demo29 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool = new MyThreadPool ( 4 );
for (int i = 0; i < 1000; i++) {
int id = i;
myThreadPool.submit ( new Runnable () {
@Override
public void run() {
System.out.println ("执行任务"+id);//因为内部类变量捕获,所以打印不了i
}
} );
}
}
}