并发底层原理:线程、资源共享、volatile 关键字

4 篇文章 0 订阅
2 篇文章 0 订阅

本博客记录了在学习并发底层的一些知识笔记:包括中底层并发概念。

1、线程

并发将程序划分为独立分离运行的任务。每个任务都由一个执行线程 来驱动,我们通常将其简称为线程
而一个线程就是操作系统进程中单一顺序的控制流。因此,单个进程可以拥有多个并发执行的任务。
线程模型为编程带来了便利,它简化了在单一程序中同时交织在一起的多个操作的处理。线程的一大好处是可以使你从这个层次抽身出来,即代码不必知道它允许在单核还是多核的机器上。所以使用线程是一种建立透明的,可扩展的程序的方法。

1.1 定义任务

线程可以驱动任务,因此你需要一种描述任务的方式,这可以由Runnable 接口来提供。要像定义任务,只需实现 Runnable 接口并编写 run() 方法,使得该任务可以执行你的命令。
下面是显示火箭发射倒计时的代码:

public class LiftOff implements Runnable{
    protected int countDown = 10;
    private static int taskCount = 0;
    private final int id = taskCount++;

    public LiftOff(int countDown) {
        this.countDown = countDown;
    }

    public LiftOff() {
    }

    public String status() {
        return "#" + id + "("+ (countDown > 0 ? countDown : "LiftOff!") + "),";
    }

    @Override
    public void run() {
        while (countDown-- > 0) {
            System.out.println(status());
            Thread.yield();
        }
    }
}

标识符 id 可以用来区分任务的多个实例,它是final的,因为它一旦初始化后就不希望被修改。
任务的run()方法通常总会有某种形式的循环,通常run()方法被写成无限循环的形式,这就意味着,除非有某个条件可以终止它,否则它将永远运行下去。(稍后介绍如何安全的终止线程)
run()方法中对静态方法Thread.yield()的调用是对线程调度器(Java 线程机制的一部分,可以将CPU 从一个线程转移给另一个线程)的一种建议,它在声明:“我已经执行完生命周期最重要的部分了,此刻正是切换给其他任务执行的大好时机。”它是选择性的,但是你可以在示例中看到有趣的输出:你更有可能看到任务换进换出的证据。
LiftOff.java 使用yield()在各种不同的LiftOff任务之间产生良好的处理机制

public class MainThread {
    public static void main(String[] args) {
        LiftOff launch = new LiftOff();
        launch.run();
    }
}
/*
output:
#0(9),
#0(8),
#0(7),
#0(6),
#0(5),
#0(4),
#0(3),
#0(2),
#0(1),
#0(LiftOff!),
 */

这个示例的run()不是由单独的线程驱动的,它是在主线程中直接调用的(并没有产生新的线程)。
当从 Runnable 导出一个类时,它必须具有run(),但是这个方法并无特别之处----它不产生任何内在的线程能力。要实现线程行为,你必须显式的将一个任务附到线程上。

1.2 Thread 类

Java 并发的核心机制是 Thread 类。将 Runnable 对象转变为工作任务的传统方式是把它提交给一个 Thread 构造器,下面的示例展示了如何使用:

public class BasicThreads {
    public static void main(String[] args) {
        Thread t = new Thread(new LiftOff());
        t.start();
        System.out.println("Waiting for LiftOff");
    }
}
/*
output:
Waiting for LiftOff
#0(9),
#0(8),
#0(7),
#0(6),
#0(5),
#0(4),
#0(3),
#0(2),
#0(1),
#0(LiftOff!),
 */

Thread 构造器只需要一个Runnable 对象。当调用 Thread 对象的 start() 方法时将为该线程执行必须的初始化操作(调用start时才初始化,而不是在new 的时候),然后调用 Runnable的 run()方法,以便在这个新线程中启动该任务。
"Waiting for LiftOff"在倒计时完成之前就出现了,说明调用的 LiftOff.run() 方法执行是由不同的线程执行的,因此你仍旧可以执行 main()线程中的其他操作(这种情况并不限于主线程,任何线程都可以启动另一线程)。因此,程序会同时运行两个方法, main()LiftOffrun()是“同时”执行的代码。

你可以很容易的添加更多的线程去驱动更多的任务。下面示例演示了线程之间是如何呼应的:

public class Test01 implements Runnable{
    private static int taskCount;
    private final int id = taskCount++;

    public Test01() {
        System.out.println("Test01 started:id = " + id);
    }

    @Override
    public void run() {
        System.out.println("大话西游" + id);
        //
        Thread.yield();
        System.out.println("大话西游" + id);
        Thread.yield();
        System.out.println("大话西游" + id);
        Thread.yield();
        System.out.println("Test01 ended:id = " + id);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            new Thread(new Test01()).start();
        }
    }
}
/*
output:
Test01 started:id = 0
Test01 started:id = 1
Test01 started:id = 2
大话西游0
大话西游2
大话西游0
大话西游1
大话西游1
大话西游1
Test01 ended:id = 1
大话西游0
大话西游2
Test01 ended:id = 0
大话西游2
Test01 ended:id = 2
 */

每次都会产生不同的输出,这种交换是由线程调度器自动控制的
main()创建 Thread 对象时,它并没有捕获任何对这些对象的引用。每个 Thread 都“注册”了自己,因此确实有一个对它的引用,而且在它的任务退出其 run()并死亡之前,垃圾回收器无法清除它。因此,一个线程会创建一个单独的执行线程,在对 start()的调用完成后,它仍然会继续存在。

每次前三个先打印出主线程的数据可能是主线程更优先。

1.3 使用 Executor

java.util.concurrent 包中的执行器(Executor)将为你管理 Thread 对象,从而简化了并发编程。

  • Executor 在客户端和任务执行之间提供了一个间接层;与客户端直接执行任务不同,这个中介将在中间与它们两个交互。
  • Executor 允许你管理异步任务的执行,而无需显式的管理线程的生命周期。
  • Executor 是一个接口,它只有一个方法:void execute(Runnable command);

我们可以使用 Executor 来代替在之前的示例中显式的创建 Thread 对象。LiftOff 对象知道如何运行具体的任务,与命令设计模式一样,它暴露了要执行的单一方法。
ExecutorService (具有服务生命周期的Executor,例如关闭;继承了 Executor接口)知道如何构建恰当的上下文来执行 Runnable 对象。
在下面的示例中,将为每个任务都创建一个线程。注意:ExecutorService 对象是使用 Executors 的静态方法创建的:这个方法返回新创建的线程池,这个方法可以确定其 Executor类型

public class CachedThreadPool {
    public static void main(String[] args) {
        ExecutorService es = Executors.newCachedThreadPool();
        for (int i = 0; i < 3; i++) {
            //第一步先在主线程循环中完成了
            es.execute(new LiftOff());
        }
        es.shutdown();
    }
}
/*
output:
----
----
----
#0(9),
#0(8),
#0(7),
#0(6),
#0(5),
#0(4),
#1(9),
#2(9),
#0(3),
#0(2),
#2(8),
#0(1),
#0(LiftOff!),
#1(8),
#2(7),
#1(7),
#2(6),
#2(5),
#1(6),
#2(4),
#1(5),
#2(3),
#1(4),
#2(2),
#1(3),
#2(1),
#1(2),
#2(LiftOff!),
#1(1),
#1(LiftOff!),
 */

非常常见的情况是,单个的 Executor 被用来创建和管理系统中的所有任务
对shutdown 方法的调用可以防止新任务提交给这个 Executor,当前线程(main)将继续运行在shutdown 方法被调用之前提交的所有任务。运行完自后尽快退出。

public class FixedThreadPool {
    public static void main(String[] args) {
        //构造器参数是线程的数量
        ExecutorService es = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            es.execute(new LiftOff());
        }
        es.shutdown();
    }
}

你可以替换Executors.newCachedThreadPool();为不同的 Executor
可替换的线程池种类有了 FixedThreadPool ,你就可以一次性预先执行代价高昂的线程分配,因而也就可以控制线程的数量了。并且这样做还可以节省时间,因为你不用为每个任务都固定地付出创建线程的开销
注意:在任何线程池中,现有线程在可能的情况下,都会被自动复用
SingleThreadExecutor 就像是线程数量为1的 FixedThreadPool 。这对于在另一个线程中连续运行的任何事物(长期存活的任务)来说, 都是很有用的,例如:监听进入的套接字连接的任务。对于在线程中运行的短任务也同样方便。例如:更新本地或远程日志的小任务,或者事件分发线程。
如果向SingleThreadExecutor 提交了多个任务,那么这些任务将排队。
CachedThreadPool:在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,它是Executors的首选。

1.4 从任务中产生返回值

Runnable 是执行工作的的独立任务,但是它不返回任何值。如果你希望返回值,那么应该使用 Callable 接口而不是 Runnable 接口。

  • Runnable 是一个函数式接口,只有一个 public abstract void run();.
  • Callable 也是一个函数式接口,不同之处在于它的方法V call(); 是有返回值的。且这个方法必须使用 ExecutorService.submit() 方法调用它
class TaskWithResult implements Callable<String> {
    private final int id;

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

    @Override
    public String call() throws Exception {
        return "Result of TaskWithResult: " + id;
    }
}
public class CallableDemo {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newCachedThreadPool();
        ArrayList<Future<String>> futures = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            futures.add(threadPool.submit(new TaskWithResult(i)));
        }
        for (Future<String> fs : futures) {
            try {
                System.out.println(fs.get());
            } catch (InterruptedException e| ExecutionException e) {
                e.printStackTrace();
            }finally {
                threadPool.shutdown();
            }
        }
    }
}
/*
output:
Result of TaskWithResult: 0
Result of TaskWithResult: 1
Result of TaskWithResult: 2
Result of TaskWithResult: 3
Result of TaskWithResult: 4
Result of TaskWithResult: 5
Result of TaskWithResult: 6
Result of TaskWithResult: 7
Result of TaskWithResult: 8
Result of TaskWithResult: 9
 */

submit()会产生 Futrue 对象,它用Callable 返回结果的特定类型进行了参数化。你可以用 isDone() 方法来查询 Future 是否已经完成。当任务完成时,它具有一个结果,你可以调用 get()来获取该结果。你也可以直接调用get,但它会先阻塞,直到结果准备就绪。

public class FixedThreadPool {
    public static void main(String[] args) {
        //构造器参数是线程的数量
        ExecutorService es = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 5; i++) {
            if (i == 2) {
                try {
                    TimeUnit.MILLISECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //旧模式
            /*if (i == 2) {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }*/
            es.execute(new LiftOff());
        }
        es.shutdown();
    }
}

对 sleep 的调用可以抛出InterruptedException 异常。java 5 引入了更加显式的 sleep() 版本,作为 TimeUnit 类的一部分,它允许你指定 sleep() 延迟的时间单元,因此可以提供更好的可阅读性。

1.6 优先级

线程的优先级将该线程的重要性传递给了调度器。尽管 CPU 处理现有线程集的顺序是不确定的,但是调度器将倾向于让优先权更高的线程先执行。但是,并不意味着优先权低的线程得不到执行(优先权不会导致死锁)。优先级低的线程只是执行的频率较低。但是,所有线程都应该以默认的优先级执行,试图操纵线程优先级通常是一种错误。
下面是一个演示优先级等级的示例:

public class SimplePriorities implements Runnable{
    private int countDown = 5;
    private volatile double d; //没有最佳化
    private int priority;

    public SimplePriorities(int priority) {
        this.priority = priority;
    }

    @Override
    public String toString() {
        return Thread.currentThread()+": " + countDown ;
    }

    @Override
    public void run() {
        //设置优先级
        Thread.currentThread().setPriority(priority);
        while (true) {
            //下面是一个昂贵的、无法干预的操作
            for (int i = 1; i < 1000000000; i++) {
                d += (Math.PI + Math.E) / i;
                if (i % 1000 == 0) Thread.yield();
            }
            System.out.println(this);
            if (--countDown == 0) return;
        }
    }

    public static void main(String[] args) {
//        ExecutorService service = Executors.newFixedThreadPool(5);
        ExecutorService service = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            service.execute(
                    new SimplePriorities(Thread.MIN_PRIORITY)
            );
        }
        service.execute(new SimplePriorities(Thread.MAX_PRIORITY));
        service.shutdown();
    }
}
/*
output:pool-1-thread-4:这是构造器自动生成的名字,第二个字段是优先级;
Thread[pool-1-thread-4,10,main]: 3
Thread[pool-1-thread-3,1,main]: 3
Thread[pool-1-thread-1,1,main]: 3
Thread[pool-1-thread-2,1,main]: 3
Thread[pool-1-thread-4,10,main]: 2
Thread[pool-1-thread-3,1,main]: 2
Thread[pool-1-thread-1,1,main]: 2
Thread[pool-1-thread-2,1,main]: 2
Thread[pool-1-thread-4,10,main]: 1
Thread[pool-1-thread-1,1,main]: 1
Thread[pool-1-thread-2,1,main]: 1
Thread[pool-1-thread-3,1,main]: 1
 */
  • toString() 方法被覆盖,以便使用 Thread.toString() 方法来打印线程的名称、线程的优先级以及线程所属的“线程组”。你可以通过构造器来自己设置这个名称,如pool-1-thread-1,pool-1-thread-6等。覆盖后的 toString 还打印了线程的倒计数值。
  • 注意:你可以在一个任务的内部,通过调用 Thread.currentThread()来获得对驱动该任务的 Thread 对象的引用
  • 可以看到,最后一个线程的优先级最高,其余所有线程的优先级被设为最低。注意:优先级是在 run() 的开头设定的,在构造器中设置它们没有任何好处,因为 Executor 在此刻还没有开始执行任务
  • run()里,执行了10 0000 0000 次开销相当大的浮点运算,变量d 是volatile 的,以确保不进行任何编译器优化。如果不加如这些运算,将看不出差别。
  • 如果知道已经完成了在run() 方法循环的一次迭代过程中所需的工作是什么,就可以给线程调度机制一个暗示:你的工作已经差不多完成了,可以将 CPU 给别的线程了(这只是一个暗示,没有任何机制保证它会被采纳)。当调用 yield() 方法时,你也是建议具有相同优先级的其他线程可以运行。

LiftOff.java 使用yield()在各种不同的LiftOff任务之间产生良好的处理机制

1.7 后台线程

后台线程,是指在程序运行的时候在后台提供一种通用服务的线程。这种线程并不属于程序中不可或缺的部分。因此,当所有的非后台线程结束时,程序也就终止了,同时会杀死进程中的所有后台进程。反过来说:只要有任何非后台线程还在运行,程序就不会终止。

public class SimpleDaemons implements Runnable{

    @Override
    public void run() {
        try {
            while (true) {
                TimeUnit.MILLISECONDS.sleep(500);
                print(Thread.currentThread() + " " + this);
            }
        } catch (InterruptedException e) {
            print("sleep() interrupted");
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread daemon = new Thread(new SimpleDaemons());
            //设置它为后台进程在这个线程开始之前
            daemon.setDaemon(true);
            daemon.start();
        }

        print("All daemons started ");
        try {
            TimeUnit.MILLISECONDS.sleep(1750);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
/*
output:
All daemons started 
Thread[Thread-8,5,main] SimpleDaemons@11f3ef5e
Thread[Thread-5,5,main] SimpleDaemons@f2733ba
Thread[Thread-0,5,main] SimpleDaemons@6b52350c
Thread[Thread-1,5,main] SimpleDaemons@42337127
Thread[Thread-7,5,main] SimpleDaemons@2d74268a
Thread[Thread-6,5,main] SimpleDaemons@60cb300b
Thread[Thread-9,5,main] SimpleDaemons@1fe9b76c
Thread[Thread-3,5,main] SimpleDaemons@7e7e475e
Thread[Thread-2,5,main] SimpleDaemons@47c48106
Thread[Thread-4,5,main] SimpleDaemons@c622f0c
。。。
 */

必须在线程启动之前调用 setDaemon() 方法,才能把它设置为后台线程。
因为这里只有main 一个前台进程,当他结束后,就没有什么能阻止程序终止了。当把main的sleep 时长设置变化时,可以看到不同的打印结果。
SimpleDaemons.java 创建了显式的线程,以便可以设置它们的后台标志。

1.8 编码的变体

public class SelfManaged implements Runnable{
    private int countDown = 3;

    public SelfManaged() {
        Thread t = new Thread(this);
        t.start();
    }

    @Override
    public String toString() {
        return Thread.currentThread().getName() +"("+ countDown + "),";
    }

    @Override
    public void run() {
        while (true) {
            System.out.println(this);
            if (--countDown == 0) return;
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new SelfManaged();
        }
    }
}
/*
output:Thread-X 代表第几个线程
Thread-0(2),
Thread-0(1),
Thread-1(2),
Thread-1(1),
 */

通过适当的Thread 构造器为 Thread 对象赋予具体的名称,这个名称可以通过使用getName() 从toString 中获得。
上面的示例是更常用的自管理的Runnable:注意:start()是在构造器中调用的。但是你要意识到:在构造器中启动线程可能会变得很有问题,因为另一个任务可能会在构造器结束之前开始执行,这意味着该任务能够访问处于不稳定状态 的对象。这就是不显示创建线程的原因之一。
所以通过使用内部类将线程代码隐藏在类中将会很有用。

class InnerThread1 {
    private int countDown = 5;
    private Inner inner;

    private class Inner extends Thread {
        Inner(String name) {
            super(name);
            start();
        }

        @Override
        public void run() {
            try {
                while (true) {
                    print(this);
                    if (--countDown == 0) return;
                    sleep(10);
                }
            } catch (InterruptedException e) {
                print("sleep() interrupted");
            }
        }
        @Override
        public String toString() {
            return getName()+": "+ countDown;
        }
    }

    public InnerThread1(String name) {
        inner = new Inner(name);
    }
}

//使用匿名内部类
class InnerThread2 {
    private int countDown = 5;
    private Thread t;

    public InnerThread2(String name) {
        t = new Thread(name){
            @Override
            public void run() {
                try {
                    while (true) {
                        print(this);
                        if (--countDown == 0) return;
                        sleep(10);
                    }
                } catch (InterruptedException e) {
                    print("sleep() interrupted");
                }
            }

            @Override
            public String toString() {
                return getName() + ": " + countDown;
            }
        };
        t.start();
    }
}

//不同的方法将相同的代码作为任务运行
class ThreadMethod {
    private int countDown = 5;
    private Thread t;
    private String name;

    public ThreadMethod(String name) {
        this.name = name;
    }
    public void runTask() {
        if (t == null) {
            t = new Thread(name){
                @Override
                public void run() {
                    try {
                        while (true) {
                            print(this);
                            if (--countDown == 0) return;
                            sleep(10);
                        }
                    } catch (InterruptedException e) {
                        print("sleep() interrupted");
                    }
                }

                @Override
                public String toString() {
                    return getName()+": "+countDown;
                }
            };
            t.start();
        }
    }
}
public class ThreadVariations {
    public static void main(String[] args) {
        new InnerThread1("InnerThread1");
        new InnerThread2("InnerThread2");
        new ThreadMethod("ThreadMethod").runTask();
    }
}
/*
output:
InnerThread1: 5
InnerThread2: 5
ThreadMethod: 5
ThreadMethod: 4
InnerThread2: 4
InnerThread1: 4
ThreadMethod: 3
InnerThread1: 3
InnerThread2: 3
ThreadMethod: 2
InnerThread1: 2
InnerThread2: 2
InnerThread1: 1
InnerThread2: 1
ThreadMethod: 1
 */

InnerThread1 创建了一个扩展自Thread 的匿名内部类,并且在构造器中创建了这个匿名内部类的一个实例。但是你只是为了使用Thread的能力,因此其实不必创建匿名内部类。所以 InnerThread2 展示了另一种方式:在构造器中创建一个匿名的 Thread 子类,并将其向上转型为 Thread 引用 t 。如果类中的其他方法需要访问 t,那它们可以通过 Thread 接口来实现,并且不需要了解该对象的确切类型
ThreadMethod 类展示了在方法内部如何创建线程如果该线程只执行辅助操作,而不是该类的重要操作,那么这与该类的构造器内部启动线程相比,是一种更加有用且合适的方式
所以,推荐的方式是在方法内部创建线程而不是在构造器中。

下面是按照上面的类修改的类:用来实现计算总和的斐波那契数字数量

class Fibonacci{
    private static ExecutorService es;

    private static int fib(int n) {
        if (n == 0 || n==1) return 1;
        return fib(n - 1) + fib(n - 2);
    }

    public static synchronized Future<Integer> runTask(final int n) {
        assert es != null;
        return es.submit(() -> {
            int sum = 0;
            for (int i = 0; i < n; i++) {
                sum += fib(i);
            }
            return sum;
        });
    }

    public static synchronized void init() {
        if (es == null) es = Executors.newCachedThreadPool();
    }

    public static synchronized void shutdown() {
        if (es != null) es.shutdown();
        es = null;
    }
}

public class FibonacciSum {
    public static void main(String[] args) {
        ArrayList<Future<Integer>> results = new ArrayList<>();
        Fibonacci.init();
        for (int i = 0; i <= 5; i++) {
            results.add(Fibonacci.runTask(i));
        }
        Thread.yield();
        Fibonacci.shutdown();
        for (Future<Integer> fi : results) {
            try {
                System.out.println(fi.get());
            } catch (InterruptedException e| ExecutionException e) {
                e.printStackTrace();
            }
        }
    }
}
/*
output:
0
1
2
4
7
12
 */

1.9 术语

到目前为止,你应该会对要执行的任务和驱动它的线程之间有一个困惑,这在Java 中尤为明显,因为你对线程没有任何控制权(在使用执行器时为甚,因为它会替你处理线程的创建和管理)。你创建任务,并通过某种方式将一个线程附着到任务上,以使得线程驱动任务。
在Java 中,Thread 类自身不执行任何操作,它只是驱动赋予它的任务,但是线程和任务不是“是一个”的关系。
为了分清,我们将在描述要执行的工作时使用术语:任务。只有在引用到驱动任务的具体机制时,才使用“线程”

1.10 加入一个线程

一个线程可以在其他线程之上调用join()方法,其效果是等待一段时间直到第二个线程结束才继续执行。如果某个线程在另一个线程 t 上调用join(),此线程将被挂起,直到目标线程 t 结束才恢复(即t.isAlive()为假)。
也可以在调用 join 方法时带上一个超时参数(单位可以是毫秒和纳秒),这样如果目标线程在这段时间结束后还没有结束,join()方法总能返回。
join()方法的调用可以被中断,做法是在调用线程上调用 interrupt()方法。
下面这个示例演示了所有这些操作:

class Sleeper extends Thread {
    private int duration;

    public Sleeper(String name, int sleepTime) {
        super(name);
        duration = sleepTime;
        start();
    }

    @Override
    public void run() {
        try {
            sleep(duration);
        } catch (InterruptedException e) {
            System.out.println(getName() + " was interrupted. " + "isInterrupted(): " + isInterrupted());
        }
        System.out.println(getName() + " has awakened");
    }
}

class Joiner extends Thread {
    private Sleeper sleeper;

    public Joiner(String name, Sleeper sleeper) {
        super(name);
        this.sleeper = sleeper;
        start();
    }

    @Override
    public void run() {
        try {
            sleeper.join();
        } catch (InterruptedException e) {
            System.out.println("Interrupted");
        }
        System.out.println(getName() + " join completed");
    }
}
public class Joining {
    public static void main(String[] args) {
        Sleeper
                sleeper = new Sleeper("Sleeper", 1500),
                grumpy = new Sleeper("Grumpy", 1500);
        Joiner
                dopey = new Joiner("Dopey", sleeper),
                doc = new Joiner("Doc", grumpy);
    }
}
/*
output:
Grumpy has awakened
Sleeper has awakened
Doc join completed
Dopey join completed
 */

1.11 捕获异常

由于线程的本质,使得你不能捕获从线程中逃逸的异常。

public class ExceptionThread implements Runnable{

    @Override
    public void run() {
        throw new RuntimeException();
    }

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        service.execute(new ExceptionThread());
    }
}
/*
output:
Exception in thread "pool-1-thread-1" java.lang.RuntimeException
	at com.gui.demo.thingInJava.concurrency.ExceptionThread.run(ExceptionThread.java:16)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run
 */

将main的主体放入到try-catch块是没有用的,也就是说异常是无法捕获的,一旦异常越过了任务的run()方法,它就会传播至控制台,除非采用特殊步骤来捕获它。
为了解决这个问题,我们要修改Executors产生线程的方式。Thread.UncaughtExceptionHandler 允许你在每个Thread 对象上都附着一个异常处理器。
为了使用它,我们创建了一个新类型的ThreadFactory,它将每个新创建的Thread上附着了一个上面的异常。我们将这个工厂传递给Executors创建的ExecutorService的方法:

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 service = Executors.newCachedThreadPool(new HandlerThreadFactory());
        service.execute(new ExceptionThread2());
    }
}
/*
output:
HandlerThreadFactory@6d03e736 creating new Thread
created Thread[Thread-0,5,main]
eh = MyUncaughtExceptionHandler@568db2f2
run() by Thread[Thread-0,5,main]
eh = MyUncaughtExceptionHandler@568db2f2
HandlerThreadFactory@6d03e736 creating new Thread
created Thread[Thread-1,5,main]
eh = MyUncaughtExceptionHandler@2f8d3330
caught java.lang.RuntimeException
 */

2、资源共享

有了并发可以同时做多件事情了,但是,两个或多个现场彼此之间互相干涉的问题就出现了。比如:两个线程同时访问一个账户,改变同一个值等。
当你开始执行某些任务时,我们可以通过两种不同的方式捕获结果:通过副作用或者通过返回值。
从语法上讲,副作用的方式看起来更容易:你只需要使用结果去操纵环境中的一些对象就行。例如:执行计算任务,直接就可以将结果写入集合中。
这种方法的问题是:集合是典型共享资源的,当多个任务同时执行时,任何任务都可以读写共享资源。这揭示了资源竞争的问题
解决此问题的一种方法是使用能够应对资源竞争的集合,如果多个任务同时尝试对此集合进行写操作,那么这个集合将可以应对这样的问题。你会发现Java 并发库中有很多尝试解决资源竞争问题的类。

2.1 资源竞争

考虑下面的例子,其中一个任务产生偶数,而其他任务则消费这些数字。这里,消费者任务的唯一工作就是检查偶数的有效性。
首先,定义EvenChecker,即消费者任务,因为它将在随后所有的示例中被复用。为了将它与我们要试验的各种类型生成器解耦,我们将创建一个名为 IntGenerator 的抽象类,它包含了EvenCheck 必不可少的方法:即一个 next()方法和一个可以执行撤销的方法。
这个类没有实现Generator 接口,因为它必须产生一个int,而泛型不支持基本类型的参数

public abstract class IntGenerator {
    //    private volatile boolean canceled = false;旧模式定义
    private AtomicBoolean canceled = new AtomicBoolean();//新模式

    public abstract int next();

    public void setCanceled() {
        canceled.set(true);
    }

    public boolean isCanceled() {
        return canceled.get();
    }
}

setCanceled()方法改变 AtomicBoolean 类型的 canceled 标志位的状态,而isCanceled()方法则告诉标志位是否设置过了。因为 canceled 标志位是 AtomicBoolean 类型,由于它是原子性的,即诸如赋值和返回值这样的简单操作在发生时没有中断的可能(不可中断的),因此这些操作不存在中间状态。之后进行详细描述。

任何 IntGenerator 都可以使用下面的 EvenChecker 类进行测试:

public class EventChecker implements Runnable{
    private IntGenerator generator;
    private final int id;

    public EventChecker(IntGenerator generator, int id) {
        this.generator = generator;
        this.id = id;
    }

    @Override
    public void run() {
        while (!generator.isCanceled()) {
            //实现它的子类都会重写next()方法,但是传递这样的父类接口没有问题
            int val = generator.next();
            if (val % 2 != 0) {
                System.out.println(val + " not even!");
                generator.setCanceled();//取消所有消费者
            }
        }
    }

    //可以测试各种类型的IntGenerator
    public static void test(IntGenerator gp, int count) {
        /*System.out.println("Press Control-C to exit");
        ExecutorService service = Executors.newCachedThreadPool();
        for (int i = 0; i < count; i++) {
            service.execute(new EventChecker(gp, i));
        }
        service.shutdown();*/


        List<CompletableFuture<Void>> futures = IntStream.range(0,count)//产生数字序列流
                .mapToObj(i->new EventChecker(gp,i))//产生多个EventChecker实例,但是gp是同一个对象
                .map(CompletableFuture::runAsync)//runAsync产生CompletableFuture,异步线程启动
                .collect(Collectors.toList());
        futures.forEach(CompletableFuture::join);
    }

    //
    public static void test(IntGenerator gp) {
        new TimedAbort(4, "No odd numbers discovered");
        test(gp,10);
    }
}

通过这种方式,共享公共资源(IntGenerator)的任务可以观察该资源的终止信号。这可以消除所谓竞争条件,即两个或更多的任务竞争响应某个条件,因此产生冲突或结果不一致的情况。
你必须仔细考虑并防范并发系统失败的所有可能途径,例如,一个任务不能依赖于另一个任务,因为任务关闭的顺序是不受控制的。所以,在这里通过使任务依赖于非任务对象,我们可以消除潜在的竞争对象
test()方法通过启动大量使用相同 IntGenerator 的 EvenChecker,设置并执行对任何类型的 IntGenerator 的测试。如果 IntGenerator 引发失败,那么 test() 将报告它并返回。
EvenChecker 任务总是读取和测试从 IntGenerator 返回的值。注意:如果generic.isCanceled()为true,则run()将返回,这将告知 EvenChecker.test()中的Executors该任务完成了
在本设计中,共享公共资源(IntGenerator)的任务会监视该资源的终止信号。这就消除了所谓的竞争条件,即两个或更多的任务争先去响应一个状态,从而产生碰撞及不一样的结果。
你必须仔细考虑并防止并发系统失败的所有方式。例如,一个任务不能依赖于另一个任务,因为任务关闭的顺序无法得到保证。通过使任务对象依赖于非任务对象,我们可以消除潜在的竞争条件。

上面方法中调用了 TimedAbort 类:

public class TimedAbort {
  private volatile boolean restart = true;
  public TimedAbort(double t, String msg) {
    CompletableFuture.runAsync(() -> {
      try {
        while(restart) {
          restart = false;
          TimeUnit.MILLISECONDS
            .sleep((int)(1000 * t));
        }
      } catch(InterruptedException e) {
        throw new RuntimeException(e);
      }
      System.out.println(msg);
      System.exit(0);
    });
  }
  public TimedAbort(double t) {
    this(t, "TimedAbort " + t);
  }
  public void restart() { restart = true; }
}

/*
public static CompletableFuture<Void> runAsync(Runnable runnable) { return asyncRunStage(asyncPool, runnable);}
CompletableFuture的runAsync中使用lambda 表达式创建一个Runnable,该表达式使用runAsync() 静态方法,它的结果会立即返回。
因此,TimedAbort 不会持续保持任何打开的任务(它会直接将任务关闭,如果此时有一个任务正在运行,则会等待该任务执行完再关闭),如果一个任务占用了太长时间,它仍将终止该任务(所以 TimedAbort 被称为守护进程)。
TimedAbort 还允许你 使用restart() 方法重启任务,在某些有用的活动进行时保持打开状态。

public class Nap {
  public Nap(double t) { // Seconds
    try {
      TimeUnit.MILLISECONDS.sleep((int)(1000 * t));
    } catch(InterruptedException e) {
      throw new RuntimeException(e);
    }
  }
  public Nap(double t, String msg) {
    this(t);
    System.out.println(msg);
  }
}
/*=================================================*/
public class TestAbort {
    public static void main(String[] args) {
        new TimedAbort(5);
        System.out.println("Napping for 4");
        new Nap(4);
    }
}
/*
output:先输出第一行,等待1秒后输出第二行
Napping for 4
TimedAbort 1.0
 */

这里的执行流程是这样的,TimedAbort 作为守护进程,如果其他进程比他执行的时间要长,则当它执行完流程后,不会在管其他进程会直接关闭任务。如果其他进程比它的执行时间要短,则在其他进程执行完流程后,也会关闭任务。意思就是它只允许其他进程比它执行时间短,否则就不会执行出结果

我们看到的第一个 IntGenerator 有一个可以产生一系列偶数值的next()方法:

public class EvenGenerator extends IntGenerator{
    private int currentEvenValue = 0;

    @Override
    public int next() {
        ++ currentEvenValue;//[1]这里是危险的地方
        ++ currentEvenValue;
        return currentEvenValue;
    }

    public static void main(String[] args) {
        EventChecker.test(new EvenGenerator());
    }
}
/*
output:
2053 not even!
2059 not even!
2057 not even!
2055 not even!
 */
  • [1] 一个任务有可能在另外一个任务执行第一个对 currentEvenValue 的自增操作之后,但是没有执行第二个操作之前,调用 next() 方法。这将使这个值处于危险的状态。

为了证明这是可能发生的,EvenChecker.test() 创建了一组 EvenChecker 对象,以连续读取 EvenGenerator 的输出并测试检查每个数值是否都是偶数。如果不是,就会报告错误。
多线程程序的部分问题是,即使存在bug,如果失败的可能性很低,程序仍然可以正确显示。
重要的是要注意到自增操作自身需要多个步骤,并且在自增过程中任务可能会被线程挂起(也就是说,自增不是原子性的操作)。所以,如果不保护任务,即使单纯的自增也不是线程安全的。
该示例并不总是在第一次非偶数产生时终止,所有任务都不会立即关闭,这是并发程序的典型特征。

2.2 解决资源竞争

为了使并发工作有效,你需要某种方式来阻止两个任务访问同一个资源。防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。
基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方案。这意味着在给定时刻只允许一个任务访问共享资源。通常是通过在代码前面加上一条锁语句来实现的,这就使得在一段时间内只有一个任务可以运行这段代码。因为锁语句产生了一种互斥效果,这种效果常常被称为互斥量。
当释放锁后,可以通过yield() 和 setPriority() 来给线程调度器建议,但是这些建议未必有用,这主要取决于你的jvm实现。
Java 提供关键字 synchronized 关键字来实现。当任务要执行被synchronized 关键字保护的代码片段的时候,它将检查锁是否可用,然后获取锁,执行代码,最后再释放锁。
共享资源:一般是以对象形式存在的内存片段,但也可以是文件、输入/输出端口(打印机)。
要控制对共享资源的访问,得先把它包装进一个对象。然后把所有要访问这个资源的方法标记为 synchronized 。
声明synchronized 方法的方式:synchronized void f(){/*......*/}
所有对象都自动含有单一的锁(也称为监视器)。当在对象上调用其任意 synchronized 方法时,此对象即被加锁,这是该对象上的其他 synchronized 方法只有等到前一个方法调用完毕并释放锁之后才能调用此对象。
注意:在使用并发时,将域设置为private 是非常重要的,否则,synchronized 关键字就不能防止其他任务直接访问域,这样就会产生冲突。
针对每个类,也有一个锁(作为类的Class对象的一部分),所以synchronized static (静态同步)方法可以在类的范围内防止对static 数据的并发访问。
什么时候使用同步呢?它的规则是这样的:

  • 如果你正在写一个变量,它可能接下来被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步,并且读写线程都必须用相同的监视器锁同步
  • 如果类中有超过一个方法在处理临界区数据,那么你必须同步所有相关方法。如果只同步其中一个方法,那么其他方法可以忽略对象锁,并且可以不受限制的调用。

同步控制 EvenGenerator ,通过加入 synchronized 关键字,可以防止不希望的线程访问:

public class SynchronizedGenerator extends IntGenerator{
    private int currentEvenValue = 0;
    @Override
    public synchronized int next() {
        ++ currentEvenValue;
        Thread.yield();
        ++ currentEvenValue;
        return currentEvenValue;
    }

    public static void main(String[] args) {
        EventChecker.test(new SynchronizedGenerator());
    }
}

Thread.yield();的调用被插入到了两个递增操作之间,以提高在 currentEvenValue 是奇数时上下文切换的可能性。因为互斥可以防止多个任务进入临界区,所以这不会产生任何失败。但是如果发生失败,调用 Thread.yield() 是一种促使其发生的有效方式。

2.3 使用显式的Lock锁

java.util.concurrent.locks 中的显式的互斥机制使得Lock 对象必须被显式的创建、锁定和释放。
因此,与内建的锁形式相比,代码缺乏优雅性。但是解决某些特定问题它更加灵活。
下面是重写的 SynchronizedGenerator :

public class MuteEvenGenerator 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) {
        EventChecker.test(new MuteEvenGenerator());
    }
}

MuteEvenGenerator 添加了一个被互斥调用的锁,并使用 lock()unlock()方法在 next()内部创建了临界资源。
try-finally 是lock 的模板,尽管try-finally 所需的代码比 synchronized 关键字要多,但是这也代表了显式的 Lock 对象的优点之一。如果 同步关键字出现异常,那么就只会抛出异常而无法去做清理工作以维持系统的良好状态。
大体上,当你使用 synchronized 关键字时,需要写的代码更少,出错更少,所以只有在特殊情况下,才会显示使用 Lock 对象。例如:用synchronized 关键字不能尝试着获取锁且最终获取锁会失败,或者尝试着获取锁一段时间,然后放弃它。

public class AttemptLocking {
    private ReentrantLock lock = new ReentrantLock();
    public void untimedLock() {
        //尝试获得锁
        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) {
            throw new RuntimeException(e);
        }
        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.untimedLock();
        al.timed();
        CompletableFuture.runAsync(() -> {
            al.lock.lock();
            System.out.println("acquired");
        });
        new Nap(0.2);
        al.untimedLock();
        al.timed();
    }
}
/*
output:
tryLock(): true
tryLock(2,TimeUnit.SECONDS): true
acquired
tryLock(): false
tryLock(2,TimeUnit.SECONDS): false
 */

ReentrantLock 可以尝试或者放弃获取锁,因此如果某些任务已经拥有锁,你可以决定放弃并执行其他操作,而不是一直等到锁释放,就像untimedLock()方法那样。而在timed方法中,则尝试获取可能在2秒后没成功而放弃的锁。
在main() 中,一个单独的线程被匿名类锁创建,并且它会获得锁,因此可以让之前两个方法去竞争。

显式锁比起内置同步锁提供更细致的加锁和解锁控制。

2.4 volatile 关键字

volatile 可能是Java中最微妙和最难用的关键字。现在已经有替换方法,如果你在代码中看到它,这段代码很可能是过时的。
使用volatile 有三个理由:

  1. 字分裂
  2. 可见性
  3. 重排与 Happen-Before 原则

原子操作是不能被线程调度机制中断的操作,一旦操作开始,那么它一定可以在可能发生的“上下文切换”之前(切换到其他线程执行)执行完毕。原子性可以应用于除了 long 和 double 之外的所有基本类型之上的“简单操作”。JVM 将64位(对于 long 和 double) 变量的读取和写入当做两个分离的 32 位操作来执行,这就产生了一个读取和写入操作中间发生上下文切换,从而导致不同的任务可能看到不同的结果(这被称为字分裂)。
所以 volatile 关键字应用而生,使用 volatile 关键字可以阻止自分裂,但是 synchronized 和atomic 类之一也可以做到。
可见性是指:在Java 中,Java总是尽可能地提高执行效率,会进行优化代码执行顺序。所以CPU 中的缓存的主要目的是避免从主存中读取数据。当并发时,有时不清楚 Java 什么时候该将值从主存刷新到本地缓存 ---- 而这个问题称为缓存一致性
每个线程都可以在处理器缓存存储变量的本地副本。 如果你将一个域声明为 volatile 的,那么只要对这个域产生了写操作,那么所有的读操作都可以看到这个更改。即便使用了本地缓存,情况也如此,volatile 域会立即被写入到主存中,而读取操作就发生在主存中。在非volatile 域上的原子操作不必刷新到主存中去,因此其他读取该域的任务也不必看到这个新值。如果多个任务在同时访问某个域,那么这个域就该是 volatile 的,否则,就只能由同步来确保安全。同步也会导致刷新,所以如果一个域完全由 synchronized 方法或语句来防护,那就不必设置其为 volatile的。
使用 volatile 而不是 synchronized 的唯一安全的情况是类中只有一个可变的域。所以,你的第一选择是 synchronized 关键字。

public class AtomicityTest implements Runnable{
    private int i = 0;

    public int getValue() {
        return i;
    }
    private synchronized void evenIncrement() {
        i++;
        i++;
    }
    @Override
    public void run() {
        while (true) {
            evenIncrement();
        }
    }

    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        AtomicityTest at = new AtomicityTest();
        exec.execute(at);
        while (true) {
            int val = at.getValue();
            if (val % 2 != 0) {
                System.out.println(val);
                System.exit(0);
            }
        }
    }
}

该程序在找到第一个奇数值并终止,我的机器在 i = 1 时就终止跳出了。尽管 return i 确实是原子性操作,但是缺少同步(synchronized)使得其数值可以在处于不稳定的中间状态时被读取(也就是说,你上锁的任务只是针对所有要修改这个资源的任务,而像读取不操作的任务就可以不去参与竞争而共享这个资源而读取到了中间状态,所以想要读取任务也排队就得给读取也加上锁)。除此之外,由于 i 也不是 volatile 的,因此还存在可视性问题。而对于这样单个元素的这些所有问题使用原子类就可以解决(比如AtomicInteger)
所以这里getValue()evenIncrement()必须都是 synchronized 的。
基本上,如果一个域可能会被多个任务同时访问,或者这些任务中至少有一个是写入任务,那么你就应该将这个域设置为volatile的。如果你这么做了,就相当于告诉编译器不要执行任何移除读取和写入的优化,这么操作的目的是用线程中的局部变量维护对这个域的精确同步。实际上,读取和写入都是直接针对内存的,而不会被缓存。

重排与 Happen-Before 原则:只要结果不会改变程序表现,Java 可以通过重排指令来优化性能。而 volatile 关键字可以阻止重排 volatile 周围的读写指令。这项原则保证 volatile 变量读写之前发生的指令先于它们的读写发生之前。同样,任何跟随 volatile 变量之后读写的操作都保证发生在它们的读写之后。

2.5 Josh 的序列号

创建一个产生序列号的类:

public class SerialNumbers {
    private volatile int serialNumber = 0;
    public int nextSerialNumber() {
        return serialNumber++;
    }
}

为了测试SerialNumber ,再创建一个不会耗尽内存的集合,假如需要很长时间来检测问题。

public class CircularSet {
    private int[] array;
    private int size;
    private int index = 0;

    public CircularSet(int size) {
        this.size = size;
        array = new int[size];
        //初始化不产生值
        Arrays.fill(array, -1);
    }

    public synchronized void add(int i) {
        array[index] = i;
        //覆盖之前的值:index相当于后移一位
        index = ++ index % size;
    }

    public synchronized boolean contains(int val) {
        for (int i = 0; i < size; i++) {
            if (array[i] == val) {
                return true;
            }
        }
        return false;
    }
}

add()contains()方法是线程同步的,以防止线程冲突。SerialNumberChecker 类包含一个存储最近序列号的 CircularSet 变量,以及一个填充数值给CircularSet 的方法,方法可以确保序列号唯一。

public class SerialNumberChecker implements Runnable {
    private CircularSet serials = new CircularSet(1000);
    private SerialNumbers producer;

    public SerialNumberChecker(SerialNumbers producer) {
        this.producer = producer;
    }

    @Override
    public void run() {
        while (true) {
            int serial = producer.nextSerialNumber();
            if (serials.contains(serial)) {
                System.out.println("Duplicate: " + serial);
                System.exit(0);
            }
            serials.add(serial);
        }
    }

    static void test(SerialNumbers producer) {
        for (int i = 0; i < 10; i++) {
            CompletableFuture.runAsync(new SerialNumberChecker(producer));
        }
        new Nap(4, "No duplicate detected");
    }
}

test()方法创建多个任务来竞争单独的SerialNumbers 对象。这时参与竞争的 SerialNumberChecker 任务就会可能产生重复的序列号。
当我们测试基本的 SerialNumbers 类,它会失败:

public class SerialNumberTest {
    public static void main(String[] args) {
        SerialNumberChecker.test(new SerialNumbers());
    }
}
/*
output:
Duplicate: 114961
 */

所以 volatile 关键字并没有起作用。要解决这个问题,我们需要将 synchronized 关键字添加到 nextSerialNumber() 方法:

public class SynchronizedSerialNumbers extends SerialNumbers {
    private int serialNumber = 0;
    @Override
    public synchronized int nextSerialNumber() {
        return serialNumber++;
    }

    public static void main(String[] args) {
        SerialNumberChecker.test(new SynchronizedSerialNumbers());
    }
}
/*
output:
No duplicate detected
 */

显而易见,synchronized 关键字保证了线程的安全性,这也是为什么推荐使用synchronized 的原因。

2.6 原子类

Java 5 引入了原子变量类:AtomicInteger、AtomicLong、AtomicReference 等。
这些类提供了原子性升级,这些快速、无锁的操作,是利用了现代处理器上可用的机器级原子性。
下面,使用AtomicInteger 重写 unsafereturn.java :

public class AtomicIntegerTest implements Runnable{
    private AtomicInteger i = new AtomicInteger(0);
    public int getAsInt() {
        return i.get();
    }

    public void evenIncrement() {
        i.addAndGet(2);
    }

    @Override
    public void run() {
        while (true) {
            evenIncrement();
            if (getAsInt() % 2 != 0) {
                System.out.println(getAsInt());
                System.exit(0);
            }
        }
    }
    public static void main(String[] args) {
        ExecutorService exec = Executors.newCachedThreadPool();
        AtomicIntegerTest ait = new AtomicIntegerTest();
        exec.execute(ait);
        new TimedAbort(4, "No odd numbers discovered");
    }
}
/*
output:
No odd numbers discovered
 */

现在,我们通过使用 AtomicInteger 消除了 synchronized 关键字。
下面使用 AtomicInteger 来改写 SynchronizedEvenProducer.java:

public class AtomicEvenProducer extends IntGenerator {
    private AtomicInteger currentValue = new AtomicInteger(0);

    @Override
    public int next() {
        return currentValue.addAndGet(2);
    }

    public static void main(String[] args) {
        EventChecker.test(new AtomicEvenProducer());
    }
}
/*
output:
No odd numbers discovered
 */

这些都是对单一字段的简单示例:当你创建更加复杂的类时,你必须确定哪些字段需要保护,在某些情况下,你可以任然需要在方法上使用同步关键字

2.7 临界区

有时,你只是想防止多线程访问方法中的部分代码,而不是整个方法。要隔离的代码称为临界区
创建临界区也是使用 synchronized 关键字,但是不同的是语法。语法如下:使用synchronized 指定某个对象作为锁用于同步控制花括号内的代码:

	synchronized (syncObject) {
            //这里的代码只能一次允许一个任务访问
    }

这也被称为同步代码块 。在进入此代码前,必须得到syncObject 的锁,如果其他线程已经得到这个锁,就得等到锁被释放以后,才能进入临界区。
通过使用同步代码块,而不是对整个方法进行同步控制,可以使多个任务访问对象的时间性能得到显著提高。下面的例子会统计成功访问method()的计数并发起任务来竞争method() 方法。

abstract class Guarded {
    AtomicLong callCount = new AtomicLong();

    public abstract void method();

    @Override
    public String toString() {
        return getClass().getSimpleName() + ": " + callCount.get();
    }
}

class SynchronizedMethod extends Guarded {

    @Override
    public void method() {
        new Nap(0.01);
        //因父类字段没有private,所以子类可以读取到父类的字段
        callCount.incrementAndGet();
    }
}

class CriticalSection extends Guarded {

    @Override
    public void method() {
        new Nap(0.01);
        synchronized (this) {
            callCount.incrementAndGet();
        }
    }
}

class Caller implements Runnable {
    private Guarded g;

    Caller(Guarded g) {
        this.g = g;
    }
    private AtomicLong successfulCalls = new AtomicLong();
    private AtomicBoolean stop = new AtomicBoolean(false);

    @Override
    public void run() {
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                stop.set(true);
            }
        },2500);
        while (!stop.get()) {
            g.method();
            successfulCalls.getAndIncrement();
        }
        System.out.println("-> " + successfulCalls.get());
    }
}
public class SynchronizedComparison {
    static void test(Guarded g) {
        List<CompletableFuture<Void>> callers =
                Stream.of(new Caller(g),new Caller(g),new Caller(g),new Caller(g))
                    .map(CompletableFuture::runAsync)
                    .collect(Collectors.toList());
        callers.forEach(CompletableFuture::join);
        //显示 CriticalSection 中可用的 `method()` 有多少
        System.out.println(g);
    }

    public static void main(String[] args) {
        test(new CriticalSection());
        test(new SynchronizedMethod());
    }
}
/*
output:
-> 159
-> 159
-> 159
-> 159
CriticalSection: 636
-> 48
-> 45
-> 50
-> 18
SynchronizedMethod: 161
 */

Guarded 类负责跟踪 callCount 中成功调用 method() 的次数。SynchronizedMethod 的方式是同步控制整个 method() 方法,而 CriticalSection 的方式是使用同步控制块来仅同步 method 方法的一部分代码。这样,控制时长的 Nap 对象可以排除到同步控制块外。输出会显示 CriticalSection 中可用的 method() 有多少。
请记住:使用同步控制代码块是由风险的,它要求你确切知道同步控制块外的非同步代码是实际上要线程安全的
Caller 是在Nap控制的时长内尽可能多地调用 method() 方法(并报告调用次数)。为了构建这个时间周期,我们会使用虽然有点过时但仍可以很好的工作的 java.util.Timer 类。此类接收一个 TimerTask 参数,但该参数并不是函数式接口,所以我们不能用 lambda 表达式,必须显式的创建该类对象。当超时的时候,定时对象将设置 AtomicBoolean 类型的 stop 字段为 true,这样循环就会退出。
test()方法接收一个 Guarded 类对象并创建了四个 Caller 任务,所有这些任务都添加到同一个 Guarded 对象上,因此它们竞争来获取使用 method()方法的锁
你会看到依次运行到下一次运行的输出变化。结果表明:CriticalSection 方式比起 SynchronizedMethod 方式允许更多的访问 method()方法。

2.8 在其他对象上同步

synchronized 块必须给定一个在其上进行同步的对象。并且最合理的方式是,同步正在使用其方法的当前对象:synchronized(this)。在这种方式中,当 synchronized 块获得锁的时候,那么该对象其他的 synchronized 方法和临界区就不能被调用了。而临界区的目的就是减小同步的范围。

class DualSynch {
    ConcurrentLinkedQueue<String> trace = new ConcurrentLinkedQueue<>();

    public synchronized void f(boolean nap) {
        for (int i = 0; i < 5; i++) {
            trace.add(String.format("f() " + i));
            if (nap) new Nap(0.01);
        }
    }

    private Object syncObject = new Object();

    public void g(boolean nap) {
        synchronized (syncObject) {
            for (int i = 0; i < 5; i++) {
                trace.add(String.format("g() " + i));
                if (nap) new Nap(0.01);
            }
        }
    }
}

public class SyncOnObject {
    static void test(boolean fNap, boolean gNap) {
        DualSynch ds = new DualSynch();
        new Runnable() {
            @Override
            public void run() {

            }
        };
        /**下面代码的思想是这样的:SyncOnObject类的test方法会将两个同步方法(因为DualSynch没有实现Runnable,所以没有run方法)异步执行,并将执行后的结果打印出来。
 		*	f()和g() 的方法会根据传入的boolean来进行是否休眠,在循环的次数内休眠n*0.01秒。而在同步锁的作用下,即使下一条test方法执行也会阻塞,并且同时阻塞的是两个方法
 		*/
        List<CompletableFuture<Void>> cfs = Arrays.stream(new Runnable[]{
                () -> ds.f(fNap), () -> ds.g(gNap) })
                .map(CompletableFuture::runAsync)
                .collect(Collectors.toList());
        cfs.forEach(CompletableFuture::join);
        ds.trace.forEach(System.out::println);
    }

    public static void main(String[] args) {
        test(true, false);
        System.out.println("*********");
        test(false,true);
    }
}
/*
output:
f() 0
g() 0
g() 1
g() 2
g() 3
g() 4
f() 1
f() 2
f() 3
f() 4
*********
f() 0
g() 0
f() 1
f() 2
f() 3
f() 4
g() 1
g() 2
g() 3
g() 4
 */

有时必须在另一个对象上同步,但是如果这样做,就必须保证所有相关的任务都是在同一个对象上同步的。
DualSync.f()方法(同步整个方法)在this 上同步,而g()方法有一个同步代码块,因此,这两个同步是互相独立的。在test()方法中运行时的 fNap 和 gNap 标志变量分别指示 f() 和 g() 是否应该在其 for 循环中调用 Nap() 方法。f()线程休眠时,该线程继续持有它的锁,但是你可以看到这并不阻止调用g(),反之亦然。就是因为这两个相关的任务是在同一个对象上同步的

3.无锁集合

集合章节强调集合是基本的编程工具,这也要求包含并发性。因此,早期的集合如Vector和 HashTable 有许多使用 synchronized 机制的方法,当这些集合不是在多线程应用中使用时,就会产生不接接受的开销。在之后的1.2版本中,新的集合库是非同步的,而给 Collection 类赋予了各种 static synchronized 修饰的方法来同步不同的集合类型,它可以让你选择是否对集合使用同步。Java 5 添加的新的集合类型,专门用于增加线程安全性能。
无锁集合:只要读取者仅能看到已完成修改的结果,对集合的修改就可以同时发生在读取发生时。这是通过一种策略实现的。

复制策略

使用“复制”策略进行修改,修改过程是不可见的,仅当修改后的结构与“主”数据结构安全的交换后,读取者才会看到修改。

比较并交换(CAS)

在CAS中,你从内存中获取一个值,并在计算新值时保留原始值,然后使用CAS指令,它将原始值与当前内存中的值比较,如果相等,则将旧值替换为新计算的值,所有操作都在一个原子操作中完成。如果比较失败(不等),则不修改,因此出现失败是因为另一个值在这段时间修改了内存。在这种情况下,你的代码必须再次尝试获取一个新的原始值并重复该操作。
如果内存只存在轻量竞争,CAS操作几乎不会重复。因此会很快。相反,synchronized 操作每次要考虑获取和释放锁的成本,这要昂贵的多。随着内存竞争的增加,使用CAS的操作会变慢,这是对更多资源竞争的动态响应。
最重要的是,许多现代处理器的汇编语言都有CAS指令,并且也会被JVM中的CAS操作调用。CAS指令在硬件层面是原子性的,和你期望的一样快

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值