进程与线程

进程与线程

进程的产生

最初的计算机只能接受一些特定的指令,计算机等待用户输入,用户每输入一个指令,计算机就做出一个操作。大多时候,计算机都处在等待状态。因此效率低下。

批处理操作系统

用户将多个需要执行的程序写在磁带上,然后交由计算机去读取并逐个执行这些程序,并将输出结果写在另一个磁带上。

程序是用某种编程语言编写的,能完成一定任务的代码集合,它是一段静态代码。

但是由于批处理操作系统的指令运行方式仍然是串行的,内存中始终只有一个程序在运行,后面的程序需要等待前面的程序执行完成后才能开始执行,而前面的程序有时会由于I/O操作、网络等原因阻塞,所以批处理操作效率也不高

进程的提出

在批处理系统中,内存只能存放一个程序,为了使内存存放多个程序,便有了进程的概念。

进程就是应用程序在内存中分配的空间,也就是正在运行的程序,各个进程之间互不干扰。同时进程保存着程序每一个时刻运行的状态。

CPU为每个进程分配一个时间片,若时间片结束后进程还在运行,则暂停该进程的运行,并将CPU分配给另一个进程。这个过程称为上下文切换。如果进程在时间片结束前阻塞或结束,则CPU立即进行切换,不用等待时间片用完。

从宏观上看,同一时段是在执行多个任务,可以说进程让操作系统的并发成为了可能,但事实上,对于单核CPU而言,某一时刻只有一个任务在占用CPU。

线程的产生

随着时间的推移,人们并不满足一个进程在一段时间只能做一件事情,如果一个进程有多个子任务时,只能逐个得执行这些子任务,很影响效率。

比如说杀毒软件的检测功能,若某一项检测出现问题,这会影响到后面的检测。

为了让这些子任务同时执行,便有了线程的概念:一个线程执行一个子任务,一个进程中包含多个线程。

总结的说,进程让操作系统的并发性成为了可能,而线程让进程的内部并发成为了可能。

为什么使用多线程?

虽然通过多进程的方式也可以实现并发,但多线程有如下的优点:

  • 多进程的通信较为复杂,而线程的通信比较简单。通常情况下我们需要使用共享资源,而进程与进程之间是存在内存分隔的,数据是分开的,这使得数据共享变得复杂;而线程之间共享所属进程占用的内存地址空间和资源,数据共享简单。
  • 进程单独占有一定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息,开销较小。

不过线程也有如下的缺点:

  • 进程单独占有一定的内存地址空间,一个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;一个线程崩溃可能影响整个程序的稳定性,可靠性较低。
  • 虽然进程与进程间共享数据较为复杂,但同步简单;而线程是共享数据简单,同步复杂。

进程与线程的本质区别在于是否单独占有内存地址空间及其它系统资源(比如I/O),以及进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位(CPU分配时间的单位)

 

上下文切换

上下文切换是指 CPU 从一个进程(或线程)切换到另一个进程(或线程),而上下文是指某一时间点 CPU 寄存器和程序计数器的内容。

寄存器:CPU内部的少量速度很快的闪存,通常存储和访问计算过程的中间值提高计算机程序的运行速度。

程序计数器:一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置。

由于线程存在创建和上下文切换的开销,因此并发执行不一定比串行快。

并发:同一时间段内,多个任务都在执行。

并行:单位时间内,多个任务同时执行。

如何减少上下文切换

  • 无锁并发编程:多线程竞争锁时,会引起上下文切换。因此我们可以用一些方法避免使用锁,比如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
  • CAS算法:Java的Atomic包使用CAS算法来更新数据,不需要加锁。
  • 使用最少线程:避免创建不需要的线程。比如任务很少,却创建了很多线程来处理,这样会造成大量线程出于等待状态。
  • 协程:在单线程里实现多任务的调度,并维持多个任务间的切换。

 


线程的相关类和接口

新建线程有两种方法:继承Thread类,并重写run方法;实现Runnable接口的run方法;

继承Thread类

hread类是一个Runnable接口的实现类,它的构造方法使用了私有的init方法来实现初始化。

// 片段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:线程的名字,多个线程的名字是可以重复的。默认名字见片段2;
  • inheritThreadLocals:可继承的ThreadLocal;

继承Thread类的示例代码如下,当程序中调用start()方法后,虚拟机会先为我们创建一个线程,然后等到这个线程第一次得到时间片时再调用run()方法。

public class Test{
    public static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("MyThread");
        }
    }

    public static void main(String[] args) {
        Thread myThread = new MyThread();
        myThread.start();
    }
}

实现Runnable接口

Runnable接口如下所示

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

使用示例如下:

public class Test {
    public static class MyThread implements Runnable {
        @Override
        public void run() {
            System.out.println("MyThread");
        }
    }

    public static void main(String[] args) {
        new MyThread().run();
    }
}

 

Runnable接口与Thread类的比较

(1)由于Java“单继承,多实现”的特性,Runnable接口使用起来比Thread更灵活。

(2)如果使用线程时不需要使用Thread类的诸多方法,显然使用Runnable接口更为轻量。

(3)Runnable接口出现,降低了线程对象和线程任务的耦合性。

因此我们通常优先使用“实现Runnable接口”的方式来自定义线程类。

 

Callable、Future与FutureTask

使用RunnableThread来创建线程有一个弊端,即方法是没有返回值的。而有时我们希望开启一个线程去执行一个任务,并且这个任务执行完成后有一个返回值。那么我们可以使用Callable接口与Future类。

Callable接口

Callable接口只有一个方法,该方法有返回值且支持泛型。

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

我们一般通过配合线程池工具ExecutorService来使用Callable接口:通过ExecutorService的submit方法来让一个Callable接口执行,我们可以通过返回的Future的get方法得到结果。

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接口

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,但如果只是为了可取消,而不需要返回值,则可以声明形式类型、并返回 null作为底层任务的结果。

 

FutureTask类

Future接口的cancel,get方法实现起来复杂,因此JDK提供了FutureTask类来供我们使用。FutureTask类实现了RunnableFuture接口的,而RunnableFuture接口同时继承了Runnable接口和Future接口。

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接口的那段示例代码比较,可以发现此处的submit方法没有返回值,它实际调用了submit(Runnable task)方法,而之前调用的是submit(Callable<T> task)方法。

 


线程状态及主要转化方法

线程状态

线程的状态分为6种:

(1)新建状态NEW:该状态的线程尚未启动,即还未执行start()方法。

start()方法代码如下,方法内部有一个变量threadStatus。第一次调用start()方法后,threadStatus的值会改变(不等于零),如果再次调用则会抛出IllegalThreadStateException异常。

public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {

        }
    }
}

(2)运行状态RUNNABLE:处于该状态的线程可能在Java虚拟机中运行,也可能在等待其他系统资源(比如I/O)。该状态实际包括了传统操作系统的ready和running状态。

(3)阻塞状态BLOCKED:该状态的线程正等待锁的释放。

4)等待状态WAITING:造成该状态的方法有:

1)Object.wait():使当前线程处于等待状态直到另一个线程唤醒它;

2)Thread.join():等待线程执行完毕,底层调用的是Object实例的wait方法;

3)LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。

(5)超时等待状态TIMED_WAITING:线程等待一个具体的时间,时间到后会被自动唤醒。造成该状态的方法有:

1)Object.sleep(long millis):使当前线程睡眠指定时间;

2)Object.wait(long timeout):线程休眠指定时间,等待期间可以通过notify()/notifyAll()唤醒;

3)Thread.join(long millis):等待当前线程最多执行millis毫秒

4)LockSupport.parkNanos(long millis):除非获得调用许可,否则禁用当前线程进行线程调度直到指定时间。

(6)终止状态TERMINATED:该状态的线程已执行完毕

 

wait方法,yield方法,sleep方法和join方法

wait方法

线程调用wait()方法前 必须持有对象的锁,调用方法时会先释放当前的锁,直到有其他线程调用notify()/notifyAll()方法唤醒等待锁的线程。该方法可以加上指定等待时间的参数,经过指定时间long之后它会自动唤醒,无论其他线程是否唤醒他。

notify() / notifyAll()方法

由于notify()方法只会唤醒单个等待锁的线程,因此如果有多个线程都在等待这个锁的话,则不一定会唤醒到之前调用wait()方法的线程。同样的。调用notifyAll()方法唤醒所有等待锁的线程之后,也不一定会马上把时间片分给刚才放弃锁的那个线程,具体要看系统的调度。

sleep方法

使用该方法会暂时使线程停止执行,并不会释放锁。时间到后,线程会重新进入RUNNABLE状态。

join方法

若在某一线程A中调用了线程B的join()方法,则A需要等待B执行完后,A线程才能继续。该方法不会释放锁。

yield方法

当前线程放弃CPU资源,将CPU让给其他任务。不过存在 该线程刚放弃CPU,立马又获得CPU时间片的可能。

 

线程中断

某些情况下,我们在线程启动后发现不需要它继续执行,则可以中断线程。此处中断的意思只是把线程的中断标志置为true(默认是flase),而非真正的停止线程。

Thread.interrupt()

将调用该方法的线程对象的断标志置为true。

Thread.interrupted()

判断 当前线程(而非调用该方法的线程对象)是否被中断。该方法在返回结果前,会清除线程的中断状态(即置为false),因此如果连续两次调用该方法,第二次的结果是false(除这两次之间线程再次中断的情况)。

测试代码如下,我们将当前线程(即main线程)中断,通过两次调用worker.interrupted()来判断当前线程是否被中断:第一次为true,第二次为false。

public class Test {
    public static void main(String[] args) throws Exception {
        Thread worker = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("worker线程启动");
                System.out.println("worker线程终止");
            }
        });
        worker.start();

        Thread.sleep(200);
        Thread.currentThread().interrupt();
        System.out.println("worker interrupted:" + worker.interrupted());
        System.out.println("worker interrupted:" + worker.interrupted());
    }
}

输出:

Thread.isInterrupted()

判断 调用该方法的线程对象是否被中断。该方法不会清除中断标志。

    public static void main(String[] args) throws Exception {
        Thread worker = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 1000; i++) {
                    System.out.println("i= " + i);
                }
            }
        });
        worker.start();

        Thread.sleep(10);
        worker.interrupt();

        System.out.println("worker interrupted:" + worker.isInterrupted());
        System.out.println("worker interrupted:" + worker.isInterrupted());

    }

输出:

 

当睡眠时遇到中断

当在sleep中的线程被中断时,会抛出InterruptedException 异常。如果我们在线程的run方法中捕获这个异常,会发现此时的中断标志位为false

测试代码如下,当我们抛出异常时,中断状态已被清除,因此输出的是false。

public class Test {
    public static void main(String[] args) throws Exception {
        Thread worker = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("worker线程启动");

                try {
                    Thread.sleep(10000);
                }catch (InterruptedException e) {
                    //输出false
                    System.out.println("run方法 interrupted:" + Thread.currentThread().isInterrupted());
                }
                System.out.println("worker线程终止");
            }
        });
        worker.start();

        Thread.sleep(10);
        worker.interrupt();

        System.out.println("main线程停止");
    }
}

如果我们希望isInterrupted方法返回true,则可以在isInterrupted方法前再次中断该线程,这样两次的输出都是true

public class Test {
    public static void main(String[] args) throws Exception {
        Thread worker = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("worker线程启动");

                try {
                    Thread.sleep(10000);
                }catch (InterruptedException e) {
//                    再次中断线程
                    Thread.currentThread().interrupt();
                    System.out.println("run方法 interrupted:" + Thread.currentThread().isInterrupted());
                }
                System.out.println("worker线程终止");
            }
        });
        worker.start();

        Thread.sleep(10);
        worker.interrupt();
        System.out.println("run方法 interrupted:" + worker.isInterrupted());
        System.out.println("main线程停止");
    }
}

 

 


线程组

线程组的作用是批量管理线程或线程组,每个线程必然存在于一个线程组中。若在newThread时没有显式指定所属线程组,则默认将当前执行new Thread的线程所在的线程组设置为自己的线程组,比说在main函数里new Thread,则该线程属于main线程组。

线程组是一个树状结构,线程组里可有线程对象和线程组。

 

线程组的常用方法

static int enumerate(Thread[] tarray): 将线程组中的子线程以复制的形式拷贝到groupList中。

int activeGroupCount(): 取得当前线程组对象中的子线程组数量,返回的值至少一个估计值。

通过上面的方法,我们将新建的一个线程ta放入新建的线程组group,然后取出线程组里的线程,

public class Test {
    public static void main(String[] args) throws Exception {

        ThreadGroup group = new ThreadGroup("A");
//       当前线程组group的子线程数量为0
        System.out.println("当前子线程数量为:" + group.activeCount());
//        group默认属于main线程组
        System.out.println(group.getParent().getName());
        
        Thread ta = new Thread(group, new Runnable() {
            @Override
            public void run() {
                System.out.println("所属线程组:" + Thread.currentThread().getThreadGroup().getName());
            }
        }, "ta");
        ta.start();
        
        Thread[] threads = new Thread[group.activeCount()];
//       复制
        group.enumerate(threads);
        for(int i = 0; i < threads.length; i++) {
            System.out.println(threads[i].getName());   //输出ta
        }
    }
}

同样我们可以把新建的一个线程组放入当前线程组:

public class Test {
    public static void main(String[] args) throws Exception {
        System.out.println("当前线程:" + Thread.currentThread().getName() + "所属的线程组名为:" +
                Thread.currentThread().getThreadGroup().getName() + ",该组有线程组数量:" +
                Thread.currentThread().getThreadGroup().activeGroupCount());

        ThreadGroup group = new ThreadGroup("新的组"); //默认加到main组
        System.out.println("当前线程:" + Thread.currentThread().getName() + "所属的线程组名为:" +
                Thread.currentThread().getThreadGroup().getName() + ",该组有线程组数量:" +
                Thread.currentThread().getThreadGroup().activeGroupCount());
            //把main线程组里包含的所有线程组 复制到线程组数组groupList中。
        ThreadGroup[] groupList = new ThreadGroup[Thread.currentThread().getThreadGroup().activeGroupCount()];
        Thread.currentThread().getThreadGroup().enumerate(groupList);
        for(int i = 0; i < groupList.length; i++) {
            System.out.println("线程组名称:" + groupList[i].getName());
        }
    }
}

输出:

 


线程通信

各个线程都有自己私有的线程上下文,互不干涉,但各个线程也需要通信。除了前面介绍过的join(),sleep()方法,线程还可以通过几种方法/原理来进行通信。

在多线程环境中,如果多个线程同时操作一个共享的变量,则可能会导致数据不准确,产生冲突。因此我们需要同步,所谓的同步就是按先后次序来运行,即多个线程之间按一定顺序执行。为了达成同步,我们可以通过锁来实现。

Java对象可以被当成锁来使用,同一时间一个锁只能被一个线程持有,而其他线程需要等待锁的释放。

首先来看一段没有锁的代码,该代码生成两个线程ta,tb分别输出打印值。在没有锁的情况下,两个线程各自独立工作,输出语句不是同步的。

public class Test {
    private static Object lock = new Object();
    public static void main(String[] args) throws Exception {

        Thread ta = new Thread(new Runnable() {
            @Override
            public void run() {
              //  synchronized (lock) {
                    for (int i = 0; i < 100; i++) {
                        System.out.println("Thread A " + i);
                    }
               // }

            }
        });
        Thread tb =  new Thread(new Runnable() {
            @Override
            public void run() {
             //   synchronized (lock) {
                    for (int i = 0; i < 100; i++) {
                        System.out.println("Thread B " + i);
                    }
             //   }
            }
        });

        ta.start();
//      主线程睡眠,ta线程拿到锁
//        Thread.sleep(10);
        tb.start();
    }
}

如果我们希望两个线程的输出语句是同步的,比如A输出完所有语句后,再到B输出。我们可以通过创建一个对象作为锁,在上面代码中,把相关的注释取消即可。

 

等待/通知

等待/通知机制是基于Object类的wait()方法和notify()notifyAll()方法来实现的。

执行wait()方法前需要线程A先拿到锁lock,然后线程A可以通过lock.wait()方法进入等待状态,此时锁被释放。另一个线程B获得锁开始执行后,它可以选择在某一时刻使用lock.notify(),此时线程B还未释放锁,除非他使用lock.wait()方法释放锁,或者他执行任务,就会自动释放锁。

 

信号量

对一个关键代码段,若我们希望在某一时刻只有一个线程能进入该代码段,则可以使用锁或者等待/通知方式。而如果在某一时刻,关键代码段可以有多个线程进入,我们可以使用信号量。

在进入关键代码段前,线程必须获取一个许可,执行完代码段后,释放许可。许可的总数n表示某一时刻最多同时有n个线程在代码段。

 

管道流

管道流与I/O流相关,当一个线程希望像另一个线程发送信息(例如字符串),则可以使用管道通信:一个线程发送数据到输出管道,另一个线程从输入管道中读取数据

JDK提供了PipedWriter、 PipedReader、 PipedOutputStream、 PipedInputStream。其中,前面两个是基于字符的,后面两个是基于字节流的。

现在通过PipedOutputStream、 PipedInputStream来写入和读出字符串0~299。

public class Test {
    static class WriteTask {
        public void write(PipedOutputStream out) {
            try {
                System.out.println("开始写入:");
                for(int i = 0; i < 300; i++) {
                    String data = "" + (i + 1);
                    out.write(data.getBytes());
                    System.out.println(data);
                }
                System.out.println();
                out.close();
            }catch (IOException e)      { e.printStackTrace(); }
        }
    }

    static class ReadTask {
        public void read(PipedInputStream input) {
            try {
                System.out.println("开始读入:");
                byte[] bArray = new byte[20];
                //若没有数据可读,则读线程会阻塞在此处
                int readLength = input.read(bArray);

                while(readLength != -1) {
                    String curReadData = new String(bArray, 0, readLength);
                    System.out.println(curReadData);
                    readLength = input.read(bArray);
                }
                System.out.println();
                input.close();
            }catch (IOException e)      { e.printStackTrace(); }
        }
    }

    static class WriteThread extends Thread {
        private WriteTask writeTask;
        private PipedOutputStream out;

        public WriteThread(WriteTask writeTask, PipedOutputStream out) {
            this.writeTask = writeTask;
            this.out = out;
        }

        @Override
        public void run() {
            writeTask.write(out);
        }
    }

    static class ReadThread extends Thread {
        private ReadTask readTask;
        private PipedInputStream input;

        public ReadThread(ReadTask readTask, PipedInputStream input) {
            this.readTask = readTask;
            this.input = input;
        }

        @Override
        public void run() {
            readTask.read(input);
        }
    }

    public static void main(String[] args) throws Exception{
        WriteTask writeTask = new WriteTask();
        ReadTask readTask = new ReadTask();


        PipedOutputStream out = new PipedOutputStream();    //PipedReader
        PipedInputStream input = new PipedInputStream();    //PipedWriter

        out.connect(input);     //或者input.connect(out)

        new ReadThread(readTask, input).start();
        Thread.sleep(2000);
        new WriteThread(writeTask, out).start();
    }
}

 


参考资料

https://github.com/RedSpider1/concurrent

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值