《实战Java高并发程序设计》笔记

《实战Java高并发程序设计》笔记

Linus:The only place where parallelism matters is in graphical or the server side,where we already largely have it. Pushing it anywhere is just pointless

一 走入并行世界

1.2 你必须知道的几个概念:

同步和异步

同步:同步调用一旦开始,调用者必须等到方法返回后才能继续后续的行为

异步:异步方法更像一个消息传递,一旦开始,方法调用就会立刻返回,调用者可以继续后续的操作.异步方法通常会在一个线程中真实的执行.如果异步调用需要返回结果,那么当这个调用真实完成时,则会通知调用者.

并发和并行

并发:偏重于多个任务交替执行.

并行:真正意义的同时执行.

通常两者不做区分.

临界区

临界区用来表示一种公共资源或者说共享数据,可以被多个线程使用,但是每一次,只能有一个线程使用它,一旦临界区资源被调用,其他线程想要这个资源只能等待.

例子:打印机

阻塞和非阻塞

如上

死锁,饥饿,和活锁

死锁:

饥饿:某一个或者多个线程因为某种原因无法获得所需要的资源,导致一直无法执行下去.

活锁:如果两个线程的"智力"不够,且都秉持着"谦让"的原则,主动将资源释放给他人使用,那么导致两个资源不断地在两个线程之间跳动,没有一个线程可以同时拿到所有资源正常执行.

1.3 并发级别
  • [ ] 阻塞 无饥饿 无障碍 无锁 无等待
1.5 JMM

原子性:一个操作是不可被中断的

可见性:如果一个线程修改了一个全局变量,那么其他线程马上就可以知道这个改动.

  • [ ] 有序性:理解 指令重排
  • [ ] Happen-Before 规则:

二 Java 并行程序基础

2.1 线程

线程的定义:

线程的生命周期:线程的所有状态都在 Thread 中的 State 枚举中定义

public enum State{
  NEW,
  RUNNABLE,
  BLOCKED,
  WAITING,
  TIMED_WAITING,
  TERMINATED;
}

NEW 状态表示刚刚创建的线程,这种线程还没有开始执行.等到线程的 start()方法调用时,才表示线程开始执行,当线程执行时,处于 RUNNABLE 状态,表示线程所需的一切资源都准备好了,如果线程在执行过程遇到了 synchronized 同步块,就会进入 BLOCKED 阻塞状态,这时表示线程会暂停执行,直到获得请求的锁,WAITING 和 TIMED_WAITING都表示等待,TIMED_WAITING 表示一个有时限的等待.等待的事件,比如,通过 wait()方法等待 notify()方法,而通过join() 方法等待的线程则会等待目标线程的终止,一旦等到了期望的事件,线程就会再次执行,进入 RUNNABLE 状态.等线程执行完毕后,则进入 TERMINATED 状态,表示结束

2.2 初始线程

Thread t1 = new Thread();
t1.start;
Thread t1 = new Thread();
t1.run; //不能新建一个线程,而是在当前线程中调用 run 方法,只是作为一个普通的方法调用
Thread t1 = new Thread(){
  @Override
  public void run(){
    System.out.println("Hello I am t1");
  }
} //匿名内部类:创建一个继承自 Thread 的匿名类对象
t1.start;
public class CreatThread implements Runnable{
  @Overide
  public void run(){
    System.out.println("Oh,I am Runnable!");
  }
  publc static void main(String[] args){
    Thread t1 = new Thread(new CreateThread());
    t1.start();
  }
}

2.2.2 终止线程

为什么弃用 Thread.stop()

Thread.stop()方法在终止线程时,会直接终止线程,并立即释放这个线程所持有的锁,而这些锁恰恰是用来维持对象一致性的.

线程中断

线程中断并不会使线程立即退出,而是给线程发送一个通知,告诉目标线程,有人希望你退出了.至于目标线程接到通知后如何处理,则完全由目标线程自行决定.

public void Thread.interrupt() //中断线程(只是设置中断状态,并不是会停止线程)
public boolean Thread.isIntertupted() //判断是否被中断
public static boolean Thread.interrupted()//判断是否被中断,并清除当前中断状态
public class Test {
    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("被中断了!");
                        break;
                    }
                    try {
                        Thread.sleep(2000);//由于中断抛异常,此时会清楚中断标记
                    } catch (InterruptedException e) {
                        System.out.println("睡眠的时候被中断了!");
                        Thread.currentThread().interrupt();
                    }
                    Thread.yield();
                }
            }
        };
        t1.start();
        Thread.sleep(2000);
        t1.interrupt();
    }
}

//输出:睡眠的时候被中断了!
//        被中断了!
等待(wait)和通知(notify)

等待方法和通知方法并不是 Thread 类中的,而是在 Object 类中,这也意味着任何对象都可以调用这两个方法

在线程 A 中调用了 obj.wait() 方法,那么线程 A 就会停止执行,转为等待状态.线程 A 会一直等待到其他线程调用了 obj.notify()方法为止.这时,obj 显然成了多个线程之间通信的有效手段.

Object.wait()方法不能随便调用.他必须包含在对应的 synchronized 语句中,无论是 wait 方法还是 notify 方法,都必须先获得目标对象的一个监视器

如果一个线程调用了 wait 方法,那么他就会进入 object 对象的等待队列,这个队列中可能有多个线程,当 notify 方法被调用时,他就会从这个等待队列中 随机选择一个线程,并将其唤醒.这个选择是不公平的,完全随机的

注意:Object.wait()方法和 Thread.sleep()方法都可以让线程等待若干时间.除 wait()方法可以被唤醒外,另一个主要区别时 wait()方法会释放目标对象的锁,而 Thread.sleep()方法不会释放任何资源

2.2.6 等待线程结束(join)和谦让(yield)

join():需要等待依赖线程执行完毕,才能继续执行.

public final void join() throws InterruptedException//它会一直阻塞当前线程,直到目标线程执行完毕
public final synchronized void join(long millis) throws InterruptedException//给出了最大等待时间
  • [ ] 分析 join 方法的本质
public static native void yield()//会使当前线程让出 CPU,让出后依然会进行 CPU 资源的争夺
2.3 volatile 与 JMM

并不能真正保证线程安全,他只能确保一个线程修改了数据之后,其他线程能够看到这个改动.

2.4 线程组
public class ThreadGroupName implements Runnable{
    public static void main(String[] args) {
        ThreadGroup tg = new ThreadGroup("PrintGroup");
        Thread t1 = new Thread(tg,new ThreadGroupName(),"T1");
        Thread t2 = new Thread(tg,new ThreadGroupName(),"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 AM " + groupAndName);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }


    }
}
2.5 守护线程(Daemon)

如垃圾回收线程,JIT 线程都可以理解为守护线程.与之相对应的用户线程,用户线程可以理解为是系统的工作线程.如果用户线程全部结束,作为意味着这个程序实际上无事可做了,守护线程要守护的对象已经不存在了,那么整个应用程序就应该结束,因此,当一个 java 应用内只有守护线程时,java 虚拟机就会自然退出

public class DaemonDemo{
    public static class DaemonT 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 t1 = new Thread(new DaemonT());
        t1.setDaemon(true);//设置守护线程必须在线程 start()之前设置,否则会出异常
        t1.start();

        Thread.sleep(2000);
    }
}

2.6 线程优先级
high.setPriority(Thread.MAX_PRIORITY)//public final static int MIN_PRIORITY = 1
low.setPriority(Thread.MIN_PRIORITY)//10

优先级高的线程在竞争资源时会更有优势,但这依然是个概率问题

2.7 synchronized 关键字

简单用法整理:

  1. 指定加锁对象
  2. 直接作用于实例方法
  3. 直接作用于静态方法
2.8 初学者常犯的错误:错误的加锁

三 JDK 并发包

3.1.1 重入锁

对于 ReentrantLock 的几个重要方法,整理如下

  • lock():获得锁,如果被占用,则等待
  • lockInterruptibly():获得锁,但优先响应中断
  • tryLock():尝试获得锁,如果成功,则返回 true,失败则返回 false.该方法不等待,直接返回
  • tryLock(Long time,TimeUnit unit):在给定时间内尝试获得锁
  • unlock():释放锁
3.2.2 重入锁的好搭档:Condition
public class ReentrantLockCondition implements Runnable{
    public static ReentrantLock lock = new ReentrantLock();
    public static Condition condition = lock.newCondition();//记住怎么关联condition


    @Override
    public void run() {
        try{
            lock.lock();
            condition.await();//会让当前线程等待,同时释放当前锁,当其他线程使用 signal()方法时,线程会重新获得锁并继续执行,或者当线程中断时,也能跳出等待
            System.out.println("GOING ON");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

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


        Thread t1 = new Thread(new ReentrantLockCondition());
        t1.start();
        Thread.sleep(2000);
        lock.lock();
        condition.signal();
        lock.unlock();//如果如果不释放,虽然已经唤醒了线程t1,但是由于他无法重新获得锁,因而无法继续执行
    }
}
3.2 线程复用:线程池

(突然的感想,笔记记得越详细,脑子记得也就越牢固)

为什么会出现线程池:

  1. 如果为每一个小的任务都创建一个线程,则很有可能出现创建和销毁线程所占用的时间大于该线程真实工资所消耗的时间的情况,反而会得不偿失.
  2. 线程本身也占用内存空间,大量的线程会抢占珍贵的内存资源,大量的线程回收也会给 GC 造成压力,延长 GC 的停顿时间
3.2.2 jdk对线程池的支持

Executor 框架

Executors 类扮演着线程池工厂的角色,通过 Executors 可以取得一个拥有特定功能的线程池.

//Executors 类中的一些工厂方法如下
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue());
    }
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue()));
    }
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue());
    }
//  ThreadPoolExecutors extends 自 AbstractExecutorsService,后者 implements 自 ExecutorService,而 ExecutorService 又 extends  自Executor,具体情况查看源码.
public class ThreadPoolDemo implements Runnable{

    @Override
    public void run() {
        System.out.println(System.currentTimeMillis()+" Thread ID :"+Thread.currentThread().getId());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ThreadPoolDemo threadPoolDemo = new ThreadPoolDemo();
        ExecutorService es = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            es.submit(threadPoolDemo);

        }
    }
}
3.2.3 核心线程池的内部实现
/**
     * Creates a new {@code ThreadPoolExecutor} with the given initial
     * parameters.
     *
     * @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
     *                指定了线程池中的线程数量
     * @param maximumPoolSize the maximum number of threads to allow in the
     *        pool
     *                指定了线程池中的最大线程数量
     * @param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.
     *                当线程池线程数量超过 corePoolSize 时,多余的空闲线程存活的时间,即超过 corePoolSize 的空闲线程
     *                在多长时间内会被销毁
     * @param unit the time unit for the {@code keepAliveTime} argument
     *                keepAliveTime 的单位
     * @param workQueue the queue to use for holding tasks before they are
     *        executed.  This queue will hold only the {@code Runnable}
     *        tasks submitted by the {@code execute} method.
     *                任务队列,被提交但尚未执行的任务
     * @param threadFactory the factory to use when the executor
     *        creates a new thread
     *                线程工厂,用于创建线程,一般用默认的即可
     * @param handler the handler to use when execution is blocked
     *        because the thread bounds and queue capacities are reached
     *                拒绝策略,当任务太多来不及处理时,如何拒绝任务
     * @throws IllegalArgumentException if one of the following holds:
* {@code corePoolSize < 0}
* {@code keepAliveTime < 0}
* {@code maximumPoolSize <= 0}
* {@code maximumPoolSize < corePoolSize} * @throws NullPointerException if {@code workQueue} * or {@code threadFactory} or {@code handler} is null */ public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }

书上这里对 BlockingQueue workQueue 与 ThreadFactory threadFactory 与 RejectedExecutionHandler handler 解释的很清晰,我就不啰嗦了....也是因为字太多了...

要想理解线程池,需理解 :

  1. 自定义线程创建
  2. 拒绝策略
  3. 线程池的核心调度代码
  4. BlockingQueue
//把这个例子给理解好,就通了!
public class ThreadPoolDemo implements Runnable{

    @Override
    public void run() {
        System.out.println(System.currentTimeMillis()+" Thread ID :"+Thread.currentThread().getId());
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadPoolDemo threadPoolDemo = new ThreadPoolDemo();
        ExecutorService es = new ThreadPoolExecutor(5, 5, 0L,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingDeque(),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        Thread t = new Thread(r);
                        t.setDaemon(true);//设置守护线程
                        System.out.println("CREATE T");
                        return t;
                    }
                }
        );
        for (int i = 0; i < 10 ; i++) {
            es.submit(threadPoolDemo);
        }
        Thread.sleep(3000);//修改这个值,看看不一样的情况,理解一下

    }

}
3.2.6 扩展线程池
3.2.7 优化线程池线程数量
3.2.8 堆栈 去哪里了:在线程池中寻找堆栈
3.2.9 Fork/Join 框架

3.3 JDK 并发容器

四 锁的优化及注意事项

事实上,在单核 CPU 上采用并行算法的效率一般要低于原始的串行算法的效率,根本原因在于对于多线程应用来说,系统除了处理功能需求外,还需要额外维护多线程环境的特有信息,如线程本身的元数据,线程的调度,线程上下文的切换等.因此,并行计算之所以能提高系统的性能,并不是因为他"少干活"了,而是因为并行计算可以更合理的进行任务调度,充分利用各个 CPU 资源.

有助于提高锁性能的几点建议

1.减少所持有的时间

你可能感兴趣的:(java,并发编程)