Java学习之多线程并发

简介

到此为止,我们学到的基本上都是有关顺序编程的知识,即程序中所有事物在任意时刻都只能执行一个步骤。
编程问题中相当大的一部分都可以通过使用顺序编程来解决。然而,对于某些问题,如果能够并发地执行程序中的多个部分,则会变得非常方便。并发编程可以使得程序的处理速度得到极大的提高。但是在得到提高的同时,并发也会带来一些问题,当并行执行的任务彼此开始互相干涉时,时机的并发问题就会接踵而至。
了解并发可以使我们意识到明显正确的程序可能会展现出不正确的行为。



并发的多面性


更快的执行

现在的计算机基本上都是多核处理器,这意味我们的程序可以并行处理。而并发通常是提高运行在单个处理器上程序的性能。这听起来好像并发并没有什么用,事实上,我们考虑一下,在单核处理器上运行的并发程序开销确实应该比该程序的所有部分都顺序执行的开销要大,因为线程之间存在上下文切换的开销。从表面上看,让每个程序去占据一个处理器进行顺序处理会开销小一些,并且可以节省上下文切换的代价。

但是我们如果考虑到阻塞问题,我们就会发现,如果我们的程序都是顺序执行,一旦遇到了某个阻塞情况我们的程序就会无法执行下去,并且占据处理器,直到线程可以继续执行下去。但是如果使用并发来编写,即使某个程序发生了阻塞,仍然不会影响其他程序的继续执行。

实现并发最直接的方式是在操作系统级别使用进程。进程是运行在它自己的地址空间内的程序。多任务操作系统通过周期性地将CPU从一个进程切换到另一个进程,来实现同时运行多个进行。因为操作系统通常会将进程互相隔离开,使得它们不会互相干涉。但是Java所使用的并发系统会共享内存和I/O这样的资源,以使得这些资源不会被多个任务同时访问。



基本的线程机制

并发编程使我们可以将一个程序分割成多个分离的独立的任务。通过多线程机制,这些任务中的每一个都将由执行线程来驱动。一个进程可以有多个线程,多个线程共享一个进程的资源。因此,单个进程可以拥有多个并发执行的任务。


定义任务

线程可以驱动任务,因此我们需要一种任务的抽象,这可以由Runable接口来提供。如果需要定义任务,只需要实现Runable接口并编写run()方法,使得该任务可以执行我们得命令。

下面例子中的LiftOff任务将显示发射的倒计时

class LiftOff implements Runnable{
    
    
    private Integer countDown = 10;
    
    @Override
    public void run() {
    
    
        while(countDown-->0){
    
    
            System.out.println("倒计时:"+countDown);
        }
        System.out.println("发射~");
    }
}

当从Runable导出一个类时,它必须具有run()方法,但是注意,这个方法与常规方法并没有差异,也就是说直接执行run()方法依旧是单线程执行。若要实现线程行为,必须显式地将一个任务附着到线程上


Thread类

将Runable对象转变为工作任务的方式就是将它提交给一个Thread构造器,下面例子中展示了如何使用Thread来驱动上面的任务

public static void main(String[] args) {
    
    
        new Thread(new LiftOff()).run();
        /**
         * 倒计时:9
         * 倒计时:8
         * 倒计时:7
         * 倒计时:6
         * 倒计时:5
         * 倒计时:4
         * 倒计时:3
         * 倒计时:2
         * 倒计时:1
         * 倒计时:0
         * 发射~
         */
    }

Thread构造器只需要一个Runable对象。调用Thread对象的start方法为该线程执行必须的初始化操作,然后调用Runable的run方法,以便在这个新线程中启动任务。注意,由于我们开辟了一条新的线程去执行任务,而不是在原有线程的基础上去顺序执行任务,所以在执行run方法时,main中的代码也将继续执行下去。

如果存在多个线程任务,不同的任务的执行将在线程被换进交换出时混在一起。这种交换是由线程调度器自动控制的。如果存在多处理器,线程调度器将会在这些处理器之间默默地分发线程。

在main中创建Thread对象时,它并没有补获任何对这些对象的引用。在使用普通对象时。如果没有引用,将会被垃圾回收。但是在使用Thread时,就不会这个样子了,每个Thread在任务执行完毕之前,都会一直存着。


Executor

虽然我们可以创建线程来完成我们的任务,但是在多线程形况下,如果频繁地创建线程,会因为线程过多而导致栈内存溢出。因为每个线程,虚拟机都会为其分配单独地栈内存。同时,除了内存溢出的风险,由于创建线程需要耗费大量的资源,所以我们需要某种方式来为我们控制线程的数量,同时在常规状态下,保持线程而不需要在使用时重新创建。
JavaSE5的java.util.concurrent包中的执行器Executor将为我们管理Thread对象,从而简化并发编程。Executor在客户端和任务执行之间提供了一个间接层;与客户端直接执行任务不同,这个中介对象将执行任务。Executor允许我们管理异步任务的执行,而无需显式地管理线程的声明周期。

我们重新构建上面的发射任务,增加了id标识来标识任务的编号

class LiftOff implements Runnable{
    
    
    private Integer countDown = 10;
    private static int count = 0;
    private final int id = count++;

    @Override
    public void run() {
    
    
        while(countDown-->0){
    
    
            System.out.println(id+"----倒计时:"+countDown);
        }
        System.out.println("发射~");
    }
}

我们使用Executor来代替显式创建线程的方式。ExecutorService(具有服务声明周期的Executor,例如关闭)直到如何构建恰当的上下文来执行Runable对象。下面示例中,CachedThreadPool将为每个任务都创建一个线程。注意ExecutorService对象是使用静态的Executor方法创建的

public static void main(String[] args) {
    
    
        ExecutorService exec = Executors.newCachedThreadPool();
        for(int i=0;i<5;i++){
    
    
            exec.execute(new LiftOff());
        }

        /**
         * 0----倒计时:9
         * 1----倒计时:9
         * 4----倒计时:9
         * 0----倒计时:8
         * 0----倒计时:7
         * 2----倒计时:9
         * 2----倒计时:8
         * 2----倒计时:7
         * ....
         */
    }

newCachedThreadPool

前面例子中使用的newCachedThreadPool是一种没有线程数量上限的线程池,这意味着一旦并发数过大,很容易引起内存溢出。在没有任务时,该线程池并不会保持任何线程,它会在创建的所有线程达到生产时间上限60s时,没有新任务的情况下销毁线程,如果有新任务,线程池同时还有线程未过期的空闲线程则复用线程,过期或者没有空闲则创建新线程。采用SynchronousQueue装等待的任务,这个阻塞队列没有存储空间,这意味着只要有请求到来,就必须要找到一条工作线程处理他,如果当前没有空闲的线程,那么就会再创建一条新的线程。


FixedThreadPool

与newCachedThreadPool不同,FixedThreadPool创建的是一种定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。也就是说,线程池中的线程数量是固定的。通过构造时使用corePoolSize和maximunPoolSize来控制其长期保持的线程数量和一个最大线程数量。keepAliveTime为0,意味着一旦有多余的空闲线程,就会被立即停止掉。由于阻塞队列是一个无界队列,因此永远不可能拒绝任务。


SingleThreadExecutor

SingleThreadExecutor可以看作是线程数量为一的FixedThreadPool。如果向SingleThreadExecutor提交了多个任务,那么这些任务将进行排队,每个任务都会在下个任务开始前结束运行,所有任务使用同一条线程。SingleThreadExecutor执行线程是有序的,采用的阻塞队列为LinkedBlockingQueue。


注意:虽然java为我们封装了三种直接可以使用的线程池,但在阿里的程序员手册中并不支持着三种线程池,因为它们认为程序员需要根据实际情况去对线程的数量使用的拒绝策略以及线程存活时间等参数进行设计,而不是简单地将java库中的这三个线程池作为首要使用选项。


从任务中生产返回值(Callable)

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

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

        /**
         * 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
         */
    }
}

class TaskWithResult implements Callable<String>{
    
    
    private int id;
    public TaskWithResult(int id){
    
    
        this.id = id;
    }

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

submit()会产生Future对象,通过Future对象我们就可以获取到我们的任务的执行结果。isDone()方法查询Future是否已经完成,当任务完成时,它具有一个结果,我们可以调用get()来获取该结果。当然,如果不适应isDone()直接get()也没有问题,get()将阻塞直到结果准备就绪。


优先级

线程的优先级将该线程的重要性传递给了线程调度器,调度器将倾向于让优先权最高的线程先执行。但是这并不意味着低优先级的线程永远得不到执行,优先级只代表获取到cpu时间片的概率。
在绝大多数时间里,所有线程有应该以默认的优先级运行。

我们可以通过getPriority()和setPriority()来读取和修改线程的优先级

Thread.currentThread().setPriority(priority);

后台线程

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

public class SynchronizedDemo6 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        for (int i=0 ;i<10;i++){
    
    
            Thread daemon = new Thread(new SimpleDaemons());
            daemon.setDaemon(true);
            daemon.start();
        }
        System.out.println("All daemons started");
        TimeUnit.MILLISECONDS.sleep(175);
    }
}

class SimpleDaemons implements Runnable{
    
    
    @Override
    public void run() {
    
    
        try {
    
    
            while(true){
    
    
                TimeUnit.MILLISECONDS.sleep(100);
                System.out.println(Thread.currentThread()+" "+this);
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。
一旦main完成了工作,所有的后台程序都会被结束掉


编码的变体

到目前为止,所有的任务类都是通过实现Runable接口来完成实现的。但是除了这种方式我们还可以直接继承Thread。

class MyThread extends Thread{
    
    
    @Override
    public void run() {
    
    
        for (int i=0;i<10;i++){
    
    
            System.out.println(i);
        }
    }
}

除此之外我们还能够通过调用适当的Thread构造器为Thread对象赋予具体的名称,这个名称可以通过getName()获取到。

public static void main(String[] args) {
    
    
        Thread thread1 = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                System.out.println(Thread.currentThread().getName()+"开始启动~");
            }
        },"1号线程");

        thread1.start();
        /**
         * 1号线程开始启动~
         */
    }

注意,start()如果在构造器中被调用将可能会引发问题,因为另一个任务可能会在构造器结束前开始执行,这意味着有些初始化工作还未完成,而任务却能够访问那些处于不稳定的对象(未完成初始化的对象)。因此我们建议使用Executor而不是显式地创建Thread对象去执行任务。


捕获异常

由于线程的本质特性,我们无法通过常规的try-catch捕获从线程中逃逸的异常。一旦异常逃出任务的run方法,它就会向外传播到控制台。除非我们为线程提供一个异常处理器

public class SynchronizedDemo9 {
    
    
    public static void main(String[] args) {
    
    
        Thread thread = new Thread(new SimpleTask9());
        thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
    
    
            @Override
            public void uncaughtException(Thread t, Throwable e) {
    
    
                System.out.println("catch "+e);
            }
        });

        thread.start();
        /**
         * catch java.lang.RuntimeException: Thread-0发生的异常
         */
    }
}

class SimpleTask9 implements Runnable{
    
    

    @Override
    public void run() {
    
    
        throw new RuntimeException(Thread.currentThread().getName()+"发生的异常");
    }
}

但是注意,当我们使用线程池时我们无法直接接触到线程,所以没办法为每个线程设置异常处理器。因此,我们需要为线程池提供我们重写的线程工厂,并在工厂方法中为每个线程设置异常处理器。

下面例子中,在线程工厂中为每个线程提供了实现Thread.UncaughtExceptionHandler接口的异常处理器。

public class SynchronizedDemo8 {
    
    
    public static void main(String[] args) {
    
    
        Executor executor = Executors.newCachedThreadPool(new ThreadFactory() {
    
    
            @Override
            public Thread newThread(Runnable r) {
    
    
                Thread thread = new Thread(r);
                thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler(){
    
    
                    @Override
                    public void uncaughtException(Thread t, Throwable e) {
    
    
                        System.out.println("catch "+e.getMessage());
                    }
                });
                return thread;
            }
        });
        executor.execute(new SimpleTask());
    }

    static class SimpleTask implements Runnable{
    
    

        @Override
        public void run() {
    
    
            throw new RuntimeException("发生异常~~");
        }
    }
}


临界资源

可以把单线程程序当作在问题域求解的单一实体,每次只能做一件事,因为只有一个实体,所以永远不用担心诸如“两个实体视图使用同一个资源”这样的问题。
有了并发就可以同时做很多事情了,但是,两个或多个线程彼此相互干涉的问题也就出现了。如果不防范这种冲突,就可能发生两个线程同时视图访问同一个银行账户等诸多问题。


不正确地访问资源

下面代码中,以多个售票员卖票为例展示不正确的访问资源

//临界资源演示
        Runnable r4 = ()->{
    
    
            while(Ticket.tickets>0) {
    
    
                System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + Ticket.tickets--);
                try {
    
    
                    Thread.sleep(300);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }

        };
        Thread s1 = new Thread(r4,"售票员1");
        Thread s2 = new Thread(r4,"售票员2");
        Thread s3 = new Thread(r4,"售票员3");
        Thread s4 = new Thread(r4,"售票员4");

        s1.start();
        s2.start();
        s3.start();
        s4.start();

class Ticket{
    
    
    public static int tickets= 100;
}

运行结果:
在这里插入图片描述
上述买票问题我们会发现多个线程之间在卖票时余票出现了问题。


解决共享资源竞争

前面的示例展示了使用线程时的一个基本问题:你永远都不知道一个线程何时在运行。想象一下,你坐在桌边拿着叉子,正准备去叉盘中的最后一片食物时,这片事物突然消失了,因为你的线程被挂起了,而另一个用餐者进入并吃掉了它,但是你还不知情。这正是在偏斜并发程序时需要注意的问题,我们需要某种方式来防止两个任务访问相同的资源。
防止这种冲突的方法就是当资源被一个任务使用时,为其加上一把锁。这样在使用者解开锁之前,其他任务将无法访问它了。而在锁解开时,另一个任务就可以锁定并使用它。
基本上所有的并发模式在解决冲突问题时,都是从采用序列化访问共享资源的方式。这意味着在给定时刻只允许一个任务访问共享资源。通常这是通过在代码前加上一条锁语句来实现的,这就使得在一段时间内只有一个任务可以运行这段代码。因为锁语句产生了一种互相排斥的效果,因此这种机制常被称为互斥量(mutex)

Java以提供关键字synchronized的形式,为防止资源冲突提供了内置支持。当任务要执行被syncgronized关键字保护的代码片段的时候,它将检查锁是否可用,然后获取锁,执行代码,释放锁。

共享资源一般四以对象形式存在的内存片段,但也可以是文件、输入输出端口,或者是打印机。要控制对共享资源的访问呢,得先把他包装进一个对象,然后把所有要访问这个资源的方法标记为synchronized。如果某个任务处于一个对同步方法的访问中,那么在这个线程从该方法返回前,其他所有想访问该方法的线程都会被阻塞。

对于前面的售票问题,我们可以通过增加synchronized关键字来解决

public class SynchronizedDemo10 {
    
    
    public static void main(String[] args) {
    
    
        //临界资源演示
        Runnable r4 = ()->{
    
    
                while(Ticket.tickets>0) {
    
    
                    synchronized ("2"){
    
    
                        if(Ticket.tickets>0){
    
    
                            System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + --Ticket.tickets);
                        }
                    }
                    try {
    
    
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }

        };
        Thread s1 = new Thread(r4,"售票员1");
        Thread s2 = new Thread(r4,"售票员2");
        Thread s3 = new Thread(r4,"售票员3");
        Thread s4 = new Thread(r4,"售票员4");

        s1.start();
        s2.start();
        s3.start();
        s4.start();
        /**
         * ...
         * 售票员1卖出一张票,剩余:6
         * 售票员2卖出一张票,剩余:5
         * 售票员3卖出一张票,剩余:4
         * 售票员1卖出一张票,剩余:3
         * 售票员2卖出一张票,剩余:2
         * 售票员4卖出一张票,剩余:1
         * 售票员3卖出一张票,剩余:0
         * 
         */
    }


}

class Ticket{
    
    
    public static int tickets= 100;
}


在上面例子中,我们在增加synchronized之后,售票的剩余票数开始正常,并没有多售的现象。

所有对象都含有一个单一的锁(也成为监视器)。当在对象上调用任意synchronized方法的时候,此对象都会被加锁,这时该对象上的其他synchronized方法只有等到前一个方法被调用完毕并释放锁之后才能被调用。对于一个对象来说,其所有的同步方法都共享同一个锁,者可以被用来防止多个任务同时访问同一个方法。

注意,在使用并发时,将域设置为私有是非常重要的,否则synchronized就不能阻止其他任务直接访问域,这样会产生冲突

一个任务可以多次获得对象的锁。如果一个方法在同一个对象上调用了第二个同步方法,JVM会跟踪对象被加锁的次数,如果一个对象被解锁,其计数为0.在任务第一次给对象加锁时,计数变为1。每当这个相同的任务在这个对象上获得锁时,计数都会增加。显然只有首先获得了锁的时候才被允许获取多个锁。

针对每个类也有一个锁(它是类的Class的一部分),所以synchronized static方法可以在类的范围内防止对static数据的并发访问。

前面例子中我们没有在方法上加synchronized关键字,而是直接在方法内部增加了synchronized代码块,我们可以看到前面代码的synchronized后面跟着一个参数,那就是锁,但这个锁我们可以自定义,我们可以认为常规的synchronized方法就是默认将对象锁作为synchronized的锁参数。


使用显式的Lock对象

除了使用synchronized关键字进行同步加锁之外,我们还能够通过Lock对象进行显式加锁。Lock对象必须被显式地创建、锁定和释放。因此它与内建的锁形式相比,代码缺乏优雅性。但是对于解决某些类型的问题来说,它更加灵活。下面使用lock来重写售票互斥

public static void main(String[] args) {
    
    
        Lock lock = new ReentrantLock();
        //临界资源演示
        Runnable r4 = ()->{
    
    
                while(Ticket.tickets>0) {
    
    
                    lock.lock();
                    if(Ticket.tickets>0){
    
    
                        System.out.println(Thread.currentThread().getName() + "卖出一张票,剩余:" + --Ticket.tickets);
                    }
                    lock.unlock();
                    try {
    
    
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }

        };
        Thread s1 = new Thread(r4,"售票员1");
        Thread s2 = new Thread(r4,"售票员2");
        Thread s3 = new Thread(r4,"售票员3");
        Thread s4 = new Thread(r4,"售票员4");

        s1.start();
        s2.start();
        s3.start();
        s4.start();

注意,如果我们使用互斥锁的形式去替代同步方法,我们最好是将unlock()放在finally子句中,并且在try中尝试返回,因为这样可以确保unlock()动作不会提前发生,过早地将数据暴露给第二个任务。

虽然使用lock对象看似更加地繁琐,但事实上,这种形式可以为我们提供一些便利。如果在synchronized代码中出现异常,那我们没有办法去做一些清理收尾工作,但是如果是使用lock对象,出翔异常时,我们还有机会在finally代码中做一些清理工作。

大体上,当沃恩使用synchronized关键字时,需要写的代码量更少,并且用户错误出现的可能性也会降低,因此只有在解决特殊问题时,才使用显式的lock对象。比如我们可以通过lock对象实现一个自旋锁,要实现这些,我们必须使用concurrent类库

下面代码中,我们使用了tryLock实现了如果线程没有获得锁,可以去执行其他任务,而不是堵塞在那儿。

public class SynchronizedDemo11 {
    
    
    public static void main(String[] args) {
    
    
        Executor executor = Executors.newCachedThreadPool();
        executor.execute(new Task11());
        executor.execute(new Task11());
        /**
         * 1号线程抢到吃饭资格,开始吃饭
         * 2号线程没抢到吃饭资格,只能先去打太极
         */
    }
}

class Task11 implements Runnable{
    
    
    private static int idCount=0;
    private final int id = ++idCount;
    private static Lock lock = new ReentrantLock();


    @Override
    public void run() {
    
    
        if(lock.tryLock()){
    
    
            System.out.println(id+"号线程抢到吃饭资格,开始吃饭");
            try {
    
    
                Thread.sleep(5000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            lock.unlock();
        }else {
    
    
            System.out.println(id+"号线程没抢到吃饭资格,只能先去打太极");
        }
    }

    public int getId() {
    
    
        return id;
    }
}

下面例中通过tryLock我们实现了自旋锁,我们的线程在未获得锁的情况下,会再次尝试获取锁,只要五次都获取失败才会放弃获取锁

public class SynchronizedDemo11 {
    
    
    public static void main(String[] args) {
    
    
        Executor executor = Executors.newCachedThreadPool();
        executor.execute(new Task11());
        executor.execute(new Task11());
        /**
         * 1号线程抢到饭,开始吃饭
         * 2号线程第1次抢饭失败
         * 2号线程第2次抢饭失败
         * 2号线程第3次抢饭失败
         * 2号线程第4次抢饭失败
         * 2号线程第5次抢饭失败
         */
    }
}

class Task11 implements Runnable{
    
    
    private static int idCount=0;
    private final int id = ++idCount;
    private static Lock lock = new ReentrantLock();


    @Override
    public void run() {
    
    
        int count=5;
        while (count-- > 0){
    
    
            if(lock.tryLock()){
    
    
                System.out.println(id+"号线程抢到饭,开始吃饭");
                try {
    
    
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                lock.unlock();
                break;
            }
            System.out.println(id+"号线程第"+(5-count)+"次抢饭失败");

        }

    }

    public int getId() {
    
    
        return id;
    }
}

显式的Lock对象在加锁和释放锁方面,相对于内建的synchronized锁来说,还赋予了更细粒度的控制力。


原子性和易变性

原子操作是不能被线程调度机制中断的操作,一旦操作开始,那么它一定可以在发生上下文呢切换之前执行完毕。

原子性可以应用于除了long和double之外所有基本类型之上的简单操作。对于读取和写入除long和double之外的基本类型这样的操作,可保证它们会被当作原子操作。但是JVM将64位的读取和写入当作两个分离的32位操作来执行,这就产生了一个在读写操作中间发生上下文切换,从而导致不同的任务可以看到不正确结果的可能性。但是当你定义long或double变量时,使用了volatile关键字,就会获得原子性。

Volatile是如何保证域的同步性的

我们现在使用的大多数机器都是多核处理器,由于多个处理器拥有多个CPU,而每个CPU会拥有自己的本地内存(高速缓存)。每个CPU在执行操作前会预先将多条操作命令和数据读取到本地内存中,然后进行操作,当操作完成后再一次性刷新到主存中。这就会引起一个问题,当多个CPU同时读取一个变量时,也就是一个变量同时出现在了不同的本地内存中,一旦某个cpu执行的是一个修改操作,就会导致数据的不一致性问题,即其他cpu读取的是一个不正确的数据值,这时候我们就需要引入volatile关键字。把目标变量声明为volatile(不稳定的,这就指示JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。一般说来,多任务环境下各任务间共享的标志都应该加volatile修饰。

Volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。注意,被synchronized修饰的方法和代码块也是被直接刷新到主存中的。

在这里插入图片描述

volatile为什么对自增操作无效

我们前面提到,对于用volatile修饰的域的读写操作是直接面向主存的。因此,通过这种方式可以避免脏读。但是这条法则在面对自增操作时会失效。首先,自增操作分成三步:

  1. 从主存读取变量值到cpu寄存器
  2. 寄存器里的值+1
  3. 寄存器的值写回主存
    假设N个线程同一时候运行到了第一步。这就意味着现在这个变量处于N个线程的寄存器中,所以当某个线程将新值刷新到主存中时,其他的线程也都完成了修改,它们并不会重新去读这个变量。

原子类

对多线程访问同一变量,我们需要加锁,而锁是比较消耗性能的,jdk1.5之后,新增的原子操作类提供了一种简单、性能高效、线程安全地更新一个变量的方式,这些类同样位于juc包下的atomic包下,发展到jdk1.8,该报共有17个类,囊括了原子更新基本类型、原子更新数组、原子更新新属性、原子更新引用。

下面写一个个原子类使用的简单例子
AtomicIndteger实现一个线程安全的自增

/**
 * AtomicInteger  demo
 */
public class AtomicIntegerDemo {
    
    
    private static AtomicInteger sum = new AtomicInteger(0);
 
    public static void  increase(){
    
    
        sum.incrementAndGet();
    }
 
    public static void main(String[] args) throws InterruptedException{
    
    
        for (int i=0;i<10;i++){
    
    
            new Thread(()->{
    
    
                for (int j=0;j<10;j++){
    
    
                    increase();
                    System.out.println(sum);
                }
            }).start();
 
        }
 
    }
}

线程本地存储

防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。线程本地存储是一种自动化机制,可以为使用相同变量的每个不同线程都创建不同的存储

我们防止任务在共享资源上产生冲突的第一种方式是通过对共享资源进行加锁,确保同一时间只有一个线程能够对共享资源进行操作。其实除了加锁以外,我们还能够将共享资源变成局部变量的形式,这样对于每个线程都会拥有自己的变量,而线程之间是互相隔离的,不会对其他线程上的变量造成影响。但这种方式只会造成过多的资源浪费,就像一个公司里每个人都需要使用打印机,但是公司不可能为每个人都配备一台打印机。

因此,我们本章引入概念叫做线程本地存储。那什么是线程本地存储,我们可以这么认为,将线程作为键,并将我们需要的共享资源作为值,这样,当每个线程去取变量时,都能够取到属于自己的变量。

举个例子,现在有一场考试,考试为了公平起见需要所有人上交手机,所以大家就都把手机放到同一个箱子里。但是我们要确保我们去拿的时候能够拿回自己的手机

public class SynchronizedDemo13 {
    
    
    public static void main(String[] args) {
    
    
        Executor executor = Executors.newCachedThreadPool();
        for(int i=0;i<5;i++){
    
    
            executor.execute(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    Box.putPhone(new Phone(Thread.currentThread().getName()));
                    Thread.yield();
                    System.out.println("Thread "+Thread.currentThread().getName()+"拿到了"+Box.getPhone().getHost()+"的手机");
                }
            });
        }
        /**
         * Thread pool-1-thread-1拿到了pool-1-thread-1的手机
         * Thread pool-1-thread-2拿到了pool-1-thread-2的手机
         * Thread pool-1-thread-3拿到了pool-1-thread-3的手机
         * Thread pool-1-thread-5拿到了pool-1-thread-5的手机
         * Thread pool-1-thread-4拿到了pool-1-thread-4的手机
         */
    }
}

class Box{
    
    
    private static ThreadLocal<Phone> phones = new ThreadLocal<>();

    public static void putPhone(Phone phone){
    
    
        phones.set(phone);
    }

    public static Phone getPhone(){
    
    
        return phones.get();
    }
}

class Phone{
    
    
    private String host;

    public Phone(String host){
    
    
        this.host = host;
    }

    public String getHost(){
    
    
        return host;
    }
}

另外还在其他书上看到过,如果线程自然死亡,那么ThreadLcoal的数据也会跟着消失,如果是线程池,在线程会被复用的情况下,要手动清除。可以把map中的值set成null,让GC可以工作,也可以调用ThreadLocal的remove方法。

并发编程实战这本书里也提到ThreadLocal会降低代码的可重用性,并在类之间引入隐含的耦合性,因此在使用时要格外小心。



线程状态

借用Java 并发编程的艺术》图一张
​​在这里插入图片描述
线程的状态会影响任务的执行,一个线程可以具有多种状态,而每种状态都会因为不同的原因而产生。

一个线程可以处于以下四种状态之一

  • 初始态:线程被创建完成,但还未被调用start()
  • 就绪态:在这种状态下,只要被分配到了CPU时间片,线程就可以运行。
  • 阻塞态:线程能够运行,但是由于某种条件阻止了它的运行。在阻塞期间,调度器将忽略线程,不会为其分配任何时间片。直到线程重新进入就绪状态。
  • 死亡:线程执行完毕。通常是任务结束或者被中断。

进入阻塞态的原因

一个任务进入阻塞状态,可能有如下原因:

  1. 通过sleep(milliseconds)使任务进入休眠状态,这种情况下,任务会在指定时间内不会运行,一旦时间结束,重新自动进入就绪态。
  2. 通过wait()使线程挂起。直到线程得到了notify()或notifyAll()消息,线程才会进入就绪态。
  3. 任务正在等待某个输入输出完成
  4. 任务试图在某个对象上调用其同步方法,但是为获得锁。

中断

很多时候我们在线程中的任务会因为一些状况使得我们不希望让其继续执行下去,这时候我们就需要通过某种方式来控制器流程,让其提前结束任务执行。
这里有三种方式,并且我们会为其讲解其优劣:

1.cancel标记

我们可以为我们的自定义现线提供一个取消标记,线程中的任务可以不断去检查这个标记来判断是否需要提前终止任务。

public class SynchronizedDemo15 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        SimpleThread15 thread = new SimpleThread15();
        thread.start();
        Thread.sleep(3000);
        thread.cancel();

        /**
         * thread executing...
         * thread executing...
         * thread executing...
         * thread executing...
         * thread executing...
         * thread executing...
         * thread has been cancelled...
         */
    }
}

class SimpleThread15 extends Thread{
    
    
    private boolean cancelFlag = false;

    @Override
    public void run() {
    
    
        try {
    
    
            while (!cancelFlag){
    
    
                System.out.println("thread executing...");
                sleep(500);
            }
            System.out.println("thread has been cancelled...");
        } catch (InterruptedException e) {
    
    
            System.out.println(e.getMessage());
        }
    }

    public void cancel(){
    
    
        cancelFlag = true;
    }
}

根据结果可以看到任务在执行3秒后,被主线程通过cancel方法取消掉了。在打开取消标记的同时,线程也就结束了运行。
这种方式可以实现任务的中断,但是缺点有两点:

  1. 我们需要自定义线程增加canel标记,同时由于需要使用取消标记,所以我们的run方法需要与Thread进行绑定,也就是只能通过重写Thread的run方法来编写任务。而不同将任务与线程分离去实现Runable接口。
  2. 由于我们通常使用线程组去管理线程,我们一般无法直接与线程进行交互,并且由于线程需要与任务绑定,那么线程池中的线程将无法执行来自于其他外来的任务,因为线程池中的线程全部被绑定了任务

2.抛出异常

我们通过抛出异常也可以实现线程的中断。

public class SynchronizedDemo16 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        SimpleThread15 thread = new SimpleThread15();
        thread.start();
        Thread.sleep(3000);
        thread.cancel();

        /**
         * thread executing...
         * thread executing...
         * thread executing...
         * thread executing...
         * thread executing...
         * thread executing...
         * thread has been cancelled...
         */
    }
}

class SimpleThread16 extends Thread{
    
    
    private boolean cancelFlag = false;

    @Override
    public void run() {
    
    
        try {
    
    
            while (true){
    
    
                System.out.println("thread executing...");
                sleep(500);
                if(cancelFlag){
    
    
                    throw new InterruptedException();
                }
            }

        } catch (InterruptedException e) {
    
    
            System.out.println("thread has been cancelled...");
            System.out.println(e.getMessage());
        }
    }

    public void cancel(){
    
    
        cancelFlag = true;
    }
}


但是通过抛出异常也会面临一些问题:

  1. 异常处理问题:我们抛出异常后就意味着我们需要为线程设置异常处理程序来处理异常,并实现一些收尾工作。但是我们一般使用的是线程池,这意味着我们无法为每个任务都设置一个异常处理程序,因为线程池中的线程是通用的,随即处理任务的。因此我们需要在run方法中手动处理所有的异常来确保任务能够完美地做一些清理工作。
  2. 由于线程池分离线程和任务,我们没有办法动态地去控制任务的中断标记来抛出异常。

3.interrupt方法

事实上,java中的Thread已经为我们提供的线程中断的方法了。通过interrupt方法可实现任务的动态中断。

public static void main(String[] args) throws InterruptedException {
    
    
        Thread thread = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                    while (true){
    
    
                        System.out.println("executing...");
                        Thread.sleep(300);
                    }
                } catch (InterruptedException e) {
    
    
                    System.out.println("Thread has been interrupted...");
                }
            }
        });
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
        /**
         * executing...
         * executing...
         * executing...
         * executing...
         * Thread has been interrupted...
         */
    }

上面是单个线程的实现方式,我们可以看到在线程启动后,线程会在循环内不断打印语句,但是在一秒后,我们的主线程通过调用interrupt()方法中断了线程的执行,让他出现了异常。
注意:虽然我们上面通过interrupt()方法中断了线程的运行,但事实上,该方法除了sleep()状态以外无法中断线程的任何操作,在线程中存在一个中断标记,我们通过interrupt()方法将其设置为true,而我们的sleep方法会检查该标记,如果为true则抛出中断异常,因此我们知道处理sleep以外,线程是不会因为该标记发生中断的(包括锁池中与阻塞时都不会去检查中断标记),但是中断标记为我们动态中断提供了帮助,我们只需要在任务中不断检查线程的中断标记就可以实现由外部线程控制目标线程的动态中断。

下面提供线程池版本的中断方式

public static void main(String[] args) throws InterruptedException {
    
    
        ExecutorService executor = Executors.newCachedThreadPool();
        Future future = executor.submit(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                    while (!Thread.currentThread().isInterrupted()){
    
    
                        System.out.println("executing...");
                        Thread.sleep(300);
                    }
                    System.out.println("Thread has been interrupted...");
                } catch (InterruptedException e) {
    
    
                    System.out.println("Thread has been interrupted...");
                }
            }
        });


        Thread.sleep(1000);
        future.cancel(true);
        /**
         * executing...
         * executing...
         * executing...
         * executing...
         * Thread has been interrupted...
         */
    }

线程池中无法直接与线程直接交互,因此我们无法直接调用线程的中断方法,但是我们可以通过线程池的submit来启动任务,这样就可以通过返回的Future对象来持有任务的上下文,然后通过cancel发法来控制线程的中断情况。

补充

Thread.interrupted():判断当前线程中断标记,并将其改回false。
Thread.currentThread().isInterrupted():判断当前线程中断标记,不修改中断标记。



线程之间的交互协作

当我们使用线程同时运行多个任务时,我们需要借助锁来同步两个任务的行为,从而使得一个任务不会干涉另一个任务的资源。但是这种方式只能解决资源竞争的问题,我们还需要一些方式使得多个线程可以互相协调来完成同一个任务,比如有个工地,甲方聘请了两个施工队,施工队A负责铺设钢结构,而施工队B则负责浇筑水泥,但我们知道,水泥的浇筑需要建立在钢结构已经铺设完成的情况下,这时先决条件,不能够交换顺序,因此我们需要一种方式来使得人物之间能够有一些交互来共同完成任务。


wait()和notify()

wait()方法可以使我们的线程等待某个条件发生变化,而改变这个条件超出当前方法的控制能力。因此线程只能等待。但是我们一定不希望在任务检查这个条件的同时,不断进行空循环,这被称为忙等待,通常是一种不良的CPU周期使用方式,只会不断浪费CU的性能。因此我们需要wait()在等待外部条件变化前将线程挂起,并且只有notify()和notifyAll()发生时,任务才会被唤醒去检查所产生的变化。

调用sleep()时锁并没有被释放,调用yield()也是这样的情况。但是放一个任务执行到wait()时,线程的执行会被挂起,对象上的锁被释放。这就意味着另一个锁可以获得这个锁。我们可以这么认为,wait()就是在告诉外界:我刚刚已经完成了所能完成的事,因此我要在这里等待,但是在我等待期间并不会阻碍其他线程执行同步方法。

wait有两个版本:

  • 接受毫秒数,基本上与sleep相似,但是wait()会在等待期间释放锁。但是也可以被提前换醒或者等到时间截止自动苏醒。
  • 无参数,表示会无限期等待直到被唤醒。

wait()、notify()和notifyAll()有一个特点,它们并不是Thread的一部分,而是作为基类Object的一部分。这看起来很奇怪,因为这三个方法是针对线程的功能现在却作为基类的一部分而实现。事实上因为这些方法是对锁的操作,而这些方法操作的锁也是所有对象的一部分,所以我们可以将wait()放在同步控制方法中,而不用考虑这个类是实现Thread还是Runable接口。其实这三个方法只能在同步方法和同步块中被调用,而sleep()可以在非同步方法中调用,因为sleep不需要操作锁。如果在非同步方法中调用这三个方法,可以通过编译,毕竟这个是属于Object的一部分方法,但是在运行期间会出现IllegalMonitorStateException异常。
总而言之,当我们需要调用者三个方法我们需要针对锁对象去调用,也就是说我们必须在拿到锁的情况下才能调用这三个方法。

public static void main(String[] args) throws InterruptedException {
    
    
        ExecutorService executor = Executors.newCachedThreadPool();
        Future task1 = executor.submit(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                    synchronized ("1"){
    
    
                        while (true){
    
    
                            System.out.println("任务1正在执行...");
                           // Thread.sleep(100);
                            if(Thread.currentThread().isInterrupted()){
    
    
                                System.out.println("任务1暂停执行...");
                                "1".wait();

                            }
                        }
                    }
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        });

        Future task2 = executor.submit(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                    synchronized ("1"){
    
    
                        while (true){
    
    
                            System.out.println("任务2正在执行...");
                            //Thread.sleep(100);
                            if(Thread.currentThread().isInterrupted()){
    
    
                                System.out.println("任务2暂停执行...");
                                "1".wait();

                            }
                        }
                    }
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        });

        Thread.sleep(100);
        task1.cancel(true);
        Thread.sleep(100);
        executor.shutdownNow();

        /**
         * ...
         * 任务1正在执行...
         * 任务1正在执行...
         * 任务1正在执行...
         * 任务1暂停执行...
         * 任务2正在执行...
         * 任务2正在执行...
         * 任务2正在执行...
         * ...
         */

    }

上面例子中我们发布了两个任务,然后任务1会在执行0.1秒后调用wait(),由于任务1和任务2需要的锁对象都是"1"对象,因此,任务二会在任务1释放锁之前被阻塞。我们通过运行结果看出,在执行wait()之后,被阻塞的线程2也开始运行,而任务1则停止运行。事实上,如果任务2再次调用notify()或者notifyAll(),任务1将被再次唤醒,进入锁池等待锁的释放,当竞争到锁之后会用上次wait()的地方往后继续执行。

notify()和notifyAll()是对锁对象进行操作的,正常情况,当有多个线程争夺锁时,第一个拿到锁的任务执行完毕之后就会有第二个线程抢到锁进行执行,但是如果是因为wait()进入等待,那么即使它需要的锁被释放它也不会去抢占锁,因为它已经被挂起,需要notify()或notifyAll()进行唤醒,前者唤醒等待该锁的wait等待队列中随机某个任务进入锁池抢占锁,而后者会唤醒所有等待该锁的挂起队列进入锁池抢占锁。


生产者和消费者问题

线程协作中生产者消费者问题是最为经典的案例之一,我们通过这个问题来对线程协作已经上面三个方法进行进一步理解。
首先,生产者负责生产产品,消费者负责消费产品,但是消费者一定是在生产者生产出物品之后才能进行消费,而生产者也不会无限生产产品,它一定会等到生产到目标产品数后等到产品销售一空之后再继续生产,毕竟谁也不能保证产品一定就能卖完。

首先我们创建一个产品类,产品上有一个id条码,是在生产时有生产者写入的

class Product{
    
    
    private int id;

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

    @Override
    public String toString() {
    
    
        return "product"+id;
    }
}

我们再创建一个商店类,生产者生产的产品需要放到商店销售,同时消费者也会再商店消费产品。同时,商店由于规模原因只能放置三十件商品。

class Shop{
    
    
    private LinkedList<Product> products = new LinkedList<>();

    public void addProduct(Product product){
    
    
        products.add(product);
    }

    public Product getProduct(){
    
    
        return products.pop();
    }

    public boolean isEmpty(){
    
    
        return products.isEmpty();
    }

    public boolean isFull(){
    
    
        return products.size()>=30;
    }
}

有了商店之后我们就需要供货商来生产产品了,生产者会不停地制造商品直到产满商品后不再继续生产,当商品销售一空时再被唤醒继续开始生产。

class Producer implements Runnable{
    
    
    private Shop shop;
    Random random = new Random();

    public Producer(Shop shop){
    
    
        this.shop = shop;
    }


    @Override
    public void run() {
    
    
        try {
    
    
            while (true) {
    
    
                Thread.sleep(300);
                synchronized (shop) {
    
    
                    if (shop.isFull()) {
    
    
                        System.out.println("完成产品生产...");
                        shop.wait();
                    }else {
    
    
                        if(!shop.isEmpty()){
    
    
                            shop.notifyAll();
                        }
                        Product product = new Product(random.nextInt(100));
                        shop.addProduct(product);
                        System.out.println("生产者生产了一件产品:" + product);
                    }

                }
            }
        } catch(InterruptedException ex){
    
    
            ex.printStackTrace();
        }
    }
}

对于客户来说,只要商店的产品不为空,我就可以消费

class Consumer implements Runnable{
    
    
    private Shop shop;
    private static int count = 0;
    private final int id = ++count;

    public Consumer(Shop shop){
    
    
        this.shop = shop;
    }


    @Override
    public void run() {
    
    
        try{
    
    
            while (true){
    
    
                Thread.sleep(350);
                synchronized (shop){
    
    
                    if(shop.isEmpty()){
    
    
                        System.out.println("商品卖完了...");
                        shop.notifyAll();
                        shop.wait();
                    }else {
    
    
                        System.out.println(id+"号顾客消费商品:"+shop.getProduct().toString());
                    }

                }
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

场景

public static void main(String[] args) {
    
    
        Shop shop = new Shop();

        ExecutorService executor = Executors.newCachedThreadPool();
        executor.execute(new Producer(shop));
        executor.execute(new Consumer(shop));
        executor.execute(new Consumer(shop));

        /**
         * ...
         * 生产者生产了一件产品:product58
         * 生产者生产了一件产品:product78
         * 2号顾客消费商品:product58
         * 生产者生产了一件产品:product86
         * 1号顾客消费商品:product78
         * 2号顾客消费商品:product86
         * 生产者生产了一件产品:product48
         * 1号顾客消费商品:product48
         * 商品卖完了...
         * ...
         */
    }

上面例子中我们制造了一个生产者和两个消费者,消费者不断消费生产者生产出的产品。这就是我们通过wait()、notify()以及notify()可以实现的线程间的交互案例。


使用显式的Lock和Condition

事实上,除了同步方法和同步块,我们还经常会使用Lock对象进行线程同步控制,我们可以在同步块中通过对锁对象通过wait()登封昂发进行线程控制,那么如果是使用Lock对象该如何,其实,Lock对象也为我们提供了允许线程挂起的Condition,我们可以通过再Condition上调用await()来挂起线程,然后当条件发生变化时通过signal()和signalAll()来唤醒在这个condition上被挂起的线程。

当Lock锁使用公平模式的时候,可以使用Condition的signal(),线程会按照FIFO的顺序冲await()中唤醒。当每个锁上有多个等待条件时,可以优先使用Condition,这样可以具体一个Condition控制一个条件等待。

https://www.jianshu.com/p/be2dc7c878dc


同步队列

wait()和notify()方法以一种非常低级的方式解决了任务交互的问题,但在许多情况下我们可以瞄向更高的抽象级别,使用同步队列来解决任务协作问题,同步队列在任何时刻都只允许一个任务插入或移除元素。在java.util.concurrent.BlockingQueue接口中提供了这个队列,这个接口有大量的实现,我们可以使用LinkedBlockingQueue,它是一个无界队列,还可以使用ArrayBlockingQueue,它具有固定的尺寸,因此我们可以在它被阻塞前向其中放置有限数量的元素。
如果消费者任务视图从队列中获取对象,而此时为空,那么这些队列还可以挂起消费者任务,并当有更多元素可用时恢复消费者任务。相较于notify等,它更加简单可靠。

下面例子中我们通过对生产者消费者队列进行重构

首先将同步队列作为产品容器,并设置长度为10。

class Shop2{
    
    
    private BlockingQueue<Product> products = new ArrayBlockingQueue<Product>(10);

    public void addProduct(Product product){
    
    
        products.add(product);
    }

    public Product getProduct() throws InterruptedException {
    
    
        return products.take();
    }

}

产品对象保持不变

class Product{
    
    
    private int id;

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

    @Override
    public String toString() {
    
    
        return "product"+id;
    }
}

在生产者消费者中我们去除了所有的同步控制

class Producer implements Runnable{
    
    
    private Shop shop;
    Random random = new Random();

    public Producer(Shop shop){
    
    
        this.shop = shop;
    }


    @Override
    public void run() {
    
    
        try {
    
    
            while (true) {
    
    
                Thread.sleep(300);
                synchronized (shop) {
    
    
                    if (shop.isFull()) {
    
    
                        System.out.println("完成产品生产...");
                        shop.wait();
                    }else {
    
    
                        if(!shop.isEmpty()){
    
    
                            shop.notifyAll();
                        }
                        Product product = new Product(random.nextInt(100));
                        shop.addProduct(product);
                        System.out.println("生产者生产了一件产品:" + product);
                    }

                }
            }
        } catch(InterruptedException ex){
    
    
            ex.printStackTrace();
        }
    }
}

class Consumer implements Runnable{
    
    
    private Shop shop;
    private static int count = 0;
    private final int id = ++count;

    public Consumer(Shop shop){
    
    
        this.shop = shop;
    }


    @Override
    public void run() {
    
    
        try{
    
    
            while (true){
    
    
                Thread.sleep(350);
                synchronized (shop){
    
    
                    if(shop.isEmpty()){
    
    
                        System.out.println("商品卖完了...");
                        shop.notifyAll();
                        shop.wait();
                    }else {
    
    
                        System.out.println(id+"号顾客消费商品:"+shop.getProduct().toString());
                    }

                }
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

最后看看运行场景,可以发现加入阻塞队列后即便不需要任何同步控制,生产者和消费者之间也可以很好地协作下去。

public static void main(String[] args) {
    
    
        Shop shop = new Shop();

        ExecutorService executor = Executors.newCachedThreadPool();
        executor.execute(new Producer(shop));
        executor.execute(new Consumer(shop));
        executor.execute(new Consumer(shop));

        /**
         * ...
         * 生产者生产了一件产品:product58
         * 生产者生产了一件产品:product78
         * 2号顾客消费商品:product58
         * 生产者生产了一件产品:product86
         * 1号顾客消费商品:product78
         * 2号顾客消费商品:product86
         * 生产者生产了一件产品:product48
         * 1号顾客消费商品:product48
         * 商品卖完了...
         * ...
         */
    }


suspend()和resume()

不建议使用,基本舍弃了。虽然它可以做到所谓的线程暂停。

原因如下:

  • 容易造成同步对象被独占:当我有一个线程占用同步资源时发生暂停,那么在被resume()之前该线程所占用的资源将没有线程可以访问。
  • 导致数据不同步:当我有一个线程在修改临界资源时发生暂停,我们无法保证整个修改已经完成,如果只修改了一般却发生了暂停,那么其他线程在访问时拿到的数据就会与暂停线程恢复后修改的数据出现差异。

详细可以访问:

独占:https://www.jianshu.com/p/a075800838e8
数据不同步:https://www.jianshu.com/p/03f9b7cf8c07


stop()

不建议使用,基本舍弃了。虽然它可以做到所谓的线程暂停。

原因:

线程不安全:对使用stop(),是因为它不安全。它会解除由线程获取的所有锁定,当在一个线程对象上调用stop()方法时,这个线程对象所运行的线程就会立即停止,假如一个线程正在执行:synchronized void { x = 3; y = 4;} 由于方法是同步的,多个线程访问时总能保证x,y被同时赋值,而如果一个线程正在执行到x = 3;时,被调用了 stop()方法,即使在同步块中,它也干脆地stop了,这样就产生了不完整的残废数据。而多线程编程中最最基础的条件要保证数据的完整性,所以请忘记线程的stop方法,以后我们再也不要说“停止线程”了。而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。结果很难检查出真正的问题所在。



死锁

死锁感觉没什么好讲的,最为典型的案例就是哲学家就餐问题,对于这个案例我们可以参考http://c.biancheng.net/view/1233.html。

死锁的形成主要有四个条件:

  • 请求等待条件
  • 互斥条件
  • 不可剥夺条件
  • 循环等待条件


新类库中的构件

javaSE5中引入了大量的新类来解决并发问题。


CountDownLatch

他被用来同步一个或多个任务,强制它们等待由其他任务执行的一组操作完成。
我们可以向CountDownLaunch对象设置一个初始计数值,任何通过该对象调用await()的方法都将被阻塞,直到这个计数值到0。而减小这个计数值的方式就是调用它的counDown()。

假设我们有一组操作是准备操作,同时准备操作的数量是固定的,我们可以通过CountDownLatch来设计一个需要等待准备操作完成才能正式进行工作的案例。

假设我们现在有一个游戏,叫做123木头人,这个游戏必须在喊完“123木头人”的情况下才能睁开眼睛。因此我们通过CountDownLatch来模拟这个过程。

public class SynchronizedDemo23 {
    
    
    public static void main(String[] args) {
    
    
        Executor executor = Executors.newCachedThreadPool();
        CountDownLatch countDownLatch = new CountDownLatch(1);
        executor.execute(new OpenEyes(countDownLatch));
        executor.execute(new Ready(countDownLatch));
        /**
         * 1
         * 2
         * 3
         * 木头人
         * 睁开眼睛
         */
    }
}

class Ready implements Runnable{
    
    
    private CountDownLatch countDownLatch;

    public Ready(CountDownLatch countDownLatch){
    
    
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
    
    
        try {
    
    
            System.out.println("1");
            Thread.sleep(1000);
            System.out.println("2");
            Thread.sleep(1000);
            System.out.println("3");
            Thread.sleep(1000);
            System.out.println("木头人");
            Thread.sleep(1000);
            countDownLatch.countDown();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

class OpenEyes implements Runnable{
    
    
    private CountDownLatch countDownLatch;

    public OpenEyes(CountDownLatch countDownLatch){
    
    
        this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
    
    
        try{
    
    
            countDownLatch.await();
            System.out.println("睁开眼睛");
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

我们可以发现我们是先执行睁眼的线程,但是由于CountDownLatch还没归零,因此该线程会阻塞到CountDownLatch归零为止。

注意: CountDownLatch是一个一次性的,一旦归零就不可恢复不可二次使用。如果希望有一个可复用的CountDownLatch可以使用CyclicBarrier


DelayQueue

这是一个误解的BlockingQueue,用于放置实现了Delayed接口对象,其中的对象只有才事件到期后才能从队列中取走。这种队列是有序的,即对头对象一定是第一个到期的。如果没有到期的对象,通过poll()将返回null值。

在现实生活中,就存在很多需要用到延时的情况,拿我们在银行的定期存款作为一个例子,我们都知道,每个银行都会有一些定期存的业务,因为定期存的利息会比活期高,但是一旦定期存后就不能随意取出,必须等到期限满后才能取出。

public class SynchronizedDemo24 {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Executor executor = Executors.newCachedThreadPool();
        //银行开业
        Bank bank = new Bank();
        executor.execute(bank);

        //路人甲存入一笔存款
        bank.deposit("111111",3000d,3);


        //路人乙存入一笔存款
        bank.deposit("222222",10000d,6);

        //路人甲是否取到钱
        boolean flag1 = false;
        //路人乙是否取到钱
        boolean flag2 = false;
        while (true){
    
    
            Thread.sleep(1000);
            if(!flag1){
    
    
                flag1 = bank.getDeposition("111111")>0?true:false;
            }

            if(!flag2){
    
    
                flag2 = bank.getDeposition("222222")>0?true:false;
            }

            if(flag1 && flag2){
    
    
                break;
            }
        }
        /**
         * 111111存入了一笔3000.0RMB的3月的定期存款
         * 222222存入了一笔10000.0RMB的6月的定期存款
         * 当前111111账户的存款还有1月到期
         * 111111账户取出0.0RMB
         * ...
         * 当前111111账户的存款还有0月到期
         * 111111账户取出0.0RMB
         * 当前222222账户的存款还有2月到期
         * 222222账户取出0.0RMB
         * 111111的定期到期可以取出......
         * 111111账户取出3012.5RMB
         * 当前222222账户的存款还有1月到期
         * ...
         * 当前222222账户的存款还有0月到期
         * 222222账户取出0.0RMB
         * 222222的定期到期可以取出......
         * 222222账户取出10041.666666666666RMB
         */
    }
}

class Bank implements Runnable{
    
    
    private DelayQueue<Deposition> depositionsInTime = new DelayQueue<>();

    private LinkedList<Deposition> depositionsOutTime = new LinkedList<>();

    public void deposit(String account,Double money,Integer months){
    
    
        depositionsInTime.add(new Deposition(account, money, months));
        System.out.println(account+"存入了一笔"+money+"RMB的"+months+"月的定期存款");
    }

    public Double getDeposition(String account){
    
    
        Double money = 0d;
        synchronized (depositionsOutTime){
    
    
            for(Deposition d:depositionsOutTime){
    
    
                if(d.getAccount().equals(account)){
    
    
                    money+=d.getMoney();
                    depositionsOutTime.remove(d);
                }
            }
        }

        if(money == 0){
    
    
            for (Deposition d:depositionsInTime) {
    
    
                if(d.getAccount().equals(account)){
    
    
                    System.out.println("当前"+account+"账户的存款还有"+d.getDelay(TimeUnit.SECONDS)+"月到期");
                }
            }
        }

        System.out.println(account+"账户取出"+money+"RMB");

        return money;
    }


    @Override
    public void run() {
    
    
        try {
    
    
            while (true){
    
    
                Thread.sleep(500);
                Deposition deposition;
                while ((deposition = depositionsInTime.poll()) != null){
    
    
                    deposition.setMoney(deposition.getMoney()*(1+(0.05/12.0)));
                    System.out.println(deposition.getAccount()+"的定期到期可以取出......");
                    depositionsOutTime.add(deposition);
                }
            }
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    }
}

class Deposition implements Delayed {
    
    
    private final String account;
    private Double money;
    private long trigger;

    public Deposition(String account,Double money,Integer months){
    
    
        this.account = account;
        this.money = money;
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.SECOND,months);
        this.trigger = calendar.getTimeInMillis();
    }

    @Override
    public long getDelay(TimeUnit unit) {
    
    
        return unit.convert(trigger - System.currentTimeMillis(),TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
    
    
        Deposition deposit = (Deposition) o;
        return this.trigger> deposit.trigger?1:-1;
    }

    public String getAccount() {
    
    
        return account;
    }

    public Double getMoney() {
    
    
        return money;
    }

    public void setMoney(Double money) {
    
    
        this.money = money;
    }
}

上面例子中我们将银行作为延迟对象的持有者,每笔定期存款都作为一个延时对象,然后甲乙两个路人会在存款后不断尝试取款,但是只有在到期后他们才会取出,同时一旦到期银行也会打印到期信息,相当于模拟了我们的短信通知。


PriorityBlockingQueue

PriorityBlockingQueue是一个支持优先级的无界阻塞队列,直到系统资源耗尽。默认情况下元素采用自然顺序升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。但需要注意的是不能保证同优先级元素的顺序。PriorityBlockingQueue也是基于最小二叉堆实现,使用基于CAS实现的自旋锁来控制队列的动态扩容,保证了扩容操作不会阻塞take操作的执行。

详细的原理可以参考这篇博文https://www.cnblogs.com/yaowen/p/10708249.html



总结

并发相关的知识点远远不止这些,我们还需要更多地学习。其实每个构件的内在框架与使用逻辑都是值得我们去学习的,所以关于构件和锁的机制会在后面不断去更新新的博客,尽量做到每个章节只讲一种构件,剖析其思想与优劣。

猜你喜欢

转载自blog.csdn.net/qq_33905217/article/details/109767045