并发编程

多线程设计模式:第一篇 - Java线程基础

2018-10-09  本文已影响0人  张angang强吖

一,线程基础

1,基础概念

    一个线程就是运行在一个进程上下文中的一个逻辑流,而进程是程序执行的实例。系统中每个运行着的程序都运行在一个进程上下文环境中,进程上下文由程序正确运行所必须的状态组成,包括程序代码,数据,程序运行栈,寄存器,指令计数器,环境变量以及进程打开的文件描述符集合,这些都保存在进程控制块中。

    现代操作系统调度的最小单位是线程,也叫轻量级进程,在一个进程里可以创建多个线程,每个线程也有自己的运行上下文环境,包括唯一的线程ID,栈空间,程序计数器等。多个线程运行在同一个进程环境中,因此共享进程环境中的堆,代码,共享库和打开的文件描述符。

    Java是天生的多线程程序,main() 方法由一个被称为主线程的线程调用,之后在 main() 方法中可以再生出更多的自定义线程。

2,线程启动和暂停

    Java线程启动有两种方式,一种是通过继承 Thread 类,一种是通过实现 Runnable 接口,示例代码如下:

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

/**
 * @author koma <komazhang@foxmail.com>
 * @date 2018-10-09 17:21
 */
public class StartThreadDemo {
    public static void main(String[] args) {
        StartThreadDemo startThreadDemo = new StartThreadDemo();
        startThreadDemo.testThread();
    }

    public void testThread() {
        ThreadDemo1 threadDemo1 = new ThreadDemo1();
        threadDemo1.start();

        ThreadDemo2 threadDemo2 = new ThreadDemo2();
        new Thread(threadDemo2).start();
    }

    class ThreadDemo1 extends Thread {
        @Override
        public void run() {
            System.out.printf("%s run...\n", Thread.currentThread().getName());
        }
    }

    class ThreadDemo2 implements Runnable {
        @Override
        public void run() {
            System.out.printf("%s run...\n", Thread.currentThread().getName());
        }
    }
}

    通过示例代码可以得知,由于同一个线程不能启动多次(即调用多次 start() 方法),继承 Thread 类实现的线程中的 run() 方法如果要在多个线程中执行,则需要 new 多次 ThreadDemo1,而对于实现 Runnable 接口类的 ThreadDemo2 则只需要 new 一次即可。这两种方法各有适用场景,需灵活运用。

    Java多线程编程后期较为常用的方式是使用 Executors 框架,利用该框架启动线程的实例代码如下(这会比较常见):

ThreadFactory threadFactory = Executors.defaultThreadFactory();
threadFactory.newThread(threadDemo2).start();

    这里重用了 ThreadDemo2 的实例,同时框架内部使用了线程池技术,这个后续再讨论。

    线程暂停最基本的方法是通过 Thread 类的 sleep 方法,这会让线程进入到休眠状态等待一段时间再运行,线程却不会退出。让线程退出的方法一种是通过中断,另外一种则是设置标识位,这种方法相比是比较优雅的一种方式,因为这给了线程充分的时间去执行现场清理工作,从容退出。三种方式的举例代码如下:

/**
 * @author koma <komazhang@foxmail.com>
 * @date 2018-10-09 17:21
 */
public class StartThreadDemo {
    public static void main(String[] args) {
        StartThreadDemo startThreadDemo = new StartThreadDemo();
        startThreadDemo.testThread();
    }

    public void testThread() {
        ThreadDemo1 threadDemo1 = new ThreadDemo1();
        ThreadDemo2 threadDemo2 = new ThreadDemo2();
        ThreadDemo3 threadDemo3 = new ThreadDemo3();
        threadDemo1.start();
        threadDemo2.start();
        threadDemo3.start();

        //让线程充分运行
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
        }

        threadDemo1.interrupt();
        threadDemo2.interrupt();
        threadDemo3.exit();

        //等待线程终止
        try {
            threadDemo2.join();
            System.out.println("threadDemo2 exit");
            threadDemo3.join();
            System.out.println("threadDemo3 exit");
            threadDemo1.join();
            System.out.println("threadDemo1 exit");
        } catch (InterruptedException e) {
        }
    }

    class ThreadDemo1 extends Thread {
        @Override
        public void run() {
            while (true) {
                System.out.printf("%s run...\n", Thread.currentThread().getName());
                try {
                    Thread.sleep(5000); //线程暂停3秒之后继续运行
                } catch (InterruptedException e) {
                }
            }
        }
    }

    class ThreadDemo2 extends Thread {
        @Override
        public void run() {
            while (true) {
                System.out.printf("%s run...\n", Thread.currentThread().getName());
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    //线程在暂停中收到中断信号,做出反应并退出
                    System.out.printf("%s will exit...\n", Thread.currentThread().getName());
                    break;
                }
            }
        }
    }

    class ThreadDemo3 extends Thread {
        private boolean flag = false;

        @Override
        public void run() {
            while (true) {
                System.out.printf("%s run...\n", Thread.currentThread().getName());
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                }

                if (flag) {
                    //线程运行中判断标记位,当标记位被设置之后执行清理工作并退出
                    System.out.printf("%s will exit...\n", Thread.currentThread().getName());
                    break;
                }
            }
        }

        public void exit() {
            this.flag = true;
        }
    }
}

    运行实例代码观察输出可以知道,threadDemo2 因为收到中断信号而退出,threadDemo3 因为标志位被设置而退出,只有 threadDemo1 在一直运行。

    观察代码运行还可以发现,调用线程的 interrupt() 方法会打断 sleep 过程,即该方法可以使线程的 sleep 方法立即抛出一个 InterruptedException 异常,而不去关心 sleep 时间是否到期。

3,线程互斥

    多线程程序中的各个线程的运行时机是由操作系统调度确定的,而不能进行人工干预,因此当多个线程操作同一个堆实例时由于运行时机的不确定性导致运行结果不可预测,这在某些情况下会引发程序错误。

    这种由于多个线程同时操作而引起错误的情况称为数据竞争或竞态条件,这种情况下就需要进行线程互斥处理。在 Java 中最简单的互斥操作是通过 synchronized 关键字。

import java.util.Random;

/**
 * @author koma <komazhang@foxmail.com>
 * @date 2018-10-09 23:26
 */
public class SyncThreadDemo {
    public static void main(String[] args) {
        SyncThreadDemo syncThreadDemo = new SyncThreadDemo();
        syncThreadDemo.test();
    }

    public void test() {
        Thread1 thread1 = new Thread1(new Data());
        //启动四个线程
        new Thread(thread1).start();
        new Thread(thread1).start();
        new Thread(thread1).start();
        new Thread(thread1).start();
    }

    class Thread1 implements Runnable {
        private Data data;
        private Random random = new Random();

        public Thread1(Data data) {
            this.data = data;
        }

        @Override
        public void run() {
            while (true) {
                this.changeData();
            }
        }

        private void changeData() {
            int count = this.data.getCount();
            //睡眠随机时间,模拟线程被抢占
            try {
                Thread.sleep(this.random.nextInt(500));
            } catch (InterruptedException e) {
            }
            this.data.setCount(count+1);
            System.out.println(Thread.currentThread().getName()+"-count: "+this.data.getCount());
        }
    }

    class Data {
        private int count = 0;

        public int getCount() {
            return count;
        }

        public void setCount(int count) {
            this.count = count;
        }
    }
}

    上面代码示例中,changeData() 这个方法在没有做线程互斥时,打印的 count 值变化混乱,没有按预期多线程自增。当给其增加线程互斥之后才能实现预期效果,如下:

private synchronized void changeData() {
    int count = this.data.getCount();
    //睡眠随机时间,模拟线程被抢占
    try {
        Thread.sleep(this.random.nextInt(500));
    } catch (InterruptedException e) {
    }
    this.data.setCount(count+1);
    System.out.println(Thread.currentThread().getName()+"-count: "+this.data.getCount());
}

    synchronized 关键字实现的原理是在执行其包含的方法前,线程会先去尝试获得一把锁,只有成功获得锁的线程才能执行方法,而没有获得锁的线程则会等待,直到方法被执行完成返回之后锁被释放,其它线程才能再去竞争锁,这样就保证了方法每次只运行一个线程执行,实际上在这里把并行的逻辑串行化了。

    上述示例代码中的 run() 方法还可以写成下面这样:

@Override
public void run() {
    while (true) {
        synchronized (this) { //同步代码块
            int count = this.data.getCount();
            //睡眠随机时间,模拟线程被抢占
            try {
                Thread.sleep(this.random.nextInt(500));
            } catch (InterruptedException e) {
            }
            this.data.setCount(count+1);
            System.out.println(Thread.currentThread().getName()+"-count: "+this.data.getCount());
        }
    }
}

    两种写法说明,synchronized 关键字加在方法声明上实际上持有的锁是 this 对象的锁。但是当方法同时声明为 static 时, synchronized 持有的锁就变成了类的锁,这和 this 对象的锁存在明显差异。因为 this 对象的锁是类实例的锁,那么类实例化一次就会有一把锁,而类始终只有一个,因此类的锁总是只有一把。

    把上述实例代码中的 test() 方法和 run() 方法改写,如下:

public void test() {
    Data data = new Data();
    new Thread(new Thread1(data)).start();
    new Thread(new Thread1(data)).start();
    new Thread(new Thread1(data)).start();
    new Thread(new Thread1(data)).start();
}

@Override
public void run() {
    while (true) {
        synchronized (Thread1.class) { //使用类的锁
        //synchronized (this) { //使用类实例锁
            int count = this.data.getCount();
            //睡眠随机时间,模拟线程被抢占
            try {
                Thread.sleep(this.random.nextInt(500));
            } catch (InterruptedException e) {
            }
            this.data.setCount(count+1);
            System.out.println(Thread.currentThread().getName()+"-count: "+this.data.getCount());
        }
    }
}

    这里在 synchronized 代码块中使用类的锁和在 static 方法上加上 synchronized 关键字声明意义一样,因此这里使用代码块方式说明。如上代码描述中所示,在 synchronized 代码块上如果继续使用 this 锁,则依然无法达到预期效果,根本原因是现在每个线程都有一个类实例,导致每个线程中的 this 锁是独立的,而使用类的锁时,则代码会如预期运行,原因就是类的锁只有一把,和类实例个数无关。

4,线程协作

    所谓线程间协作,一种是上面说的线程之间在某一刻要互斥的顺序运行,一种则是类似于生产者-消费者模式,线程之间合作完成任务,当任务状态满足或者不满足时需要线程之间相互通知。这种机制就是通知/等待机制,使用 java Object 对象的 wait(),notify(),notifyAll() 方法来实现,下面的代码示例使用该机制实现了一个简单的生产者-消费者模式。

/**
 * @author koma <komazhang@foxmail.com>
 * @date 2018-10-10 00:35
 */
public class CooperationThread {
    public static void main(String[] args) {
        CooperationThread cooperationThread = new CooperationThread();
        cooperationThread.test();
    }

    public void test() {
        Product product = new Product();
        new Thread(new Consumer(product)).start();
        new Thread(new Consumer(product)).start();
        //让 Consumer 充分运行
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
        }
        new Thread(new Producer(product)).start();
    }

    class Product {
        private int count = 0;

        public void produce() {
            count++;
            System.out.println("produce: "+count);
        }

        public void consume() {
            count--;
            System.out.println("consume: "+count);
        }

        public boolean canConsume() {
            return this.count > 0;
        }

        public boolean canproduce() {
            return this.count == 0;
        }
    }

    class Producer implements Runnable {
        private Product product;
        public Producer(Product product) {
            this.product = product;
        }

        @Override
        public void run() {
            while (true) {
                synchronized (this.product) {
                    while (!this.product.canproduce()) {
                        try {
                            this.product.wait();
                        } catch (InterruptedException e) {
                        }
                    }

                    this.product.produce();
                    this.product.notifyAll();
                }
            }
        }
    }

    class Consumer implements Runnable {
        private Product product;
        public Consumer(Product product) {
            this.product = product;
        }

        @Override
        public void run() {
            while (true) {
                synchronized (this.product) {
                    while (!this.product.canConsume()) {
                        try {
                            this.product.wait();
                        } catch (InterruptedException e) {
                        }
                    }

                    this.product.consume();
                    this.product.notifyAll();
                }
            }
        }
    }
}

    wait() 方法是让当前线程进入到调用 wait() 方法的对象的等待队列中,而 notify(),notifyAll() 方法则是从调用对象的等待队列中唤醒一个或全部线程。规定调用 wait(),notify(),notifyAll() 前需要先获取调用对象的锁,同时在调用 wait() 方法之后,刚刚获取到的对象的锁会被释放,以便其它线程有机会去竞争锁,而在调用 notify(),notifyAll() 方法之后则不会主动释放锁,因为可能在这之后当前线程还有别的工作需要做完才能释放锁。

    由于在调用 wait() 之后线程会阻塞在当前位置,当调用 notify 之后线程会从当前位置继续往下执行,但是由于这时有可能 product 的状态恰好又被其它线程改变,那么当前线程继续往下执行就会产生意外的情况,因此我们通常的调用 wait() 的方法是放到一个 while 循环中,像下面这样:

while (!this.product.canConsume()) {
    try {
        this.product.wait();
    } catch (InterruptedException e) {
    }
}

    这种方法会使得我们的代码更加健壮。另外一个会使代码更加健壮的做法是尽量使用 notifyAll() 而不是 notify(),因为调用 notify() 方法只唤醒等待队列中的一个线程,那么对于等待队列中既有消费者,又有生产者时,那么当消费者线程调用 notify() 有可能会还是唤醒消费者线程,如果这种情况的概率较大,则程序便会停止但是不报错。

5,线程状态转换

    线程包括 NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED 这几种状态,可以通过 Thread 的 getState() 方法获取到。线程在整个生命周期中经历的状态转换都可以包括到这张图中

二,多线程程序的评价标准

1,安全性

    安全性是指不损坏对象,即对象的状态或值一定要复合预期设计。当一个类被多线程调用时,如果也能保证对象的安全性,则该类称为线程安全类,否则称为线程不安全类。

2,生存性

    生存性是指在任何时刻,程序的必要处理一定能够完成,这也是程序正常运行的必要条件,也称为程序的活性。常见的场景是程序运行存在死锁或活锁,导致程序不能够正常运行,这就违反了线程的生存性。

3,可复用性

    可复用性是指类能够重复利用,主要目标是提高程序的质量。

4,性能

    性能是指程序能够快速的,大批量的执行处理,主要目标是提高程序的质量。性能的主要指标包括:吞吐量 - 单位时间内完成的处理数量,越大表示性能越高;响应性 - 指从发出请求到收到请求响应的时间间隔,越短表示性能越高;容量 - 是指可同时进行的处理数量,越多表示性能越高;

上一篇下一篇

猜你喜欢

热点阅读