《实战Java高并发程序设计》读书笔记(一):线程

第一章 走入并行世界

几个概念

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 {
    @Override
    public String call() throws Exception {
        return "hello";
    }

    public static void main(String[] args) {
        FutureTask 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);
    }
}

 

你可能感兴趣的:(读书笔记,Java多线程,读书笔记,并发编程)