多线程的最全基本操作用法

线程概念

进程是系统分配资源的最小单位,线程是系统调度的最小单位。一个进程内的线程之间是可以共享资源的

演化过程:

进程 —》 线程(轻量级的进程) —》 协程(轻量级的线程)

线程提升性能

我们用一个线程来完成15亿次的循环,记录运行时间
再用三个线程,每个线程执行5亿次循环,这样一共也是15亿次,记录运行时间

/**
 * Created with IntelliJ IDEA.
 * Description:进程性能提升
 * User: starry
 * Date: 2021 -04 -25
 * Time: 19:10
 */
public class ThreadDemo3 {

    //执行的循环次数
    private static final Long count = 5_0000_0000L;

    public static void main(String[] args) throws InterruptedException {

//        System.out.println(Thread.currentThread().getName());

        //调用多线程方法
        concorrency();

        //调用单线程的方法
        serial();

    }

    //单线程执行方法
    private static void serial() {
        //开始
        Long stime = System.currentTimeMillis();    //记录当前时间毫秒时间戳
//        System.nanoTime();  //记录当前时间的纳秒数(更精确)
        int a = 0;
        for (int i = 0; i < 3 * count; i++) {
            a++;
        }
        Long etime = System.currentTimeMillis();
        System.out.println("单线程执行的时间是" + (etime-stime));
    }

    //多线程的方法
    private static void concorrency() throws InterruptedException {
        //开始时间
        Long stime = System.currentTimeMillis();

        //todo: 执行 30 亿次循环
        //创建了线程任务
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //具体业务
                int a = 0;
                for (int i = 0; i < count; i++) {
                    a++;
                }
            }
        });
        //开始执行线程
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                int b = 0;
                for (int i = 0; i < count; i++) {
                    b++;
                }
            }
        });
        t2.start();

        //让主线程执行 10 亿次
        int c = 0;
        for (int i = 0; i < count; i++) {
            c++;
        }

        //等待线程 t1 和 t2 执行完成之后,才统计时间
        t1.join();
        t2.join();

        Long etime = System.currentTimeMillis();
        System.out.println("多线程执行了" + (etime-stime));

    }

}

在这里插入图片描述
可以看到多线程的执行时间明显高于单线程

但是为什么3个线程一起执行,时间并不是一个线程的三分之一呢?

因为线程在创建的时候是有性能开销的,并且cpu在调度线程的时候,是抢占式调度,所以也有先后划分
,所以在理想情况下,单线程的时间是n个线程的n倍,但是在实际情况中,肯定会比理想情况下用的时间多

线程的创建方式

方式一:继承 Thread 类

方法一:继承 Thread 类并重写run方法

	static class MyThread extends Thread {
        @Override
        public void run() {
            //线程执行任务
            System.out.println("线程名称:" +
                    Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        //创建了线程
        Thread t1 = new MyThread();
        //运行新线程
        t1.start();
        System.out.println("当前线程的名称(主线程):" +
                Thread.currentThread().getName());
    }

方法二:使用匿名类创建 Thread 子类对象

        Thread t1 = new Thread() {
            @Override
            public void run() {
                System.out.println("线程名:" +
                        Thread.currentThread().getName());
            }
        };
        t1.start();
        

继承Thread类的方式的缺点:
Java 语言的设计中,只能实现单继承,如果继承了Thread类,也就不能继承其他类了。

方式二: 实现 Runnable 接口

Java不能多继承,但是可以实现多个接口
(3种写法)

方法1:构建类来实现Runnable接口,创建该类,新建线程

    static class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("线程名:" +
                    Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        //1.新建 Runnable 类
        MyRunnable runnable = new MyRunnable();
        //2.新建 Thread
        Thread thread = new Thread(runnable);
        //3.启动线程
        thread.start();
    }

方法2:使用匿名类创建 Runnable 子类对象

        //匿名内部类的方式实现线程
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程名:" +
                        Thread.currentThread().getName());
            }
        });
        thread.start();

方式3:lambda + 匿名 runnable的实现方式

        //lambda + 匿名 runnable的实现方式
        Thread thread = new Thread(() -> {
            System.out.println("线程名:" +
                    Thread.currentThread().getName());
        });
        thread.start();

方式三:实现 Callable 接口

(1种写法)

创建并得到线程的执行结果
实现 Callable 接口 + Future 的方式

    //创建了线程任务和返回方法
    static class MyCallable implements Callable<Integer> {

        @Override
        public Integer call() throws Exception {
            //生成一个随机数
            int num = new Random().nextInt(10);
            System.out.println("子线程:" +
                    Thread.currentThread().getName() + ",随机数:" + num);
            return num;
        }
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1.创建一个 Callable
        MyCallable myCallable = new MyCallable();
        //2.创建一个 FutrueTask 对象接收返回值
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        //3.创建线程
        Thread thread = new Thread(futureTask);
        //启动线程
        thread.start();
        
        //得到线程结果
        int result = futureTask.get();
        System.out.println(String.format("线程名:%s,数字%d",Thread.currentThread().getName(),result));
    }

获取当前线程引用

方法说明
public static Thread currentThread();返回当前线程对象的引用
public class ThreadDemo {
	public static void main(String[] args) {
		Thread thread = Thread.currentThread();
	System.out.println(thread.getName());
}

线程休眠

线程休眠一共有 3 种方式:

  • 方式一:Thread.sleep(1000);
  • 方式二:TimeUnit.SECONDS.sleep(1);(SECONDS为秒单位,还有day,month等)
  • 方法三:Thread.sleep(TimeUnit.SECONDS.toMillis(1));

休眠当前线程

方法说明
public static void sleep(long millis) throws InterruptedException休眠当前线程 millis 毫秒
public static void sleep(long millis, int nanos) throws InterruptedException可以更高精度的休眠
public class ThreadDemo {
	public static void main(String[] args) throws InterruptedException {
		System.out.println(System.currentTimeMillis());
		Thread.sleep(3 * 1000);
		System.out.println(System.currentTimeMillis());
	}
}

练习:使用两个线程来打印“AABBCCDD”

要点在于休眠,在每次线程打印后都休眠以下,这样两个线程就会同时打印A,而不是一个线程一次性打印完ABCD

    public static void main(String[] args) {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                char a = 'A';
                for(int i = 0; i < 4; i++) {
                    System.out.print((char)(a+i));
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                char b = 'A';
                for(int i = 0; i < 4; i++) {
                    System.out.print((char)(b+i));
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t2.start();
    }

Thread 的常见构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target, String name)使用 Runnable 对象创建线程对象,并命名
Thread(ThreadGroup group,Runnable target)线程可以被用来分组管理,分好的组即使线程组

代码演示

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

ThreadGroup:线程分组

可以将一类线程归为一组,并且进行信息的打印,查看一组线程的具体行为。

    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("开始起跑了" +
                        Thread.currentThread().getName());
                int num = 1 + new Random().nextInt(5);
                try {
                    Thread.sleep(num * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("我到终点了:" +
                        Thread.currentThread().getName());
            }
        };
        //定义分组
        ThreadGroup group = new ThreadGroup("百米赛跑第一组");

        //创建运动
        Thread t1 = new Thread(group,runnable,"张三");
        Thread t2 = new Thread(group,runnable,"李四");
        t1.start();
        t2.start();

        // 打印线程分组的详情
//        group.list();

        //等待所有选手到达
        while (group.activeCount() != 0) {

        }

        //宣布成绩
        System.out.println("宣布成绩");
    }

使用线程的activeCount()方法,如果线程活跃度不为null,就原地等待,这样就会等两个线程都到终点后,才宣布成绩。

运行结果:
在这里插入图片描述

Thread 常见属性

属性获取方法
IDgetId()
名称getName()
状态getState()
优先级getPriority()
是否后台线程isDaemon()
是否存活isAlive()
是否被中断isInterrupted()

代码演示

        Thread t1 = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"张三");
        System.out.println("线程状态:" + t1.getState());    //NEW
        t1.start();
        System.out.println("线程状态:" + t1.getState());    //RUNNABLE
        System.out.println("线程ID:" + t1.getId());        //12
        System.out.println("线程名称:" + t1.getName());     //张三
        System.out.println("线程优先级:" + t1.getPriority());    //5
        System.out.println("线程是否为后台进程:" + t1.isDaemon());   //false
        System.out.println("线程是否存活:" + t1.isAlive());   //true
        System.out.println("线程是否被中断:" + t1.isInterrupted());    //false

线程优先级

新创建的线程默认优先级是 5

线程优先级为 1 - 10 ,越大,执行权重越大,越重要,先执行

这里举个例子

Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("线程名:" +
                        Thread.currentThread().getName());
            }
        };

        for (int i = 0; i < 10; i++) {
            Thread t1 = new Thread(runnable,"小优先线程");
            Thread t2 = new Thread(runnable,"中小优先线程");
            Thread t3 = new Thread(runnable,"大小优先线程");

            t1.setPriority(1);  //最小优先级
//            t1.setPriority(Thread.MIN_PRIORITY);
            t3.setPriority(Thread.MAX_PRIORITY);
            t1.start();
            t2.start();
            t3.start();
        }

结果
在这里插入图片描述
这里可以看到,我们预期的是优先级高的先执行,但是我们从结果可以看出,并不是这样的。

那这里为什么是交替出现的,并没有根据我们预期的优先级打印呢?
其实,即使设置了线程的优先级,一样无法确保这个线程一定先执行,因为它有很大的随机性。它并无法控制执行哪个线程,因为线程的执行,是抢占资源后才能执行的操作,而抢点资源时,最多是给于线程优先级较高的线程一点机会而已,能不能抓住可是不一定的。。

说到底就一句话:线程优化级较高的线程不一定先执行

后台线程

线程的类型:

  1. 后台线程(守护线程)
  2. 用户线程(默认线程)

两者关系:守护线程是用来服务用户线程的,用户线程就是上帝,守护线程就是服务员

进程退出条件:没有用户线程运行的时候进程就i会结束

	public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("i:" + i);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t1.start();
        System.out.println("t1是否为守护线程:" + t1.isDaemon());
    }

这里可以看到此用户线程把所有数字打印完了
在这里插入图片描述
我们给线程设置为守护线程**t1.setDaemon(true);**后

可以看到此守护线程就打印了一次,因为守护线程是用来服务的,优先级很低,当没有用户线程时,守护线程就不会服务,当然就不会执行,所以就打印了一次。
在这里插入图片描述

使用场景:Java 垃圾回收器、TCP的健康检测

注意事项:

  1. 守护线程的设置必须在开启线程之前(如果设置守护线程在开始线程之后,那么程序就会报错,并且设置的守护线程值不能生效)
  2. 在守护线程里面创建的线程默认情况下全部是守护线程

启动线程

覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。

  • 覆写 run 方法是提供给线程要做的事情的指令清单
  • 线程对象可以认为是把 李四、王五叫过来了
  • 而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。

start 和 run 之间的区别?

  1. run 属于普通方法;而 start 属于启动线程的方法
  2. run 方法可以执行多次,而 start 方法只能执行一次

中断线程

中断线程的三种方式:

  1. 使用全局自定义的变量来终止线程
  2. 使用线程提供的终止方法 interrupt 来终止线程
  3. 使用线程提供的 stop 来终止线程(被废弃的方法)

使用全局自定义的变量来终止线程

举例说明

    // 全局自定义变量
    private static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {

        //转账线程
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (!flag) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("别烦我,我正在转账呢");
                }
                System.out.println("终止转账");
            }
        });
        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(310);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //改变变量的状态来终止线程的进行
                System.out.println("停止交易,有内鬼");
                flag = true;
            }
        });
        t2.start();

        t1.join();
        t2.join();
    }

在这里插入图片描述
可以看到它并没有立刻终止线程,因为改变状态时,t1线程已经进入循环并休眠了,sleep期间不能打断。

所以得到结论:
该终止线程的方法比较温柔,再拿到终止指令之后,需要执行完当前的任务才会真正的停止线程

使用 interrupt 来终止线程

方法说明
public void interrupt()中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,清除中断标志变,否则设置标志位
public static boolean interrupted()判断当前线程的中断标志位是否设置,调用后清除标志位 ,
public boolean isInterrupted()判断对象关联的线程的标志位是否设置,调用后不清除标志位

举例说明:

	public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //判断线程的终止状态
                while (!Thread.interrupted()) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
//                        e.printStackTrace();
                        break;
                    }
                    System.out.println("别烦我,我正在转账");
                }
                System.out.println("终止线程");
            }
        });
        t1.start();

        Thread.sleep(310);
        //终止线程
        System.out.println("有内鬼,终止交易");
        t1.interrupt(); //用来终止线程
    }

在这里插入图片描述

对比可以看出来:

  • 使用全局自定义标识终止线程执行的时候,比较温柔,收到终止指令后,会把手头的任务执行完再执行。
  • 使用 interrupt 在收到终止指令之后会立马结束执行。

注意:这里的 interrupted 并不是就直接终止线程,而是给线程这样一个终止指令,具体怎么终止的需要依靠代码设置,比如这段代码在休眠的时候终止线程会抛出异常,我们在捕获到异常后break,使其跳出循环,不执行后面代码,所以这个break才是真正终止线程的具体代码。

当有异常的时候,线程会终止,isInterrupted()状态改为true
但是如果线程调用了 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,清除中断标志,isInterrupted()变为默认状态 false ,这就是为什么上述代码要加break来终止,否则就会死循环。

Thread.interrupted()和Thread.currentThread().isInterrupted()的区别
Thread.interrupted():执行判断线程终止位 true 之后,就会将状态重置为 false ,因为是全局的
Thread.currentThread().isInterrupted():只能使用一次,不会复位线程的终止状态,因为是私有的

例如:

    public static void main(String[] args) {

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
//                    System.out.println(Thread.interrupted());
                    System.out.println(Thread.currentThread().isInterrupted());
                }
            }
        });
        t1.start();

        t1.interrupt();

    }

如果用 Thread.currentThread().isInterrupted() 结果会全是 true
如果用 Thread.interrupted() 结果第一个为true,后面的全为 false

这也就证明了上述结论。

等待线程

join使用方式

方法说明
public void join()等待线程结束
public void join(long millis)等待线程结束,最多等 millis 毫秒
public void join(long millis, int nanos)同理,但可以更高精度

代码举例

    public static void main(String[] args) throws InterruptedException {

        //定义公共任务
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() +
                        "上班");
                try {
                    //表示工作时间
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() +
                        "下班");
            }
        };

        //定义星星线程
        Thread t1 = new Thread(runnable,"星星");
        t1.start();

        //等待星星执行完成
        t1.join();
//        t1.join(900);

        //定义狒狒线程
        Thread t2 = new Thread(runnable,"狒狒");
        t2.start();

    }

如果不等待的话就是同时上班,同时下班
如果等待的话就是星星先上班,上完班后,狒狒接班上

线程状态

线程状态是一个枚举类型,我们通过代码获取到所有线程状态

public class ThreadState {
	public static void main(String[] args) {
		for (Thread.State state : Thread.State.values()) {
			System.out.println(state);
		}
	}
}
  • NEW:新建状态,没有调用线程 start() 之前的状态
  • RUNNABLE:运行状态(Running执行中,Ready就绪)
  • BLOCKED:阻塞状态
  • WAITING:等待状态,没有明确的等待结束时间,wait() 没有任何参数
  • TIMED_WAITING:超时等待状态,有明确的等待结束状态,比如sleep
  • TERMINATED:终止状态

代码举例1

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        System.out.println("没调用start之前的状态:" + t1.getState());
        t1.start();
        System.out.println("调用start之后的状态:" + t1.getState());
        Thread.sleep(100);  //确保t1线程在休眠期间,主线程先休眠一会
        System.out.println("t1的状态:" + t1.getState());

        //等待线程执行完成
        t1.join();
        //也可以让主线程空转来等待t1结束
//        while (!t1.isAlive()) {
//        }
        System.out.println("t1线程执行完成之后的状态:" + t1.getState());
    }

执行结果:
在这里插入图片描述

代码举例2

    public static void main(String[] args) throws InterruptedException {
        //创建一把锁
        Object obj = new Object();

        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (obj) {
                    try {
                        // 表示线程进行无期休眠状态
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        System.out.println("没调用start之前的状态:" + t1.getState());
        t1.start();
        System.out.println("调用start之后的状态:" + t1.getState());
        Thread.sleep(100);  //确保t1线程在休眠期间,主线程先休眠一会
        System.out.println("t1的状态:" + t1.getState());

    }

执行结果:
在这里插入图片描述
可以看到用 wait() 无限期休眠状态下是 WAITING

线程状态图

在这里插入图片描述

yiled关键字

作用:出让 cpu 的执行权

特点:不一定能正常出让 cpu 的执行权

    private static final int maxSize = 1000;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < maxSize; i++) {
                //出让 CPU 执行权
                Thread.yield();
                System.out.println("我是t1");
            }
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < maxSize; i++) {
                System.out.println("我是t2");
            }
        });
        t2.start();
    }

如果 t1 线程使用 yield 方法出让执行权,执行结果是 t1 最后执行完
如果 t1 线程不使用 yield 方法出让执行权,执行结果是 t2 最后执行完

虽然说 t1 出让了执行权,但是也并不是全部出让了,t1 和 t2 都会执行,这也应证了不一定能正常出让 cpu 的执行权,这和cpu的调度有关


版权声明:本文为starry1441原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。