thingkinginjava--第21章 并发(一)

如果您在本篇中未找到问题的解决方案,请留言告知,并会在后期更新,坚持互相帮助、共同学习的目的

如有错误之处,欢迎大家指正


只有变得多疑而自信,才能用Java编写出可靠的多线程代码

Java的线程机制是抢占式的,这表明调度机制会会周期性的终端线程,将上下文切换到另一个线程,从而为每个线程都提供时间片,使得每个线程都分配到数量合理的时间去驱动它的任务。

定义任务

我们使用Runnable接口来描述一个任务,只需要实现Runnable接口并编写run()方法。

驱动任务

Thread类

将Runnable对象转变为工作任务的方式

使用Executor(juc)

使用java.util.concurrent包(juc)包中的执行器(Executor)管理Thread对象

Executor是在客户端和任务执行之间提供的一个间接层;ExecutorService是具有服务声明周期的Executor,例如关闭,shutdown()方法。

  • CachedThreadPool

    CachedThreadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程。它是合理的Executor的首选。

  • FixedThreadPool

    使用FixedThreadPool可以一次性的预先执行代价高昂的线程分配,因而也就可以限制线程的数量了。

  • SingleThreadExecutor

    SingleThreadExecutor就像是线程数量为1的FixedThreadPool。SingleThreadExecutor会序列化所有提交给它的任务,并会维护它自己的悬挂任务队列。如果向SingleThreadPool提交了多个任务那么这些任务将排队,每个任务都会在下一个任务开始之前运行结束,所有的任务将使用相同的线程。

从任务中返回值

Runnable是执行工作的独立任务,但是它不返回任何值。如果希望任务在完成时能够返回一个值,那么可以实现Callable接口而不是Runnable接口。Callable是一种具有类型参数的泛型,它的类型参数表示的是从方法call(),而不是run()中返回的值,并且必须使用ExecutorService.submit()方法来调用它。

package com.atyouyou.concurrency;

import java.util.ArrayList;

import java.util.List;

import java.util.concurrent.*;

public class CallableDemo implements Callable<String>{

    private int id;

    public CallableDemo(int id) {
        this.id = id;
    }

    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        List<Future<String>> results = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            results.add(exec.submit(new CallableDemo(i)));
        }
        try {
        for (Future<String> fs :
                results) {
                System.out.println(fs.get());
        }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } finally {
            exec.shutdown();
        }

    }

    @Override
    public String call() throws Exception {
        return "result value is " + id;
    }
}

运行结果:

这里写图片描述

submit()方法会产生Future对象,它用Callable返回结果的特定类型进行了参数化。可以使用isDone()方法类查询Future是否已经完成。当任务完成时,它具有一个结果,你可以调用get()方法获取结果。我们也可以不用isDone()方法进行检查就直接调用get(),在这种情况下,get()方法将阻塞,直至结果准备就绪。(拓展:超时的get(long timeout, TimeUnit unit))

线程休眠

影响任务行为的一种简单方法是调用sleep(),这将使任务中止执行给定的时间。对sleep()的调用可以抛出InterruptedException异常,它在run()中被捕获,因为异常不能跨线程传播回main(),所以必须在本地处理所有在线程内部产生的异常。

new Runnable(){
    @Override
    public void run() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
};

使用**TimeUni**t类可以指定sleep()的延迟单元。

创建一个任务,它将睡眠1至10秒之间的随机数量的时间,然后显示它的睡眠时间并退出。创建并运行一定数量的这种任务。

package com.atyouyou.concurrency;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

public class work660 implements Callable<Integer> {
@Override
public Integer call() throws Exception {

    int time = (int)(Math.random() * 10) + 1;

    TimeUnit.SECONDS.sleep(time);

    return time;
}

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

    ExecutorService exec = Executors.newCachedThreadPool();

    List<Future<Integer>> results = new ArrayList<>();

        for (int i = 0; i < 10; i++) {

            results.add(exec.submit(new work660()));
        }
        for (Future<Integer> fs :
                results) {
            System.out.println(fs.get());
        }
    }
}

TODO:同步控制,编写自己的协作例程

线程优先级

线程的优先级将该线程的重要性传递给了调度器。尽管CPU处理现有线程集的顺序是不确定的,但是使调度器将倾向于让优先权最高的线程先执行。然而,这并不是意味着优先权较低的线程将得不到执行(也就是说,优先权不会导致死锁)。优先权较低的线程仅仅是执行的频率较低。

使用getPriority()来读取现有的线程的优先级,通过setPriority()来修改它。

  • MAX_PRIORITY
  • NORM_PRIORITY
  • MIN_PRIORITY

线程的让步yield()

暗示可以让别的线程使用CPU(这仅仅是一个暗示),建议具有相同优先级的其他线程可以运行。

后台线程daemon

setDaemon()函数必须在线程启动前调用,并且当所有前台线程执行完毕后,后台线程会立即停止(尽管使finally语句块,也可能被中断)

术语

来源是,在编写多线程相关的程序时,我们看到要执行的任务与驱动它的线程之间有一个差异,在Java中表现为我们对于Thread类实际没有任何的控制权(并且这种隔离在使用执行器时更加明显,因为执行器替你处理线程的创建和管理)。我们创建任务,将线程以某种方式附着在任务上,以使得这个线程可以驱动任务

Thread类自身不执行任何操作,它只是执行赋予它的任务。

Java的线程机制(抢占式)基于来自C的低级的p线程方式。

加入一个线程join()

如果在一个线程在另一个线程t上调用t.join(),此线程将被挂起,直到目标线程t结束才恢复(即t.isAlive()返回为假)。

join()方法的调用可以被中断,做法是在调用线程上调用interrupt()方法

捕获异常

由于线程的本质特性,使得我们不能捕获从线程中逃逸的异常。如果要捕获传播逃逸到控制台的异常,就可以使用Executor来解决这个问题了。

  1. 位于Thread类中的接口,我们可以为每个Thread对象都附着一个异常处理器。

    @FunctionalInterface
    public interface UncaughtExceptionHandler {
        /**
         * Method invoked when the given thread terminates due to the
         * given uncaught exception.
         * <p>Any exception thrown by this method will be ignored by the
         * Java Virtual Machine.
         * @param t the thread
         * @param e the exception
         */
        void uncaughtException(Thread t, Throwable e);
    }
    

    Thread.UncaughtExceptionHandler.uncaughtException()方法会在线程因未捕获的异常而临近死亡时被调用。

    package com.atyouyou.concurrency;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ThreadFactory;
    
    class ExceptionThread2 implements Runnable {
    
        @Override
        public void run() {
            Thread t = Thread.currentThread();
            System.out.println("run() by " + t);
            System.out.println("eh = " + t.getUncaughtExceptionHandler());
            throw new RuntimeException();
        }
    }
    
    class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
    
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            System.out.println("caught " + e);
        }
    }
    
    class HandlerThreadFactory implements ThreadFactory {
    
        @Override
        public Thread newThread(Runnable r) {
    
            System.out.println(this + " creating new Thread");
            Thread t = new Thread(r);
            System.out.println("created " + t);
    
            t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
    
            System.out.println("eh = " + t.getUncaughtExceptionHandler());
    
            return t;
        }
    }
    
    public class CaptureUncaughtException {
    
        public static void main(String[] args) {
    
            ExecutorService exec = Executors.newCachedThreadPool(new HandlerThreadFactory());
    
            exec.execute(new ExceptionThread2());
    
            exec.shutdown();
    
        }
    }
    

    运行结果:

  2. Thread类中:

    • static Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler()

      返回线程由于未捕获到异常而突然终止时调用的默认处理程序。

    • Thread.UncaughtExceptionHandler getUncaughtExceptionHandler()

      返回该线程由于未捕获到异常而突然终止时调用的处理程序。

    • static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)

      未捕获到的异常处理首先由线程控制,然后由线程的 ThreadGroup 对象控制,最后由未捕获到的默认异常处理程序控制。如果线程不设置明确的未捕获到的异常处理程序,并且该线程的线程组(包括父线程组)未特别指定其 uncaughtException 方法,则将调用默认处理程序的 uncaughtException 方法。

    • void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)

      设置该线程由于未捕获到异常而突然终止时调用的处理程序。

    因此如果代码中处处使用相同的异常处理器,上面1中的main()函数代码可以修改为:

    public static void main(String[] args) {
    
        Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
    
        ExecutorService exec = Executors.newCachedThreadPool();
    
        exec.execute(new ExceptionThread2());
    
        exec.shutdown();
    }
    

    运行结果:

    这里因为没有使用我们自定义的ThreadFactory,则会使用默认的工厂创建。

    这里写图片描述

  3. 拓展阅读

共享受限资源

不正确的访问资源:

  • IntGenerator.java:

    public abstract class IntGenerator {
    
        private volatile boolean canceled = false;
    
        public abstract int next();
    
        public void cancel(){
            canceled = true;
        }
    
        public boolean isCanceled(){
            return canceled;
        }
    }
    
  • EvenChecker.java读取测试从与其相关的IntGenerator返回的值

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class EvenChecker implements Runnable{
    
        private IntGenerator generator;
        private final int id;
    
        public EvenChecker(IntGenerator generator, int id) {
            this.generator = generator;
            this.id = id;
        }
    
        @Override
        public void run() {
            while (!generator.isCanceled()){
                int val = generator.next();
                if (val % 2 != 0){
                    System.out.println(val + " not even!");
                    generator.cancel();
                }
            }
        }
    
        public static void test(IntGenerator gp, int count) {
    
            ExecutorService exec = Executors.newCachedThreadPool();
            for (int i = 0; i < count; i++) {
                exec.execute(new EvenChecker(gp, i));
            }
    
            exec.shutdown();
        }
    
        public static void test(IntGenerator gp){
            test(gp, 10);
        }
    
    }
    
  • EvenGenerator.java

    public class EvenGenerator extends IntGenerator {
    
        private int currentEvenValue = 0;
    
        @Override
        public int next() {
    
            ++currentEvenValue;
    
            //Thread.yield();   Cause failure faster
    
            ++currentEvenValue;
    
            return currentEvenValue;
        }
    
        public static void main(String[] args) {
    
            EvenChecker.test(new EvenGenerator());
    
        }
    }
    

    其中一个任务就是产生偶数,而其他任务消费这些数字。这里消费者任务的唯一工作就是检查偶数的有效性。

    test()方法启动大量使用相同IntGenerator的EvenChecker

在Java中,递增不是原子操作。

解决共享资源竞争

什么时候使用同步?

如果你正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且,读写线程都必须用相同的监视器锁同步。

synchronized
  • 对象锁

    如果某一个任务处于一个对标记为synchronized的方法的调用中,那么在这个线程从方法返回之前,其它所有要调用类中任何标记为synchronized方法的线程都会被阻塞

    例如:

    `synchronized void f(){/*.......*/}`
    
    `synchronized void f(){/*.......*/}`
    
    • 所有对象都自动含有单一的锁(也称为监视器)。当在对象上调用其任意sychronized方法的时候,此对象都被加锁,这是该对象上的其他synchronized方法只有等到前一个方法调用完毕并释放了锁之后才能被调用。

    • 对于前面的方法,如果某个任务对象调用了f(),对于同一个对象而言,就只能等到f()调用结束并释放了锁以后,其他任务才能对对象调用f()和g()。所以,对于每个特定对象来说,其所有synchronized方法共享一个锁

    • 一个任务可以多次获得对象锁。如果一个方法在同一个对象上调用了第二个方法,后者又调用了同一对象上的另一个方法,就会发生这种情况。JVM负责跟踪对象被加锁的时候,计数变为1.每当这个相同的任务在这个对象上获得锁时,计数都会递增。显然,只有首先获得了锁的任务才允许继续获得多个锁。每当任务离开一个synchronized方法,计数递减,当计数为零的时候,锁被完全释放,此时别的任务就可以使用此资源

    修改前面的代码:

    SynchronizedEvenGenerator.java

    public class SynchronizedEvenGenerator extends IntGenerator {
    
        private int currentEvenValue = 0;
    
        @Override
        public synchronized int next() {
            ++currentEvenValue;
    
            //Thread.yield();
    
            ++currentEvenValue;
    
            return currentEvenValue;
        }
    
        public static void main(String[] args) {
    
            EvenChecker.test(new SynchronizedEvenGenerator());
    
        }
    }
    
  • 类锁

    针对每个类,也有一个锁(作为类的Class对象的一部分)

    修改前面的代码:

    MutexEvenGenerator.java

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class MutexEvenGenerator extends IntGenerator {
    
        private int currentEvenValue = 0;
        private Lock lock = new ReentrantLock();
        @Override
        public int next() {
            lock.lock();
            try {
                ++currentEvenValue;
                Thread.yield();
                ++currentEvenValue;
                return currentEvenValue;
            } finally {
                lock.unlock();
            }
        }
    
        public static void main(String[] args) {
            EvenChecker.test(new MutexEvenGenerator());
        }
    }
    

    注意,return语句必须在try子句中出现,以确保unlock()不会过早发生,从而将数据暴露给了第二个任务。

    以上面代码为例,如果将return currentEvenValue;放于finally语句块之后,则程序的运行结果会与没有同步时相同,即next()仍会产生奇数的情况。原因是,在当前线程执行完finally语句块之后,持有的对象锁被释放,而在执行return语句之前,CPU转去执行其他线程,当前线程被阻塞。

TODO:finally语句块和return的执行顺序

使用显示的Lock对象

ReentrantLock

  • Lock接口:

    package java.util.concurrent.locks;
    
    import java.util.concurrent.TimeUnit;
    
    public interface Lock {
        void lock();
    
        void lockInterruptibly() throws InterruptedException;
    
        boolean tryLock();
    
        boolean tryLock(long var1, TimeUnit var3) throws InterruptedException;
    
        void unlock();
    
        Condition newCondition();
    }
    

    Lock对象必须被显示的创建、锁定和释放。

    此程序演示了尝试着获取锁一段时间,然后放弃它即tryLock()方法的调用示例:

    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class AttemptLocking {
    
        private Lock lock = new ReentrantLock();
    
        public void untimed(){
    
            boolean captured = lock.tryLock();
    
            try {
                System.out.println("tryLock() " + captured);
            } finally {
                if (captured) {
                    lock.unlock();
                }
            }
        }
    
        public void timed(){
    
            boolean captured = false;
    
                try {
                    captured = lock.tryLock(2, TimeUnit.SECONDS);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                try {
                    System.out.println("tryLock(2, TimeUnit.SECONDS) " + captured);
                } finally {
                    if (captured){
                        lock.unlock();
                    }
                }
        }
    
        public static void main(String[] args) {
    
            final AttemptLocking al = new AttemptLocking();
            al.untimed();
            al.timed();
            new Thread(){
                {setDaemon(true);}
                @Override
                public void run() {
                    al.lock.lock();
                    System.out.println("acquired");
                }
            }.start();
    
            // 此处代码替换了原文中的Thread.yield()
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            al.untimed();
    
            al.timed();
        }
    }
    

    这里写图片描述

synchronized和Lock的比较:
  • 当使用synchronized关键字时,如果某些事物失败了,那么就会抛出一个异常。但是我们没有机会去做任何清理工作,以维护系统使其处于良好状态。使用显示的Lock对象,我们就可以使用finally子句将系统维护在正确的状态了。
  • 一般只有在解决特殊的问题时,才使用显示的Lock对象。例如,用synchronized关键字不能尝试着获取锁且最终获取锁会失败,或者尝试着获取锁一段时间,然后放弃它
  • 显示的Lock对象在加锁和释放锁方面,相对于内建的synchronized锁来说,还赋予了更加细粒度的控制力。这对于专有的同步结构是很有用的。例如用于遍历链接列表中的节电的节点传递的加锁机制(也称为锁耦合),这种遍历代码必须在释放当前节点的锁之前捕获下一个节点的锁。

原子性和易变性

Goetz测试:如果你可以编写用于现代微处理器的高性能JVM,那么就有资格去考虑是否可以避免同步。

原子性可以应用于除long和double之外的所有基本类型之上的”简单操作”。对于读取和写入除long和double之外的基本类型变量这样的操作,可以保证它们会被当作不可分(原子)的操作。

但是当我们定义long或double变量时,如果使用volatile关键字,就会获得(简单的赋值与返回操作的)原子性

原子类

临界区

有时我们希望防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法。通过这种方式分离出来的代码段称为临界区,使用synchronized

synchronized(syncObject){
    ...
}

synchronized关键字不属于方法特征签名的组成部分,所以可以在覆盖方法的时候加上去。

在其他对象上同步

中断

你不能中断正在试图获取synchronized锁或者试图执行I/O操作的线程。

线程之间的协作

wait()notify()notifyAll()

sleep()wait()的区别:
  1. sleep()在调用后,不释放对象上的锁;而wait()在调用后,对象上的锁被释放,因此,该对象上的其他synchronized方法可以被调用。

    这是thinkinginjava中的一个简单的示例:

    WaxOMatic.java有两个过程:一个是将蜡涂到Car上,一个是抛光它。抛光任务在涂蜡任务完成之前,是不能执行其他工作的,而涂蜡任务是在涂另一层蜡之前,必须等待抛光任务完成。

    package com.atyouyou.concurrency.waxomatic;
    
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.TimeUnit;
    
    /*
    buff:抛光     Wax :涂蜡
     */
    class Car {
    
        private boolean waxON = false;
    
        /*涂蜡完毕,可以抛光*/
        public synchronized void waxed() {
            waxON = true;
            notifyAll();
        }
    
        /*抛光完毕,可以涂蜡*/
        public synchronized void buffed() {
            waxON = false;
            notifyAll();
        }
    
        /*等待涂蜡*/
        public synchronized void waitForWaxing() throws InterruptedException {
            while (waxON == false) {
                wait();
            }
        }
    
        /*等待抛光*/
        public synchronized void waitForBuffing() throws InterruptedException {
            while (waxON == true) {
                wait();
            }
        }
    }
    
    class WaxOn implements Runnable {
    
        private Car car;
    
        public WaxOn(Car car) {
            this.car = car;
        }
    
        @Override
        public void run() {
            try {
                while (!Thread.interrupted()) {
                    System.out.println("Wax On!");
                    // 延时用来模拟涂蜡需要耗费的时间
                    TimeUnit.MILLISECONDS.sleep(200);
                    car.waxed();
                    car.waitForBuffing();
                }
            } catch (InterruptedException e) {
                System.out.println("Exiting via interrupt");
            }
            System.out.println("Ending Wax On task");
        }
    }
    
    class WaxOff implements Runnable {
    
        private Car car;
    
        public WaxOff(Car car) {
            this.car = car;
        }
    
        @Override
        public void run() {
            try {
                while (!Thread.interrupted()) {
                    car.waitForWaxing();
                    System.out.println("Wax Off!");
                    TimeUnit.MILLISECONDS.sleep(200);
                    car.buffed();
                }
    
            } catch (InterruptedException e) {
                System.out.println("Exit via interrupt");
            }
            System.out.println("Ending Wax off task");
        }
    }
    
    public class WaxOMatic {
    
        public static void main(String[] args) throws InterruptedException {
    
            Car car = new Car();
    
            ExecutorService exec = Executors.newCachedThreadPool();
            exec.execute(new WaxOff(car));
            exec.execute(new WaxOn(car));
    
            TimeUnit.SECONDS.sleep(5);
    
            exec.shutdownNow();
    
        }
    }
    

    这里写图片描述

    这里写图片描述

    可以看到结果中我们的确通过wait()、notifyAll(),进而修改Car中的waxOn的值来实现了线程之间的通讯,该结果的产生主要还是注意wait()sleep()的是否释放锁的区别,正式因为wait()调用后释放了它所持有的对象锁,我们可以去调用该对象的其他synchronized方法。

    此处的notifyAll()的调用也可以换做notify()notify()不是对notifyAll()的优化。

  2. wait()notify()notifyAll()有一个比较特殊的方面,那就是这些方法是基类Object的一部分,而不是Thread的一部分。尽管开始看起来有点奇怪–仅仅针对线程的功能却作为通用基类的一部分而实现,不过这是有道理的,因为这些方法操作的锁也是所有对象的一部分(即验证了那句话:所有对象都自动含有单一的锁(也称为监视器))。

    synchronized(x){
        x.notifyAll();
    }
    

    在我们调用ExecutorService.shutdownNow()时,它会调用所有由它控制的线程的interrupt()。

    只能在同步控制方法或同步控制块里调用wait()notify()notifyAll()。如果在非同步控制块里面调用这些方法,程序能通过编译,运行时将得到IllegalMonitorStateException异常。

    Java线程理论机制的讨论中,令人困惑的描述:notifyAll()将唤醒“所有正在等待的任务”。这是否意味着在程序中的任何地方,任何处于wait()状态中的任务都将被任何对notifyAll()的调用唤醒呢?

    情况并非如此,事实上,当notifyAll()因某个特定锁而被调用时,只有等待这个锁的任务才会被唤醒

拓展

  1. 测试当前线程是否已经中断(interrupted与isInterrupted)?

    • interrupted()

      public static boolean interrupted()

      测试当前线程是否已经中断。线程的中断状态由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回 false(在第一次调用已清除了其中断状态之后,且第二次调用检验完中断状态前,当前线程再次中断的情况除外)。

    • isInterrupted()

      public boolean isInterrupted()

      测试线程是否已经中断。线程的中断状态不受该方法的影响

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值