线程,多线程

本文详细介绍了线程的基本概念、多线程的优势、创建线程的方法(继承Thread类和实现Runnable接口)、线程的启动与停止、线程安全、死锁预防、生产者消费者模型、ThreadLocal和join()方法的应用,以及原子操作的相关知识。
摘要由CSDN通过智能技术生成

线程

什么是线程,什么是多线程

  • 线程简单理解可以理解为我们工作中的一个个任务,而多线程是指在程序中同时执行了两个或两个以上线程,也可以理解为在工作中同时执行多个任务,比如单线程在餐厅里面一次只能处理一个客人的点菜、上菜、买单等操作就得等上一个客人服务完后再去服务下一个客人,而多线程的情况下就可以将任务分给不同的服务员,一个负责点单,一个负责上菜,一个负责买单,大大的提高了工作效率,客人的体验感也好。

多线程的好处是什么

  • 多线程的好处首先就是可以提高程序的响应速度,同时可以处理多个任务,提高用户使用体验

熟悉多线程之前我们先来了解一下线程

创建线程的方式

  • 创建线程有两种方式,第一种是继承Thread类,重写run()方法来定义线程的执行逻辑,另一种是
    实现Runnable接口,也是实现run()方法来定义线程的执行逻辑,然后通过创建当前类的实例,将它作为参数传递给Thread类来创建新的线程,下面我们一起来看一下

  • 通过Thread类来创建线程

public class Test extends Thread {

    @Override
    public void run() {
        System.out.println("这是子线程");
        System.out.println(Thread.currentThread());
    }

    public static void main(String[] args) {
        Test thread = new Test();
        thread.start();
    }
    // 打印结果:这是子线程,Thread[Thread-0,5,main]
}

在这个例子中,我们创建了一个继承自Thread类的Test子类。在run()方法中,我们输出一条消息表示该线程正在运行。在main()方法中,我们创建了一个Test实例,并通过调用start()方法来启动线程。

  • 通过Runnable来创建线程
public class Test2 implements Runnable{

    @Override
    public void run() {
        System.out.println("这是子线程");
    }

    public static void main(String[] args) {
        Test2 test2 = new Test2();
        Thread thread = new Thread(test2);
        thread.start();
    }
}

在这个例子中,我们创建了一个实现了Runnable接口的Test2类。在run()方法中,我们输出一条消息表示该线程正在运行。在main()方法中,我们创建了一个Test2实例,并将其传递给Thread构造函数来创建一个Thread对象。然后调用thread.start()方法来启动线程。

通过Runnable接口的方式创建线程的方式更灵活,因为在Java中只能继承一个父类,但可以实现多个接口,如果当前类以及继承其它类了就没法再通过继承Thread的方式来创建线程了

线程的启动方式,区别是什么

  • 线程有2种启动方式,一种是通过Thread类调用它的start()方法来启动线程,第二种是使用线程池提交一个Runnable任务,Thread调用start()
    传一个Runnable的接口也可以,下面我们来看一下示例,鉴于上面以及有写过Thread调用start()方法来启动线程,这里就不重复演示了,我们就看一下线程池是怎么启动的。
    public static void main(String[] args) throws InterruptedException {
        // 创建一个固定大小为3的线程池
        ExecutorService executor = Executors.newFixedThreadPool(3);

        // 提交任务给线程池执行
        for (int i = 1; i <= 5; i++) {
            final int index = i;
            Thread.sleep(1000);
            executor.execute(new Runnable() {
                public void run() {
                    System.out.println("线程名"+Thread.currentThread().getName()+" "+index);
                }
            });
        }
        // 关闭线程池
        executor.shutdown();
    }
    //打印内容:
    线程名pool-1-thread-1 1
    线程名pool-1-thread-2 2
    线程名pool-1-thread-3 3
    线程名pool-1-thread-1 4
    线程名pool-1-thread-2 5
}

可以看到我们使用了一个固定大小为3的线程池,使用execute方法向线程池提交了5个任务。每个任务都是一个Runnable对象,在任务执行时就会调用run方法去执行,我们可以看到由于线程池大小为3,所以同时最多有3个任务会被执行,而剩余的任务会等待空闲线程。

线程的停止方式有哪些

  • 线程的停止方式有中断法,标记法,还可以通过stop来结束掉,不过使用stop有可能会让数据出现不安全的问题,还可能会出现死锁的现象,比如我stop掉了,但锁还没有释放,那其它线程就会一直等待造成死锁,还有正在运行的时候stop掉了但是后续还有逻辑都没有执行,就可能会导致数据出现不正常和脏数据的结果。

  • 而标记法也就是是通过设置一个boolean类型的标记,通过在循环中判断标志位的值是true还是false来进行退出

  • 中断法主要的是有三个方法,interrupt(),isInterrupt()和interrupted(),但是注意interrupt()并不能真正的中断线程,需要和isInterrupt配合来响应中断线程,而interrupted()则是清除线程的中断状态的。

线程的生命周期

  • 新建:从创建一个线程出来到start()这个线程之间的状态

  • 就绪:当调用start()方法启动线程后就进入了就绪状态,等待获取cpu资源来运行

  • 运行:当获取到了cpu资源后线程就进入了运行状态,在这个状态中可能会发生阻塞,如果发生了阻塞就会同时释放掉cpu的资源

  • 阻塞:阻塞状态的线程在被唤醒过后会重新回到就绪状态再次等待cpu分配资源重新执行

  • 销毁:当线程正常执行完,或被强制结束,出现异常导致结束都会进入这个状态,线程会被销毁掉释放资源

什么是线程安全,有哪些方法可以保证线程安全

  • 首先出现线程安全问题是因为在多线程的情况下,因为线程之间的执行是没有规律的,当多个线程访问一个共同的数据进行读写时导致最终结果不正确所出现的问题,而避免这种问题实现线程安全的方式可以使用volatile关键字对这个公共变量进行修饰,保证不同线程对这个变量进行操作时的一致可见性,还有使用锁机制synchronized,在线程访问某个数据时进行加锁操作,这个时候其它线程不能进行访问,只能等该线程读取完成并释放锁后其它线程才能继续访问。

线程死锁的原因,如何避免线程死锁

  • 线程死锁就是在多线程的情况下两个或两个以上的线程在等待其它锁对象的过程中发现其它锁对象也在等自己没有四方的锁对象,于是出现了相互都在等待对释放锁对象,这种现象就叫做死锁,我们来看一下死锁的例子
public class Test5 {

    public static Object obj1 = new Object();
    public static Object obj2 = new Object();

    public static void main(String[] args) {

        new Thread(() -> {
            synchronized (obj1) {
                System.out.println("线程1持有obj1");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj2) {
                    System.out.println("线程1持有obj2");
                }
            }
        }).start();

        new Thread(() -> {
            synchronized (obj2) {
                System.out.println("线程2持有obj2");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj1) {
                    System.out.println("线程2持有obj1");
                }
            }
        }).start();
    }
    //运行结果:线程1持有obj1,线程2持有obj2
}

可以看到这两个线程线程1先获取了obj1的锁,然后尝试获取obj2的锁;而线程2先获取了obj2的锁,然后尝试获取obj1的锁。由于两个线程之间存在相互依赖,即在持有一个锁的同时试图获取另一个锁,互相等待对方释放锁,最终导致了死锁的发生。而形成线程死锁有四个条件,互斥条件,不可剥夺条件,请求并持有条件,环路等待条件,我们只需要改变其中一个就可以解决死锁的问题了。

生产者消费者模型

  • 生产者消费者主要是包含了两类线程,一类是生产者线程,用于生产数据,一类是消费者用来消费数据,为了解耦生产者和消费者的关系,还会有一个共享的数据缓冲区域,当生产者生产了数据就放入共享区域类,消费者则从共享区域类取数据就行了,当共享数据区装满的状态下就会阻塞生产者继续生产数据到里面,当数据区为空的时候就会阻塞消费者继续消费,生产者消费者主要用来多线程之间的并发协作的功能,确保数据的正确性和线程安全性,想实现生产者消费者问题可以采用三种方式:1. 使用Object的waut/notify的消息通知机制,2. 使用Lock的Condition的await/signal的消息通知机制,3. 使用BlockingQueue实现。下面我们来通过代码认识一下
public class Test7 {
    //涉及 线程安全  线程通信
    public static void main(String[] args) {
        Provider provider = new Provider();
        Consume consume = new Consume();
        DataSource dataSource = new DataSource();
        provider.dataSource = dataSource;
        consume.dataSource = dataSource;
        //运行
        provider.start();
        consume.start();


    }

    /*
     *生产者
     */
    static class Provider extends Thread {

        public DataSource dataSource;

        @Override
        public void run() {
            super.run();
            for (int i = 0; i < 100; i++) {
                //生产数据
                dataSource.add("包子:" + i);
            }
        }
    }

    /*
     *消费者
     */
    static class Consume extends Thread {

        public DataSource dataSource;

        @Override
        public void run() {
            super.run();
            for (int i = 0; i < 100; i++) {
                //消费数据
                String data = dataSource.getData();
            }
        }
    }

    /*
     *数据源
     */
    static class DataSource {
        private List<String> stringList = new ArrayList<>();

        public synchronized void add(String data) {
            while (!isNull()) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("生产数据:" + data);
            stringList.add(data);//生产往里存放数据
            notifyAll();
        }

        public boolean isNull() {
            return stringList.size() == 0;
        }

        public synchronized String getData() {
            String data = null;
            while (isNull()) {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            data = stringList.get(0);
            stringList.remove(0);
            System.out.println("拿到了消费数据没有:" + data);
            notifyAll();
            return data;
        }
    }
}

可以看到生产者线程通过循环向数据源中添加数据,每次添加都会检查数据源是否为空。如果数据源不为空,则生产者线程进入等待状态,直到有消费者线程取走数据。当数据源为空时,生产者线程将生成的数据添加到数据源中,并唤醒等待的线程。消费者也是通过循环从数据源中获取数据,如果数据源为空,则消费者线程进入等待状态,直到生产者线程添加新的数据,当数据源不为空时,消费者线程从数据源中取出数据并删除,然后唤醒所有等待的线程

线程的通信方式有哪些

  • 线程的通信方式有轮循,handler,wait,notify,notifyAll,Synchronized,Locklai来进行通信,但在业务中我们要根据不同的场景来选择适合的方式

wait和sleep的区别

  • wait方法必须配合synchronized一起使用,sleep可以单独使用

  • sleep方法必须要传递一个时间的参数,过了这个时间之后,线程会自动唤醒, wait方法需要通过notify()或notifyAll()来唤醒

  • sleep没有释放锁对象,wait有释放锁对象,我们来验证一下

来看看sleep:

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

        Object lock = new Object();

        new Thread(()->{
            synchronized (lock){
                System.out.println("新线程获取到锁:"+ LocalDateTime.now());
                try {
                    Thread.sleep(2000); System.out.println("新线程释放锁:"+ LocalDateTime.now());
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();
        //等待线程先获得锁
        Thread.sleep(200);
        System.out.println("主线程尝试获取锁:"+ LocalDateTime.now());
        // 在新线程休眠之后尝试获取锁
        synchronized (lock){
            System.out.println("主线程获取到锁:"+ LocalDateTime.now());
        }
    }
    打印结果:
    新线程获取到锁:2023-09-26T16:22:42.446508400
    主线程尝试获取锁:2023-09-26T16:22:42.636999900
    新线程释放锁:2023-09-26T16:22:44.459139100
    主线程获取到锁:2023-09-26T16:22:44.459139100
}

可以看到在调用了sleep之后,在主线程里尝试获取锁却没有成功,只有 sleep 执行完之后释放了锁,主线程才正常的得到了锁,这说明 sleep 在休眠时并不会释放锁。

再来看看wait:

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

        Object lock = new Object();

        new Thread(()->{
            synchronized (lock){
                System.out.println("新线程获取到锁:"+ LocalDateTime.now());
                try {
                    lock.wait(2000);
                    System.out.println("新线程释放锁:"+ LocalDateTime.now());
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();
        //等待线程先获得锁
        Thread.sleep(200);
        System.out.println("主线程尝试获取锁:"+ LocalDateTime.now());
        // 在新线程休眠之后尝试获取锁
        synchronized (lock){
            System.out.println("主线程获取到锁:"+ LocalDateTime.now());
        }
    }
    //打印结果:
    新线程获取到锁:2023-09-26T16:25:37.457730500
    主线程尝试获取锁:2023-09-26T16:25:37.650216900
    主线程获取到锁:2023-09-26T16:25:37.650216900
    新线程释放锁:2023-09-26T16:25:39.473228500
}

可以看到当调用了wait之后,主线程立马尝试获取锁成功了,这就说明wait休眠时是释放锁了的。

线程阻塞的原因

  • 调用了sleep方法会时线程在指定的时间间隔内暂停执行

  • 调用了wait方法会释放掉锁,进入阻塞状态等待notify()或notifyAll()唤醒

  • 当前线程想获取锁对象,但有其它线程正持有锁的时候就在阻塞状态

  • 一个线程调用另一个线程的join()方法时,它会被阻塞直到被等待线程执行完毕

Synchronized在静态方法和非静态方法上锁的对象分别是什么

  • 在静态方法上使用synchronized关键字锁的是当前类对应的字节码对象,在非静态方法上锁的是当前对象

volatile是如何使用的,是什么原理

  • volatile关键字使变量的值能够被多个线程及时读取,确保变量的值再多个线程之间保持一致,避免脏读的可能,简单描述就是volatile修饰的变量的值发生改变时其他线程能够立即看到最新的值,volatile的原理其实是通过内存屏障来实现的,会在写操作前面插入一个StoreStore屏障,确保前面的写操作完成并对其他处理器可见,才能继续后面的写入,其他线程在读取该volatile变量时,能够看到最新的值进行操作,但是需要注意volatile关键字提供了顺序性和可见性,但并不能保证原子性,如果需要保证操作的原子性,仍然需要使用其他的同步机制,比如synchronized

解释一下Synchronized和Lock的区别

  • Synchronized是一种同步锁,用来修饰代码块,实例方法,静态方法,它可以保证被修饰的代码块或者方法在任意时刻只能被一个线程访问,之后在该线程执行完释放锁后才能诶其它线程访问。

  • Lock实现了比Synchronized更加广泛的锁操作,Lock可以实现公平锁、可重入锁、可中断锁等,可以根据具体需求选择Lock的不同实现类来满足特定的需求

  • Synchronized一次只能锁定整个方法或代码块,这样其它的线程将被阻塞,而Lock可以在代码中的特定位置上锁或者同时锁定多个对象的不同部分,还有Synchronized会一直等待锁的释放,无法中断,而Lock它提供了更好的可中断性,可以使用lockInterruptibly()方法来获取锁,在等待期间响应中断请求,从而提高了代码的灵活性

线程局部变量ThreadLocal 是什么?

  • ThreadLocal可以在不同的线程中让同一个变量具有不同的值,也可以说是每个线程都持有一个自己的变量,我们来看一下使用方式
public class Test8 {

    private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            threadLocal.set("早上");
            System.out.println("thread1: " + threadLocal.get());
        });

        Thread thread2 = new Thread(() -> {
            threadLocal.set("晚上");
            System.out.println("thread2: " + threadLocal.get());
        });

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
    }
    打印结果:thread1: 早上
              thread2: 晚上
}

在这个实例中定义了一个 ThreadLocal 对象,然后创建了两个线程,在一个线程中将 threadLocal 的值设置为 value1,在第二个线程中将threadLocal的值设置为value2,由于每个线程都有自己独立的threadLocal实例,所以它们之间不会互相干扰,每个线程获取到的值都是各自的线程局部变量值。

什么是线程的join()方法?它的作用是什么?

  • 当一个线程调用另一个线程的join()方法时,它会暂停自己的执行,直到被调用的线程执行完毕才能继续执行,比如同事之前一起工作,我先完成了,但是我的同事还没有完成,那我就等他完成之后再一起提交上去。这样的过程就像调用了join()方法一样,它会先暂停(阻塞)我的工作,直到我的同事完成之后才行,总之join()可以帮助我们实现多线程间的协作和同步。

什么是原子操作?Java中有哪些原子操作类?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值