JAVA高并发程序设计(葛一鸣著)读书笔记

本文为JAVA高并发程序设计(葛一鸣著)读书笔记。这本书对于刚刚入门的童鞋来讲可能有点深,我推荐可以先看看Java多线程编程核心技术(高洪岩著)一书。

第一章 走入并行世界

什么是同步和异步?
同步就是必须做完了一件事才能去做另外一件事。
异步就是一旦开始,方法调用就会立即返回。异步方法会在另外一个线程里面执行。如果有返回值的话,等异步方法执行完成就会通知调用者。

并发和并行?
并行就是真实的同时在进行运算,需要多核CPU或多个CPU。
并发就是快速切换,让你以为是同事再进行。

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

阻塞与非阻塞
阻塞和非阻塞用来形容多线程之间的互相影响。比如一个线程占用了临界区的资源,那么其他所有需要这个资源的线程就必须在临界区中等待。等待会使线程挂起,就叫做阻塞。
非阻塞强调的是没有一个线程可以妨碍其他线程的执行。

死锁,饥饿锁和活锁
死锁指的是都占用了别人的跑道,导致大家到运行不了了。
饥饿锁指的是线程的优先级太低,一直有优先级较高的在执行。
活锁指的是线程们都都在礼让其他的线程,导致谁也没有执行。
如下,为我之前写的死锁的一段代码:

/**
 * 实现一个死锁
 * 
 * @author YangHang
 * @Date 2019年1月13日 下午9:29:56
 *
 */
public class DeadLock {
    
    public static void main(String[] args) {

        MyThread1 thread1 = new MyThread1(new DeadLockObject());
        
        MyThread2 thread2 = new MyThread2(new DeadLockObject());
        
        thread1.start();
        thread2.start();
    }

}

class DeadLockObject {

    public void method1() {

        synchronized (DeadLockObject.class) {
            out.println(Thread.currentThread().getName() + "进入了method1的锁1");

            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
            }

            synchronized (Object.class) {
                out.println(Thread.currentThread().getName() + "进入了method1的锁2");
                out.println("method1执行成功!!!");
            }

        }
    }

    public void method2() {

        synchronized (Object.class) {
            out.println(Thread.currentThread().getName() + "进入了method2的锁1");

            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
            }

            synchronized (DeadLockObject.class) {
                out.println(Thread.currentThread().getName() + "进入了method2的锁2");
                out.println("method2执行成功!!!");
            }
        }

    }
}

class MyThread1 extends Thread {

    private DeadLockObject object;

    public MyThread1(DeadLockObject object) {
        this.object = object;
    }

    @Override
    public void run() {
        object.method1();
    }

}

class MyThread2 extends Thread {

    private DeadLockObject object;

    public MyThread2(DeadLockObject object) {
        this.object = object;
    }

    @Override
    public void run() {
        object.method2();
    }

}
并发的级别

由于临界区的存在,多线程之间的并发必须要受到控制。根据控制并发的策略,我们可以把并发的级别分为阻塞、无饥饿、无障碍、无锁、无等待几种。

阻塞
一个线程是阻塞的,那么其他线程释放资源之前,当前线程无法继续执行。当我们使用synchrobized关键字或重入锁时,我们得到的就是阻塞的线程。

无饥饿
首先饥饿是怎么产生的呢,是因为低优先级的线程始终没有办法执行,从而导致了饥饿。无饥饿就是锁是公平的,所有来的线程都要排队,这样就不会产生饥饿了。

无障碍
就是所有的线程都可以大摇大摆的走进临界区去修改数据,如果数据修改坏了就回滚,最终可能导致没有一条线程可以走出来。

无锁
在无锁的情况下,所有的线程都能尝试对临界区进行访问,但不同的是,无锁的并发保证必然有一个线程能在有限的步骤内完成操作离开临界区。

无等待
无等待要求所有的线程都必须在有限的步骤内完成,这样就不会引起饥饿的问题。

第二章 JAVA并行程序基础

进程和线程
进程就是线程的容器,线程就是轻量级的进程。

线程的生命周期
在这里插入图片描述
其中WAITING和TIMED_WATIING是有时限的等待和无时限的等待。

新建线程的两种方式:

/**
 * <pre>
 * 新建线程的两种方式
 *   1. 继承Thread类
 *   2. 实现Runnable接口
 * </pre>
 *
 * @author YangHang
 * @Date 2019/3/21 12:14
 */
public class NewThread extends Thread {

    @Override
    public void run() {
        System.out.println("您好, 多线程!");
    }

    public static void main(String[] args) {
        Thread t1 = new NewThread();
        t1.start();

        Thread t2 = new Thread(new NewThread2());
        t2.start();
    }
}

class NewThread2 implements Runnable {
    public void run() {
        System.out.println("您好, 多线程!");
    }
}

终止线程
stop方法,已经过时,使用这个方法终止线程,有可能造成数据不一致。
可以使用return关键字来终止线程。理解起来就是通过return关键字跳出当前的方法。

线程中断
interrupt方法不是真正的中断线程,而是给其一个中断标志位,具体要不要中断,还得看线程自己本身的处理。
Thread.interrupt //中断线程
Thread.isInterrupted() //判断是否被中断
Thread.interrupted() //判断是否被中断,并清除当前中断状态。

等待(wait)和通知(notify)
首先,wait、notify、notifyall必須在同步代码块中调用。否则会抛出运行时错误。
另外notify会随机唤醒一个该锁对象等待队列中的一个线程,而notifyall会全部唤醒。
另外调用notify或者notifyall不会马上唤醒,而是执行完线程中的内容再去唤醒。
每一个对象都有自己等待队列,当某个线程执行到了obj.wait();方法的时候就会进入这个对象的等待队列,当再有线程执行到obj.notify();方法的时候会随机在等待队列中唤醒一个线程(非公平)。
另外调用wait、notify和notifyall方法必须先获得对应对象的监视器,也就是锁为对应对象的同步方法。否则会抛运行时异常。

/**
 * 验证wait和notify的API
 * 
 * @see Object#wait()
 * @see Object#notify()
 * 
 * @author YangHang
 * @Date 2019年1月16日 下午10:29:49
 *
 */
public class WaitAndNotify {

    public static void main(String[] args) {

        WaitAndNotifyObject object = new WaitAndNotifyObject();
        
        MyThread1 thread1 = new MyThread1(object);
        MyThread2 thread2 = new MyThread2(object);
        MyThread3 thread3 = new MyThread3(object);
        
        thread1.setName("A");
        thread2.setName("B");
        thread3.setName("C");
        
        thread1.start();
        thread3.start();
        
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
        }
        
        thread2.start();
    }
}

class WaitAndNotifyObject {

    public synchronized void method1() {

        for (int i = 0; i < 100; i++) {
            out.println(Thread.currentThread().getName() + " : " + i);

            if (i == 33) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                }
            }
        }
    }

    public synchronized void method2() {

        for (int i = 0; i < 100; i++) {
            out.println(Thread.currentThread().getName() + " : " + i);

            if (i == 55) {
                this.notifyAll();
            }
        }
    }
}

class MyThread1 extends Thread {

    private WaitAndNotifyObject object;
    
    public MyThread1(WaitAndNotifyObject object) {
        this.object = object;
    }
    
    @Override
    public void run() {
        
        object.method1();
    }
    
}

class MyThread2 extends Thread {

    private WaitAndNotifyObject object;
    
    public MyThread2(WaitAndNotifyObject object) {
        this.object = object;
    }
    
    @Override
    public void run() {
        
        object.method2();
    }
    
}

class MyThread3 extends Thread {

    private WaitAndNotifyObject object;
    
    public MyThread3(WaitAndNotifyObject object) {
        this.object = object;
    }
    
    @Override
    public void run() {
        
        object.method1();
    }
    
}

挂起和继续执行
这两个方法也是被废弃的,不推荐使用suspend();去挂起线程的原因是它在挂起线程的同时不会释放任何锁,直到resume();操作.如果resume();方法执行到了suspend();方法之前就GG了,而且调用了suspend();方法之后,线程还处于Runnable状态,坑爹,这怎么排查。
那么,如何用wait和notify方法实现线程的挂起和继续执行呢?

class GoodSuspendObject {

    private boolean isSuspend;

    /**
     * 使线程挂起的方法
     */
    public synchronized void susp() {

        if (isSuspend == false) {
            try {
                isSuspend = true;
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 使线程继续执行的方法
     */
    public synchronized void resu() {

        if (isSuspend == true) {
            this.notify();
            isSuspend = false;
        }
    }

}

等待线程结束和谦让
在某个线程里面调用某个线程的join();方法就是说我让某个线程先执行。然后我再执行。
join(); 无限等待
join(long millis); 最多等待millis,再多我就不等了。

/**
 * <pre>
 * {@link Thread#join()} 
 * 演示join线程的例子
 * </pre>
 * 
 * @author YangHang
 * @Date 2019年3月21日 下午9:23:33
 *
 */
public class JoinTest {

    public volatile static int x;

    public static class AddThread extends Thread {

        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                x++;
            }
        }

    }

    public static void main(String[] args) {

        AddThread t1 = new AddThread();
        t1.start();
        try {
            t1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println(x);
        
    }
}

yield方法就是让当前的线程让出CPU,意思就是告诉CPU我最近不想执行了,但是会立即开始抢资源,没准有立即抢到了,你说气人不气人。

volatile与JMM(Java内存模型)
volatile就是告诉虚拟机,用这个关键字修饰的变量随时有可能被修改,修改之后要马上通知其他的线程。

分门别类的管理:线程组
就是相当于线程的List,把同种类型的线程放到一个“篮子”里。
主要的方法有两个
activeGroup(); 获得活动的线程总数。
list(); 打印这个线程组的所有线程信息。

守护线程(Daemon)
最典型的就是“垃圾回收线程”, 只要在JVM实例中有“非守护线程”的存在, “守护线程”就会辛勤的工作。 一旦没有了"非守护线程"也就是“用户线程”, 那么所有的“守护线程”就会自动销毁。
守护线程的设置必须在开启线程之前。

线程的优先级
在操作新系统中, 线程可以划分优先级, 优先级较高的线程得到的CPU资源较多, 也就是CPU优先执行优先级较高的线程对象中的任务。
优先级具有继承性, 线程的优先级和开启这条啊线程的优先级保持一致。(一条子线程中开启另外一条子线程没有问题, 实测主线程中开启一条子线程, 不会继承)

线程安全的概念与关键字synchronized
如下代码:

public class UnSafeTest extends Thread {

    static UnSafeTest t1 = new UnSafeTest();

    static volatile int i = 0;

    public void add() {
        i++;
    }

    @Override
    public void run() {
        for (int x = 0; x < 10000; x++) {
            add();
        }
    }

    public static void main(String[] args) {
        Thread t0 = new Thread(t1);
        Thread t2 = new Thread(t1);
        t0.start();
        t2.start();
        try {
            t0.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println(i);
    }

}

最终i总是不尽如人意的等于20000,这就是线程不安全造成的。因为i++可以拆解为三个步骤,如果两个都读了,一个写了另外一个再写很可能只加了1而不是2,为了解决这个问题我们就得说说同步方法了。
static synchronized 和 sychronized(当前类.class) 效果一样(都是使用当前类的.class文件当锁)而同步方法是直接使用调用对象当做锁。
于是乎,我们这么写,最后的执行结果就是20000了。

public class UnSafeTest extends Thread {

    static UnSafeTest t1 = new UnSafeTest();

    static volatile int i = 0;

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

    @Override
    public void run() {
        for (int x = 0; x < 10000; x++) {
            add();
        }
    }

    public static void main(String[] args) {
        Thread t0 = new Thread(t1);
        Thread t2 = new Thread(t1);
        t0.start();
        t2.start();
        try {
            t0.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println(i);
    }

}

并发下的ArrayList

/**
 * <pre>演示并发下不安全的ArrayList</pre>
 *
 * @author YangHang
 * @Date 2019/3/22 10:20
 */
public class NoSafeArrayList {

    static List<String> list = new ArrayList<>();

    static class NoSafeThread implements Runnable {

        public void run() {

            for (int i = 0; i < 200; i++) {
                list.add("YH");
            }
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new NoSafeThread());
        Thread t2 = new Thread(new NoSafeThread());
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(NoSafeArrayList.list.size());
    }
}

在多线程的环境中使用ArrayList,可以会出现三种情况:
1.正常
2.数量变少
3.报错,因为ArrayList内部的一致性被破坏了。

并发下诡异的HashMap
并发下使用HashMap,可能会遇到的问题。
1.正常
2.数量变少
3.程序永远无法结束,因为链表被损坏了。

锁对象的改变
锁对象改变了, 线程去判断持有的锁对象就不一样了,但是要是改变对象的属性的话, 是完全不受影响的。

第三章 JDK并发包

多线程的团队协作:同步控制

同步控制是并发程序中必不可少的重要手段。synchronized关键字就是最简单的控制方法,它决定了一个线程是否可以访问临界区的资源。

关键字synchronized的功能拓展:重入锁

重入锁可以完全代替关键字synchronized。在JDK5早期版本重入锁的性能远高于synchronized,但从JDK1.6开始,JDK在synchronized上做了大量的优化,二者的性能就差不多了。
重入锁在JDK并发包中使用java.util.concurrent.locks.ReentrantLock来实现的。

/**
 * 重入锁演示
 * 
 * @author YangHang
 * @Date 2019年3月23日 下午12:38:42
 *
 */
public class ReentrantThread implements Runnable {

    public static ReentrantLock lock = new ReentrantLock();

    public static volatile int x;

    @Override
    public void run() {

        for (int i = 0; i < 100; i++) {
            lock.lock();
            try {
                x++;
            } finally {
                lock.unlock();
            }
        }

    }

    public static void main(String[] args) {

        Thread t1 = new Thread(new ReentrantThread());
        Thread t2 = new Thread(new ReentrantThread());
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(ReentrantThread.x);

    }

}

其为什么叫重入锁呢?
因为这个锁是可以重入进入的,当然指的是一个线程。不过要注意,重入了几次,就要释放几次,就是lock了几次就要unlock几次。除了这些重入锁还提供了一些高级的功能。

  • 中断响应
/**
 * 演示死锁, 然后中断线程。
 * 
 * <pre>
 * 重入锁<{@link java.util.concurrent.locks.ReentrantLock}
 *      的响应特性。
 * </pre>
 * 
 * @author YangHang
 * @Date 2019年3月23日 下午3:26:34
 *
 */
public class DiedLockAndInterrupt implements Runnable {

    static ReentrantLock lock1 = new ReentrantLock();

    static ReentrantLock lock2 = new ReentrantLock();

    int lock;

    public DiedLockAndInterrupt(int lock) {
        this.lock = lock;
    }

    @Override
    public void run() {

        try {
            if (lock == 1) {
                // 先锁1,再锁2
                lock1.lockInterruptibly();

                Thread.sleep(1000);

                lock2.lockInterruptibly();
            } else {
                // 先锁2,再锁1
                lock2.lockInterruptibly();

                Thread.sleep(1000);

                lock1.lockInterruptibly();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (lock1.isHeldByCurrentThread()) {
                lock1.unlock();
            }
            if (lock2.isHeldByCurrentThread()) {
                lock2.unlock();
            }
        }
    }

    public static void main(String[] args) {
        DiedLockAndInterrupt r1 = new DiedLockAndInterrupt(1);
        DiedLockAndInterrupt r2 = new DiedLockAndInterrupt(2);

        Thread t1 = new Thread(r1, "线程1");
        Thread t2 = new Thread(r2, "线程2");

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

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // t2标记中断,
        t2.interrupt();
    }
}

对于关键字synchronized来说,如果一个线程在等待锁,那么结果只肯呢个有两种,要么就是获取这把锁继续前行, 要么就是等待。而使用重入锁,则提供另外一种可能,那么就是线程可能被中断。也就是等待锁的过程中,线程就可以取消对锁的强求(已经占有的锁也会被释放)。

  • 锁申请等待限时
    下面这段代码展示了限时等待锁的使用。
/**
 * 演示延时等待锁
 * 
 * @author YangHang
 * @Date 2019年3月23日 下午5:18:35
 *
 */
public class TimeThread implements Runnable {

    static ReentrantLock lock = new ReentrantLock();

    @Override
    public void run() {

        try {
            if (lock.tryLock(5, TimeUnit.SECONDS)) {
                Thread.sleep(6000);
            } else {
                System.out.println("get lock failed");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {

        Thread t1 = new Thread(new TimeThread());
        Thread t2 = new Thread(new TimeThread());

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

}

tryLock有两个参数,等多长时间等不到就不等了,就不获取你的锁了。如果没有参数的话,那么就是尝试获取锁,如果获取到了就返回true,没有获取到就返回false,不会等待,也避免了死锁。

  • 公平锁
    就是线程A请求了锁1,线程B也请求了锁1,那么当锁1可用的时候,是线程A还是线程B可以获得锁1呢,其实默认是随便选取的,但是重入锁有一个构造参数可以变成先来后到,也就是公平的锁,public ReentrantLock(boolean fair);这样就变成了公平锁,但是问题就来了,这样一个公平锁必然就会有一个有序队列,实现成本会比较高,性能可能会比较低下。

重入锁的总结:

  1. lock() 获取锁,如果锁已经被占用,那么则等待。
  2. lockInterruptibly() 获取锁,但优先响应中断。
  3. tryLock() 尝试获取锁,成功则返回true,失败则返回false。该方法不等待,立即返回。
  4. tryLock(long time,TimeUnit unit) 在给定的时间内尝试获取锁。
  5. unlock() 释放锁。

重入锁的好搭档:Condition
Condition实例在Lock接口中定义了获取的方式, newCondition();
提供的方法:
await()方法会使当前线程等待,同时释放当前锁,当其他线程使用signal()方法或者signalAll()方法的时候,线程会获取锁并继续执行。或者线程被中断时,也能跳出等待,这和Object.wait()和像。
awaitUninterrubtibly()方法与await()方法基本相同,但是它并不会在等待中中断。
signal唤醒一个线程,signalAll()唤醒所有的线程。和Object.notify类似。

/**
 * 演示如何使用Condition进行线程之间的通信
 * 
 * @see java.util.concurrent.locks.Condition
 *      <p>
 * @author YangHang
 * @Date 2019年3月23日 下午8:53:19
 *
 */
public class ReenterLockCondition implements Runnable {

    static ReentrantLock lock = new ReentrantLock();

    static Condition condition = lock.newCondition();

    @Override
    public void run() {
        lock.lock();
        try {
            condition.await();
            System.out.println("Thread is go on");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {

        Thread t1 = new Thread(new ReenterLockCondition());
        t1.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        lock.lock();
        condition.signal();
        lock.unlock();
    }
}

允许多个线程同时访问:信号量(Semaphore)
信号量为多线程协作提供了更为强大的控制方法。从广义上说,信号量是对锁的扩展。无论是内部锁synchronized还是重入锁ReentrantLock,一次都只允许一个线程访问一个资源,而信号源却可以指定多个线程,同时访问某一资源。信号量主要提供了以下构造函数:
public Semaphore(int permits)
public Semaphore(int permits,boolean fair) // 第二个参数可以指定是否公平。
主要有一下几个方法:

  • public void acquire() //尝试获取一个准入的许可。若无法取得,线程则会等待,直到有线程释放一个许可获取当前线程被中断。
  • public void acquireUninterruptibly() //和上面类似,只是不响应中断。
  • public boolean tryAcquire() //尝试获取一个许可,如果成果则返回true,失败则返回false,它不会进行等待,会立即返回。
  • public boolean tryAcquire(long time,TimeUnit unit) //与上面类似,但是会等待对应的时间。
  • public void release() // 线程访问结束之后释放一个许可。

ReadWriteLock读写锁
ReadWrite是JDK5中提供的读写分离锁。

  • 读-读不互斥:读读之间不阻塞。
  • 读-写互斥:读会阻塞写,写也会阻塞读。
  • 写写互斥:写写会互相阻塞。
/**
 * 演示读写锁
 * 
 * @author YangHang
 * @Date 2019年3月24日 下午10:41:19
 *
 */
public class ReadWriteThread implements Runnable {

    static int data;// 临界区的数据

    static ReentrantReadWriteLock writeReadLock = new ReentrantReadWriteLock();

    static Lock writeLock = writeReadLock.writeLock();// 写锁

    static Lock readLock = writeReadLock.readLock();// 读锁

    int flag;

    public ReadWriteThread(int flag) {
        this.flag = flag;
    }

    @Override
    public void run() {

        if (flag == 1) { // 进行读操作
            try {
                readLock.lock();
                System.out.println("读取临界区的数据为" + data);
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                readLock.unlock();
            }
        } else {
            try {
                writeLock.lock();
                data++;
                System.out.println("写取临界区的数据,写完之后变成" + data);
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                writeLock.unlock();
            }
        }

    }

    public static void main(String[] args) {
        // 创建20条线程去读数据
        for (int i = 0; i < 20; i++) {
            Thread t1 = new Thread(new ReadWriteThread(1));
            t1.start();
            if (i < 5 && i > 2) { // 创建两条线程去写
                Thread t2 = new Thread(new ReadWriteThread(0));
                t2.start();
            }
        }
    }

}

理解就是同一个读写锁产生的读锁和写锁。读锁被线程占有的时候,写锁是不可以被占有的。写锁被线程占有的时候,读锁和写锁都不能被线程占有。

倒计数器:CountDownLatch

可以这么理解,就是火箭发射之前都需要完成哪几个固定的步骤,那么必须在调用了CountDownLantch#await();方法的地方的当前线程等待,直到CountDownLantch#countdown();到了构造函数CountDownLantch(int count);的count数量之后才能继续通行,也可以理解成火箭发射。

/**
 * 演示倒计数器--CountDownLantch
 * 
 * @author YangHang
 * @Date 2019年3月26日 下午9:53:44
 *
 */
public class CountDownDemo {

    private final static CountDownLatch countDownLantch = new CountDownLatch(10);

    public static void main(String[] args) {

        // 主线程运行
        System.out.println("火箭准备发射!");

        try {
            // 开始10个准备工作
            for (int i = 0; i < 10; i++) {

                new Thread(new Runnable() {

                    @Override
                    public void run() {
                        System.out.println("完成一项准备工作。");
                        countDownLantch.countDown();

                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            }

            countDownLantch.await();

            System.out.println("发射成功!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

循环栅栏:CyclicBarrier
CyclicBarrier是另外一种多线程并发控制工具。它和CountDownLatch非常类似,也可以实现线程间的技术等待,但是他的功能比CountDownLatch更加强大。
1: 它计数完成之后可以重新计数。
2: 它是会把每一个调用了await();方法的线程阻塞,直到凑齐了计数器,再一起放行。放行之后触发回调线程。

**
 * 循环栅栏:CyclicBarrier
 * 
 * <p>
 * 
 * @author YangHang
 * @Date 2019年3月25日 下午1:07:43
 *
 */
public class CyclicBarrierTest {

    static class Soldier implements Runnable {

        private String name;

        private CyclicBarrier cyclicBarrier;

        public Soldier(CyclicBarrier cyclicBarrier, String name) {
            this.cyclicBarrier = cyclicBarrier;
            this.name = name;
        }

        @Override
        public void run() {
            try {
                System.out.println(name + "士兵准备工作!");
                cyclicBarrier.await();
                System.out.println(name + "士兵开始工作!");
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            } finally {

            }
        }

    }

    static class Result implements Runnable {

        @Override
        public void run() {
            System.out.println("所有的任务都完成了!");
        }

    }

    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(10, new Result());
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(new Soldier(cyclicBarrier, "士兵" + i));
            t.start();
        }
    }

}

线程阻塞工具类:LockSupport
这个阻塞的方法是一个静态的方法(LockSupport.park();),直接用类名点去调用即可。在线程的任意位置调用即可。而且如果在这个工具类调用part方法之前调用了,LockSupport.unpark(Thread thread);方法那么就不会再阻塞了。避免了永远也醒不了的尴尬。
而且阻塞之后线程的状态会显示为WAITTING状态。

/**
 * 演示线程阻塞工具类LockSupport
 * 
 * @author YangHang
 * @Date 2019年3月26日 下午10:21:15
 *
 */
public class LockSupportDemo {

    private static class LockSupportThread implements Runnable {

        @Override
        public void run() {

            System.out.println("线程运行");

            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 阻塞了,线程。
            LockSupport.park();

            System.out.println("线程运行完成");
        }

    }

    public static void main(String[] args) {

        Thread t1 = new Thread(new LockSupportThread());
        t1.start();

        LockSupport.unpark(t1);
        System.out.println("完成响应。");
    }

}

Guava与RateLimiter限流
Guava是Google下的一个核心类库,提供了一大批设计精良、使用方便的工具类。我们可以认为Guava是JDK的一个补充。RateLimiter就是Guava的一个限流工具。就是在一定时间类最多处理几个线程。

/**
 * Guava下的限流工具类--RateLimiter
 * 
 * @author YangHang
 * @Date 2019年3月26日 下午10:59:26
 *
 */
public class RateLimiterDemo {

    // 限制了每秒中最多请求两次
    private static RateLimiter limiter = RateLimiter.create(2);

    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
            limiter.acquire();
            System.out.println(i);
        }

        for (int i = 0; i < 100000000; i++) {
            if (!limiter.tryAcquire()) {
                continue;
            }
            System.out.println(i);
        }

    }

}

如果调用了RateLimiter#acquire();方法那么将会被限流,一秒钟能处理几次是由RateLimiter.create(int count); count决定的。但是tryAcquired();方法会返回true or false。就意味着对多余的请求,可以不管他了。

线程的复用:线程池
注意:在实际生产环境中,线程的数量必须得到控制。盲目的创建大量的线程对系统性能是有害的。
在使用了线程池之后,创建线程变成了从线程池获取空闲的线程,关闭线程变成了向线程池归还线程。

  1. 固定大小的线程池
    通过Euecutors#newFixedThreadPool(int count);来创建固定大小的线程池。
/**
 * 固定大小的线程池的使用
 * 
 * @see java.util.concurrent.Executors#newFixedThreadPool(int)
 * 
 * @author YangHang
 * @Date 2019年3月27日 上午11:24:59
 *
 */
public class FixedThreadPoolDemo {

    private static class Task implements Runnable {

        @Override
        public void run() {
            System.out.println(System.currentTimeMillis());

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    public static void main(String[] args) {

        Task task = new Task();
        ExecutorService es = Executors.newFixedThreadPool(5);

        for (int i = 0; i < 10; i++) {
            es.submit(task);
        }
    }

}

会发现线程是五个五个运行的,中间差了一秒钟。

  1. 计划任务
/**
 * 按照固定的速率执行线程任务
 * 
 * <p>
 * 
 * @author YangHang
 * @Date 2019年3月27日 下午12:23:50
 *
 */
public class ScheduleAtFixedRateDemo {

    public static void main(String[] args) {

        ScheduledExecutorService ses = Executors.newScheduledThreadPool(10);

        ses.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(System.currentTimeMillis() / 1000);
            }
        }, 0, 2, TimeUnit.SECONDS);
    }

}

刨根问底:核心线程池的内部实现
观看线程池的底部实现,基本上都是调用了ThreadPoolExecutor。这个类最重要的构造函数是:
public ThreadPoolExecutor(int corePoolSize, //线程池中的线程的数量
int maximumPoolSize,//指定了线程池中最大的线程数量
long keepAliveTime,//当线程池的线程数量超过了corePoolSize时,多余的空闲线程存活的时间
TimeUnit unit,//keepAliveTime的单位
BlockingQueue workQueue,//任务队列,被提交,但是尚未被执行的任务
ThreadFactory threadFactory,//线程工厂,用于创建线程,一版使用默认的即可
RejectedExecutionHandler handler //拒绝策略,当任务过多的时候如何拒绝);
栗子:演示自定义线程池和拒绝策略的使用。

/**
 * 演示自定义线程池和拒绝策略
 * 
 * @author YangHang
 * @Date 2019年3月27日 下午3:11:01
 *
 */
public class RejectThreadPoolDemo {

    private static class MyTask implements Runnable {

        @Override
        public void run() {
            System.out.println(System.currentTimeMillis() + " ThreadId:" + Thread.currentThread().getId());

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    public static void main(String[] args) {

        MyTask myTask = new MyTask();

        ThreadPoolExecutor es = new ThreadPoolExecutor(5, 5, 0, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>(10),
                Executors.defaultThreadFactory(), new RejectedExecutionHandler() {

                    @Override
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        System.out.println(r.toString() + " is discard ");
                    }
                });

        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            es.submit(myTask);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

自定义线程创建:ThreadFactory
ThreadFactory是一个接口,里面只有一个方法。在重写这个newThread(Runnable r);的方法时候,我们可以做一些操作,比如可以任性的把所有线程池创建的线程都设置为守护线程。

我的应用我做主:拓展线程池
线程池的shutdown();方法是关闭线程池,这是一个比较安全的做法。如果当有的线程正在执行,shutdown不会暴力的终止所有的任务,他会等所有的任务都执行完毕了再关闭线程池。可以简单的理解只是向线程池发送了一个关闭信号而已,但是在这个方法执行之后,这个线程池就不能再接受其他的新任务了。

不要发明重复的轮子:JDK的并发容器

  • 线程安全的HashMap
    一个名为SynchronizedMap的Map,它的底部维护一个一个正常的Map,然后把Map的所有方法都用mutex同步锁同步了,这样就线程安全了。
    一个更加专业的并发HashMap是ConcurrentHashMap,它位于java.util.concurrent包内,专门为并发进行了性能优化,因此更适合多线程的场景。
    另外可以使用Collections.synchronizedMap(new HashMap());来包装我们的HashMap这样生成的Map就是线程安全的。

  • 有关List的线程安全
    类似于对HashMap的包装,我们也可以使用Collections.synchronizedList(new LinkedList< String>());来包装任意的List,这样得到的List也是线程安全的。

  • 高效读写的队列:ConcurrentLinkedQueue
    这个类是在高并发环境下性能最好的队列。

  • 高效读取:不变模式下的CopyOnWriteArrayList

第四章 锁的优化及注意事项

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

  • 尽量减少锁持有的时间
    必须要进行同步的代码再进行同步。

  • 缩小锁粒度
    所谓减小锁粒度,就是指缩小锁定对象的范围,从而降低锁冲突的可能性,进而提高系统的并发能力。
    就想ConcurrentHashMap,不是对整个HashMap加锁,而是内部又细分为16个小的HashMap,称之为段。再默认情况下,一个ConcurrentHashMap被分为16个段。不同段之间的就是完全的并行。

  • 用读写分离锁来代替独占锁
    在读多写少的场合使用读写锁可以提升系统的并发能力。

  • 锁分离
    在LinkedBlockingQueue的实现中,task()函数和put()函数分别实现了从队列中获取数据和往队列里面增加数据的功能。虽然两个函数都对队列进行了修改,但是由于LinkedBlockingQueue是基于链表的,因此两个操作分别对应的队列的前端和尾端,并不冲突。所以take()和put()使用了两把锁。

  • 锁粗化
    就是不能不挺的请求锁释放锁,能合并的用一个锁的还是要用一个锁。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员大航子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值