Java并发编程实战-读书笔记

记录一些阅读过程中我认为需要记录的地方。

线程安全如何保证
  1. 禁止跨线程共享变量
  2. 不可变对象
  3. 同步(加锁)
共享状态&非共享状态

例如:
Servlet的某个实现类里的每个自定义方法是非共享状态。但是该类如果引入了一个类变量,那么该实现类处于共享状态。
(注意,Servlet的实例只有一份,来一个请求会有一个线程去执行对应Servlet实例的service方法)

check then act

问题就在于check完之后,act之前,观察结果已经无效(被篡改)引发了非预期的异常。
例如经典的惰性初始化(单例模式)的错误写法:

if(instance == null) {
    instance = new Object
}
return instance
共享状态的复合操作

例如i++
注意,即使该变量是线程安全的,复合操作依旧会带来线程安全问题。例如vector的put-if-absent

synchronized

Java内置的互斥锁。注意,其机制是per-thread,不是per-invocation,这就意味着该内置锁是可以重入的。

客户端加锁

经典的put-if-absent问题,例如我们使用Vector,虽然说其内部实现是将一系列方法用synchronized加锁,但是如果你想要实现如下逻辑:如果不存在某个元素,那么增加进去,你可能会这样实现:

if(!vector.contains('X'))
    vector.add('X')

这就是上文提到的复合操作,是存在同步问题的,我们需要在整个复合操作上加锁,也就是客户端加锁。

synchronized(vector) {
    if(!vector.contains('X'))
    vector.add('X')
}

而我们的例如ConcurrentHashMap原生就提供类似方法。

经典例子

接下来提供一个缓存实现的经典例子:
定义一个接口:

public interface Computable {
    V compute(A arg);  
}

假设,所有该接口的实现的compute方法都需要耗费大量时间,我们需要缓存该结果,需要设计一个缓存类。类似于如下:

public class CacheResult2 implements Computable {

    private Map cache = new HashMap<>();

    private Computable computable;

    public CacheResult2(Computable computable) {
        this.computable = computable;
    }

    @Override
    public V compute(A arg) {
        V result = cache.get(arg);
        if (result == null) {
            result = cache.put(arg, computable.compute(arg));
        }
        return result;
    }
}

我们使用一个HashMap缓存结果,显然存在并发问题。接着,我们可能会对该方法加上synchronized关键字,那么可能会出现一种情况:假如此时来了三个线程,t1,t2,t3,t1想计算arg是1的某个值,t2想计算arg为2的某个值,t3和t1一样,计算arg是1的某个值,那么有一种极端情况是t1,t2,t3分别拿到锁,这样对于t3花费的时间可能还没有不做缓存的时间还久。换一种思路。我们使用线程安全的集合类,这种方式同样存在问题:
如果某种计算十分耗费时间,那么很可能有多个线程同时在进行这个计算(注意是同一个arg)操作,那么效率同样很低。
最终的解决办法就是使用Future,当某个线程发现某个arg的结果正在计算中,那么他不会再进行计算。还有一个注意点是请使用map的putIfAbsent方法,确保原子性,否则同样可能出现多次计算的问题,尽管这种可能性相对上一种情况会很小很小。给出代码:

public class CacheResult implements Computable {

    private Map> cache = new ConcurrentHashMap<>();

    private Computable computable;

    public CacheResult(Computable computable) {
        this.computable = computable;
    }

    @Override
    public V compute(A arg) {
        while (true) {
            Future future = cache.get(arg);
            if (future == null) {
                Callable callable = () -> computable.compute(arg);
                FutureTask futureTask = new FutureTask<>(callable);
                future = cache.putIfAbsent(arg, futureTask);  //原子性
                if (future == null) {  //保证只可能有一个线程进行计算动作
                    future = futureTask;
                    futureTask.run();
                }
            }
            try {
                return future.get();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
                cache.remove(arg);
            }
        }

    }
}
Interrupt

此动作并不会中断某个线程,而是将该线程设置一个中断状态。由对应线程来决定是否要停止。
当你的代码调用了一个需要抛出InterruptedException的方法时,
通常有2种做法:
传递给上层。或者调用interrupt恢复中断状态。(如果第一处catch不想对异常做处理,给后面的catch处理,那就需要重新intereupt当前线程)(当捕捉了InterruptedException,中断状态清除):

try {
    
} catch(InterruptedException e) {
    Thread.currentThread.interrupt();//恢复中断状态
}

注意,调用某个线程的Interrupt方法,在不同上下文中是有不同的效果的,参见JDK注释:

If this thread is blocked in an invocation of the {@link * Object#wait() wait()}, {@link Object#wait(long) wait(long)}, or {@link * Object#wait(long, int) wait(long, int)} methods of the {@link Object} * class, or of the {@link #join()}, {@link #join(long)}, {@link * #join(long, int)}, {@link #sleep(long)}, or {@link #sleep(long, int)}, * methods of this class, then its interrupt status will be cleared and it * will receive an {@link InterruptedException}. * *

If this thread is blocked in an I/O operation upon an {@link * java.nio.channels.InterruptibleChannel InterruptibleChannel} * then the channel will be closed, the thread's interrupt * status will be set, and the thread will receive a {@link * java.nio.channels.ClosedByInterruptException}. * *

If this thread is blocked in a {@link java.nio.channels.Selector} * then the thread's interrupt status will be set and it will return * immediately from the selection operation, possibly with a non-zero * value, just as if the selector's {@link * java.nio.channels.Selector#wakeup wakeup} method were invoked. * *

If none of the previous conditions hold then this thread's interrupt * status will be set.

catch块中对应的就是最后一种情况:

If none of the previous conditions hold then this thread's interrupt status will be set. 

会让当前线程重新恢复中断状态。

闭锁

CountDownLatch:

public long worksCost(List works) throws InterruptedException {
        CountDownLatch start = new CountDownLatch(1);
        CountDownLatch end = new CountDownLatch(works.size());
        for (Runnable work : works) {
            new Thread(() -> {
                try {
                    start.await();
                    try {
                        work.run();
                    } finally {
                        end.countDown();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        long times = System.currentTimeMillis();
        start.countDown();
        end.await();
        return System.currentTimeMillis() - times;
    }

FutureTask:
调用其get方法时,需要处理2个异常:
InterruptedException:当前等待get线程被打断。
ExecutionException:执行过程出现异常。异常被封装成该类抛出。
以下例子展示了一个普遍的用法:

public class DataLoader {

    FutureTask futureTask = new FutureTask(() -> {
        return null;//sth need time to load from file or db
    });

    Thread thread = new Thread(futureTask);

    public DataLoader() {
        thread.start();
    }

    public Object load() throws InterruptedException {
        try {
            return futureTask.get();
        } catch (ExecutionException e) {
            if (e.getCause() instanceof SomeKnownException) {
                throw (SomeKnownException)e.getCause();
            } else {
                throw SomeRuntimeException;
            }
        }
    }

}

ExecutorCompletionService

ExecutorCompletionService实现了CompletionService,可以简单的理解为:Executor & BlockingQueue的结合体,作为一组计算的句柄。避免了我们一直去手动轮询任务是否完成,而是直接借助一个阻塞队列,其内部原理其实就是重写FutureTaskdone方法,在对应的task结束后将该task放入队列。源代码如图:

private class QueueingFuture extends FutureTask {
        QueueingFuture(RunnableFuture task) {
            super(task, null);
            this.task = task;
        }
        protected void done() { completionQueue.add(task); }
        private final Future task;
    }
Executor#invokeAll

如果想要这种效果:执行一组任务,一次性拿到结果集合(Future),并且还可以设定一个时间,时间到了还没有完成的任务cancel掉。那么使用invokeAll就可以很方便的完成。其实现位于AbstractExcutorService该抽象类中:

public  List> invokeAll(Collection> tasks)
        throws InterruptedException {
        if (tasks == null)
            throw new NullPointerException();
        ArrayList> futures = new ArrayList>(tasks.size());
        boolean done = false;
        try {
            for (Callable t : tasks) {
                RunnableFuture f = newTaskFor(t);
                futures.add(f);
                execute(f);
            }
            for (int i = 0, size = futures.size(); i < size; i++) {
                Future f = futures.get(i);
                if (!f.isDone()) {
                    try {
                        f.get();
                    } catch (CancellationException ignore) {
                    } catch (ExecutionException ignore) {
                    }
                }
            }
            done = true;
            return futures;
        } finally {
            if (!done)
                for (int i = 0, size = futures.size(); i < size; i++)
                    futures.get(i).cancel(true);
        }
    }
多线程的好处&坏处

好处:

  1. 多处理器下更好的利用cpu
  2. 单处理器下实现服务更好的吞吐量(例如IO阻塞)
  3. 更容易组织复杂的程序(Servlet来一个请求启动一个线程;Socket)

坏处

  1. 上下文切换带来的性能损耗
  2. 需要对部分数据进行同步处理,同样有性能损耗
  3. 同步可能带来死锁

你可能感兴趣的:(Java多线程,Java基础)