初识java多线程(Thread)

目录

1、认识线程

1.1概念

1.2为什么要有线程

2、线程的使用

2.1线程创建

2.1.1继承Thread 进行实现

2.1.2 实现Runnable 接口进行实现

2.1.3 Callable + Futrue 

2.2常见的构造方法

2.3线程的常用属性

2.4 线程的分类

2.4.1 守护线程

2.4.2 用户线程

2.5 线程的常用方法

2.5.1 isAlive()

2.5.2 join()线程等待

2.5.3 interrupt()线程终止

2.5.4 yield 让出CPU的执行权

2.5.5 线程的休眠

3、线程的安全问题※

3.1 抢占式执行  

3.2 多个线程同时修改同一个变量

3.3 操作是非原子性问题

3.4 内存可见性问题

3.5 指令重排序问题



1、认识线程

1.1概念

线程组成了进程,进程是操作系统分配资源的最小单元,而线程是操作系统调度的最小单位,一个线程就是一个执行流,每个线程有自己独立的代码,多个线程之间“同时”执行着多份代码。

1.2为什么要有线程

“并发编程”成为刚需,单核CPU的发展遇到瓶颈,想要提高算力,需要多核CPU(可以简单地理解为多个CPU组成),虽然多进程也能实现“并发编程”,但线程比进程更加轻量:

①创建线程比创建进程更快

②销毁线程比销毁进程更快

③调度线程比调度进程更快

※进程和线程的区别:

①从属关系:进程是由线程组成的,每个进程至少包含一个线程,即主线程

②共享资源的方式:同一个进程的线程之间共享一个内存空间                                                          

③描述侧重点不同:进程是系统分配资源的最小单位,线程是系统调度的最小单位。

④上下文的切换速度不同:线程上下文切换速度较快。

⑤操作对象不同:进程由OS调度操作,线程由程序员编码进行操纵。

※切记 线程也不是越多越好,如果创建的线程太多,可能会导致“狼多肉少”的情况,也就会造成恶意争抢和线程过度调用的问题,反而降低了效率。

2、线程的使用

2.1线程创建

2.1.1继承Thread 进行实现

/**
 * 继承Thread 创建线程
 */
public class ThreadDemo3 {
    public static void main(String[] args) {
        //获得当前的线程
        Thread mainThread = Thread.currentThread();
        System.out.println("线程名称:"+mainThread.getName());
        Thread thread = new MyThread();
        //开启线程
        thread.start();
    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        //具体的业务执行代码
        Thread thread = Thread.currentThread();
        try {
            Thread.sleep(600);//sleep 为Thread类中的静态方法 用类调用
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("线程的名称:"+thread.getName());
    }
}

※ 不常用这种继承的用法,因为java有单继承局限,一旦继承了Thread类就无法继承其他类

2.1.2 实现Runnable 接口进行实现

/**
 * 实现Runnable 接口新建线程
 */
public class ThreadDemo4 {
    public static void main(String[] args) {
        //创建Runnable
        MyThread2 myThread2 = new MyThread2();
        //创建一个线程
        Thread thread = new Thread(myThread2);
        //启动线程
        thread.start();
    }
}
class MyThread2 implements Runnable{

    @Override
    public void run() {
        //具体的业务代码
        Thread thread = Thread.currentThread();//得到当前线程

        System.out.println("线程执行:"+thread.getName());
    }
}

②Runnable接口匿名内部类实现

/**
 * Runnable 匿名内部类来创建
 */
public class ThreadDemo5 {
    public static void main(String[] args) {
        //匿名内部类
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                //业务代码
                Thread t = Thread.currentThread();
                System.out.println("执行任务"+t.getName());
            }
        });
        //启动线程
        thread.start();
    }
}

③ 使用Lambda 来创建 Runnable JDK8 以上推荐此种

/**
 * 使用lambda 来创建 Runnable
 */
public class ThreadDemo6 {
    public static void main(String[] args) {
        Thread thread = new Thread(()->{
            //具体的业务
            Thread t = Thread.currentThread();
            System.out.println("任务执行"+t.getName());
        });
        //启动线程
        thread.start();
    }
}

2.1.3 Callable + Futrue 

以上创建的方式有一个共同的问题,那就是没有返回值,也就是线程执行完成之后,主线程没有办法拿到新的线程执行结果的,所以有了如下方式:

import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class ThreadDemo9 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                //新线程执行的业务代码
                String[] arrs = new String[]{"java","MySQL","Thread"};
                //Random random = new Random();
                //随机返回一个字符串
                String result = arrs[new Random().nextInt(3)];
                System.out.println(Thread.currentThread().getName()+"---字符串"+result);
                return result;
            }
        });
        //创建新线程
        Thread thread = new Thread(futureTask);
        //启动线程
        thread.start();
        String result = futureTask.get();
        System.out.println(Thread.currentThread().getName()+"--新线程的返回值"+result);
    }
}

2.2常见的构造方法

 ①初始化线程名

 

 ②分组

import java.util.Random;

public class ThreadDemo12 {
    public static void main(String[] args) {
        // 1.创建一个线程分组(女子100米比赛)
        ThreadGroup group = new ThreadGroup("thread-group");
        // 2.定义一个公共的任务(线程的任务)
        Runnable runTask = new Runnable() {
            @Override
            public void run() { // 业务(任务)
                // 生成一个 1-3 秒的随机数
                int num = (1 + new Random().nextInt(3));

                // 跑了 n 秒之后到达了终点
                try {
                    Thread.sleep(num * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 得到执行此方法的线程
                Thread t = Thread.currentThread();
                System.out.println(t.getName() + "——选手到达终点:" + num + "s");
            }
        };
        // 3.线程(运动员)
        Thread t1 = new Thread(group, runTask); // 创建选手 1
        Thread t2 = new Thread(group, runTask); // 创建选手 2
        Thread t3 = new Thread(group, runTask); // 创建选手 3
        // 开跑
        t1.start();
        t2.start();
        t3.start();
        // 所有人全部到达重点之后宣布成绩
        while (group.activeCount() != 0) {
        }
        System.out.println("宣布比赛成绩");
    }
}

2.3线程的常用属性

①线程ID

②线程的名称

③线程的状态

 

所有线程的状态:

for(Thread.State item : Thread.State.values()){
        System.out.println(item);
}

※线程状态的转变

线程状态(共计6种)

NEW                 新建状态,当线程被创建,但是未启动之前的状态

RUNNABLE      运行状态  细化为:运行(得到时间片运行中状态)和就绪(未得到时间片就绪状态)两种状态

BLOCKED          阻塞状态(如果遇到锁,线程就会变为阻塞状态等待另一个线程释放锁)

WAITING            无限期等待状态

TIMED_WAITING  有限期等待状态 

TERMINATED        销毁状态,当线程执行结束之后变为此状态。

④线程的优先级

※ 注意事项:同时启动多个线程,多个线程设置了不同的优先级,并不是优先级最高的就一定先执行完之后再执行低优先级的线程,而是高优先级的线程获取到CPU时间片的概率更多,整个的执行大致符合高优先级的线程先执行完。

2.4 线程的分类

2.4.1 守护线程

又称为后台线程,为用户线程服务的,当一个程序中所有的用户线程都结束之后,那么守护线程也会结束,获取当前线程是否为守护线程:thread.isDaemon()

 true = 守护线程  false = 用户线程

2.4.2 用户线程

main 线程 默认为用户线程

 ※结论:

        main线程(主线程)默认为非守护线程

        在用户线程中创建子线程也是用户线程(默认情况下)

        在守护线程中创建c

设置守护线程

 判断守护线程

 注意事项:线程的类型不能在线程的运行期间设置,也就是说线程的设置不能再start之后进行,必须在之前,否则JVM会报错。

守护线程vs用户线程

用户线程在java程序中相当重要,JVM一定要等所有的用户线程执行完之后才能自然结束,而守护线程是为用户线程服务的,所以当所有的用户线程执行完毕后不管守护线程是否在执行,JVM都会退出执行。

2.5 线程的常用方法

2.5.1 isAlive()

2.5.2 join()线程等待

       等待某个线程执行完之后,再执行后续代码

 与while(t.isAlive())相较,写法更加优雅,运行时所用的资源更少,用whileu循环等待的化,程序在一直执行。

2.5.3 interrupt()线程终止

a)使用自定义标识符来终止线程

b)使用interrupt()终止线程

配合 Thread.interrupted()  或者 Thread.currentThread().isInterrupted() 来使用

/**
 * 使用 interrupt 终止线程
 */
public class ThreadInterrupet2 {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println("终止标志位:" +
                    Thread.currentThread().isInterrupted());
            while (!Thread.interrupted()) {
//            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("正在转账...");
//                try {
//                    Thread.sleep(1000);
//                } catch (InterruptedException e) {
                    e.printStackTrace();
//                    break;
//                }
            }
            System.out.println("啊?险些误了大事!");
//            System.out.println("终止标志位2:" + Thread.currentThread().isInterrupted());
//            System.out.println("终止标志位2:" + Thread.currentThread().isInterrupted());
//            System.out.println("终止标志位2:" + Thread.currentThread().isInterrupted());
//            System.out.println();
            System.out.println("终止标志位4:" + Thread.interrupted());
            System.out.println("终止标志位4:" + Thread.interrupted());
            System.out.println("终止标志位4:" + Thread.interrupted());
        });
        // 启动线程
        thread.start();

        Thread.sleep(100);


        // 终止线程
        thread.interrupt();

        System.out.println("有内鬼,终止交易!");

    }
}

※isInterrupted 和 Interrupted 的区别:

① interrupted 属于静态方法,所有程序都可以直接调用的全局方法;而isInterrupted 属于某个实例的方法,需要用实例来调用

②interrupted 在使用之后会重置标识符,而isInterrupted不会重置标识符

2.5.4 yield 让出CPU的执行权

 yield 方法会让出CPU的执行权,让线程调度器重新调度线程,但还是有一定的机率再一次调用到出让CPU的线程上的,这一次它就会继续往下执行,因为yield 已经执行过了(不会再次yield)。

2.5.5 线程的休眠

        ①使用sleep

        ②使用TimeUnit

 使用TimeUnit 可以更便捷,不用去计算,而使用sleep,单位只能是毫秒,如果需要休眠较长的时间单位还需要计算。

3、线程的安全问题※

程序在多线程的执行环境下,程序的执行结果不符合预期。

线程安全问题导致的原因:

3.1 抢占式执行  

3.2 多个线程同时修改同一个变量

//预期的返回结果为0,一个进行++,一个进行--,
但如果开启多线程,最后返回的不为预期结果,
这时可以采取一下措施:1、两个线程分割开来,
一个执行完毕后再开启另一个;2、同时开启两个线程,
但这两个线程独立操作自己的变量。


public class ThreadDemo16 {
    static class Counter {
        // 变量
        private int number = 0;

        // 循环次数
        private int MAX_COUNT = 0;

        public Counter(int MAX_COUNT) {
            this.MAX_COUNT = MAX_COUNT;
        }

        // ++ 方法
        public int incr() {
            int temp = 0;
            for (int i = 0; i < MAX_COUNT; i++) {
                temp++;
            }
            return temp;
        }

        // -- 方法
        public int decr() {
            int temp = 0;
            for (int i = 0; i < MAX_COUNT; i++) {
                temp--;
            }
            return temp;
        }

        public int getNumber() {
            return number;
        }
    }

    static int num1 = 0;
    static int num2 = 0;

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

        Counter counter = new Counter(100000);

        Thread t1 = new Thread(() -> {
            // ++ 操作
            num1 = counter.incr();
        });


        Thread t2 = new Thread(() -> {
            // -- 操作
            num2 = counter.decr();
        });
        // 启动多线程进行执行
        t2.start();
        t1.start();

        // 等待两个线程执行完
        t1.join();
        t2.join();
        // 打印结果
        System.out.println("最终结果:" + (num1 + num2));
    }
}

3.3 操作是非原子性问题

非原子性问题就是可拆分,不是一部操作,比如++/-- 操作:

 

 线程前两步的加载和数据更新都正常进行,到了第三步写回CPU时,由于线程一得到了时间片,而线程二没得到时间片,因此线程一先将-1写进CPU,而稍后线程二得到时间片,继续执行,此时再写入CPU就覆盖了原先的-1的值,使结果变成了1,与预期结果不符。

3.4 内存可见性问题

可见性:一个线程对共享变量值的修改,能够被另一个线程看到。

※前置知识 java 内存模型(JMM):java虚拟机规范中定义了Java内存模型,⽬的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到⼀致的并发效果。

 线程之间共享主内存,每一个线程都有自己的工作内存,当线程需要读取一个共享变量时,需要从主内存拷贝一份到自己的工作内存,进行更新后再写入主内存,但主内存中的数据不可见,所以可能导致线程一已经修改了主内存中的数据,而线程二不知道,又对其进行了修改。(与③很像)

3.5 指令重排序问题

JVM有一套自己的最优执行顺序(编译器优化),然后将写的代码顺序打乱,按自己的执行顺序来,导致与预期不符。

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值