《Java多线程编程核心技术》阅读(一)

一、 进程与线程的

  1.1 进程与线程的区别及查看方法

  •   进程是操作系统运行的一个程序单元,是系统管理的最基本单元,资源分配和调度的独立单位,单任务系统的缺点是存在任务等待时,CPU处于等待状态即空转,导致资源浪费,windows中可以在任务管理器查看每个进程,例如QQ.exe,linux中可以使用top或者ps查看。
  •   线程是进程中独立运行的子任务,例如windows中每个任务下的子任务,如下图:


    进程及线程示意图
  •   在linux中各个语言均提供了查看的命令及方法,例如java中可以使用jps查看各个java进程及其名称和pid,利用jstack job_id查看每个线程的运行状态。

  1.2 多线程的优势及劣势

  •   异步执行,降低CPU等待时空转的时间,通过多任务上下文切换充分利用CPU的资源,切换时每个线程会将独有的栈变量入栈,在下次获取到执行权时再次从栈中获取变量。
  •   利用阻塞时的空闲CPU资源 线程数≈(运行时间+阻塞时间)/ 运行时间,故线程数量并非越多越好,计算密集型任务线程数数量最好为N+1,IO密集型任务线程数量最好为2N+1
  •   均分运行资源,让多个任务同时推进而非只服务于一个任务。

  1.3 并发与并行的区别

  并发与并行均表示任务的一起执行,并发偏重于逻辑上的同时执行,并行侧重于物理并行例如硬件FPGA,由于线程切换快,感觉上就是并行,但实际上并非如此。

二、java中的线程基本操作

  2.1 java多线程创建入门

    main线程,垃圾回收线程(deamon)为最常见的线程,使用Thread.currentThead()获取当前线程,例如Thread.currentThead.getName(),Java中线程的创建方式主要有以下三种:

  •    继承Thead类重新run方法,然后执行start方法即可,一般会将对象传入Thead构造,可以方便的对线程进行其他操作以及隔离非线程相关的操作方法,例如设定优先级,命名等,Thead源码结构为public class Thread implements Runnable,由于java属于单继承,故一般不建议采用该方法来创建线程。
  •    实现Runnable接口,由Thead原型可知,可以直接实现Runnaable接口来创建线程,可以避免由于单继承导致无法继承其他类的弊端,Runnable接口原型如下:
@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface Runnable is used
     * to create a thread, starting the thread causes the object's
     * run method to be called in that separately executing
     * thread.
     * 

* The general contract of the method run is that it may * take any action whatsoever. * * @see java.lang.Thread#run() */ public abstract void run(); }

   实现类只需要实现run方法即可,由于Runnable接口同时也支持函数式编程,故也可以使用使用lambda表达式来创建一个匿名线程,实现Runnable接口后可以传入Thead构造方法创建线程,也可执行start

  •    实现Callable接口,可以创建带返回值的线程,接口原型如下:
@FunctionalInterface
public interface Callable {
    /**
     * 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;
}

    实现该接口的V call() throws Exception方法,该方法返回值是一个泛型,故需要在实现或者使用时指定泛型的类型,然后将Callable的实现类交由FutuerTask,封装成Thead的传入参数(实际还是Runnable),然后传入Thead再执行start方法,FutuerTask及其继承的接口原型如下

public class FutureTask implements RunnableFuture
public interface RunnableFuture extends Runnable, Future
public interface Future

    使用方法如下:

        // lambda表达式创建Callable throws Exception,并实现 V call()方法
        Callable callable = () -> {
            System.out.println('a');
            return 1;
        };
        // 封装成Runnable 供Thead创建线程
        FutureTask futureTask = new FutureTask<>(callable);
        Thread call = new Thread(futureTask,"futureTask");
        call.start();
        // 获取返回值
        System.out.println(futureTask.get());

  2.2 三种创建方式的比较

  •   继承Thead来创建线程,有利于类方便的获取线程的各种状态,比如alivename等,但缺点在于无法再继承其他类且无返回值
  •   实现Runnable接口创建线程,需要实现Runnable接口,可以避免java的单继承问题,但只能通过Thead.currentThead来获取线程的状态和属性,无返回值.
  •   实现Callable接口再结合FutureTask既可以避免单继承问题,也可以有返回值FutureTask.get或者超时版本get(long timeout, TimeUnit unit)来拿到线程的执行结果,有利于异步计算结果汇总。

  2.2 线程安全

  •   在每个线程变量都独立时即没有共享变量,各个都能够独立且正确的完成工作,若存在共享变量则由于内存空间和每个线程的栈数据不一致的情况,这块是重点,后续再仔细分析。

  2.3 线程的基本信息

  •    直接查看Thead类的属性即可,主要关注的属性如下:

    private volatile String name;// 线程名称,养成好的编程习惯,应该给线程命名利于调试
    private int            priority;    // 线程优先级,越大优先级越高 1-10 默认为5
    private boolean     daemon = false;// 是否是守护线程
    private long stackSize; // 线程堆栈大小 默认1MByte
    private long tid;          // 线程ID
    private static long threadSeqNumber; // 静态的线程计数器,用于给新线程增加ID
    public final static int MIN_PRIORITY = 1; //最小优先级
    public final static int NORM_PRIORITY = 5;// 默认优先级
    public final static int MAX_PRIORITY = 10;// 最大优先级
    private volatile int threadStatus = 0; //线程的状态
//状态定义为Thead的一个内部枚举类:
    public enum State{
      NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
    }
  •     线程分为守护线程用户线程除了守护线程就是用户线程,setDeamon(boolean on) 必须在start调用前,否则会抛出异常,例如垃圾回收线程,程序运行完毕,JVM会等待用户线程退出而不会等待守护线程,jstack会包含'deamon'字样的为守护线程.

 2.4 线程的状态

  进入任意java线程的实现类均可查看到对应的状态,进入Thead.State枚举类可查看各个状态和说明,各个状态如下:

      NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED;
  •    NEW 新建后尚未启动的线程,即start方法执行前的状态,需要注意的是对于执行start方法后的线程状态是不确定的,因为无法明确知道线程会进入哪种状态,测试过程如下:
        // 封装成Runnable 供Thead创建线程
        FutureTask futureTask = new FutureTask<>(callable);
        Thread call = new Thread(futureTask, "futureTask");
        int a = 1;
        while (a == 1){
            System.out.println(call.getState());// 中断打印出NEW 
        }
        call.start();
  •    TERMINATED线程任务完成后,退出之后的状态。
  •    RUNNABLE中文翻译:RUNNABLE状态表示线程正在java虚拟机中执行,但可能正在阻塞等待其他资源,例如处理器。既然是等待资源,那么存在IO阻塞时线程状态也依然是RUNNABLE状态利用jsp查看java进程id,然后jstack id,即可查看该进程下的所有线程.
"main" #1 prio=5 os_prio=0 tid=0x0303dc00 nid=0x3184 runnable [0x02e9f000]  //线程信息概览
   java.lang.Thread.State: RUNNABLE        // 注意该处状态为 RUNNABLE
        at java.io.FileOutputStream.writeBytes(Native Method)// 以下为当前执行的方法栈
        at java.io.FileOutputStream.write(FileOutputStream.java:326)
        at java.io.BufferedOutputStream.flushBuffer(BufferedOutputStream.java:82)
        at java.io.BufferedOutputStream.flush(BufferedOutputStream.java:140)
        - locked <0x0a9d48d8> (a java.io.BufferedOutputStream)
        at java.io.PrintStream.write(PrintStream.java:482)
        - locked <0x0a9b41e8> (a java.io.PrintStream)
        at sun.nio.cs.StreamEncoder.writeBytes(StreamEncoder.java:221)
        at sun.nio.cs.StreamEncoder.implFlushBuffer(StreamEncoder.java:291)
        at sun.nio.cs.StreamEncoder.flushBuffer(StreamEncoder.java:104)
        - locked <0x0a9b4188> (a java.io.OutputStreamWriter)
        at java.io.OutputStreamWriter.flushBuffer(OutputStreamWriter.java:185)
        at java.io.PrintStream.newLine(PrintStream.java:546)
        - locked <0x0a9b41e8> (a java.io.PrintStream)// 此处可以看出,print方法是线程安全的
        at java.io.PrintStream.println(PrintStream.java:824)
        - locked <0x0a9b41e8> (a java.io.PrintStream)// 此处可以看出,print方法是线程安全的
        at com.simple.TicketDemo.main(TicketDemo.java:54)
  •    BLOCKED翻译线:线程状态被阻塞,等待监视器锁定。 *处于阻塞状态的线程正在等待监视器锁定输入同步块/方法或调用 {@link Object#wait()Object.wait}后重新进入同步块/方法,阻塞状态不涉及进程外的阻塞(如IO阻塞),只描述JVM内部并发/主动休眠等原因导致的线程阻塞,3种细分:

   (1)blocked专指等待monitor进入synchronized块或方法的线程状态,利用上述jstack的方法获取其状态如下:

          java.lang.Thread.State: BLOCKED (on object monitor)

   (2)waiting,两个方法会导致线程进入该状态Unsafe.getUnsafe.park(boolean var1, long var2)Object.wait(),前者用于阻塞某个线程,典型场景是使用了JUC包内提供的同步器或同步数据结构,它们的内部依赖LockSupport类阻塞线程,该类进一步调用了Unsafe.park(), 它的jstack输出为:

        java.lang.Thread.State: WAITING (parking)

   后者jstack输出如下。Thread#join()也是基于java自带的monitor/condition机制实现的:

      java.lang.Thread.State: WAITING (on object monitor)

   (3)timed_waitingUnsafe.park()Object#wait()的超时版本或者Thead.sleep(long ms)会让线程进入这个状态:

      java.lang.Thread.State: TIMED_WAITING (sleeping)
线程状态转换图

  2.5 线程常用的方法

  •    Thead.currentThead()获取当前代码段正在被哪个线程调用以及该线程的信息。
  •    isAlive()获取当前线程是否处于活动状态,活动状态指的是已经启动但未终止的状态,即线程状态转换图中的黄色、绿色以及红色状态。
  •    sleep()让当前线程处于休眠状态X毫秒(已经放弃持有锁,否则将占锁休眠),此处会放弃CPU资源,系统会再次调度线程CPU资源分配(不区分优先级)
  •    静态方法Thead.yield ()让当前线程重新执行,即放弃CPU资源并再次获取,此时CPU资源只能被当前同优先级或者更高优先级的线程获得。
  •    非静态的join()让一个线程 B “加入”到另外一个线程 A 的尾部。在线程 A 执行完毕之前,线程 B 不能工作,即在B中执行A.join(),会使B线程阻塞至A线程执行完毕。
    Thread t = new MyThread();
    t.start();// 启动线程t
    t.join();// 停止执行阻塞等待线程t执行完毕,若线程t没有存活则执行跳过。
  •    sleepyield方法的区别:
        (1)sleep(long ms)有参数,而yield()无参数,二者都为Thead类的静态方法。
        (2)sleep会放弃当前线程的CPU资源(无锁),其它所有线程均可争夺CPU资源,包括执行sleep(0),sleep(0)的可用于当前线程执行需要的资源还未完备时,暂时放弃执行,以提高CPU使用率
        (3)yield会放弃执行权,然后重新争夺CPU资源,此时只有与yield同优先级或者高优先级的线程才可能拿到CPU资源。
        (4)线程执行sleep方法后转入阻塞(blocked)状态,而执行 yield 方法后转入就绪(ready)状态.

  2.5 停止线程

  •    停止线程共有三种方法:
        (1)使用stop()暴力停止线程,立即停止一个正在运行的线程,该方法是不安全的,例如事务操作中强行停止一个线程可能造成意想不到的意外未及时释放锁资源将导致数据不一致
        (2)使用标志,然后利用该标志判断是否需要return
        (3)使用interrupt和异常中断线程,该方法只提供一个标志位供线程使用,并不会直接停止线程。
class Ticket {
    private int number = 3000000;
    Lock lock = new ReentrantLock();

    public void sale() {
        //lock.lock();
        //try {
            while (number > 0) {
                //Thread.sleep(100);
                System.out.println(Thread.currentThread().getName() + "输出第 " + (number--) + " 张票");
            }
        //} catch (Exception e) {
          //  e.printStackTrace();
       // } finally {
           // lock.unlock();
        //}
    }
}
//Thread sale1 = new Thread(ticket::sale, "售票员1");
//sale1.start();
//sale1.interrupt();  并不能停止目标线程,在线程中捕获interruptedException异常,可以安全的处理异常和退出线程
  •    判断线程是否停止:
        (1)Thread#interrupt() 方法,用于中断线程。调用该方法的线程的状态为将被置为”中断”状态,执行后对应的线程会抛出interruptedExeception,该异常可由线程捕获并做出对应的处理。
        (2)Thread#interrupted() 静态方法,用于查询当前线程状态,并清除状态置为false,若线程被中断,第一次调用返回true,第二次调用就会返回false.
// Thread.java
public static boolean interrupted() {
    return currentThread().isInterrupted(true); // 清理
}
private native boolean isInterrupted(boolean ClearInterrupted);

     (3)Thread#interrupted() 方法,查询中断状态,不会清除状态。

// Thread.java

public boolean isInterrupted() {
    return isInterrupted(false); // 不清除
}
private native boolean isInterrupted(boolean ClearInterrupted);

  2.2 线程的优先级与暂停线程

   suspend()resume可用于暂停与恢复线程执行,其具有独占与不同步的缺点,目前已经启用@Deprecated
   
线程的优先级会继承创建它的线程的优先级,CPU尽量将执行资源让给优先级高的线程,但不是全部具有一定规则性,说明线程优先级与代码块执行顺序无关,线程还具有随机性,即高优先级的线程不一定先执行完,总结为线程的优先级具有继承性、规则性、随机性三大特点。

你可能感兴趣的:(《Java多线程编程核心技术》阅读(一))