随着工作时间越来越久,慢慢的接触到的并发场景也越来越多,不得不说,这是每个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<Runnable>(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关键字,线程就会进行同步操作,一个线程执行完才会释放锁给下一个等待的线程执行,这样就不会有共享资源的情况发生。 还有一种方法就是用锁的机制也可以完成资源的锁定,但是要注意锁的释放问题。