Java 多线程(详细)

目录

1.认识线程(Thread)

2.线程与进程的优劣势.

3.创建线程(Thread)

   方法一:继承Thread类

方法二:实现Runnable接口(可执行的)

方法三:实现匿名内部类,创建Thread的子类对象

方法四:实现匿名内部类,创建Runnable字类对象

方法五:lambda表达式 (推荐/常用)

4.Thread的常用构造方法 

 5.Thread的常用属性

6.启动线程 :

 7.中断线程:

8.等待线程 :

9.获取当前线程的引用 

10.线程的状态 :

11.线程安全 :

12.变量对线程安全的影响 

13.如何解决线程安全的问题 :

14.死锁 

15.解决死锁问题

16.保证内存可见性:volatile关键字

17.wait(等待) 和 notify(通知) 关键字 

18.多线程案例:

      18.1 单例模式 :

18.2.阻塞式队列(BlockingQueue) :

 18.3定时器:

18.4线程池:

总结:


1.认识线程(Thread)

    1️⃣每个线程都是一个独立的执行流,都可以单独参与cpu的调度.

    2️⃣每个进程里至少包含一个线程时(及为主线程)或多个线程,同一个进程创建多个线程时,线程会共享同一份资源(内存➕     文件描述符.)

         ⚠️:多个进程之间并不会共享同一份资源

    3️⃣ 线程是系统调度执行的基本单位

          进程是系统分配资源的基本单位

   4️⃣(同一个进程)多线程时,每个线程执行先后的顺序并不确定  因为在操作系统内核中有个“调度器的模块”,实现方式时‘随机调度’,‘抢占式调度’

2.线程与进程的优劣势.

   1️⃣.创建线程比创建进程更快

   2️⃣ .销毁线程比销毁进程更快

   3️⃣.调度线程比调度进程更快

3.创建线程(Thread)

   方法一:继承Thread类

   1️⃣继承Thread来创建一个线程类

       重写run方法,run表示线程的入口,而mian表示主线程入口

//) 继承 Thread 来创建一个线程类.
class MyThread extends Thread{
    @Override // @Override表示机器会在执行时检查一遍是否重写run方法
    public void run() {
       
            System.out.println("正在运行,run方法");
    }
}

2️⃣ 创建MyThread的实例

//调用 start 方法启动线程  才是真正的调用系统的API
  t.start();

  根据MyThread类,来创建的实例(才是真正的线程)

​
Thread t = new MyThread();

​

3️⃣调用MyThread父类Thread中成员方法 start(作用启动线程)

//调用 start 方法启动线程  才是真正的调用系统的API
       t.start();
​

4️⃣ sleep()方法是一个静态方法,属于Thread类,用于让当前正在执行的线程暂停执行一段时间

   在重写run方法中创建sleep()方法时会报错    可以选try/catch解决此问题

 在main中使用sleep方法时 也会出现报错   但是这里有两种解决方式try/catchthrows InterruptedException

引出一个问题为什么run只有一种解决方式呢?

    解答:因为父类Thread中没有throws这个异常,  如果加上throws,则修改了方法签名,够不成重写了  

              子类run是重写的,就不可以使throws异常

//括号里面数字(毫秒单位)代表休眠10000ms==1s
Thread.sleep(10000);

 整体代码:

//) 继承 Thread 来创建一个线程类.
class MyThread extends Thread{
    @Override // @Override表示机器会在执行时检查一遍是否重写run方法
    public void run() {
        while(true) {
            try {
                //Thread类中sleep方法表示
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("正在运行,run方法");
        }
    }
}
public class ThreadDome1 {
    public static void main(String[] args) throws InterruptedException {
        //2.根据Mythread类,来创建实列(才是真正的线程)
         //根据
        Thread t =new MyThread();
        Thread.sleep(1000);
        //3) 调用 start 方法启动线程  才是真正的调用系统的API
       t.start();
       while(true){
           try {
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               throw new RuntimeException(e);
           }
           System.out.println("正在运行,main方法");
       }
    }
}

方法二:实现Runnable接口(可执行的)

   需要搭配Thread类,才能在系统中真正创建出线程

//实现Runnable接口
class MyRunnable implements Runnable{
    @Override
    public void run() {
        while(true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("正在运行,Thread/run");
        }
    }
}
public class ThreadDome2 {
    public static void main(String[] args) throws InterruptedException {
        //2) 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
        //Runnable  runnable= new MyRunnable();
        Thread t = new Thread(new MyRunnable());
        //3.调用Thread类中的start方法
        //表示线程开始
        System.out.println("要开始创建线程了");
        t.start();
        while (true) {
           Thread.sleep(1000);
            System.out.println("正在运行,main");
        }
    }
}

方法三:实现匿名内部类,创建Thread的子类对象

    匿名内部类表示没有名字,不能重复使用,用一次就扔了

   其中代码中变量 t并非指向的是Thread,而是Thread的子类,具体是那个子类并不知道,因为匿名隐藏了

public class ThreadDome3 {
    //1.实现匿名内部类,  创建Thread的子类对象
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(){
            @Override
            public void run() {
                System.out.println("使用匿名类创建 Thread 子类对象");
            }

        };
        //启动线程
       t.start();
       while(true){
           Thread.sleep(1000);
           System.out.println("运行maim");
       }
    }
}

方法四:实现匿名内部类,创建Runnable字类对象

public class ThreadDome4 {
    public static  boolean fla = true;
    public static void main(String[] args) throws InterruptedException {
        //实现匿名内部类,创建Runnable子类对象
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                //条件fla为真运行,
                while (fla){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    System.out.println("正在运行run");
                }
            }
        });
        //启动线程
        t.start();
        for (int i = 0; i < 5; i++) {

            Thread.sleep(1000);
            System.out.println("正在运行main方法");
        }
        //主线程for循环执行完,fla=false,此时在执行匿名内部类run时循环条件为假跳出循环
        fla=false;
    }
}

方法五lambda表达式 (推荐/常用)

 这个写法相当于Runnable重写run方法, lambda代替了Runnable位置

lambda表达式,其实本质来讲,就是⼀个匿名函数。因此在写lambda表达式的时候,不需要关心方法名是什么。

实际上,我们在写lambda表达式的时候,也不需要关心返回值类型。

我们在写lambda表达式的时候,只需要关注两部分内容即可:参数列表和方法体

初始代码: 

public class ThreadDome5 {
    public static void main(String[] args) {
        //lambda表达式这个写法代替了Runnable重新run方法.
        //
        Thread t = new Thread(() ->{
            while(true){
                System.out.println("正在运行,Thread");
                try {
                    Thread.sleep(1001);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

            }
        });
        //启动新创建的线程
        t.start();
        System.out.println("hello main");
    }
}

使用多线程可以提高运行的效率 

4.Thread的常用构造方法 

Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

 5.Thread的常用属性

 自己创建的线程ID默认按照Thread-0,Thread-1;

  isDaemon属性作用:

守护线程也称为是否是后台线程

前台线程只要正在运行就会阻止进程结束,而后台线程并不会阻止 

咱们创建的线程,默认为前台线程,会阻止进程结束,即使main(主线程)已经执行完了,只要创建的前台线程没有结束进程就不会结束

//在start之前设置,设置为true 成为后台线程,不设置为前台线程   (不可以在start之后设置)
//前台线程会阻止线程结束, 后台线程并不会
t.isDaemon(true);

is Alive属性

 在真正创建出线程之前为false,start()只有内核中创建出PCB之后 isAlive为true

在run线程结束后内核中的线程也就结束了(内核pcb释放)isAlive为false

6.启动线程 :

                1️⃣:start方法会调用到系统的API,到系统内核中创建线程

                2️⃣:run方法(只是描述当前线程具体执行什么内容)

       面试题:start和run的区别?                                

  • run 方法:定义了线程的执行行为,即线程启动后需要执行的具体操作。
  • start 方法:负责启动一个新的线程,并让这个新线程去执行 run 方法中定义的代码。
  • 关键区别在于 : run 方法本身并不会创建或启动任何新的线程,它只是一个普通的方法调用。 而 start 方法才是真正触发线程并发执行的关键。

 7.中断线程:

   (就是让run方法结束运行)

        目前常见的有以下两种方式:
                1. 通过共享的标记来进行沟通
                2. 调用 interrupt() 方法来通知

    方法一:自定义变量作为标志为run方法的结束条件

              在main方法中设置fla为true,来结束run循环,

 

 方法二:使用 Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定
义标志位.(推荐

      // 终断t线程的睡眠(这种终断睡眠的方式依靠了java的异常处理机制。)
        t.interrupt();
//        t.stop(); //强行终止线程
        //缺点:容易损坏数据  线程没有保存的数据容易丢失

可以使用t.interrupt()方法来清除标志位  因为异常报错提前唤醒sleep

8.等待线程 :

        

join方法:让一个线程A,等待另一个线程B执行结束 后,线程A在执行

              在主线程执行t.join,就是让主线程等待t线程结束

9.获取当前线程的引用 

//获取当前线程的引用
Thread.currentThread()
//输出
System.out.println(Thread.currentThread().getName());


10.线程的状态 :

        1️⃣新建状态(NEW)Thread的对象已经有了,但还没有调用start方法

        2️⃣结束状态(terminated):Thread对象还在,内核中的线程已经结束了

        3️⃣就绪状态(Runnable):线程已经在cpu运行的,和等待cpu运行的线程

        4️⃣阻塞状态(TIMED_WAITING):可能产生于slee方法/join/用户输入等待 产生阻塞

                     等待阻塞(WAITING):产生于wait()方法,线程会释放占用资源,等待其他线程调用notify(唤醒单个线程)或isnotify(唤醒全部线程)。

                     同步阻塞(BLOCKED):产生于锁竞争导

                                

11.线程安全 :

 例子:

public class ThreadDome7 {
    //创建一个全局变量sum
    static int sum =0;
    public static void main(String[] args) throws InterruptedException {

        Thread t = new Thread(() ->{
            for (int i = 0; i < 50000; i++) {
                sum++;
            }
        });
        Thread t1 = new Thread(() ->{
            for (int i = 0; i < 50000; i++) {
                sum++;
            }
        });
        t.start();
        t1.start();
        t.join();
        t1.join();
        System.out.println(sum);

    }
}

 问题 要是单线程中执行绝对是正确的 

         但是在多线程中并发执行,因为多线程中线程的调度是随机调度的,此时逻辑就出现问题 这种情况就是bug

出现线程安全的原因:

                   1️⃣.操作系统中,线程调度是随机调度抢占式调度 (罪魁祸首)

                   2️⃣:两个线程,对同一个变量进行修改

                   3️⃣:内存可见性:当线程已经修改了变量的值,但其他线程并没看见而继续用之前的变量的值,导致出现线程安全问题

                             (可见性指, 一个线程对共享变量值的修改,能够及时地被其他线程看到

                   4️⃣:指令重排序问题

                   5️⃣:修改操作(不是原子的) 

                                原子性:指一个操作是不可分割的,不可中断的,不受其他线程影响

12.变量对线程安全的影响 

实例变量:在堆中。

静态变量:在方法区。

局部变量:在栈中。

    以上三大变量中:
        局部变量永远都不会存在线程安全问题。
        因为局部变量不共享。(一个线程一个栈。)
        局部变量在栈中。所以局部变量永远都不会共享。

     实例变量在堆中,堆只有1个。
    静态变量在方法区中,方法区只有1个。
    堆和方法区都是多线程共享的,所以可能存在线程安全问题

    局部变量+常量:不会有线程安全问题。
    成员变量:可能会有线程安全问题。
 

13.如何解决线程安全的问题 :

        synchronized关键字:  加锁—目的:把多个操作打包成一个操作具有原子性       

           进行加锁的时候,需要先准备好锁对象    (加锁和解锁都是针对锁对象进行的)

           当线程A对成员A进行加锁的操作时,线程B也想对成员A进行加锁  就会导致出现阻塞(BLOCKED),只有线程A执行完阻塞才结束,线程B才可以对成员A加锁

对Objeck变量进行加锁:  

public class ThreadDome7 {
    //随便创建一个Object变量
    static Object object =new Object();
  
    static int sum =0;
    public static void main(String[] args) throws InterruptedException {

       Thread t = new Thread(() ->{
            for (int i = 0; i < 50000; i++) {
           synchronized (object) {
               sum++;
           }

            }
        });
        Thread t1 = new Thread(() ->{
            for (int i = 0; i < 50000; i++) {
                synchronized (object) {
                    sum++;
                }
            }
        });
        t.start();
        t1.start();
        t.join();
        t1.join();
        //这里预期应该是100000
        System.out.println(sum);

    }
}

 synchronized中进入"{}"表示开始加锁,出"{}"表示加锁结束

synchronized的的特征:(可重入) 

 

还有的虽然没有加锁, 但是不涉及 "修改", 仍然是线程安全的:String 

14.死锁 

    1️⃣:加锁可以解决线程的安全问题,加锁使用不当时就会产生死锁 

    2️⃣:产生死锁的情况有以下情况:

        1.一个线程,一把锁:(如果锁不是可重入锁)同一个线程对这把锁锁两次,就会出现死锁

        2.二个线程,二把锁:这二个线程已经各锁了一把锁时,还想锁对方的锁,就会出现死锁

        3.N个线程,M锁;(哲学家就餐问题)

    3️⃣:产生死锁的必要条件:

        1.互斥使用:获得锁的过程是互斥的,一个线程拿到这把锁,另个一个线程也想拿到,就需要阻塞等待

        2.不可抢占一个线程拿到这把锁,除非该线程主动解锁,否则别的线程不能抢走

        3.请求保持一个线程拿到一把锁后,继续尝试获取下一把锁

        4.循环等待/环路等待:在发生死锁时,必然存在一个线程--资源的环形链

15.解决死锁问题

有许多中方法:

  • 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
  • 只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
  • 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件
  • 资源有序分配法(引入加锁顺序):系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件

     1️⃣: 产生死锁有四个必要条件, 我们只需破坏一个必要条件就好了

            最容易破坏的是条件四 我们只需指定加锁的顺序就好了

16.保证内存可见性:volatile关键字

​
​
import java.util.Scanner;
//没有加入volatile的代码
public class ThreadDome8 {
    static int fal=0;
    public static void main(String[] args) {
        Thread t =new Thread(() ->{
           while(fal==0){
           }
            System.out.println("循环结束");
        });
        Thread t1 =new Thread(() ->{
            Scanner scanner =new Scanner(System.in);
            System.out.println("请输入大于0得数:");
            fal= scanner.nextInt();
        });
        t.start();
        t1.start();
    }
}

​//执行效果t线程并没有获取的t1线程用户输入的值,没有跳出循环

​

t线程没有感知不到t1线程fal的变化 

1️⃣:volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了 

当我们引入volatile关键字时 给fal加入volatile   就可以跳出循环了

 

但是volatile 不保证原子性
volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见
性.
 

synchronized 也能保证内存可见性
synchronized 既能保证原子性, 也能保证内存可见性. 

17.wait(等待) 和 notify(通知) 关键字 

        1️⃣wait需要配合synchronized来使用(前提是这俩的锁是同一个对象),否则报错

        2️⃣wait()方法是让当前线程等待的,即让线程释放了对共享对象的锁,不再继续向下执行。

        3️⃣wait(long timeout)方法可以指定一个超时时间,过了这个时间如果没有被notify()唤醒,

                则函数还是会返回。如果传递一个负数timeout会抛出IllegalArgumentException异常。

        4️⃣notify()方法会让调用了wait()系列方法的一个线程释放锁,并通知其它正在等待(调用了wait()方法)的线程得到锁。

        5️⃣notifyAll()方法会唤醒所有在共享变量上由于调用wait系列方法而被挂起的线程。

public class ThreadDome9 {
    public static void main(String[] args) {
        Object object =new Object();
        Thread t =new Thread(() ->{
            synchronized (object){
                System.out.println(Thread.currentThread().getName() + " t wait之前.");

                try {
                    object.wait();

                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + " t wait之后.");
            }
        });
        Thread t1 =new Thread(() ->{
            try {
                Thread.sleep(2000);
                synchronized (object){
                    System.out.println(Thread.currentThread().getName() + " t1 notify之前");
                    object.notify();
                    System.out.println(Thread.currentThread().getName() + " t1 notify之后");

                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        });
        t.start();
        t1.start();
    }
}​

//结果
Thread-0 t wait 之前
Thread-1 t1 notify 之前
Thread-1 t1 notify 之后
Thread-0 t wait 之后

join和wait区别是:

        join是需要等待 调用此方法的线程结束后,才继续执行

        wait是需要另一个线程通notify进行通知(不要求另一个线程必须执行完)

18.多线程案例:

      18.1 单例模式 :

                           能够保证类在程序中只有唯一一份实例,而且不能创建多个实例.

                          比如上一篇中JDBC中DataSource只能有一个实例

        1️⃣: 单例的实现:有俩种实现方式,分成‘饿汉模式’和‘懒汉模式

                饿汉模式:可以说是在类加载的时候就已经创建了实例,相当于程序已启动就已经创建实例了(非常迫切的创建实例)

                                                                                                                                                                      ——————饿汉模式 

public class Singleton {
    // 私有构造器
    private Singleton() {
    }

    // 私有静态常量,保存实例
    private static final Singleton instance = new Singleton();

    // 公有静态方法,返回实例
    public static Singleton getInstance() {
        return instance;
    }
}

              懒汉模式(单线程版):类加载的时候不创建实例. 第一次使用的时候才创建实例.

                                                                                                                                                                      ——————懒汉模式

public class Singleton {
    // 私有构造器
    private Singleton() {
    }

    // 私有静态常量,保存实例
    private static Singleton instance = null;

    // 公有静态方法,返回实例
    public static Singleton getInstance() {
        //先判断如果没有创建实例时 才创建实例, 并非是启动就是创建实例
        if(instance==null) {
            instance = new Singleton();
        }
        return instance;
    }
}

                  懒汉模式(多线程版):防止在多个线程同时进行getInstance方法时创建多个实例的情况下,添加了synchronized改善了线程安全的问题

public class Singleton {
    // 私有构造器
    private Singleton() {
    }

    // 私有静态常量,保存实例
    private static Singleton instance = null;

    // 公有静态方法,返回实例
    //加个锁就是保证线程安全了
    public synchronized static Singleton getInstance() {
        //先判断如果没有创建实例时 才创建实例, 并非是启动就是创建实例
        if(instance==null) {
            instance = new Singleton();
        }
        return instance;
    }
}

                        

               懒汉模式(多线程版——改进版)

                        以下代码在加锁的基础上, 做出了进一步改动:
                                使用双重 if 判定, 降低锁竞争的频率.
                                给 instance 加上了 volatile.

​
public class Singleton {
    // 私有构造器
    private Singleton() {
    }
    // 私有静态常量,保存实例
    //加上volatile 确保其他线程访问
    private volatile static Singleton instance = null;

    // 公有静态方法,返回实例
    public  static Singleton getInstance() {
        //先判断如果没有创建实例时 才创建实例, 并非是启动就是创建实例
        if(instance==null) {
            //当有线程获取锁时,其他线程需要等待锁的释放,其他线程成功获取锁后,
            // 还需要在进行判断实例是否已创建好
            synchronized(Singleton.class){
                if(instance==null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

​

18.2.阻塞式队列(BlockingQueue) :

        BlockingQueue特点:

                1️⃣:先进先出

                2️⃣:阻塞队列为空时,取出队列元素将会阻塞等待。需要等到其他线程向队列添加元素时,

                        阻塞队列满时, 添加队列元素将会阻塞等待。需要等到其他线程从队列取元素时

                3️⃣:阻塞队列是线程安全的,多个线程并发也不会产生冲突

                4️⃣:生产者模式——向队列添加元素

                        消费者模式——向队列取元素  

                        (当队列为空时,消费者会被阻塞等待直到队列中有元素可供消费;当队列已满时,生产者会被阻塞等待直到队列有空闲位置可供添加元素。)

        

阻塞队列(BlockingQueue)的种类:


      

标准库中阻塞队列

        BlockingQueue 只是一个接口,真正实现的类是LinkedBlockingQueue

        put方法用于阻塞队列的入队列,take方法用于阻塞队列的出队列

        BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.

案例:

 生产者和消费者模型:

import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class ThreadDome14 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<Integer> queue =new LinkedBlockingQueue<Integer>();
        Thread t1 =new Thread(() ->{
            while(true){

                    try {
                        //阻塞队列取出刚刚添加的元素,赋值,打印
                        int value =queue.take();
                        System.out.println("消费水平: "+ value);

                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
            }
        });
        //启动线程
        t1.start();
        Thread t2 =new Thread(() ->{
            Random random =new Random();
            while(true){

                try {
                    Thread.sleep(1000);
                    int num =random.nextInt(100);
                    System.out.println("生产元素:"+num);
                    //阻塞队列添加用户输入的元素
                    queue.put(num);

                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        //t2线程启动
        t2.start();
        //t2线程等待t1线程执行完
        t1.join();
        //t1线程等待t2线程执行完
        t2.join();
    }
}

 18.3定时器:

        定时器是什么?  

                        1️⃣:好似一个闹钟,只要达到设定好的时间点 就执行指定的代码

                        2️⃣:标准库提供了一个Timer类,其中的核心方法Timer.schedu

                               schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后
执行 (单位为毫秒).

                                                                                         开始展示:

import java.util.Timer;
import java.util.TimerTask;

public class ThreadDome15 {
    public static void main(String[] args) {
        Timer timer =new Timer();
        //使用匿名内部类创建TimerTask
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("hello");
            }
            //定时3s后执行此代码
        },3000);
    }
}

 完整代码:

package thread;

import java.util.PriorityQueue;
import java.util.Random;
/**
 * 定时器的构成:
 * 一个带优先级的阻塞队列
 * 队列中的每个元素是一个 Task 对象.
 * Task 中带有一个时间属性, 队首元素就是即将
 * 同时有一个 worker 线程一直扫描队首元素, 看队首元素是否需要执行
 */
class MyTimertask implements Comparable<MyTimertask>{
    //在什么时间点上来执行任务
    //约定time这个ms级别的时间戳
    private long time;

    //实际要执行的代码
    private Runnable runnable;

    public long getTime(){
        return time;
    }

    //delay 希望是个相对时间
    public MyTimertask(Runnable runnable,long delay){
        this.runnable = runnable;
        //System.currentTimeMillis()表示当前时刻  + delay 相对时间 =真正要执行任务的绝对时间
        this.time = System.currentTimeMillis() + delay;
    }

    public void run(){
        runnable.run();

    }
    @Override
    public int compareTo(MyTimertask o) {
        return (int) (this.time-o.time);
    }
}
//通过这个类表示一个定时器
class MyTimer{
    //负责扫描任务队列,执行任务的进程
    private Thread t = null;
    //任务队列
    private PriorityQueue<MyTimertask>queue =new PriorityQueue();

    public void schedule(Runnable runnable,long delay){
        synchronized(locker){
            MyTimertask task =new MyTimertask(runnable,delay);
            queue.offer(task);
            //添加成功新的元素后唤醒扫描线程的wait
            locker.notify();
        }
    }
    public void cancel(){
        Thread.interrupted();
    }
    private Object locker =new Object();
    //创建一个构造方法
    public MyTimer(){
        t = new Thread(() -> {
            while(!Thread.currentThread().isInterrupted()){
                //队列为空则等待唤醒
                try {
                    synchronized(locker){
                        while(queue.isEmpty()){
                            locker.wait();
                        }
                        //队列不为空则取出队首元素
                        MyTimertask task = queue.peek();
                        //获取当前时间
                        long curtime = System.currentTimeMillis();
                        //
                        if(curtime >= task.getTime()){
                            //当前时间已经达到任务时间了
                            queue.poll();
                            task.run();
                        }else{
                            //时间没到 需要等待当前时刻减去相对时刻的时间
                            //这里面,没有用sleep的原因是sleep的等待时间是固定的,
                            // 会错过新的任务,无法释放锁 要是中途有任务也无法唤醒需要等到sleep休眠时间到
                            //Thread.sleep(task.time()-SYstem.currentTimeMillis());
                            locker.wait(task.getTime()-System.currentTimeMillis());
                        }
                    }
                } catch (InterruptedException e) {
                }
            }

        });
        t.start();
    }

}
public class ThreadDome12 {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 3000");
            }
        },3000);

        System.out.println("hello main");
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 2000");
            }
        },2000);

        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 1000");
            }
        },1000);
    }
}

18.4线程池:

        简单来说线程池最大的好处就是减少每次启动、销毁线程的损耗

Executors 创建线程池的几种方式
newFixedThreadPool: 创建固定线程数的线程池
newCachedThreadPool: 创建线程数目动态增长的线程池.
newSingleThreadExecutor: 创建只包含单个线程的线程池.
newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.
Executors 本质上是 ThreadPoolExecutor 类的封装.

 

//使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
//返回值类型为 ExecutorService
//通过 ExecutorService.submit 可以注册一个任务到线程池中.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadDome16 {
    public static void main(String[] args) {
        //使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
        //返回值类型为 ExecutorService
        //通过 ExecutorService.submit 可以注册一个任务到线程池中.
        ExecutorService service = Executors.newFixedThreadPool(10);
        service.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });
    }
}

实现线程池:

        核心操作为 submit, 将任务加入线程池中
        使用 Worker 类描述一个工作线程. 使用 Runnable 描述一个任务.
        使用一个 BlockingQueue 组织所有的任务
        每个 worker 线程要做的事情: 不停的从 BlockingQueue 中取任务并执行.
        指定一下线程池中的最大线程数 maxWorkerCount; 当当前线程数超过这个最大值时, 就不再新增
        线程了.

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
//*
// 创建线程池
// */
class MyThreadPoolExecutor {
    private List<Thread> threadList = new ArrayList<>();

    // 就是一个用来保存任务的队列.
    private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);

    // 通过 n 指定创建多少个线程
    public MyThreadPoolExecutor(int n) {
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() -> {
                // 线程要做的事情就是把任务队列中的任务不停的取出来, 并且进行执行
                while (true) {
                    try {
                        // 此处的 take 带有阻塞功能的.
                        // 如果队列为 空, 此处的 take 就会阻塞.
                        Runnable runnable = queue.take();
                        // 取出一个任务就执行一个任务即可
                        runnable.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
            threadList.add(t);
        }
    }

    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }
}

public class ThreadDemo17 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPoolExecutor executor = new MyThreadPoolExecutor(4);
        for (int i = 0; i < 1000; i++) {
            int n = i;
            executor.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("执行任务" + n + " , 当前线程为: " + Thread.currentThread().getName());
                }
            });
        }
    }
}

总结:

        如何保证线程安全的问题?

                1.保证原子性

                2.保证顺序性

                3.保证可见性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值