java核心技术—14.并发

1.由于Runnable是一个函数式接口,可以用lambda表达式建立一个实例

Runnable r = () -> { task code};

由Runnable创建一个Thread对象 Thread t = new Thread(r); 启动线程 t.start();

2.线程因如下两个原因之一而被终止:

     a).因为run方法正常退出而自然死亡。  b).因为一个没有捕获的异常终止了run方法而意外死亡。

3.守护线程 :唯一用途是为其他线程提供服务。例如计时线程,它定时地发送“计时器滴答"信号给其他线程或清空过时的高速缓存项的线程。

当只剩下守护线程时,虚拟机就退出了,由于如果只剩下守护线程,就没必要继续运行程序了。

void setDaemon(boolean isDaemon)

4.Java 线程Thread.Sleep详解

5.条件对象

 等待获得锁的线程和调用await方法的线程存在本质上的不同。一旦一个线程调用await方法,它进入该条件的等待集。该锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的signalAll方法为止。   这一调用重新激活因为这一条件而等待的所有线程。当这些线程从等待集当中移出时,它们再次成为可运行的,调度器将再次激活它们。同时,它们将试图重新进入该对象。一旦锁成为可用的,它们中的某个将从await调用返回,获得该锁并从被阻塞的地方继续执行           此时,线程应该再次测试该条件。由于无法确保该条件满足——signalAll方法仅仅是通知正在等待的线程:此时有可能已经满足条件,值得再次去检测该条件。

注意signalAll不会立即激活一个等待线程。它仅仅解除等待线程的阻塞,以便这些线程可以在当前线程退出同步方法之后,通过竞争实现对对象的访问。

public static void main(String[] args) {
    final ReentranLock reetrantLock = new ReentrantLock();
    final Condition conditon = reetrantLock.newCondition();

    Thread thread = new Thread((Runnable) () -> {
            try {
                reetrantLock.lock();
                System.out.println("我要等一个新信号"+this);
                condition.wait();
            } catch (InteruptException e) {
                e.printStackTrace();
            }
            System.out.println("拿到一个信号"+this);
            reetrantLock.unlock();
      }, "waitThread1");
       
      thread.start();

     Thread thread1 = new Thread((Runnable) () -> {
            reentrantLock.lock();
            System.out.println("我拿到锁了");
            try {
                Thread.sleep(3000);
            } catch (InterruptException e) {
                e.printStackTrace();
            }
            condition.siganAll();
            System.out.println("我发了一个信号");
            reentrantLock.unlock();
        },"signalThread");

        thread1.start();
}

运行结果如下:
我要等一个新信号。
我拿到锁了
我发了一个信号
拿到一个信号

1.线程1调用reentrantLock.lock时,线程被加入到AQS(AbstractQueuedSynchronizer)的等待队列中。
2.线程1调用await方法被调用时,该线程从AQS中移除,对应操作是锁的释放。
3.接着马上被加入到Condition的等待队列中,意味着该线程需要signal信号。
4.线程2,因为线程1释放锁的关系,被唤醒,并判断可以获取锁,于是线程2获取锁,并被加入到AQS的等待队列中。
5.线程2调用signal方法,这个时候Condition的等待队列中只有线程1一个节点,于是它被取出来,并被加入到AQS的等待队列中。 注意,这个时候,线程1 并没有被唤醒。
6.signal方法执行完毕,线程2调用reentrantLock.unLock()方法,释放锁。这个时候因为AQS中只有线程1,于是,AQS释放锁后按从头到尾的顺序唤醒线程时,线程1被唤醒,于是线程1回复执行。
7.直到释放所整个过程执行完毕。

  总结锁和条件:

          a.锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码

          b.锁可以管理试图进入被保护代码段的线程             c.锁可以拥有一个或多个相关的条件对象

          d.每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。

6.synchronized关键字

public synchronized void method() {
    method body
}
等价于
public void method() {
    this.intrinsicLock.lock();
    try {
        method body
    }
    finally {
        method body
    }
}

内部对象锁只有一个相关条件。wait方法添加一个线程到等待集中,notifyAll/notify方法解除等待线程的阻塞状态。换句话说,调用wait或notifyAll等价于

intrinsicCondition.await();
intrinsicCondituon.signalAll();

    内部锁和条件存在一些局限,包括:

         a.不能中断一个正在试图获得锁的线程          b.试图获得锁时不能设定超时     c.每个锁仅有单一的条件,可能是不够的。

建议:

      a.最好既不使用Lock/Condition也不使用synchronized关键字。在许多情况下可以使用java.util.concurrent包中的一种机制,它会为你处理所有的加锁                       b.如果synchronized关键字适合你的程序,那么尽量使用它,这样可以减少编写的代码数量,减少出错的几率。                      c.如果特别需要Lock/Condition结构提供的独有特性时,才使用Lock/Condition                     

7.Volatile域

  “如果向一个变量写入值,而这个变量接下来可能会被另一个线程读取,此时必须使用同步。”             volatile关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。      volatile变量不能提供原子性。

假定一个对象有一个布尔标记done,它的值被一个线程设置却被另一个线程查询,可以使用内部锁:
private boolean done;
public synchronized boolean isDone() { return done;}
public synchronized void setDone() { done = true;}
如果另一个线程已经对该对象加锁,isDone和setDone方法可能阻塞
在这种情况下,将域声明为volatile是合理的:
private volatile boolean done;
public boolean isDone() { return done;}
public void setDone() { done = true;}

public void flipDone() { done = !done;}   //not atomic
不能确保翻转域中的值。不能保证读取、翻转和写入不被中断。

8.final变量

将一个域声明为final,也可以安全地访问这个共享域。考虑以下声明:

final Map<String, Double> accounts = new HashMap<>();

  其他线程会在构造函数完成之后才能看到这个accounts变量。如果不适用final,就不能保证其他线程看到的是accounts更新后的值,它们可能都只是看到null,而不是新构造的HashMap。

当然,对这个映射表的操作并不是线程安全的。如果多个线程在读写这个映射表,仍然需要进行同步。

9.原子性

假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为vo

10.tryLock

tryLock方法试图申请一个锁,在成功获得锁后返回true,否则,立即返回false,而且线程可以立即离开去做其他事情.

if(myLock.tryLock()) {
    //now the thread owns the lock
    try {...}
    finally { myLock.unlock();}
} else 
    //do something else

可以调用tryLock时,使用超时参数,像这样:  if(myLock.tryLock(100, TimeUnit,MILLISECONDS))...

TimeUnit是一个枚举类型,可以取得值包括SECONDS\MILLISECONDS\MICROSECONDS和NANOSECONDS

lock方法不能被中断.如果一个线程在等待获得一个锁时被中断,中断线程在获得锁之前一直处于阻塞状态.如果出现死锁,那么,lock方法就无法终止.    然而,如果调用带有超时参数的tryLock,那么如果线程在等待期间被中断,将抛出InterruptedException异常.这是一个非常有用的特性,因为允许程序打破死锁.

也可以调用lockInterruptibly方法.他就相当于一个超时设为无限的tryLock方法.

11.阻塞队列

当试图向满的队列中添加或从空的队列中移出元素时,add\remove和element操作抛出异常.当然,在一个多线程程序中,队列会在任何时候空或满,因此,一定要使用offer\poll和peak方法来替代.

12.Callable与Future

Runnable封装一个异步运行的任务,可以被认为一个没有参数和返回值的异步方法.Callable与Runnable类似,但是有返回值.Callable接口是一个参数化的类型,只有一个方法call.

public interface Callable<V> {
    V call() throws Exception
}

类型参数是返回值的类型.例如,Callable<Integer>表示一个最终返回Integer对象的异步计算.

Future保存异步计算的结果.可以启动一个计算,将Future对象交给某个线程,然后忘掉它.Future对象的所有者在结果计算后之后就可以获得它.

 FutureTask包装器是一种非常便利的机制,可将Callable转换成Future和Runnable,它同时实现二者的接口.

Callable<Integer> myComputation = ...;
FutureTask<Integer> task = new FutureTask<Integer>(myComputation);
Thread t = new Thread(task);        //it's a Runnable
t.start();
...
Integer result = task.get();        //it's a Future

 13.执行器\线程池

构建一个新的线程涉及与操作系统的交互,因此有一定的代价.如果程序中创建了大量的生命期很短的线程,应该使用线程池.将Runnable对象交给线程池,就会有一个线程调用run方法.当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务.

执行器(Executor)类有许多静态工厂方法用来构建线程池.

newCachedThreadPool\newFixedThreadPool\newSingleThreadExecutor,这3个方法返回实现了ExecutorService接口的ThreadPoolExecutor类的对象.

可以用下列方法将一个Runnable对象或Callable对象提交给ExecutorService:

Future<?> submit(Runnable task)
Future<T> submit(Runnable task, T result)
Future<T> submit(Callable<T> task)

在使用连接池应该做的事:

        a.调用Executors类中静态的方法newCachedThreadPool或newFixedThreadPool         b.调用submit提交Runnable或Callable对象            c.如果想要取消一个任务,或如果提交Callable对象,那就要保存好返回的Future对象            d.当不再提交任何任务时,调用shutdown.

14.控制任务组

可以将一个执行器服务作为线程池使用,以提高执行任务的效率.使用执行器更有实际意义的是,控制一组相关任务.

List<Callable<T>> tasks = ..;
List<Future<T>> results = executor.invokeAll(tasks);
for(Future<T> result : results)
    processFurther(result.get());
invokeAll方法提交所有对象到一个Callable对象的集合中,并返回一个Future对象的代表,代表所有任务的解决方案.
这个方法的缺点是如果第一个任务恰巧花去很多时间,则可能不得不进行等待.将结果按可获得的顺序保存起来更有实际意义.可以用ExecutorCompletionService来进行排序

ExecutorCompletionService<T> service = new ExecutorCompletionService<>(executor);
for(Callable<T> task : tasks) service.submit(task);
for(int i=0; i<tasks.size();i++)
    processFuther(service.take().get());

15.Fork-Join框架:假设有一个处理任务,它可以很自然地分解为子任务

16.可完成Future: 利用CompletableFuture,可以指定你希望做什么,以及希望以什么顺序执行这些工作.当然,这不会立即发生,不过重要的是所有代码都放在一处.

产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

猜你喜欢

转载自blog.csdn.net/u010321349/article/details/81408447