实战java高并发程序设计读书笔记(上)

2020/07/05

第一章 走入并行世界

几个概念

1、同步(Synchronous)和异步(Asynchronous)

通常用来形容一次方法调用。同步方法调用一旦开始,必须等到方法调用返回后,才能继续后续的行为。异步方法调用更像是一个消息传递,一旦开始,方法调用就会立即返回,调用者可以继续后续的操作,异步方法通常会在另外一个线程中“真实”的执行。

2、并发(Concurrency)和并行(Parallelism)

表示多个任务一起执行。并发偏重于多个任务交替执行,而多个任务之间可能还是串行的,只是对于外部观察者来说,看起来像是同时执行的。并行指的是真正意义上的“同时执行”。

3、临界区

表示一种公共资源或者共享数据,可以被多个线程使用,但是一次只能有一个线程使用,一旦被占用,其他线程想使用就必须等待。

4、阻塞(Blocking)和非阻塞(Non-Blocking)

形容多线程间的互相影响。比如一个线程占用了临界区资源,其他线程就必须等待,导致线程挂起,叫做阻塞。非阻塞强调没有一个线程可以妨碍其他线程执行。

5、死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock)

死锁:两个或多个线程都在等待对方释放锁,导致线程都处于阻塞状态。

饥饿:一个或多个线程由于种种原因无法获得所需要的资源,导致一直无法执行。

活锁:线程为了彼此间的相应而互相礼让,导致资源不断在两个线程间跳动,却没有一个线程正常执行。

并发级别

1、阻塞

线程无法执行时进入阻塞状态。

2、无饥饿(Starvation-Free)

保证线程都有机会执行。

3、无障碍(Obstruction-Free)

乐观策略,认为多个线程之间的冲突几率不大,所有线程都无障碍的执行,检测到冲突后进行回滚。

4、无锁(Lock-Free)

包含一个无穷循环,在这个循环中,线程会不断尝试修改共享变量。无锁的并行总能保证有一个线程在有限步内是可以完成的。

5、无等待(Wait-Free)

要求所有的线程必须在有限步内完成。

回到Java:JMM

Java内存模型(JMM)的关键技术点都是围绕着多线程的原子性、可见性和有序性来建立的。

1、原子性(Atomicity)

指一个操作是不可中断的。

2、可见性(Visibility)

指一个线程修改了某一个共享变量的值时,其他线程是否能够立即知道这个修改。

3、有序性(Ordering)

程序在执行时,可能会发生指令重排,指令重排后的指令与原指令顺序未必一致。

注意:对于一个线程来说,他看到的指令执行顺序一定是一致的,指令重排不会使串行的语义逻辑发生问题。

4、哪些指令不能重排:Happen-Before规则

(1)程序顺序原则:一个线程内保证语义的串行性。

(2)volatile规则:volatile变量的写先于读发生,这保证了volatile变量的可见性。

(3)锁规则:解锁(unlock)必然发生在随后的加锁(lock)前。

(4)传递性:A先于B,B先于C,那么A必然先于C。

(5)线程的start()方法先于它的每一个动作。

(6)线程的所有操作先于线程的终结。

(7)线程的中断(interrupt())先于被中断线程的代码。

(8)对象的构造函数的执行、结束先于finalize()方法。

第二章 Java并行程序基础

线程和进程的关系:进程是线程的容器,进程可以容纳若干个线程。

线程的生命周期:

2.2 线程的基本操作

1、新建线程

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。Java可以用三种方法来创建线程:

(1)继承Thread类创建线程

通过定义Thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。然后创建Thread子类的实例,也就是创建了线程对象。最后启动线程,即调用线程的start()方法。

public class myThread1 extends Thread {

    public static void main(String[] args) {

        myThread1 t1=new myThread1();

        t1.start();

    }

    @Override

    public void run() {

        System.out.println("hello");
    }

}

(2)实现Runnable接口创建线程

Runnable是一个单方法接口,它只有一个run()方法。这也是最常用的方法。

public class myThread2 implements Runnable {


    public static void main(String[] args) {


        Thread t2 = new Thread(new myThread2());

        t2.start();


    }

    @Override

    public void run() {

        System.out.println("hello");

    }


}

(3)使用Callable和Future创建线程

*和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法**功能要强大。*

Callable接口是一个泛型接口,call()方法可以有返回值,其返回值的类型就是传递进来的类型。

call()方法可以声明抛出异常

Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。

创建并启动有返回值的线程的步骤如下:

(1)创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。

(2)使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值

(3)使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)

(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

public class myThread3 implements Callable<String> {

    @Override

    public String call() throws Exception {

        return "hello"

    }


    public static void main(String[] args) {

        FutureTask<String> futureTask = new FutureTask<>(new myThread3());

        Thread t3 = new Thread(futureTask);

        t3.start();

        try {

            System.out.println(futureTask.get());

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

2、终止线程

可以使用stop()方法来强制终止线程,但是强烈不推荐使用,有可能会引起未知错误。

建议使用退出标志来终止线程:一般来说,当run方法执行完后,线程就会退出。但有时run方法是永远不会结束的。如在服务端程序中使用线程进行监听客户端请求,或是其他的需要循环处理的任务。 在这种情况下,一般是将这些任务放在一个循环中,如while循环。如果想让循环永远运行下去,可以使用while(true){……}来处理。但要想使 while循环在某一特定条件下退出,最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出。

3、线程中断

线程中断并不会使线程立即退出,而是给线程发送一个通知,线程间接收到通知后如何处理,由目标线程自行决定。

public void Thread.interrupt()              //通知目标线程中断,即设置中断标志位。

public boolean Thread.isInterrupted()       //通过检查中断标志位判断目标线程是否被中断

public static boolean Thread.interrupted()  //判断当前线程是否中断,并清除中断标志位状态

一个简单的例子:

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

        Thread t1=new Thread(){

            @Override

            public void run() {

                while (true) {

                    if (Thread.currentThread().isInterrupted()) {

                        System.out.println("Interruted!");

                        break;

                    }

                    Thread.yield();

                }

            }

        };

        t1.start();

        t1.interrupt();

    }

}

Thread.sleep() 方法会让当前线程休眠若干时间,当线程在sleep()休眠时,如果被中断,就会产生InterruptedException异常。需要捕获异常进行处理。

4、等待(wait)和通知(notify)

等待wait() 方法和通知notify() 方法并不是Thread类中的,而是输出Object类。任何对象都可以调用这两个方法。

当在一个对象实例上调用了obj.wait() 方法后,当前线程就会在这个对象上等待(进入等待队列),直到有其他线程调用了obj.notify() 方法,它就从等待队列中随机唤醒一个线程(notifyAll() 会唤醒所有线程)。

Object.wait() 方法和notify() 方法必须包含在对应的synchronized语句中,在执行方法前首先需要获得目标对象的一个监视器。

public class SimpleWN {

    final static Object object = new Object();

    public static class T1 extends Thread {

        @Override

        public void run() {

            synchronized (object) {

                System.out.println(System.currentTimeMillis() + ":T1 start!");

                try {

                    System.out.println(System.currentTimeMillis()+":T1 wait for object");

                    object.wait();

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

            }

            System.out.println(System.currentTimeMillis()+":T1 end!");

        }

    }

    public static class T2 extends Thread {

        @Override

        public void run() {

            synchronized (object) {

                System.out.println(System.currentTimeMillis() + ":T2 start!notify one thread");

                object.notify();

                System.out.println(System.currentTimeMillis()+":T2 end!");

                try {

                    Thread.sleep(3000);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

            }

        }

    }
    public static void main(String[] args) {

        Thread t1 = new T1();

        Thread t2 = new T2();

        t1.start();t2.start();

    }

}

输出如下:

1553926016134:T1 start!

1553926016134:T1 wait for object

1553926016140:T2 start!notify one thread

1553926016140:T2 end!

1553926019141:T1 end!

T1先申请锁,wait() 方法执行后,T1进行等待,释放锁。T2获得锁,执行notify() 方法,然后休眠三秒后释放锁。T1得到通知后开始尝试获得锁,但是在三秒之后才能获得锁执行。

wait() 和sleep() 方法的重要区别是wait() 会释放目标对象的锁,而sleep() 方法不会释放任何资源。

5、挂起(suspend)和继续执行(resume)线程

方法已废弃,不推荐使用,因为挂起时不释放任何锁资源。

6、等待线程结束(join)和谦让(yeild)

一个线程的输入可能依赖于另一个线程或多个线程的输出,需要等待依赖线程执行完毕,才能继续执行,使用join() 方法实现这个功能。

public class JoinMain {

    public volatile static int i=0;

    public static class AddThread extends Thread {

        @Override

        public void run() {

            while (i < 1000000){

                i++;

            };

        }

    }


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

        AddThread at = new AddThread();

        at.start();

        at.join();//join方法的本质是让调用线程wait() 方法在当前线程对象实例上

        System.out.println(i);

    }

}

在主函数中,如果不使用join()方法等待AddThread,那么得到的i很可能是0或者一个非常小的数字。因为AddThread还没开始执行,i的值就已经被输出了。但在使用join()方法后,主线程等待AddThread执行完毕才执行,输出为1000000。join() 方法的本质是让调用线程wait()方法在当前线程对象实例上。

Thread.yeild() 方法是一个静态方法,一旦执行,会使当前线程让出CPU。但其之后还会重新参与CPU资源的争夺。

2.3 volatile与Java内存模型(JMM)

volatile定义:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。

实现原理:有volatile变量修饰的共享变量进行写操作时,转变为汇编代码时会增加一行Lock前缀的指令,该指令会做两件事情:

将当前处理器缓存行的数据写回到系统内存;这个写回内存的操作使其他CPU里缓存了该内存地址的数据无效。

当用volatile(易变的,不稳定的)关键字声明一个变量时,就等于告诉虚拟机,这个变量极有可能被某些程序或者线程修改。为了确保这个变量被修改后,所有线程都能“看到”这个改动,虚拟机就必须保证这个变量的可见性等特点。

public class Visibility {

    private static volatile boolean ready;//如果不使用volatile关键字,那么ReaderThread线程无法看到主线程的修改,导致ReaderThread线程永远无法退出

    private static int number;

    private static class ReaderThread extends Thread {

        @Override

        public void run() {

            while (!ready);

            System.out.println(number);

        }

    }

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

        new ReaderThread().start();

        Thread.sleep(1000);

        number=42;

        ready = true;

        Thread.sleep(1000);

    }

}

关键字volatile并不能代替锁,也无法保证复合操作(如i++)的原子性。

2.4 分门别类的管理:线程组

相同功能的线程可以放到一个线程组里。

public class ThreadGroupTest implements Runnable {

    public static void main(String[] args) {

        ThreadGroup tg = new ThreadGroup("PrintGroup");//建立线程组

        Thread t1 = new Thread(tg, new ThreadGroupTest(), "T1");//创建线程时指定所属的线程组

        Thread t2 = new Thread(tg, new ThreadGroupTest(), "T2");

        t1.start();
        
        t2.start();
        
        System.out.println(tg.activeCount());//获得活动线程的总数

        tg.list();

    }

    @Override

    public void run() {

        String groupAndName = Thread.currentThread().getThreadGroup().getName() +

                "-" + Thread.currentThread().getName();

        while (true) {

            System.out.println("i'm " + groupAndName);

            try {

                Thread.sleep(3000);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

        }

    }

}

2.5 驻守后台:守护线程(Daemon)

守护线程是一种特殊的线程,在后台完成一些系统性的服务。

public class DaemonTest {

    public static class myDaemon extends Thread {

        @Override

        public void run() {

            while (true) {

                System.out.println("I am alive");

                try {

                    Thread.sleep(1000);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

            }

        }

    }


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

        Thread t = new myDaemon();
        
        t.setDaemon(true);//把线程设置为守护线程,必须在start()之前

        t.start();

        Thread.sleep(2000);

    }

}

当一个Java应用中只有守护线程时,虚拟机就会自动退出。上述代码中若不把线程设置为守护线程,那t线程就会不停打印输出。

2.6 先做重要的事:线程优先级

在Java中,使用1到10表示线程优先级,数字越大优先级越高。使用setPriority()方法对线程进行设置,高优先级的线程倾向于更快的完成。

2.7 线程安全的概念与关键字synchronized

synchronized的用法:

(1)指定加锁对象:给指定对象加锁,进入同步代码前要获得给定对象的锁

public class AccountingSync implements Runnable {

    static AccountingSync instance = new AccountingSync();

    static int i=0;

    @Override//将关键字synchronized作用于一个给定对象instance
             //因此,每次线程进入被关键字synchronized包裹的代码段时,就会请求instance实例的锁。
    public void run() {

        for (int j = 0; j < 100000; j++) {
            
            synchronized (instance) {

                i++;

            }

        }

    }

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

        Thread t1 = new Thread(instance);

        Thread t2 = new Thread(instance);

        t1.start();t2.start();

        t1.join();t2.join();

        System.out.println(i);

    }
}

(2)直接作用于实例方法:相当于对当前对象实例加锁,进入同步代码前要获得当前实例的锁。

    public synchronized void increase() {   
        i++;
    }

    @Override
    public void run() {
        for (int j = 0; j < 100000; j++) 
            increase();
        }
    }
//其他代码同上

(3)直接作用于静态方法:相当于对当前加锁,进入同步代码前要获得当前类的锁。

public class AccountingSync3 implements Runnable {
    static int i=0;
    public static synchronized void increase() {
        i++;
    }
    
    @Override
    public void run() {
        for (int j = 0; j < 100000; j++) 
            increase();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new AccountingSync3());//注意,只用对当前类加锁后才能使用这种方式
//        因为如果只是对方法加锁时t1和t2线程指向了不同的对象实例。
   
        Thread t2 = new Thread(new AccountingSync3());
        t1.start();t2.start();

        t1.join();t2.join();
        
        System.out.println(i);
    }
}

Post Directory