并发艺术,先用后悟

随着工作时间越来越久,慢慢的接触到的并发场景也越来越多,不得不说,这是每个Java开发者进阶之路的必经阶段,那我们就从简单的会用开始这段并发的学习之路吧。

并发第一步,创建一个线程

通常,启动一个程序,就相当于起了一个进程。每个电脑都会运行很多程序,所以你会在进程管理器中看到很多进程,一个进程里,可能会有很多线程在运行,也可能只有一个。

main函数其实就是一个主线程。我们可以在这个主线程当中创建很多其他的线程。来看下面这段代码

public class Test {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(new Runnable() {

                @Override
                public void run() {
                    System.out.println("我叫" + Thread.currentThread().getName() + ",我喜欢吃烧烤");
                }
            });
            t.start();
        }
    }
}

创建线程最常用的方式就是声明一个实现了Runnable接口的匿名内部类;然后将它作为创建Thread对象的参数;再然后调用Thread对象的start()方法进行启动。运行的结果如下。

我叫Thread-1,我喜欢吃烧烤
我叫Thread-3,我喜欢吃烧烤
我叫Thread-2,我喜欢吃烧烤
我叫Thread-0,我喜欢吃烧烤
。。。

从运行结果可以看出,线程的执行顺序不是从0-3按照顺序来执行的,而是有一定的随机性,原因是java并发是抢占机制。

线程池初体验

java本身提供了java.util.concurrent.Executors类一系列工厂方法用于创建线程池:

public class Wanger {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();//创建一个缓存线程池

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

                @Override
                public void run() {
                    System.out.println("我叫" + Thread.currentThread().getName() + ",我喜欢吃烧烤");
                }
            };
            executorService.execute(r);
        }
        executorService.shutdown();
    }
}

运行结果:

我叫pool-1-thread-2,我喜欢吃烧烤
我叫pool-1-thread-4,我喜欢吃烧烤
我叫pool-1-thread-5,我喜欢吃烧烤
我叫pool-1-thread-3,我喜欢吃烧烤
我叫pool-1-thread-4,我喜欢吃烧烤
我叫pool-1-thread-1,我喜欢吃烧烤
我叫pool-1-thread-7,我喜欢吃烧烤
我叫pool-1-thread-6,我喜欢吃烧烤
我叫pool-1-thread-5,我喜欢吃烧烤
我叫pool-1-thread-6,我喜欢吃烧烤

调用该线程池的execute()方法可以重用以前的线程,只要该线程可用,除了newCacheedThreadPool还有其他三种线程池的创建:

  • newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

但是我们经常看到有些地方说不允许Executors来直接创建线程池,可能会出现OOM的情况,为了避免这种情况的发生,我们该怎么创建线程池呢? 直接调用ThreadPoolExecutor的构造函数来创建线程池呗。其实Executors就是这么做的,只不过没有对BlockQueue指定容量。我们需要做的就是在创建的时候指定容量。代码示例如下。

ExecutorService executor = new ThreadPoolExecutor(10, 10,
        60L, TimeUnit.SECONDS,
        new ArrayBlockingQueue(10));

如何解决并发时共享资源竞争问题

在Java中,解决共享资源竞争问题的首个解决方案就是使用关键字synchronized。当线程执行被synchronized保护的代码片段的时候,会对这段代码进行上锁,其他调用这段代码的线程会被阻塞,直到锁被释放。

下面这段代码使用ThreadPoolExecutor创建了一个线程池,池里面的每个线程会对共享资源count进行+1操作。现在,闭上眼想一想,当1000个线程执行结束后,count的值会是多少呢?

public class Wanger {
    public static int count = 0;

    public static int getCount() {
        return count;
    }

    public static void addCount() {
         count++;
    }

    public static void main(String[] args) {
        ExecutorService executorService = new ThreadPoolExecutor(10, 1000,
                60L, TimeUnit.SECONDS,
                new ArrayBlockingQueue(10));

        for (int i = 0; i < 1000; i++) {
            Runnable r = new Runnable() {

                @Override
                public void run() {
                    Wanger.addCount();
                }
            };
            executorService.execute(r);
        }
        executorService.shutdown();
        System.out.println(Wanger.count);
    }
}

运行结果的count值可能是995,996,998,但是很少会是1000,why? 这就是共享变量被占用的情况,在addCount()方法上加上synchronized关键字,线程就会进行同步操作,一个线程执行完才会释放锁给下一个等待的线程执行,这样就不会有共享资源的情况发生。 还有一种方法就是用锁的机制也可以完成资源的锁定,但是要注意锁的释放问题。

你可能感兴趣的:(并发艺术,先用后悟)