Java多线程-进程和线程

831 阅读12分钟

Java并发

什么是进程,什么是线程

进程:操作系统分配资源的最小单位

进程是程序的一次执行过程,是系统运行程序的最小单位,是启动一个计算机程序,运行到结束/终止的过程。

当系统创建进程的时候,会申请进程的ID(PID),同时为进程申请内存空间。可以把一个进程看作为一个进程控制块PCB的结构体,它需要包含:

  1. 进程的编号PID,作为进程的身份标识
  2. 进程的状态,包含新建状态、就绪状态、运行状态、阻塞状态、销毁状态
  3. 进程的执行优先级
  4. 进程的上下文,用来保存本次执行状态,以便下次继续执行
  5. 进程的内存地址

什么是上下文切换:

当发生一下情况时,进程/线程会从CPU中退出:

  • 主动让出 CPU,比如调用了 sleep(), wait() 等。
  • 时间片用完,因为操作系统要防止一个线程或者进程长时间占用CPU导致其他线程或者进程饿死。
  • 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
  • 被终止或结束运行

这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。

线程:CPU执行调度的最小单位

一个进程包含多个线程,线程是比进程更小的执行单位,每个线程可以共享一个进程的资源,在线程之间的上下文切换不太浪费资源。

一个Java程序天生就是多线程程序,可以利用JMX查看一个Java程序有哪些线程:

public static void main(String[] args) {
    ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
    ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
    for (ThreadInfo threadInfo : threadInfos) {
        System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
    }
}

运行结果:

[1] main
[2] Reference Handler
[3] Finalizer
[4] Signal Dispatcher
[5] Attach Listener
[12] Common-Cleaner
[13] Monitor Ctrl-Break

线程与进程之间的区别(摘自JavaGuide)

从JVM的角度来描述一下进程和线程的关系:

img

一个进程可以有多个线程,多个线程共享进程的堆和方法区(JDK1.8之后将方法区移除,用元空间取而代之),每个线程私有虚拟机栈、本地方法栈和程序计数器。

堆:进程中的最大的一块内存,主要用来存放进程的对象

方法区:主要用来存放已被加载的类的信息、常量、静态变量、即时编译器其编译后的代码

为什么程序计数器是私有的

程序计数器的作用

  1. 通过PC依次读入指令,从而实现代码的控制流
  2. 多线程的情况下,pc记录当前线程的执行位置,来回切换的时候就可以知道线程运行到哪里了

程序计数器私有主要是为了线程切换后能恢复到正确的执行位置

为什么虚拟机栈和本地方法栈是私有的

虚拟机栈:Java的每个方法执行的时候会开一个栈帧来存储局部变量表、操作数栈、常量池等信息,方法调用到执行完的过程是虚拟机栈入栈和出栈的过程

本地方法栈:发挥的作用类似虚拟机栈,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的

并发编程的优点和缺点

并发编程的优点不用过多赘述,总结来说就是:通过并发编程的形式可以将多核CPU的计算性能发挥到极致,提高性能。

并发编程主要有以下几个缺点:

  1. 频繁的上下文切换会造成性能损耗,过于频繁的切换无法发挥并发编程的优势。

如何减少上下文切换:

  1. 无锁并发编程:参考concurrentHashMap分段锁的思想,用不同的线程处理不同的数据,在多线程竞争的条件下可以减少上下文切换的时间
  2. CAS算法:在Atomic类中使用CAS算法来更新数据,使用了乐观锁,减少了一部分不必要的锁竞争带来的上下文切换
  3. 减少线程的使用:避免创建不需要的线程
  4. 协程:在单线程中实现多任务的调度
  1. 线程安全问题,对临界资源处理不当的话很可能会发生死锁。

在Java中实现一个死锁:

public class DeadLock {
    private static String resource_a = "A";
    private static String resource_b = "B";
​
    public static void deadLock() {
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                // 占用资源A
                synchronized (resource_a) {
                    System.out.println("get resource a");
                    try {
                        Thread.sleep(3000);
                        // 等待资源B释放
                        synchronized (resource_b) {
                            System.out.println("get resource b");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                // 占用资源B
                synchronized (resource_b) {
                    System.out.println("get resource b");
                    try {
                        Thread.sleep(3000);
                        // 等待资源A释放
                        synchronized (resource_a) {
                            System.out.println("get resource a");
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        threadA.start();
        threadB.start();
    }
​
    public static void main(String[] args) {
        deadLock();
    }
}
​

如何避免死锁:

  1. 避免一个线程同时获得多个锁
  2. 避免一个线程在锁内部占有多个资源,尽量保持一个锁只占用一个资源
  3. 尝试使用定时锁lock.tryLock(long time, TimeUnit unit)
  4. 对于数据库锁,加锁和解锁必须在一个数据库连接中

如何实现一个线程

  1. 继承Thread类,重写run方法
  1. 实现Runnable接口
public static void main(String[] args) {
    Thread byThread = new Thread() {
    @Override
        public void run() {
            System.out.println("继承自Thread类");
        }
    };
    byThread.start();
    Thread byRunnable = new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("实现Runnable接口");
        }
    });
    byRunnable.start();
    // 通过Callable接口的本质其实是基于Thread类和Runnable接口
    // 其他创建线程的方式本身也是基于Thread类和Runnable接口
    // 所有创建线程的方法只有两种
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    Future<String> submit = executorService.submit(new Callable() {
        @Override
        public String call() throws Exception {
            return "实现Callable接口";
        }
    });
    try {
        String result = submit.get();
        System.out.println(result);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        e.printStackTrace();
  
}

注意事项:由于Java不能多继承,因此在创建线程的时候尽量考虑使用接口的实现形式

Thread和Runnable的区别:

最大的区别是:

  1. Thread是类而Runnable是接口,并且Thread类还实现了Runnable接口。
  2. 实际上Runnable接口相比较Thread而言,可以更加方便的描述数据共享的概念,使用Runnable接口可以避免单继承带来的局限。

点开Thread的源码就可以看到Thread类实现了Runnable接口

class Thread implements Runnable {
    // 创建Runnable时使用的构造方法
    public Thread(Runnable target) {
        this(null, target, "Thread-" + nextThreadNum(), 0);
    }
}

而在Thread中又有一个私有的属性:

/* What will be run. */
private Runnable target;

在Thread类中线程运行需要调用start()方法,调用start()方法之后,JVM会调用线程的run()方法,结果是会有两条线程同时运行:当前线程(从调用返回到start()方法)和另一个线程(执行其run()方法)

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 */
        }
    }
}
​
private native void start0();

同时Thread类重写了Runnable接口的run()方法:

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

而Runnable接口只有一个抽象方法run():

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

Runnable和Callable的区别:

The Callable interface is similar to Runnable, in that both are designed for classes whose instances are potentially executed by another thread. A Runnable, however, does not return a result and cannot throw a checked exception.

最大的区别是

  1. Runnable没有返回值而Callable接口的任务线程能返回执行结果
  2. Callable接口实现类中的run方法允许异常向上抛出,可以在内部处理,try catch,但是Runnable接口实现类中run方法的异常必须在内部处理,不能抛出

Runnable

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

Callable

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

线程的生命周期

选自《Java并发编程的艺术》

image.png

Java中的线程是会在不通过状态之间转化的,一共有NEW, RUNNABLE, BLOCKED, WAITING, TIME_WAITING, TERMINATED六种状态:

Java 线程的状态

当一个Thread类被实例化之后就会进入NEW状态,调用Thread.start()方法之后可以进入RUNNABLE状态,是操作系统中的就绪和运行态,通过yield()方法让出CPU等待系统调用;在运行态的时候,通过wait(), join(), LockSupport.park()方法进入WAITING状态 ,通过Thread.sleep(long), wait(long), join(long), LockSupport.parkUtil(), LockSupport.parkNanos()方法进入TIME_WAITING状态,这两个等待状态都可以通过notify(), notifyAll(), LockSupport.unpark(Thread)返回Runnable状态;当线程之间竞争临界资源的时候,等待获取锁synchronized的时候,会进入BLOCKED状态;最后当线程运行结束之后,会进入最终的TERMINATED状态

当使用synchronized方法或者synchronized代码块的时候,线程进入的是BLOCKED状态

当使用locks类中的lock加锁的时候,会调用LockSupport方法,进入WAITING或者TIME_WAITING状态

线程的基本操作

线程的生命周期之中需要的基本操作,会成为线程通信的一种基本方法

native关键字:会跳过Java而调用底层的调用底层c语言的库,会进入本地方法栈,调用本地方法的本地接口JNI(Java Native Interface)。JNI 扩展了Java的使用,融合了不同的编程语言,在内存中开辟了一块Native Method Stack,通过native方法,加载本地方法库中的JNI。

interrupt()

线程中断可以理解为一个线程的标志位,表示一个运行中的线程是否被其他线程进行了终端的操作,在此之前有一个废弃淘汰的stop()方法:

@Deprecated(since="1.2")
public final void stop()

stop()方法会让线程A直接终止掉另一个线程B,线程B会立即释放锁,但不能保证一致性,而且线程A也不会知道线程B什么时候能终止...

使用interrupt()方法可以请求终止线程:

public void interrupt() {
    if (this != Thread.currentThread()) {
        checkAccess();
        // thread may be blocked in an I/O operation
        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();  // set interrupt status
                b.interrupt(this);
                return;
            }
        }
    }
    // set interrupt status
    interrupt0();
}

实际上interrupt不会真正的终止一个线程,而是告诉一个线程它需要被结束了,Java的设计者希望线程自己被终结。

中断状态可以通过 isInterrupted()来读取,并且可以通过interrupted()的操作读取和清除。

public static boolean interrupted() {
    return currentThread().isInterrupted(true);
}
public boolean isInterrupted() {
   return isInterrupted(false);
}
@HotSpotIntrinsicCandidate
private native boolean isInterrupted(boolean ClearInterrupted); 

join()

join()方法是进程之间写作的一种方式,很多时候进程的输入需要等待另外一个进程的输出,join()方法会等待这个线程死亡之后再继续进行。

public final synchronized void join(long millis)
public final synchronized void join(long millis, int nanos)
public final void join() throws InterruptedException
public final synchronized void join(long millis)
    throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;
​
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
​
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

其中最重要的代码块是:

if (millis == 0) {
    while (isAlive()) {
        wait(0);
    }
} else {
    while (isAlive()) {
        long delay = millis - now;
        if (delay <= 0) {
            break;
        }
        wait(delay);
        now = System.currentTimeMillis() - base;
    }

等待对象会一直阻塞,直到执行线程死亡

sleep()

public static void sleep(long millis, int nanos)
public static native void sleep(long millis) throws InterruptedException

很显然它是让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器。需要注意的是如果当前线程获得了锁,sleep方法并不会失去锁。

sleep()wait()的区别

  1. sleep()方法是Thread静态方法,而wait是Object实例方法
  2. wait()方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而sleep()方法没有这个限制可以在任何地方种使用。另外,wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁;
  3. sleep()方法在休眠时间达到后如果再次获得CPU时间片就会继续执行,而wait()方法必须等待notify()/notifyAll()通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。

yield()

yield()方法一旦执行会使当前线程让出CPU,在下一次竞争中,如果获得了CPU时间片同样会继续执行,很显然yield()方法会是一个static native方法。

public static native void yield();

yield()方法让出的时间片只会给优先级相同的线程

在Java中Thread类的整型变量priority表示线程的优先级,范围1~10,默认为5

private int priority;
​
/**
* The minimum priority that a thread can have.
*/
public static final int MIN_PRIORITY = 1;
/**
 * The default priority that is assigned to a thread.
 */
public static final int NORM_PRIORITY = 5;
/**
 * The maximum priority that a thread can have.
 */
public static final int MAX_PRIORITY = 10;
​
​
pubic final void setPriority(int newPriority) {
    ThreadGroup g;
    checkAccess();
    if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
        throw new IllegalArgumentException();
    }
    if((g = getThreadGroup()) != null) {
        if (newPriority > g.getMaxPriority()) {
            newPriority = g.getMaxPriority();
        }
        setPriority0(priority = newPriority);
    }
}
​
public final int getPriority() {
    return priority;
}

守护线程Daemon

守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地守护一些系统服务,比如垃圾回收线程,JIT线程就可以理解守护线程。

public class DaemonDemo {
    public static void main(String[] args) {
        Thread daemonThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        System.out.println("i am alive");
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        System.out.println("finally block");
                    }
                }
            }
        });
        daemonThread.setDaemon(true);
        daemonThread.start();
        //确保main线程结束前能给daemonThread能够分到时间片
        try {
            Thread.sleep(800);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出结果为:

i am alive
finally block
i am alive

上面的例子中daemodThread run()方法中是一个while死循环,会一直打印,但是当main线程结束后daemonThread就会退出所以不会出现死循环的情况。main线程先睡眠800ms保证daemonThread能够拥有一次时间片的机会,也就是说可以正常执行一次打印“i am alive”操作和一次finally块中"finally block"操作。紧接着main 线程结束后,daemonThread退出,这个时候只打印了"i am alive"并没有打印final块中的。

因此,这里需要注意的是守护线程在退出的时候并不会执行finally块中的代码,所以将释放资源等操作不要放在finally块中执行,这种操作是不安全的