java.util.concurrent----多线程基础

创建线程的方法

创建线程的方式一共有四种,1.继承Thread,2.实现Runnable接口,3.实现Callable接口,4.线程池
首先介绍前两种方式:

public class TestThread {

    public static void main(String[] args) {
        ThreadDemo1 td1 = new ThreadDemo1();
        ThreadDemo2 td2 = new ThreadDemo2();
        td1.start();
        new Thread(td2).start();
    }
}

class ThreadDemo1 extends Thread {

    @Override
    public void run() {
        System.out.println("thread");
    }
}

class ThreadDemo2 implements Runnable {

    @Override
    public void run() {
        System.out.println("runnable");
    }
}

Thread本身就继承了Runnable接口,所以自然也就重写了run()方法,所以在使用继承Thread和实现Runnable都需要重写run方法。
我们平时不会去采用继承Thread的方式(单继承,多实现)。

都会采用start方法来开启线程,与run相比,start方法会创建一个线程,并把线程至于可运行状态(CPU可以进行调度)。然后主线程会跳过这段代码继续向下执行。当执行到该线程的时候,会调用run()方法,执行线程内部的逻辑。如果直接使用run方法是不会开启一条线程的,相当于仍然是单线程在执行。

线程安全问题

线程安全问题存在于多个线程对共享变量的访问。线程安全主要有两个问题,一个是原子性,一个是可见性。这两个问题都可以加锁的方式解决,但在某些场合,加锁的代价很重,因此不太适合。下面会介绍不通过加锁解决线程安全的方法。

  1. 可见性:使用volatile关键字。每一个线程都会有自己的一块内存,当它们对共享数据进行修改的时候,只会更改自己线程内存中的数据,刷新到主内存中需要一定的时间,其它的线程是无法快速感知的,这样的话就会造成数据错误。volatile关键字的作用是,使用该关键子修饰的共享变量在修改的时候,会将数据同步到主内存,变量在读取的时候不从自己线程的缓存中读取,而是去主内存中读取,我们可以理解为使用了volatile后,修改和读取都是在主内存中进行的,虽然会有一定的开销,但对于加锁操作来说,开销小太多了。除此之外volatile还有禁止指令重排的功能(屏障)。
//一般用于开关使用
public class TestVolatile {

    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        new Thread(td).start();

        while (true) {
            //可以加同步锁保证可见性,但是太重了(通过Happen-before原则来实现可见性)
            if(td.isFlag()) {
                System.out.println("--------------------");
                break;
            }

        }
    }

}

class ThreadDemo implements Runnable {

    //去掉volatile以后,主方法sout不会执行
    private volatile boolean flag = false;

    @Override
    public void run() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println("flag = " + isFlag());
    }

    public boolean isFlag() {
        return flag;
    }

}
  1. 原子性:一个经典的例子i++,它是需要有两步操作的,所以在多线程的环境下会有安全问题,volatile只能保证可见性,但是并不能保证原子性。
public class TestAtomic {

    public static void main(String[] args) {
        ThreadAtomicDemo td = new ThreadAtomicDemo();
        for(int i = 0; i < 10; i++) {
            new Thread(td).start();
        }
    }

}

class ThreadAtomicDemo implements Runnable {

    //无法保证原子性,有可能会导致有相同的值出现
    //private int num = 0;

    //使用juc中Atomic包下的类,自带volatile,原子性通过CAS保证
    AtomicInteger num = new AtomicInteger(0);

    @Override
    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(getNum());
    }

    public int getNum() {
        //return num++;
        return num.getAndIncrement();
    }
}

每一个数据类型都对应着其线程安全的数据类型,例如Integer对应着AtomicInteger,其可见性通过volatile关键字来保证,原子性则是通过CAS来保证的。
CAS:硬件对于原子性的支持,举个例子,假设一个线程要对共享数据i进行加1的操作,首先它要去主内存中去取i的值,发现i为2,接下来它对i进行加1操作,同时再去主内存中取i的值,如果此时i的值仍然为2,那么就把3写回主内存,如果它发现此时i的值不为2,那么从头开始,重新尝试对i进行加1操作。

线程安全的集合类

我们会经常使用集合,在多线程的情况又如何来保证安全问题呢?最简单粗暴的方式就是对修改集合中数据的方法加同步锁。第二种方式是使用Collections.synchronizedList(new ArrayList<>()),同样也有set和map的方法,缺点是,在对集合中的数据进行修改的时候会将整张表锁住,所以在集合遍历的时候,进行修改的话,会有并发修改异常。接下来介绍一些常用的线程安全的集合类。

  1. ConcurrentHashMap:与HashTable不同的是,HashTable锁住了整张表,而ConcurrentHashMap则是使用分段锁(Segment)来表示不同的部分,每个段其实就是一个小的hashTable,它们都有自己的锁(Lock),所以当多个修改发生在不同段上的时候,就可以并发的进行。当然它也会有上述所说的异常。
  2. CopyOnWriteArrayList/CopyOnWriteArraySet:CopyOnWrite容器是写的时候复制的容器,就是我们在往集合里面写东西的时候,不是直接写而是先copy这个容器,我们在copy的容器中添加元素,之后将指针指向新容器,因此可以并发的读,不需要加锁,十分适合读多写少的并发场景。当然它的缺点一个是内存占用的问题,一个是它只能保证最终一致性。

具体选择用哪种线程安全的集合类看业务场景而定。

public class TestCopyOnWriteArrayList {

    public static void main(String[] args) {

        ThreadCopyOnWriteDemo td = new ThreadCopyOnWriteDemo();
        for(int i = 0; i < 10; i++) {
            new Thread(td).start();
        }

    }

}

class ThreadCopyOnWriteDemo implements Runnable {

    /**
     * 会有并发修改异常出现
     */
    private static List<String> list = Collections.synchronizedList(new ArrayList<>());
    /**
     * 底层通过每次修改集合都会复制出一个新的集合来保证线程安全
     * 适用于读多写少的多线程环境
     */
    //private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();

    static {
        list.add("AA");
        list.add("BB");
        list.add("CC");
    }

    @Override
    public void run() {

        Iterator<String> it = list.iterator();

        while (it.hasNext()) {
            System.out.println(it.next());
            list.add("AA");
        }

    }
}

线程之间的通讯

举一个例子说,如果我希望一个线程要等其他的线程运行完之后再去运行。

  1. CountDownLatch:
  • 允许一个或者多个线程等待其他线程完成之后再执行,比如人齐了一起吃饭。

  • CountDownLatch(int count): 构造方法,初始化计数器。

  • await(): 是当前线程在计数器为0之前一直等待,除非线程被中断。

  • countDown(): 计数器减1。

  • getCount(): 返回当前计数。

  • CountDownLatch内部有一个线程数量计数器,当一个(或多个)线程执行await方法后等待,其他的线程完成任务后,计数器减1。如果此时计数器仍然大于0,那么等待的线程继续等待。如果为0,表示其他线程任务执行完成之后,等待的线程会被唤醒。

public class TestCountDown {

    public static void main(String[] args) throws InterruptedException {
        //指定计数器5,每当有线程执行完后就减1,当减到0的时候,主线程才会执行
        final CountDownLatch latch = new CountDownLatch(5);
        TestCountDownDemo td = new TestCountDownDemo(latch);
        long start = System.currentTimeMillis();
        for(int i = 0; i < 5; i++) {
            new Thread(td).start();
        }
        //主线程等待,只有latch的计数器到0的时候才会放行
        latch.await();
        /**
         * 当其它线程全部执行完毕后才会执行
         * 用于总计算,那些需要其它线程执行完结果再去执行的场景
         */
        long end = System.currentTimeMillis();
        System.out.println("耗费时间为:" + (end - start));
    }

}

class TestCountDownDemo implements Runnable {

    private CountDownLatch latch;

    public TestCountDownDemo(CountDownLatch latch) {
        this.latch = latch;
    }

    @Override
    public void run() {

        synchronized (this) {
            try {
                for(int i = 0; i < 10; i++) {
                    if(i % 2 ==0) {
                        System.out.println(i);
                    }
                }
            } finally {
                //当线程执行完毕后减1
                latch.countDown();
            }
        }
    }
}
  1. 通过实现Callable接口来创建线程,实现Callable接口,可以将线程的结果进行返回,需要FutureTask实现类的支持,用于接收结果。简单来说就是线程能抛异常,能又返回值,返回值存在Future中。Callable实例当作参数,生成一个FutureTask的对象,然后把这个对象当作一个Runnable,作为参数令起线程。
public class TestCallable {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadCallableDemo td = new ThreadCallableDemo();
        FutureTask<Integer> future = new FutureTask<>(td);
        new Thread(future).start();
        //接收线程运算后的结果
        Integer result = future.get();
        System.out.println(result);
        System.out.println("-----------------------");

    }

}

class ThreadCallableDemo implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {

        int sum = 0;
        for(int i = 0; i <= 100; i++) {
            sum += i;
        }

        return sum;
    }
}

Future的核心思想:一个方法f,计算过程可能非常耗时,等待f返回,显然不明智。可以在调用f的时候,立刻返回一个Future,可以通过Future这个数据结构去控制方法f的计算过程。

  • get:获取计算结果(如果还没计算完,也是必须等待的)
  • cancel:还没计算完,可以取消计算过程
  • isDone:判断是否计算完
  • isCancelled:判断计算是否别取消

实际上,FutureTask也是一个Runnable,其中的run方法的逻辑就是运行Callable的call方法,然后保存结果或者异常。

读写锁

读写锁希望在共享数据的访问上,读读不互斥,读写互斥,写写互斥,而且希望读必须是最新的数据,因此需要加读写锁。

public class TestReadAndWrite {

    public static void main(String[] args) {
        ReadAndWrite rw = new ReadAndWrite();

        for(int i = 0; i < 1000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    rw.get();
                }
            }).start();
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                rw.save();
            }
        }, "write").start();
    }
}


class ReadAndWrite {

    private int number = 0;

    private ReadWriteLock lock = new ReentrantReadWriteLock();

    //读
    public void get() {

        lock.readLock().lock();

        try {
            System.out.println(Thread.currentThread().getName() + " : " + number);
        } finally {
            lock.readLock().unlock();
        }

    }

    //写
    public void save() {

        lock.writeLock().lock();

        try {
            //Thread.sleep(20);
            System.out.println(Thread.currentThread().getName());
            this.number = new Random().nextInt(100);

        } catch (Exception e) {

        } finally {
            lock.writeLock().unlock();
        }

    }
}

线程池

创建线程的第四种方式

为什么要使用线程池
  1. 减少过于频繁的创建、销毁线程,增加处理效率。
  2. 线程并发数量过多,抢占系统资源从而导致堵塞。
  3. 对线程进行简单的处理。
线程池中的参数
  • corePoolSize:线程池中核心线程的最大值。(线程池里面分为核心线程和非核心线程) PS:核心线程默认会一直存活,即使这个核心线程啥事都不干。
  • maximumPoolSize:线程总数最大值。(线程总数 = 核心线程 + 非核心线程)
  • keepAliveTime:非核心线程,闲置最长时长。
  • TimeUnit:keepAliveTime的单位。
  • BlockingQueue:线程池中的任务队列,核心线程没满,新添加的线程直接进核心线程,核心线程满了,加入任务队列,若队列满了,新建非核心线程,若超出之前设置的线程总数最大值,就会报错。
Executors提供的线程池创建配置
  1. newCachedThreadPool(): 用于处理大量短时间工作任务的线程池。
  • 试图缓存线程并重用,当无缓存线程可用时,会创建新的线程。
  • 如果线程闲置时间超过60s,则被终止并移出缓存。
  • 内部使用SynchronousQueue作为工作队列。
(corePoolSize: 0, maximumPoolSize: Integer.MAX_VALUE, keepAliveTime: 60L, TimeUnit: SECONDS, BlockingQueue: SynchronousQueue)
  1. newFixThreadPool(int nThreads):
  • 重用指定数目的线程。(nThreads)
  • 使用无界的工作队列。
  • 任务数量超过活动队列的数目,将在工作队列中等待空闲线程出现。如果有工作线程退出,将会有新的工作线程被创建,以补足指定数目的nThreads。
(corePoolSize: nThreads, maximumPoolSize: nThreads, keepAliveTime: 0L, TimeUnit: SECONDS, BlockingQueue: LinkedBlockingQueue)
  1. newSingleThreadExecutor(): 创建的是ScheduledExecutorService,也就是可以进行定时或周期性的工作调度。
  • 工作线程数目限制为1,保证所有任务都是被顺序执行。
  • 最多会有一个任务处于活动状态。
  • 不允许使用者更改线程池实例,可以避免其改变线程数目。
(corePoolSize: 1, maximumPoolSize: 1, keepAliveTime: 0L, TimeUnit: SECONDS, BlockingQueue: LinkedBlockingQueue)
  1. newScheduledThreadPool(int corePoolSize): 同样是ScheduledExecutorService
  • 会保持corePoolSize个工作线程。
  • 可以设置延迟多少秒执行。
(corePoolSize: corePoolSize, maximumPoolSize: Integer.MAX_VALUE, BlockingQueue: DelayedQueue)
public class TestThreadPool {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        SumRunnable sr = new SumRunnable();
        SumCallable sc = new SumCallable();

        /*for(int i = 0; i < 100; i++) {
            executorService.submit(sr);
        }*/

        List<Future<Integer>> list = new ArrayList<>();
        //List<Integer> list1 = new ArrayList<>();
        for(int i = 0; i < 100; i++) {
            Future<Integer> future = executorService.submit(sc);
            list.add(future);
            //list1.add(future.get());
        }
        executorService.shutdown();

        for(Future<Integer> future: list) {
            System.out.println(future.get());
        }

        /*for(Integer i: list1) {
            System.out.println(i);
        }*/
    }



}

class SumRunnable implements Runnable {

    private int number = 0;

    @Override
    public void run() {

        System.out.println(Thread.currentThread().getName());
        for(int i = 0; i <= 100; i++) {
            number += i;
        }

    }
}

class SumCallable implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {

        int number = 0;
        Thread.sleep(100);
        for(int i = 0; i <= 100; i++) {
            number += i;
        }
        return number;
    }
}

推荐阅读更多精彩内容