并发编程 (线程)

我们都知道现在硬件水平越来越强,执行速度也越来越快。为充分利用CPU资源,我们可以通过多线程来执行分片任务提升整体的执行性能。

如何创建线程#

在Java中通过new Thread().start()方法来开启一个线程,实际是调用系统层的native方法

private native void start0();

Java使用线程常用方式有四种:

1、继承Thread类,Thread 类本质上是实现了 Runnable 接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过 Thread 类的 start()实例方法

2、实现Runnable接口,如果自己的类已经 extends 另一个类,就无法直接 extends Thread,此时,可以实现一个Runnable接口

3、实现Callable接口,重写call()方法,可以返回一个 Future类型的返回值。

4、通过线程池来使用线程,将任务交给线程池执行

public class CreateThreadDemo {

	public static void main(String[] args){
		// 单继承
        new ThreadDemo().start();
        
        // 实现runnable
        new Thread(new RunnableDemo()).start();
        
        // futureTask获取返回值
        FutureTask futureTask = new FutureTask(new CallableDemo());
        new Thread(futureTask).start();
        //get()方法获取返回值
        System.out.println(futureTask.get());
	}
	
    // 缺点:单继承
    public static class ThreadDemo extends Thread {
        @Override
        public void run() {
            System.out.println("ThreadDemo:" + Thread.currentThread());
        }
    }
    
	// 不用担心单继承,但没有返回值
	public static class RunnableDemo implements Runnable {

		@Override
		public void run() {
			System.out.println("RunnableDemo:" + Thread.currentThread());
		}
	}
    
    // 
    public static class CallableDemo implements Callable<String> {
        @Override
        public String call() throws Exception {
            System.out.println("CallableDemo:" + Thread.currentThread());
            return "return CallableDemo:" + Thread.currentThread();
        }
    }
}

本质上都是调用Thread类的start0()方法来启用

线程的生命周期#

1、新建状态(NEW) 当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配 内存,并初始化其成员变量的值

2、就绪状态(RUNNABLE):当线程对象调用了 start()方法之后,该线程处于就绪状态。Java 虚拟机会为其创建方法调用栈和 程序计数器,等待调度运行。

3、运行状态(RUNNING):如果处于就绪状态的线程获得了CPU,开始执行 run()方法的线程执行体,则该线程处于运行状态。

4、阻塞状态(BLOCKED):阻塞状态是指线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。 直到线程进入可运行(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状 态。
阻塞的情况分三种: 等待阻塞(o.wait->等待对列): 运行(running)的线程执行 o.wait()方法,JVM 会把该线程放入等待队列(waitting queue) 中。 同步阻塞(lock->锁池) 运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线 程放入锁池(lock pool)中。 其他阻塞(sleep/join) 运行(running)的线程执行 Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时, JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入可运行(runnable)状态。

5、线程死亡(DEAD) 线程会以下面三种方式结束,结束后就是死亡状态。
正常结束 1. run()或 call()方法执行完成,线程正常结束。 异常结束 2. 线程抛出一个未捕获的 Exception 或 Error。 调用 stop 3. 直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用

线程的6种状态

1、初始(NEW):新创建了一个线程对象,但还没有调用start()方法。

2、运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。

3、阻塞(BLOCKED):表示线程阻塞于锁。

4、等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

5、超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

6、终止(TERMINATED):表示该线程已经执行完毕。

如何关闭线程#

1、设置退出标志,使线程正常退出

public class ThreadSafe extends Thread {
    public volatile boolean exit = false;
    public void run() {
        while (!exit){
            //do something
        }
    }
}

定义了一个退出标志,在定义exit时,使用了一个 Java 关键字 volatile,这个关键字的目的是使 exit 同步,也就是说在同一时刻只能由一个线程来修改 exit 的值。

2、使用interrupt()方法中断线程,在线程中使用isInterrupted()来判断是否存在中断标志

  • 线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时, 会使线程处于阻塞状态。当调用线程的 interrupt()方法时,会抛出 InterruptException 异常。 阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让 我们有机会结束这个线程的执行。通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的,一定要先捕获 InterruptedException 异常之后通过break来跳出循环,才能正常结束run方法。

  • 线程未处于阻塞状态:使用isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置true,和使用自定义的标志来控制循环是一样的道理。

public class ThreadSafe extends Thread {
    public void run() {
        while (!isInterrupted()){ //非阻塞过程中通过判断中断标志来退出
            try{
                Thread.sleep(5*1000);//阻塞过程捕获中断异常来退出
            }catch(InterruptedException e){
                e.printStackTrace();
                break;//捕获到异常之后,执行 break 跳出循环
            }
        }
    }
}

3、使用stop方法强行终止线程

程序中可以直接使用 thread.stop()来强行终止线程,安全主要是:thread.stop()调用之后,创建子线程的线程就会抛出ThreadDeatherror的错误,并且会释放子线程所持有的所有锁。一般任何进行加锁的代码块,都是为了保护数据的一致性,如果在调用thread.stop()后导致了该线程所持有的所有锁的突然释放(不可控制),那么被保护数据就有可能呈现不一致性,其他线程在使用这些被破坏的数据时,有可能导致一些很奇怪的应用程序错误

sleep()与wait()区别#

1、对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的

2、sleep()方法是Thread类的静态方法,可以直接通过Thread.sleep()调用。它使当前线程暂停执行一段时间,让出CPU给其他线程,但是不会释放对象的锁。

3、在调用sleep()方法的过程中,线程不会释放对象锁。而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。

4、sleep()方法可以在任何地方调用,而wait()方法只能在同步方法或同步代码块中调用。sleep()是阻塞当前线程,wait()是放弃对象锁的竞争,所以必须在同步代码块中,不然会造成线程安全问题

wait()为什么需要在同步代码块#

我们都知道wait()和notify()配合使用可以实现线程间的通信控制,wait()会阻塞当前线程并放弃锁,notify()会唤醒线程。相当于是对监视器锁的count值进行加减的操作。为了保证原子性,所以必须要在同步代码块中调用。否则会出现上下文切换,导致对锁失控,出现Lost Wake-Up Problem问题

wait()和await()的区别#

wait()是Object的方法(因为在java中每个对象有锁),必须先持有锁,否则会报监视器异常。通常配合notify()使用。用于线程之间的通信作用

await()是并发框架AQS中的Condition接口中,跟wait()一样,都必须在线程获取到锁后使用。但比wait()更加灵活

优势:

1、可以指定等待的条件和等待的超时时间,可中断

2、可以初始化多个Condition协同工作

class Example {
    final Lock lock = new ReentrantLock();
    // condition 依赖于 lock 来产生
    final Condition notFull = lock.newCondition();
    final Condition notEmpty = lock.newCondition();

    final Object[] items = new Object[100];
    int putptr, takeptr, count;

    // 生产
    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();  // 队列已满,等待,直到 not full 才能继续生产
            items[putptr] = x;
            if (++putptr == items.length) putptr = 0;
            ++count;
            notEmpty.signal(); // 生产成功,队列已经 not empty 了,发个通知出去
        } finally {
            lock.unlock();
        }
    }

    // 消费
    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await(); // 队列为空,等待,直到队列 not empty,才能继续消费
            Object x = items[takeptr];
            if (++takeptr == items.length) takeptr = 0;
            --count;
            notFull.signal(); // 被我消费掉一个,队列 not full 了,发个通知出去
            return x;
        } finally {
            lock.unlock();
        }
    }
}

守护线程#

1、守护线程--也称“服务线程”,他是后台线程,它有一个特性,即为用户线程提供公共服务,在没有用户线程可服务时会自动离开。
2、守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。
3、通过 setDaemon(true)来设置线程为“守护线程”;将一个用户线程设置为守护线程 的方式是在 线程对象创建 之前 用线程对象的 setDaemon 方法。
4、在Daemon线程中产生的新线程也是Daemon的。
5、生命周期:守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周 期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;如果还有一个或以上的非守护线程则JVM不会退出

进程、线程、协程的区别#

1、进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
2、线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相⽐进程不够稳定容易丢失数据。
3、协程是一种用户态的轻量级线程,协程的调度完全由用户控制,协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。

posted @   糯米๓  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示