Java 高并发基础 课后笔记

马士兵老师高并发编程系列 课后笔记

大纲

  • 同步器(线程通信和同步)
    • Synchronized
    • Volatile
    • AtomicXXX (AtomicInteger…)
    • ReentrantLock
    • ThreadLocal
  • 同步容器
    • ConcurrentMap/Set
    • CopyOnWriteList
    • SynchronizedList
    • ConcurrentLinkedQueue
    • BlockingQueue
    • DelayQueue
  • 线程池
    • FixedThreadPool
    • CachedThreadPool
    • SingleThreadPool
    • ScheduledThreadPool
    • WorkStealingPool
    • ForkJoinPool
    • ThreadPoolExecutor

synchronized 关键字

  1. 对某个对象加锁
  2. 锁定对象而不是代码块
  3. 锁的信息是记录在堆内存
  4. 原子操作,不可分
  5. 获得的锁事可重入的,即重入锁
  • 同步方法和不同步方法可以同时执行
  • 一个同步方法可以调用另一个同步方法
  • 继承中,子类可以调用父类的同步方法
  • 程序执行中若出现异常,默认情况(即不用 try catch 异常)锁会被释放
private Object o = new Object();
synchronized(o) { //锁一个对象
        //todo
}
synchronized(this) { //锁该对象本身
        //todo
}
public synchronized void m() { 
//等同于 synchronized (this)
        //todo
}
public synchronized static void m() {
//等同于锁定 *.class 对象
        //todo
}
* `synchronized(*.class)`等同上面锁定方法
* 不能用`synchronized(this)`静态属性和方法不需要对象来访问

Dirty Read 脏读

产生原因:业务代码中,对写加锁,对读没加锁

死锁 模拟

线程1执行过程中 先要锁定A 锁定A过程中得先锁定B
线程2执行过程中 先要锁定B 锁定B过程中得先锁定A

synchronized 优化

采用细粒度的锁,可以使线程时间变短,提高效率

synchronized(this){
    count ++;//只锁定需要的地方
}

锁定的是堆内存 new 出来的对象,不是栈内存 o 的引用

  • 应该避免将锁定对象的引用变成另外的对象
  • 不要以字符串常量作为锁定对象
String s1 = "Hello";
String s2 = "Hello";
//s1 和 s2 是同一个对象

volatile 关键字

  1. 使一个变量在多个线程间可见
  2. 即一旦该变量发生改变则通知其他线程缓冲区去主内存重新读取该变量的值(即缓存过期通知)
  3. 内存可见性,禁止重排序

volatile 和 synchronized 区别

  • synchronized 保证可见性和原子性
  • volatile 只保证可见性
  • 能用 volatile 就别加锁,提高并发性,即无锁同步
  • volatile 不能保证多个线程共同修改一个变量时所带来的不一致问题,并不能替代 synchronized

原子类 Atomic

  • 具有原子性的方法的基础类
  • 能用Atomic类的尽量用,更效率
//AtomicInteger 
AtomicInteger count;
count.incrementAndGet();//count++;
//两个原子类方法中间则不具有原子性,即判断和操作分离
if(count.get() < 1000)
//中间不具备原子性,可能让另一线程执行
count.incrementAndGet();

实践解析 1

题:实现一个容器,提供两个方法,add,size。写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束

//容器类
public class MyContainer() {
        List lists = new ArrayList();
        public void add(Object o) {
            lists.add(o);
        }
        public int size() {
            return lists.size();
        }

实现一

volatile List lists
线程2用死循环检测

实现二

volatile
wait 和 notify,使用前必须进行锁定
wait 会释放锁
notify 不会释放锁
要先让线程2执行,先wait监听,线程1再开始执行
线程1加到5之后调用notify线程2运行,再wait
等线程2执行完毕后,再notify线程1运行

实现三

volatile
门闩 `coutdownlatch’ \ ‘cyclicbarrier’ \ ‘semaphore’
门闩等待不需要锁定对象

CountDownLatch latch = new CountDownLatch(1);
        ...
if(c.size() != 5) {
        latch.await();
        //要先让线程2执行,先wait监听
}
        ...
if(c.size() == 5) {
        latch.countDown();
        //线程1加到5之后,去掉门闩,线程2不再等待并执行,并且当count的值为0时当前线程继续运行
}

当不涉及同步,只是涉及线程通信的时候

synchronized + wait/notify 显得太重
应当考虑门闩


ReentrantLock 重入锁

  1. 能替代 synchronized
  2. 灵活,需要手动上锁,手动释放
  3. 尝试锁定 tryLock (可指定执行时间,以及根据锁定结果来执行具体逻辑)
  4. lockInterruptibly() 可以对interrupt()响应(lock()方法不响应中断)
  5. 可以指定为公平锁

与 synchronized 区别

  • 更灵活,手动释放(JVM释放)
  • 效率差不多
  • 可以指定为公平锁(非公平锁)
Lock lock = new ReentrantLock();
try {
        lock.lock()//即synchronized(this)
        ...//to do 
} catch {
        ...
}finally {
        lock.unlock();//必须放finally中
}

使用tryLock进行尝试锁定,不管锁定与否,方法都将继续执行;也根据其返回值判定是否锁定;也可以指定tryLock时间,由于tryLock(time)会抛出异常,所以要注意unlock的处理,必须放到fanally中

boolean locked = false;
try {
    locked = lock.tryLock(5, TimeUnit.Seconds);
    ...// to do here
} catch {
    ...
} finally {
    if(locked) lock.unlock();
}

公平锁:
线程等待时间越长,越先得到锁,效率会降低
非公平锁:
锁一释放,哪个线程被调度即得到锁

Lock lock = new ReentrantLock(true);//true 设置为公平锁

实践解析 2

问题:写一个固定容量的同步容器,拥有put和get方法,以及getCount方法,能够支持2个生产者线程以及10各消费者线程的阻塞调用
(生产者消费者模型)

实现一(原始写法)

synchronized
wait 和 notify

public class MyContainer1 {
        final private LinkedList lists = new LinkedList<>();
        final private int MAX = 10;//最多10个元素
        private int count = 0;
        public synchronized void put(T t) {
            //生产者线程
            while(lists.size() == MAX) {
                try {
                    this.wait();
                } catch (e) {
                    ...
                }
            }
        lists.add(t);
        ++count;
        this.notifyAll();//通知消费者线程进行消费
        }
        public synchronized T get() {
            //消费者线程
            T t = null;
            while(lists.size() == 0) {
                try {
                    this.wait();
                } catch (e) {
                    ...
                }
            }
            t = lists.removeFirst();
            count --;
            this.notifyAll();//通知生产者线程进行生产
            return t;
        }
        public static void main(String[] args) {
            MyContainer1 c = new MyContainer1<>();
            //启动消费者线程
            for(int i = 0; i < 10 ; i++) {
                new Thread(() -> {
                    for(int j = 0 ; j < 5 ; j++) 
                        System.out.println(c.get());
                },"c" + i).start();
            }
            ...
            //启动生产者线程
            for(int i = 0 ; i < 2 ; i++) {
                new Thread(() -> {
                    for(int j = 0; j < 25; j++)
                        c.put("CurrentThread " + j);
                }, "p" + i).start();
            }
        }
}
            
  • 为什么用 while 而不是 if ?
    wait() 继续执行后需要再检查一次 while 条件,不给别的线程机会
    (wait 和 while 常一起使用)

  • 使用 notifyAll 而不是 notify
    notify 唤醒一个线程,可能唤醒当前 notify 同类型线程(如消费者唤醒消费者,生产者唤醒生产者),导致继续等待。
    notifyAll 唤醒所有等待线程,则会继续执行

实现二

Lock 和 Condition
await 和 signalAll
Condition 可以精确指定哪些线程被唤醒

private Lock lock = new ReentrantLock();
private Condition producer = lock.newCondition();
private Condition consumer = lock.newCondition();
public void put(T t) {
        try {
            lock.lock();
            while(lists.size() == MAX) {
                producer.await();
            }
            lists.add(t);
            ++ count;
            consumer.signalAll();//通知消费者线程消费
        } catch (e) {
            ...
        } finally {
            lock.unlock();
        }
}
public T get() {
        T t = null;
        try {
            lock.lock();
            while(lists.size() == 0) {
                consumer.await();
            }
            t = lists.removeFirst();
            count --;
            producer.signalAll();//通知生产者线程生产
        } catch (e) {
            ...
        } finally {
            lock.unlock();
        }
}

ThreadLocal

  1. 线程局部变量(volatile 线程全局变量)
  2. 用空间换时间(synchronized 是时间换空间)
  3. 效率比较高
  4. 可能导致内存泄漏
  • hibernate 中 session 就存在 ThreadLocal 中,避免使用synchronized
ThreadLocal tl = new ThreadLoacal<>();
//tl.set(10) 设置整数10
//tl.get() 获取设置值
//每个线程之间不会有影响,即线程1set后,线程2get不到1所set的值

实践解析 3(引出并发容器)

问题:有n张火车票,每张票都有一个编号,同时有10个窗口对外售票,请写一个模拟程序

  1. 容器用ArrayList,不具备线程安全,会出现超量或重复
  2. 容器用Vector,具备线程安全,但会出现判断与操作分离
  3. 容器用ArrayList,对操作加锁,效率低
  4. 容器用并发容器QueueConcurrentLinkedQueue,并发多线程链队列,效率高
Queue tickets = new ConcurrentLinkedQueue<>();
...
    while(true) {
        String s = tickets.poll();//取出队首元素并赋值给 s
        if(s == null) break;
        else System.out.println("sell ticket -- " + s);
    }

并发容器

ConcurrentMap / Set

ConcurrentHashMap (高并发,分段锁,效率高)
ConcurrentSkipListMap (高并发并且排序,插入效率低,查找快速)

  • 多线程并发下效率问题
    Hashtable 低于 ConcurrentHashMap
    前者:锁定整个对象
    后者:分段锁,将大锁拆成小锁,提高并发性

  • 对于 map / set 的选择使用
    并发低,不加锁:

    • HashMap
    • TreeMap
    • LinkedHashMap
      并发低,加锁:
    • Hashtable
    • Collections.synchronizedMap
      并发高:
    • ConcurrentHashMap
      并发高,需要排序:
    • ConcurrentSkipListMap

CopyOnWriteList

  • 写时复制
  • 读不用加锁,效率高
  • 写要加锁,效率低
    业务情景:写的少,读的多,比如事件监听器

SynchronizedList

List strs = new ArrayList<>();
List strsSync = Collections.synchronizedList(strs);
//传入一个ArrayList,返回一个新的所有方法上锁的ArrayList

ConcurrentLinkedQueue

并发链表队列 内部加锁

Queue strs = new ConcurrentLinkedQueue<>();
strs.offer("Hello");//入队,可返回成功与否
strs.poll();//队首拿出来并删掉
strs.peak();//队首拿出来不删

LinkedBlockingQueue

链表阻塞队列 无界队列

strs.put("World");//如果满了就等待
strs.take();//如果空了就等待
//可以用来设计阻塞式消费者生产者模式

ArrayBlockingQueue

数组阻塞队列 有界队列

BlockingQueue strs = new ArrayBlockingQueue<>(10);//设置容量为10的队列
strs.add("test");//入队,但超过数组边界会抛异常
strs.offer("test");//入队操作,越界不会抛异常,返回入队成功与否
strs.put("test");//容器满时会等待,程序阻塞
strs.offer("test",1,TimeUnit.SECOND);//按时间段阻塞,1秒后不能入队则不再执行该操作

DelayQueue

延时队列 无界队列
按等待时间排序,自定义排序规则 重写Comparable方法
队列元素要求实现Delayed接口,每个元素需要预先设置延时
执行定时任务

LinkedTransferQueue

链表转移队列,用于更高并发

strs.transfer("test");//消费者和生产者模式中,先启动消费者,调用该方法会直接交给消费者消费而不存入容器中

SynchronusQueue

容量为 0 同步队列 特殊的TransferQueue

strs.put("test");//阻塞等待消费者消费,直接交给消费者消费,不能放入该容器,其内部是transfer方法
strs.add("test")//会抛异常
  • 队列选择
    不同步:
    • ArrayList
    • LinkedList
      同步,低并发:
    • Colloctions.synchronizedList
    • CopyOnWriteList
      高并发:
    • ConcurrentLinkedQueue
    • BlockingQueue
      • LinkedBlockingQueue
      • ArrayBlocking
      • TransferQueue
      • SynchronusQueue
    • DelayQueue 定时任务

线程池

Executor 接口

  • 顶层接口,执行任务
void execute(Runnable cammand)

ExecutorService 接口

  • extends Executor
 Future submit(Callable task)
Future submit(Runnable task>
 Future submit(Runnable task, T result)

Callable 接口

  • Runnable 类似
  • 可以返回范型返回值
  • 可以抛出异常
V call()

Executors 工具类

  • 工厂方法,快速创建线程

ThreadPool 线程池

  • 任务执行结束后,线程不会消失,新任务来时不用新启动线程,而是空闲线程启动任务
  • 一堆线程维护两个队列(任务队列和完成队列)
ExecutorService service = Executors.newfixedThreadPool(5);//固定线程数量线程池

Future

  • FutureTask
  • Callable 返回值
  • 封装出来一些方法
FutureTask task = new FutureTask<>//创建一个返回值为 Integer 的 Callable 任务
new Thread(task).start()//启动该任务
task.get()//阻塞方法,等待执行完成后才会返回返回值

并行计算

public class ParallelComputing {
        public static void main(String[] args) throws InterruptedException, ExecutionException {
        //普通方法和多线程计算素数的个数用时比较
        long start = System.currentTimeMillis();
        List results = getPrime(1, 200000);
        long end = System.currentTimeMillis();
        System.out.prinlan(end - start);
        
        final int cpuCoreNum = 4;//线程个数
        ExecutorService service = Executors.newFixedThreadPool(cpuCoreNum);
        Mytask t1 = new MyTask(1, 80000);
        Mytask t2 = new MyTask(80001, 130000);
        Mytask t3 = new MyTask(130001, 170000);
        Mytask t4 = new MyTask(170001, 200000);
        //不平均分的原因是:数字越大计算时间越长
        Future> f1 = service.submit(t1);
        Future> f1 = service.submit(t1);
        Future> f1 = service.submit(t1);
        Future> f1 = service.submit(t1);
        start = System.out.currentTimeMillis();
        f1.get();
        f2.get();
        f3.get();
        f4.get();
        end = System.out.currentTimeMillis();
        System.out.println(end - start);
        }

        static class MyTask implements Callable> {
            int startPos, endPos;
            MyTask(int s, int e) {
                this.startPos = s;
                this.endPos = e;
            }
            @Override
            public List call() throws Exception {
                List r = getPrime(startPos, endPos);
                return r;
            }
        }

        static boolean isPrime(int num) {
            for(int i = 2; i <= num/2; i++) {
                if(num % i == 0) return false;
            }
            return true;
        }
        
        static List getPrime(int start, int end) {
            List results = new ArrayList<>();
            for(int i = start; i <= end; i++) {
                if(isPrime(i)) results.add(i);
            }
            return results;
        }
}

CachedThreadPool

  • 缓存的线程池
  • 需要启动新线程时启动新线程
  • 空闲线程超过60秒(默认)会自动销毁
ExecutorService service = Executors.newCachedThreadPool();

SingleThreadPool

  • 只有一个线程的执行的线程池
  • 保证了任务执行顺序
ExecutorService service = Executors.newSingleThreadPool();

ScheduledThreadPool

  • 定时器线程池
  • 可以替代 Timer ,线程可复用
ScheduledExecutorService service = Executors.newScheduledThreadPool(4);
service.scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) //以固定频率执行,参数(任务,第一次执行延迟,每隔多少时间,单位)

WorkStealingPool

  • 任务窃取线程池
  • 每个线程维护自己的任务队列,执行完自己队列的任务会主动去其他队列偷任务到自己队列继续执行
  • 精灵线程(后台线程,Daemon Thread)主线程不阻塞的话,看不到输出
  • 是由ForkJoinPool实现的
ExecutorService service = Executors.newWorkStealingPool();//默认根据CPU核数启动线程数

ForkJoinPool

  • 分叉 合并,有点类似递归归并排序
  • 适合大规模数据计算
  • 精灵线程(后台线程,Daemon Thread)主线程不阻塞的话,看不到输出
  • 任务需要继承自 RecursiveAction(无返回值)或 RecursiveTask(有返回值)
class MyTask extends RecursiveTask {
        ...
        @Overide
        protected V compute() {
        //do fork task
        //MyTask subTask;
        //subTask.fork()
        //return subTask.join();
        }
}

ForkJoinPool fjp = new ForkJoinPool();
MyTask task = new MyTask();
fjp.execute(task);
V result = task.join();//阻塞方法
//有点类似递归

ThreadPoolExecutor

  • 线程池背后的实现原理(除了ForkJoinPool,前面5个都返回该类的实例,只是参数不同)
  • 可以继承该类自定义线程池
public ThreadPoolExecutor(
        int corePoolsize,//线程池线程数
        int maximumPoolSize,//最大线程数
        long keepAliveTime,//空闲生存时间,超时销毁线程
        TimeUnit unit,//生存时间单位
        BlockingQueue workQueue)//任务队列容器
        )

ParallelStreamAPI

  • 运用多线程数据流,不是线程池

总结

Java 高并发基础

你可能感兴趣的:(Java 高并发基础 课后笔记)