读书笔记-Java高并发程序设计(一)

325 阅读7分钟

第一章

几个概念:

  • 同步和异步:我们在Java中常说的同步异步主要形容一次方法的调用,同步方法一经方法调用就必须等到方法结束返回值才能继续后续的行为;而异步方法一调用就立刻返回,调用者可以不受影响的继续后续行为,异步方法的工作交给另外一个线程去做。
  • 阻塞和非阻塞:阻塞和非阻塞通常用来形容多线程之间的影响;阻塞时,如果一个线程占用了临界区资源,那么所有需要访问这个资源的线程都需要等待,等待期间导致线程被挂起;非阻塞时,线程之间不互相影响他人的执行,所有线程都会尝试不断向前执行。
  • 并发级别:多线程之间的并发限制级别,主要分为,阻塞(悲观策略)、无饥饿、无障碍(乐观策略)、无锁、无等待。

同时,还需要了解与并发相关的几个原则:

  • 原子性:原子性形容一个操作是不可中断的,,即使在多线程环境中,一个操作一旦开始,就不会被其他线程干扰;例如多个线程同时对int类型静态全局变量进行赋值,虽然最终int变量可能是这些线程赋值的任意一个,但是多个线程之间的赋值操作是不互相干扰的;但是long类型则不同,因为long类型是64位,如果在32位的Java虚拟机中,对long类型的读写需要至少两次,那么long的读、写操作分为多次,不同线程之间互相干扰,不符合原子性。
  • 可见性:可见性是指当一个线程修改了某个共享变量的值,其他线程是否能立刻感知这个修改。
  • 有序性:有序性问题的原因是子啊程序执行时,可能会进行指令重排;而指令重排是为了充分压榨CPU执行效率,使用“流水线”的方式进行指令的执行,为了减少“流水线”执行的中断,将指令进行合理地重新排列。

第二章

1. 线程与进程的区别:

2. 线程的生命周期:

线程的生命周期
如图所示,是一个线程的整个生命周期,线程的状态在Thread中定义:

public enum State {
    /**
     * Thread state for a thread which has not yet started.
     */
    NEW,

    /**
     * 线程正在Java虚拟机中执行
     */
    RUNNABLE,

    /**
     * 线程遇到同步块或者同步方法,等待锁,阻塞
     */
    BLOCKED,

    /**
     * 被调用了wait()、join()、park()方法,处于等待状态,等待notify()或另一个线程的结束
     */
    WAITING,

    /**
     * 与WAITING类似,但是有时间限制
     */
    TIMED_WAITING,

    /**
     * The thread has completed execution.
     */
    TERMINATED;
}

3. 线程的基本操作

3.1 新建线程

在Java中,我们可以通过不同的方式创建新线程:

(1)Thread

Thread thread = new Thread(){
    @Override
    public void run(){
        System.out.println("thread1 is running!");
    }
};
thread.start();

上述代码使用了一名内部类,重写了Thread中的run方法,同样也可以自定义一个线程类去继承Thread,然后在类中重写run()方法;在这里我们注意到,thread调用其start()方法后线程开始执行,在start()方法的内部,新建一个新线程然后让这个线程执行run()方法。

但是有一点需要注意,不能直接调用Thread的run()方法,因为直接调用run()方法,只是在当前线程执行run()方法,而不是新创建线程。这也是start方法和run方法的最重要的一个区别。

(2)Runnable 由于Java是单继承的,所以继承关系显得比较珍贵,所以实现Runnable接口也是一种十分常用的创建线程的方式:

/**
 * Runnable的实现类,重写run方法
 */
public class RunnableImpl implements Runnable{
    @Override
    public void run() {
        System.out.println("new thread is running!");
    }
}

//新建Thread时传入Runnable的实现类,调用start方法执行线程
Thread thread = new Thread(new RunnableImpl());
thread.start();

我们也许仍有疑问,为什么调用Thread的start方法就能开启一个新线程执行方法,我们接下来一步步看看Thread内部是如何实现的:

public class Thread implements Runnable {
    private Runnable target;    //Runnable对象
    ...
    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    ...
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
}

由上述代码可知,Thread本身也是实现了Runnable接口,并且有一个参数为Runnable的构造函数,可以Runnable类型的初始化私有属性target,而Thread中默认的run()方法也是去尝试调用target的run方法。

3.2 终止线程与线程中断

我们可以使用stop()方法结束一个线程,但是这个方法已经被标注为废弃方法,因为stop()方法会立刻结束一个正在执行的线程,如果这个线程正在修改数据时被终止,就可能会造成数据的不一致,这将是一个很严重的问题。

除了stop()方法,我们可以可以设置一个标志位,在线程中每次循环都访问这个标志位,如果标志位为false就跳出循环,从而结束线程,这样就不会在线程的数据操作进行了一半时被暴力终止,造成数据的不一致。

而中断(interrupt)是Java中提供的比较完善的终止线程的工具,与上述使用标志位类似,中断并不是向stop方法一样立刻退出线程,而是通过设置标志位的方式通知线程该退出了,而线程在得到通知后如何处理完全由目标线程决定:

Thread thread = new Thread(){
    @Override
    public void run() {
        while (true){
            System.out.println("线程执行中。。");
            //利用Thread.currentThread().isInterrupted()判断是否需要被中断
            if (Thread.currentThread().isInterrupted()){
                System.out.println("线程中断!");
                //跳出循环后线程中断
                break;
            }
            do something..
            Thread.yield();
        }
    }
};
thread.start();
thread.interrupt();

但是使用Java提供的中断(interrupt)时有两点需要注意:

(1)isInterrupted()和interrupted()方法都可以判断线程是否中断,但是interrupted()在判断的基础上会清除当前的中断状态; (2)在线程为阻塞状态时,如果这时线程被中断就会抛出InterruptedException异常,如果我们使用Thread.interrupt()方法在catch异常后再次中断自己,那么在下次循环时就能退出循环,结束线程;

刚开始看到这里的时候,我有些疑惑这和我们手动设置自定义的标志位有何不同?如果我们的程序在循环体中如果正在处理数据时出现了中断异常InterruptedException,这时候线程会直接退出,因为后续数据操作的缺失而破坏数据的一致性,而执行了Thread.interrupt()方法再次中断自己,置中断标记位,继续进行后续的处理,保证数据的一致性和完整性。

3.3 wait和notify

wait和notify方法是为了支持多线程之间的协作,但是她们来自Object类而不是Thread,这意味着所有对象都可以调用这对两个方法。

在线程A中,调用了object.wait(),那么线程A就会停止运行转为等待状态,该线程进入object的等待队列,直到其他线程调用了object.notify()方法从等待队列中将其唤醒(完全随机,不公平的)。

但是值得注意的是,wait()、notify()方法并不是随便可以调用的,前提条件是要该线程获得目标对象的监视器,而wait()方法后释放这个监视器

wait()方法和sleep()方法都能让线程等待若干时间,除了wait()可以被唤醒外,wait()会释放对象锁,而sleep()不会释放任何资源。

3.4 join和yield

有时候,项目的某些工作会依赖于另一部分工作的完成,就好像炒菜要在准备食材之后才可以开始,而多线程间也存在着这种关系,Java中为我们提供了join()方法来满足这些需求。

如果在线程A中调用另一个线程的join()方法:

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread();
    thread.start();
    thread.join();
    do something..
}

上述代码表示当前线程会等到thread执行完成后继续执行。

yield()方法是一个静态方法,该方法一旦执行就会使当前线程让出CPU。但是需要注意的是,让出CPU并不代表接下来后续操作不会执行,执行了yield()方法确实是让出了CPU但是该线程仍然会去和其他线程竞争CPU资源。执行yield()方法的线程想表达这样一个意图:当前我重要的工作已经做完了,可以给其他需要的线程一个机会了。

参考

《Java高并发程序设计》--葛一鸣、郭超