java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java多线程编程

Java多线程编程:线程的原理和使用安全

作者:jjjava2.0

文章主要介绍了线程的基本概念、线程和进程的区别、线程的创建方法、线程的常见方法与属性、线程的安全问题等,线程是程序执行流的最小单位,可以并发执行并共享数据,线程的生命周期状态包括新建、就绪、运行、阻塞、死亡等

一,认识线程

1.1 什么是线程

        一个线程就是一个“执行流”,每个线程之间都可以按照顺序执行自己的代码,多个线程之间“同时”执行着多份代码。同时,线程也是 CPU 真正执行任务的单位。

举个栗子:去奶茶店买奶茶(奶茶店中有自己独立的“程序”让我们拿到奶茶,所以也可把这个看为一个线程),奶茶店有不同岗位员工,每个员工都是独立的“执行流”。如果店里只有一个员工(单线程),那速度将会很慢效率极低;多个员工分工合作(多线程),那效率会得到大幅提高,这就是多线程的优势。

2.2 线程和进程的区别

进程包含线程的每个进程至少有一个线程存在(主线程)

线程(Thread)进程
线程时cpu上调度执行的基本单位进程是操作系统中资源分配的基本单位
资源共享进程内存独立内存,互不影响
开销轻量,快速(只有第一个线程也就是和进程一起创建时,需要申请资源,后续创建则不需要)创建/销毁慢,消耗资源
奔溃影响一个崩溃,可能把同进程内的线程都带崩溃一个崩溃不影响其他

二,创建线程

2.1 继承Thread类,重写run方法

class Thread1 extends Thread{
    public void run(){
        System.out.println("线程运行代码");
    }
}

//创建实例
    Thread1 t = new Thread(); 

2.2 实现Runnable接口,重写run方法

clas Runnable1 implements Runnable{
    public void run(){
        System.out.println("线程运行代码");
    }
}
//创建实例
    Thread t = new Thread(new Runnable());

2.3  匿名内部类(本质就是方法1,2)

        2.3.1 用匿名内部类创建Tread子类对象

Thread t1 = new Thread(){
    public void run(){
        System.out.println("使用匿名类创建Tread子类对象");
    }
}

        2.3.2 用匿名内部类创建Runnable子类对象

Thread t2 = new Thread(new Runnable(){
    public void run(){
        System.out.println("使用匿名类创建Runnable子类对象");
    }
});

这两个线程对象(t1t2)的 start() 方法都只能调用一次,所以从 “启动线程” 的角度来说,它们是一次性的;但第二种写法里的 Runnable 任务逻辑,是可以被复用的。

2.4 用lambda表达式(常用)

 //使用lambda 表达式创建Runnable 子类对象
Thread t3 = new Thread (() -> System.out.println("使用匿名类创建Thread 子类对象"));


Thread t4 = new Thread (() -> {
     System.out.println("使用匿名类创建Thread 子类对象");
 });

2.5 实现Callable

// 用匿名内部类创建Callable对象
Callable<String> callableTask = new Callable<String>() {
    public String call() throws Exception {
        return "Callable任务执行完成,返回结果";
    }
};

// 包装成FutureTask,再交给Thread
FutureTask<String> futureTask = new FutureTask<>(callableTask);
Thread t5 = new Thread(futureTask);

2.6 线程池(ThreadFactory)

// 用匿名内部类创建ThreadFactory
ThreadFactory factory = new ThreadFactory() {
    private int count = 0;
    public Thread newThread(Runnable r) {
        // 自定义线程名,方便调试
        return new Thread(r, "自定义线程-" + (++count));
    }
};

// 创建固定大小线程池
ExecutorService pool = Executors.newFixedThreadPool(3, factory);

// 提交任务(用匿名内部类Runnable)
pool.submit(new Runnable() {
    public void run() {
        System.out.println("线程池中的任务执行");
    }
});

// 关闭线程池
pool.shutdown();

三,Thread类

2.1 常见构造方法

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

2.2 常见属性

属性获取方法

说明

IDgetID( )ID是线程的唯一标识,不同的线程不会重复
名称getName( )名称在各种调试工具用到
状态getState( )表示线程当前所处的一个情况
优先级getPriority( )优先级高的线程理论上来说更容易被调度到
是否后台线程isDaemon( )JVM会在一个进程的所有非后台线程结束后,才会结束运行
是否存活isAlive( )简单的理解,为run 方法是否运行结束了
是否被中断isInterrupted( ) 检查线程是否收到中断信号

四,线程相关操作

4.1 启动线程 start(  )

         上面我们介绍了覆写run( )方法创建一个线程对象,但线程对象被创建出来并不意味着线程已经开始运行了,而调用start( )方法,才真的在操作系统的底层创建出一个线程,线程才真正的独立执行

举个栗子:我(主线程)奶茶店店长,我先写了一份完整的“工作流程”(new Thread()),我在纸上写下了做奶茶的步骤(run( ) 方法),这是我自己按照流程做一杯(调用run( ),并不会启动新的线程)。然后我画了个店员的名牌,写着 “店员 A”(创建了线程对象),此时店员A开始按步骤工作(调用start( )),这是店里就我和店员A同时干活,效率提升。

4.2 中断线程

举个栗子:我(主线程)安排店员A(子线程)去做奶茶,3秒之后,我告诉店员A“先不做奶茶了,过来点单”,店员A立刻停下做奶茶,去点单。

 常见的有以下两种方法:

a)通过共享的标记来进行沟通,使用自定义变量作为标志位(volatile

b)调用interrupt(方法来通知)

代码实例:

//采用  a)方法

public class demo {
    private static class MyRunnable implements Runnable{
        //volatile相当于一个主线程和子线程公用的开关
        public volatile boolean isQuit=false;
        @Override
        public void run() {
            while(!isQuit){
        //如果没有收到停止指令,循环一直满足,开始做奶茶
                System.out.println(Thread.currentThread().getName()+":我正在忙着做奶茶");
                try{
        //做一杯奶茶需要1s,也就是1s打印一次
                    Thread.sleep(1000);//这里把光标放在sleep下用alt+enter快捷键可以直接生成try,catch
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        //不进入while循环也就是收到停止指令时
            System.out.println(Thread.currentThread().getName()+":我现在来帮忙点单");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable target=new MyRunnable();
        Thread thread=new Thread(target,"店员A");
        System.out.println(Thread.currentThread().getName()+":A,快去做奶茶");
        //让A开始工作
        thread.start();
        //我去做3秒的事,a接着做奶茶每秒打印一次
        Thread.sleep(3000);
        //sleep结束,我叫a停止做奶茶
        System.out.println(Thread.currentThread().getName()+":A先别做奶茶了,快过来帮忙点单");
        target.isQuit=true;
    }
}

volatile就是保证:主线程改了isQuit,子线程可以立刻获取。其核心就是修改了内存可见性问题(内存可见性将下面线程安全产生原因中进行详细讲解)

//采用   b)方法

public class demo2 {
    private static class MyRunnable implements Runnable{
        @Override
        public void run() {
            //while(!Thread.isterrupted())
            while (!Thread.currentThread().isInterrupted()){
                System.out.println(Thread.currentThread().getName()+":我正忙着做奶茶");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //收到中断信号,处理异常
                    e.printStackTrace();
                    System.out.println(Thread.currentThread().getName()+":收到通知,停下手中的活");
                    //需着重注意这里的break,跳出循环,结束当前任务
                    break;
                }
            }
            //循环结束,执行中断后的收尾工作
            System.out.println(Thread.currentThread().getName()+":我现在过来点单");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable target=new MyRunnable();
        Thread thread=new Thread(target,"A");
        System.out.println(Thread.currentThread().getName()+":快去做奶茶");
        thread.start();
        Thread.sleep(5000);
        System.out.println(Thread.currentThread().getName()+":A先别做奶茶了过来点单");
        thread.interrupt();
    }
}
方法类型作用是否清除中断标志
Thread.currentThread().isInterrupted()实例方法检查当前线程的中断状态❌ 不清除
Thread.interrupted()静态方法检查当前线程的中断状态✅ 会清除(调用后标志位变为false

使用Thread.interrupted( )方法来通知线程结束Thread.currentThread( ).isInterrupted( )代替自定义标志位

在这里thread收到通知有两种方法

1.如果线程因为调用wait/join/sleep等方法而阻塞挂起,则以InterruptedException 异常的形式通知,清除中断标志,这个时候可以在catch中加入break;

2.只是内部的一个中断标志被设置,thread 可以通过Thread.currentThread().isInterrupted()判断指定线程的中断标志被设置,不清除中断标志这种方式通知收到的更及时,即使线程正在sleep 也可以马上收到。

4.3 等待线程 join(  )

join可以要求多个线程间结束的线程顺序

t.join( )就是等t线程结束在完成别的,只要t不结束就一直等

方法说明
public void join()等待线程结束
public void join(long millis)等待线程结束,最多等millis毫秒
public void join(long millis,int nanos)等待线程结束,精度更高
public class demo3 {
    public static void main(String[] args) throws InterruptedException {
        Runnable target=()->{
         for(int i=0;i<3;i++){
                try {
                    System.out.println(Thread.currentThread().getName()+ ":我还在工作!");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ":我结束了!");
        };
        Thread thread1=new Thread(target,"A");
        Thread thread2=new Thread(target,"B");
        System.out.println("先让A开始工作");
        thread1.start();
        thread1.join();
        System.out.println("A工作结束,B开始工作");
        thread2.start();
        thread2.join();
        System.out.println("B工作结束");
    }
}

这里的抛异常也不需要手动去写,alt+enter快捷键即可

4.4 获取当前线程引用

已经很熟悉了就不再赘述了

public static Thread currentThread( );  //返回当前线程对象的引用

4.5 休眠线程 sleep

因为线程的调度是不可控的,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间

方法说明
public static void sleep(long millis)throws InterruptedException休眠当前线程millis毫秒
public static void sleep(long millis,int nanos)throws InterruptedException更高精度的休眠

这里的抛异常也不需要手动去写,alt+enter快捷键即可

五,线程状态

查看所有线程状态代码:

public class demo4 {
    public static void main(String[] args) {
        for (Thread.State state:Thread.State.values()) {
            System.out.println(state);
        }
    }
}

代码运行结果:

NEW:安排了工作,还未开始行动

RUNNABLE:可工作的.又可以分成正在工作中和即将开始工作.(就绪:线程在cpu上执行;线程随时可以去cpu上执行)

BLOCKED:表示排队等着其他事情(由于锁导致的阻塞)

WAITING:表示排队等着其他事情(死等,无超时时间阻塞等待)

TIMED_WAITING:表示排队等着其他事情(线程阻塞,不参与cpu调度,不继续执行,阻塞时间是有上限的)

TERMINATED:工作完成了,内核中的线程已结束,但Thread对象还在

六,线程安全

6.1 什么是线程安全

一段代码,如果多线程环境下代码运行的结果是符合预期的(在单线程环境应该的结果),则说这个程序是线程安全的。

6.2 线程安全问题产生原因

1)操作系统对于线程的调度使抢占式随即执行的(根本原因)

2)多个线程同时修改同一个变量

3)修改操作不是原子的(如果修改操作只是对应一个cpu指令,就认为是原子的)

4)内存可见性问题(内存可见性:即一个线程对共享变量值的修改能够及时的被其他线程看见)

5)指令重排序问题

七,小结

文章主要介绍了线程的基本概念、线程和进程的区别、线程的创建方法、线程的常见方法与属性、线程的安全问题题,其中,线线程是CPU执行任务的单位user补充一下,线程是程序的基本单位,线程是程序执行流的最小单位;线程之间可以并发执行,并共享数据,一个程序至少有一个线程;线;线程的生命周期状态包括新建、就绪、运行、阻塞、死亡等;线程间通信通过共享变量、信号量等互斥量等进行;线程的安全性则关键性是否变量的可见性;线程的优先级与影响线程的调度;线程池可以复用管理线程,减少线程创建销毁的开;创建线程池的方法包括自定义实现的线程池常使用的的线程池类;线程的常用方法包括is、join、sleep,interrupt等;线程的状态包括NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINMIN,;线线程安全问题主要由操作系统调度引起的、线程间共享变量的可见引起的、非原子操作引起的、指令重排序引起的。

到此这篇关于Java多线程编程:线程的原理和使用安全的文章就介绍到这了,更多相关Java多线程编程内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文