进程与线程的再认识

进程与线程

进程就是 内存给应用程序分配的空间,也就是运行的程序。

CPU采用时间片轮转的方式运行进程,

上下文切换:当一个时间片结束时,进程还在运行,则暂停这个进程的运行,并且将CPU分配给另外一个进程。

当进程暂停后,它会保存当前的状态(标识和资源),在下次切换回来根据之前的状态进行恢复,继续执行。

相比多进程实现并发,多线程有更多的优势:

  • 线程间通信更加简单,它们共享这资源,这些资源在线程间通信比较容易。
  • 进程是重量级的,而线程是轻量级的,开销相比更小

进程和线程的区别

本质区别是两者是否单独拥有着地址空间和其他系统资源

  • 进程独占地址空间在内存隔离,通信困难,同步方便,稳定性强(即使出现问题,不会影响系统)。
  • 线程共享着地址空间和资源,同步困难,通信方便,稳定性不强(如果出现崩溃,则会影响到整个程序的稳定性)。

进程是资源分配的单元,而线程是OS进行调度的单位即CPU分配时间的单位。

剖析 Thread 方法

Thread 类是⼀个 Runnable 接⼝的实现类,我们来看看 Thread 类的源码。
查看 Thread 类的构造方法,发现其实是简单调用⼀个私有的 init 方法来实现初
始化。

init 的方法签名:

// Thread类源码
// ⽚段1 - init⽅法
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals)
    
// ⽚段2 - 构造函数调⽤init⽅法
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}

// ⽚段3 - 使⽤在init⽅法⾥初始化AccessControlContext类型的私有属性
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();

// ⽚段4 - 两个对⽤于⽀持ThreadLocal的私有属性
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

init 方法的参数:

  • g :线程组,指定这个所创建的线程是在哪个线程组下的
  • target:指定要执行的任务
  • name:线程名字
  • acc:用于初始化私有变量 inheritAccessControlContext (有待考究)
  • inheritThreadLocals:可继承的 TheadLocal (重点)

Thread 类的常用方法

  • currentThread():静态⽅法,返回对当前正在执⾏的线程对象的引⽤;
  • start():开始执⾏线程的⽅法,java虚拟机会调⽤线程内的run()⽅法;
  • yield():yield在英语⾥有放弃的意思,同样,这⾥的yield()指的是当前线程愿
    意让出对当前处理器的占⽤。这⾥需要注意的是,就算当前线程调⽤了yield()
    ⽅法,程序在调度的时候,也还有可能继续运⾏这个线程的;
  • sleep():静态⽅法,使当前线程睡眠⼀段时间;
  • join():使当前线程等待另⼀个线程执⾏完毕之后再继续执⾏,内部调⽤的是
    Object类的wait⽅法实现的;

继承 Thread 和 实现 Runnable 两者比较

灵活 - 单独 - 低耦合 - 轻量

  • 由于Java“单继承,多实现”的特性,Runnable接⼝使⽤起来⽐Thread更灵活
  • Runnable接⼝出现更符合⾯向对象,将线程单独进⾏对象的封装
  • Runnable接⼝出现,降低了线程对象和线程任务的耦合性
  • 如果使⽤线程时不需要使⽤Thread类的诸多⽅法,显然使⽤Runnable接⼝更
    轻量

Callable、Future 和 FutureTask

使用 Runnable 和 Thread 是创建线程,唯一弊端是它们是没有返回值的。

JDK提供了 Callable 接口和 Future 类通过"异步"模型来实现。

Callable 接口

Callable 与 Runnable 类似,同样是只有⼀个抽象⽅法的函数式接⼝。不同的
是, Callable 提供的⽅法是有返回值的,⽽且⽀持泛型

@FunctionalInterface
public interface Callable<V> {
	V call() throws Exception;
}

Callable 接口一般配合着线程池工具 ExecutorService 的 submit() 来返回一个 Future 类,再调用 get() 来获取返回值。

// ⾃定义Callable
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
    // 模拟计算需要⼀秒
    Thread.sleep(1000);
    return 2;
}
public static void main(String args[]){
        // 使⽤
        ExecutorService executor = Executors.newCachedThreadPool();
        Task task = new Task();
        Future<Integer> result = executor.submit(task);
        // 注意调⽤get⽅法会阻塞当前线程,直到得到结果。
        // 所以实际编码中建议使⽤可以设置超时时间的重载get⽅法。
        System.out.println(result.get());
    }
}
Future 接口
public abstract interface Future<V> {
    
    public abstract boolean cancel(boolean paramBoolean);
    
    public abstract boolean isCancelled();
    
    public abstract boolean isDone();
    
    public abstract V get() throws InterruptedException, ExecutionException;
    
    public abstract V get(long paramLong, TimeUnit paramTimeUnit)
        
    throws InterruptedException, ExecutionException, TimeoutException;
}

cancel 方法是试图取消一个线程。

注意是试图取消,并不⼀定能取消成功。因为任务可能已完成、已取消、或者⼀些其它因素不能取消,存在取消失败的可能。 boolean 类型的返回值是“是否取消成功”的意思。

参数 paramBoolean 表示是否采⽤中断的⽅式取消线程执⾏。
所以有时候,为了让任务有能够取消的功能,就使⽤ Callable 来代替 Runnable

FutureTask 类

上面所说的 Future 接口的实现类就是 FutureTask

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
    * Sets this Future to the result of its computation
    * unless it has been cancelled.
    */
    void run();
}

Future 只是一个接口,我们还需要实现其他的 cancel,get,isDone 方法是非常复杂的

// ⾃定义Callable,与上⾯⼀样
class Task implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        // 模拟计算需要⼀秒
        Thread.sleep(1000);
        return 2;
}

public static void main(String args[]){
        // 使⽤
        ExecutorService executor = Executors.newCachedThreadPool();
        FutureTask<Integer> futureTask = new FutureTask<>(new Task());
        executor.submit(futureTask);
        System.out.println(futureTask.get());
    }
}

在很多⾼并发的环境下,有可能Callable和FutureTask会创建多次。FutureTask能
够在⾼并发环境下确保任务只执⾏⼀次。

线程组和线程优先级

线程组

每个Thread必然存在于⼀个ThreadGroup中,Thread不能独⽴于ThreadGroup存在。

执⾏main()⽅法线程的名字是main,如果在new Thread时没有显式指定,那么默认将⽗线程
(当前执⾏new Thread的线程)线程组设置为⾃⼰的线程组

/**
 * @Author Juniors Lee
 * @Date 2021/11/15
 */
public class ThreadGroup {

    public static void main(String[] args) {

        new Thread(()->{
            System.out.println("当前线程组的名字:"+Thread.currentThread().getThreadGroup().getName());
            System.out.println("当前线程的名字:"+Thread.currentThread().getName());
        },"A").start();

        System.out.println("执行main方法的线程的名字"+Thread.currentThread().getName());
    }
}

输出结果:

执行main方法的线程的名字:main
当前线程组的名字:main
当前线程的名字:A

ThreadGroup管理着它下⾯的Thread,ThreadGroup是⼀个标准的向下引⽤的树状结构,这样设计的原因是防⽌"上级"线程被"下级"线程引⽤⽽⽆法有效地被GC回收

线程优先级

Java中线程优先级可以指定,范围是1~10。但是并不是所有的操作系统都⽀持10级优先级的划分(⽐如有些操作系统只⽀持3级划分:低,中,⾼)。

Java只是给操作系统⼀个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系统决定

通常情况下,⾼优先级的线程将会⽐低优先级的线程有更⾼的⼏率得到执⾏。我们使⽤⽅法 Thread 类的 setPriority() 实例⽅法来设定线程的优先级。

/**
 * @Author Juniors Lee
 * @Date 2021/11/15
 */
public class ThreadPriority extends Thread{

    @Override
    public void run() {
        System.out.println(String.format("当前线程是%s,优先级是%d",Thread.currentThread().getName(),Thread.currentThread().getPriority()));
    }

    public static void main(String[] args) {

        IntStream.range(1,10).forEach(i ->{
            Thread thread = new ThreadPriority();
            thread.setPriority(i);
            thread.start();
        });
    }
}

执行结果

当前线程是Thread-7,优先级是8
当前线程是Thread-2,优先级是3
当前线程是Thread-4,优先级是5
当前线程是Thread-6,优先级是7
当前线程是Thread-1,优先级是2
当前线程是Thread-8,优先级是9
当前线程是Thread-3,优先级是4
当前线程是Thread-0,优先级是1
当前线程是Thread-5,优先级是6

Java中的优先级来说不是特别的可靠,Java程序中对线程所设置的优先级只是给操作系统⼀个建议,操作系统不⼀定会采纳。⽽真正的调⽤顺序,是由操作系统的线程调度算法决定的。

在之前,我们有谈到⼀个线程必然存在于⼀个线程组中,那么当线程和线程组的优先级不⼀致的时候将会怎样呢?

我们⽤下⾯的案例来验证⼀下:

public static void main(String[] args) {
    ThreadGroup threadGroup = new ThreadGroup("t1");
    threadGroup.setMaxPriority(6);
    Thread thread = new Thread(threadGroup,"thread");
    thread.setPriority(9);
    System.out.println("我是线程组的优先级"+threadGroup.getMaxPriority());
    System.out.println("我是线程的优先级"+thread.getPriority());
}

运行结果:

我是线程组的优先级6
我是线程的优先级6

所以,如果某个线程优先级⼤于线程所在线程组的最⼤优先级,那么该线程的优先级将会失效,取⽽代之的是线程组的最⼤优先级

复制线程组
// 复制⼀个线程数组到⼀个线程组
Thread[] threads = new Thread[threadGroup.activeCount()];
TheadGroup threadGroup = new ThreadGroup();
threadGroup.enumerate(threads);

线程状态

Java线程的6个状态:

// Thread.State 源码
public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

NEW

处于NEW状态的线程,表明还没启动,即没有调用start().

其中关于 start() 的两个问题:

  • 反复调⽤同⼀个线程的start()⽅法是否可⾏?
  • 假如⼀个线程执⾏完毕(此时处于TERMINATED状态),再次调⽤这个线程
    的start()⽅法是否可⾏?
public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

debug我们发现

  • 第⼀次调⽤start()时threadStatus的值是0。
  • 第⼆次调⽤start()时threadStatus的值不为0。

因此值不为0直接抛出异常

RUNNABLE

即正在运行

BLOCKED

即阻塞状态。

处于该状态的线程正在等待锁的释放以进入同步区。

WAITING

即等待状态。

处于等待状态转入运行状态的线程需要其他线程的唤醒。

调用以下方法会进入等待模式:

  • Object.wait():使当前线程处于等待状态直到另⼀个线程唤醒它;
  • Thread.join():等待线程执⾏完毕,底层调⽤的是Object实例的wait()⽅法;
  • LockSupport.park():除⾮获得调⽤许可,否则禁⽤当前线程进⾏线程调度

TIME_WAITING

超时等待状态。线程等待⼀个具体的时间,时间到后会被⾃动唤醒。
调⽤如下⽅法会使线程进⼊超时等待状态:

  • Thread.sleep(long millis):使当前线程睡眠指定时间;
  • Object.wait(long timeout):线程休眠指定时间,等待期间可以通过
    notify()/notifyAll()唤醒;
  • Thread.join(long millis):等待当前线程最多执⾏millis毫秒,如果millis为0,则
    会⼀直执⾏;

TERMINATED

线程状态的转换

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bFzREfCt-1636974985543)(C:\Users\Juniors Lee\Pictures\MySQL\微信截图_20211115162616.png)]

/**
 * @Author Juniors Lee
 * @Date 2021/11/15
 */
public class RunnableBlocked {

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



        Thread a = new Thread(RunnableBlocked::func,"A");
        Thread b = new Thread(RunnableBlocked::func,"B");
        Thread c = new Thread(RunnableBlocked::func,"B");

        c.start();
        b.start();
        a.start();

        c.join(1500);
        //Thread.sleep(1000);



        System.out.println("A:"+a.getState());
        System.out.println("B:"+b.getState());
        System.out.println("C:"+c.getState());

    }

    private static synchronized void func(){

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

调整 func 函数的睡眠时间 或者 main 函数的睡眠时间

线程中断

在某些情况下,我们在线程启动后发现并不需要它继续执⾏下去时,需要中
断线程。⽬前在Java⾥还没有安全直接的⽅法来停⽌线程,但是Java提供了
线程中断机制来处理需要中断线程的情况。
线程中断机制是⼀种协作机制。需要注意,通过中断操作并不能直接终⽌⼀
个线程,⽽是通知需要被中断的线程⾃⾏处理

几个中断方法:

  • Thread.interrupt():中断线程。这⾥的中断线程并不会⽴即停⽌线程,

    ​ ⽽是设置线程的中断状态为true(默认是flase);

  • Thread.interrupted():测试当前线程是否被中断。线程的中断状态受这个⽅法的影响,

    ​ 意思是调⽤⼀次使线程中断状态设置为true,连续调⽤两次会使得这个线程的中断状态重新转为false;

  • Thread.isInterrupted():测试当前线程是否被中断。

    ​ 与上⾯⽅法不同的是调⽤这个⽅法并不会影响线程的中断状态。

中断但为完全中断:

在线程中断机制⾥,当其他线程通知需要被中断的线程后,线程中断的状态
被设置为true,但是具体被要求中断的线程要怎么处理,完全由被中断线程
⾃⼰⽽定,可以在合适的实际处理中断请求,也可以完全不处理继续执⾏下
去。

线程间通信

Synchronized

通过对一个对象加锁,来实现线程的同步。

但这样的加锁方式,

线程要不断的区尝试获得锁,如果失败了,就会再次去尝试,因而会浪费CPU资源。

等待/通知机制

因而引入了基于Object类中的 notify/notifyAll 来实现多线程同步机制。

信号量

信号量就是一种基于 voliate 关键字的自己实现的通信。

volitile关键字能够保证内存的可⻅性,如果⽤volitile关键字声明了⼀个变量,在⼀个线程⾥⾯改变了这个变量的值,那其它线程是⽴⻢可⻅更改后的值的。

我们可以看到,使⽤了⼀个 volatile 变量 signal 来实现了“信号量”的模型。这⾥
需要注意的是, volatile 变量需要进⾏原⼦操作。 signal++ 并不是⼀个原⼦操
,所以我们需要使⽤ synchronized 给它“上锁”。

管道

管道是基于“管道流”的通信⽅式。JDK提供了 PipedWriter 、 PipedReader 、PipedOutputStream 、 PipedInputStream 。

其中,前⾯两个是基于字符的,后⾯两个是基于字节流的。

我们通过线程的构造函数,传⼊了 PipedWrite 和 PipedReader 对象。可以简单分析
⼀下这个示例代码的执⾏流程:

  • 线程ReaderThread开始执⾏,
  • 线程ReaderThread使⽤管道reader.read()进⼊”阻塞“,
  • 线程WriterThread开始执⾏。
  • 线程WriterThread⽤writer.write(“test”)往管道写⼊字符串,
  • 线程WriterThread使⽤writer.close()结束管道写⼊,并执⾏完毕,
  • 线程ReaderThread接受到管道输出的字符串并打印,
  • 线程ReaderThread执⾏完毕。

管道的应用场景:

使⽤管道多半与I/O流相关。当我们⼀个线程需要先另⼀个线程发送⼀个信息(⽐如字符串)或者⽂件等等时,就需要使⽤管道通信了。

通信相关的方法

Join

它的作用是让线程陷入"等待"状态,等join这个线程执行完成之后,再继续执行当前线程。

main会等thread完成后执行。

Sleep

Sleep不会释放当前的锁,而wait方法会。

sleep 与 wait 的区别:

  • wait可以指定时间,也可以不指定;⽽sleep必须指定时间。
  • wait释放cpu资源,同时释放锁;sleep释放cpu资源,但是不释放锁,所以易
    死锁。
  • wait必须放在同步块或同步⽅法中,⽽sleep可以再任意位置

ThreadLocal

TheadLocal 是本地副本变量工具类。

ThreadLocal不属于多线程的通信,而是让每个线程都有自己"独立"的变量,线程之间互不影响。

它为每个所属线程都创建了一个副本,每个线程都能访问到自己的副本变量。

最常用的是get,set方法

/**
 * @Author Juniors Lee
 * @Date 2021/11/15
 */
public class ThreadLocalDemo {

    static class ThreadA implements Runnable {

        private ThreadLocal<String> threadLocal;

        public ThreadA(ThreadLocal threadLocal){

            this.threadLocal = threadLocal;
        }

        @Override
        public void run() {
            threadLocal.set("Juniors");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("ThreadA输出:" + threadLocal.get());
        }

        static class ThreadB implements Runnable {

            private ThreadLocal<String> threadLocal;

            public ThreadB(ThreadLocal threadLocal){

                this.threadLocal = threadLocal;
            }

            @Override
            public void run() {
                threadLocal.set("Kobe");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("ThreadB输出:" + threadLocal.get());
            }
        }

        public static void main(String[] args) {

            ThreadLocal<String> threadLocal = new ThreadLocal<>();

            new Thread(new ThreadA(threadLocal)).start();
            new Thread(new ThreadB(threadLocal)).start();
        }
    }
}

如果只是线程隔离,为什么不每个线程都声明一个私有变量呢?

这是为了希望类中的某一个变量能够和线程状态有关联

最常⻅的ThreadLocal使⽤场景为⽤来解决

数据库连接、Session管理等。数据库连接和Session管理涉及多个复杂对象的初始化和关闭。

如果在每个线程中声明⼀些私有变量来进⾏操作,那这个线程就变得不那么“轻量”了,需要频繁的创建和关闭连接

InheritedThreadLocal

Inheritable是继承的意思。
它不仅仅是当前线程可以存取副本值,⽽且它的⼦线程也可以存取这个副本值。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值