1 为什么需要并发?
采用并发技术进行编程的时候大多目的是为了提高速度。很显然的是在具有多处理器的服务器上,通过并发将不同的程序片段分布在不同的处理器上运行自然会提高程序的响应速度。
那试想在单处理器的服务器上呢?由于上下文切换的代价,似乎并发程序会比顺序执行的时间开销还大。那为什么还强调并发呢?这是因为程序的运行存在阻塞的情况,如果某个任务由于外部IO条件而阻塞,那么将导致后续代码都无法执行。因此,当采用并发时可以保证其他任务不受到影响。
2 Runnable
java中可以通过实现Runnable接口并编写run()方法来实现多线程。
一个简单的通过多线程生成Fibonacci序列的代码如下:
public class RunnableFibonacciTest implements Runnable {
private int n;
public RunnableFibonacciTest(int n) {
this.n = n;
}
@Override
public void run() {
StringBuilder sb = new StringBuilder();
if (n <= 0) System.out.println("参数错误!");
if (n == 1) System.out.println("1");
if (n == 2) System.out.println("1-2");
if (n <= 2) return;
sb.append("1-2");
int pre1 = 2, pre2 = 1;
for (int i = 3; i <= n; i++) {
int temp = pre1;
pre1 = pre2 + pre1;
pre2 = temp;
sb.append("-" + pre1);
Thread.yield();//告诉CPU我当前已经执行了核心代码,可以进行CPU切换了
}
System.out.println(sb.toString());
}
}
用一个程序对其进行测试,下面两个循环输出的结果并不相同,这是因为当直接调用run方法时实际上并没有开启多线程,而仅仅是运行在main线程上:
public static void main(String[] args) {
for (int i = 0; i <= 10; i++) {
RunnableFibonacciTest runnableFibonacciTest = new RunnableFibonacciTest(i);
runnableFibonacciTest.run();
}
for (int i = 0; i <= 10; i++) {
Thread thread = new Thread(new RunnableFibonacciTest(i));
thread.start();
}
}
参数错误!
1
1-2
1-2-3
1-2-3-5
1-2-3-5-8
1-2-3-5-8-13
1-2-3-5-8-13-21
1-2-3-5-8-13-21-34
1-2-3-5-8-13-21-34-55
1-2-3-5-8-13-21-34-55-89
1-2
1-2-3
参数错误!
1
1-2-3-5
1-2-3-5-8-13
1-2-3-5-8-13-21-34-55-89
1-2-3-5-8-13-21-34
1-2-3-5-8
1-2-3-5-8-13-21
1-2-3-5-8-13-21-34-55
实际上还可以通过继承Thread类的方法实现多线程,但由于java并不支持多继承,因此通常使用实现Runnable接口的方式。
3 Executor执行器
Executor的诞生是为了简化并发编程,它将帮助我们管理异步事件的执行。
其有三个实现类分别是:
- CachedThreadPool
- FixedThreadPool
- SingleThreadExecutor
3.1 CachedThreadPool
存在多少个并发任务就会创建多少个线程。
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i <= 10; i++) {
cachedThreadPool.execute(new RunnableFibonacciTest(i));
}
cachedThreadPool.shutdown();//会执行在shutdown之前提交的所有任务
3.2 FixedThreadPool
创建固定大小的线程。
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
for (int i = 0; i <= 10; i++) {
fixedThreadPool.execute(new RunnableFibonacciTest(i));
}
fixedThreadPool.shutdown();
3.2 SingleThreadExeutor
固定大小为1的FixedThreadPool,其会顺序的将任务排序,并逐一执行
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i <= 10; i++) {
singleThreadExecutor.execute(new RunnableFibonacciTest(i));
}
singleThreadExecutor.shutdown();
结果为:
参数错误!
1
1-2
1-2-3
1-2-3-5
1-2-3-5-8
1-2-3-5-8-13
1-2-3-5-8-13-21
1-2-3-5-8-13-21-34
1-2-3-5-8-13-21-34-55
1-2-3-5-8-13-21-34-55-89
4 Callable接口
当想从线程执行完毕后获取返回值时,便需要将实现Runnable接口改为实现Callable接口,该接口提供返回类型为Future的对象。可以通过Future的get()获取返回值。一个简单的获取斐波那契数列和的例子如下:
public class CallableFibonacci implements Callable<Integer> {
private int n;
public CallableFibonacci(int n) {
this.n = n;
}
@Override
public Integer call() throws Exception {
if (n <= 0) return 0;
if (n == 1) return 1;
if (n == 2) return 3;
int pre1 = 2, pre2 = 1;
int result = 2;
for (int i = 3; i <= n; i++) {
int temp = pre1;
pre1 = pre2 + pre1;
pre2 = temp;
result += pre1;
Thread.yield();
}
return result;
}
}
在开启线程时使用的是submit方法而不再是execute方法,并且submit方法会返回Future对象,该对象包含了需要的返回值。
ExecutorService executorService = Executors.newCachedThreadPool();
List<Future<Integer>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(executorService.submit(new CallableFibonacci(i)));
}
for (Future<Integer> fs : list) {
try {
System.out.println(fs.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
5 sleep、join与后台线程
- sleep:使线程休眠:TimeUnit.MILLISECONDS.sleep(1000);
- join:thread1调用thread2.join()后,thread1会挂起直到thread2执行完毕。
- 后台线程:在开始线程之前利用thread.setDaemon(true)可以将线程设置为后台线程,后台线程在非后台线程执行完毕后也会自动销毁。
6 不正确的资源访问
在并发情况下,可能会出现多个线程同时访问一个数据的情况。这里以下面的两个例子来说明可能会出现的不正确访问情况。
6.1 偶数的产生问题
例子如下:
public abstract class IntGenerator {
private volatile boolean canceled = false;
public abstract int next();
public void cancel() {
canceled = true;
}
public boolean isCanceled() {
return canceled;
}
}
public class EvenGenerator extends IntGenerator {
private int currentEvenValue = 0;
@Override
public int next() {
++currentEvenValue;
++currentEvenValue;
return currentEvenValue;
}
}
public class EvenChecker implements Runnable {
private IntGenerator intGenerator;
public EvenChecker(IntGenerator generator) {
this.intGenerator = generator;
}
@Override
public void run() {
while (!intGenerator.isCanceled()) {
int val = intGenerator.next();
if (val % 2 != 0) {
System.out.println(val + "not even");
intGenerator.cancel();
}
}
}
public static void main(String[] args) {
EvenGenerator generator = new EvenGenerator();
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
exec.execute(new EvenChecker(generator));
}
exec.shutdown();
}
}
EvenGenerator的功能在于递增的生成一个偶数。在多线程下会出现这种的一种情况,但线程1在刚调用完一次++currentEvenValue后,该线程被挂起线程2又开始执行了,这样就会导致产生一个奇数从而导致结果的错误。
防止该问题出现的方法就是当资源被一个任务使用的使用的时候,给该资源加上锁。java中提供synchronized方法来防止资源冲突,当任务要执行synchronized关键字修饰的代码时,会首先检查锁是否可用,然后获得锁->执行代码->释放锁。
即
public int next() {
synchronized(this){
++currentEvenValue;
++currentEvenValue;
return currentEvenValue;
}
}
6.2 多线程中的递增问题
例子如下:
public class SelfAdd implements Runnable {
private int count = 0;
static int size = 1000;
static CountDownLatch countDownLatch = new CountDownLatch(size);
public int get() {
return count;
}
@Override
public void run() {
count++;
countDownLatch.countDown();
}
public static void main(String[] args) {
SelfAdd selfAdd = new SelfAdd();
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < size; i++) {
exec.execute(selfAdd);
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
exec.shutdown();
System.out.println(selfAdd.get());
}
}
上面这个例子看起来最终count会输出1000,但实际上会小于1000。这是因为递增的操作实际上并不是原子操作。
public class TestAtom {
int i = 0;
void f() {
i++;
}
}
其字节码如下:
void f();
Code:
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
需要注意的是java仅保证数据的读取与赋值是原子性的操作,从字节码上面可以看出在get与put之间还有一些操作,因此该递增操作不是原子性的。
7 TreadLocal
对于TreadLocal只需要关注三个点就好了,分别是:
- initialValue():给threadLocal一个初始值
- get()
- put()
public class ThreadLocalVariableHolder {
private static ThreadLocal<Integer> value = new ThreadLocal<Integer>() {
private Random rand = new Random(47);
protected synchronized Integer initialValue() {
return rand.nextInt(10000);
}
};
public void setValue(){
value.set(value.get() + 1);
}
public int getValue(){
return value.get();
}
}
8 线程的终端
8.1 线程的四种状态
- 新建(new):线程被创建时的状态。
- 就绪态(Runnable):线程等到CPU时间片的分配。
- 阻塞态(Blocked):某个条件例如(sleep)阻塞了线程
- 死亡态(Dead):任务已结束,例如run方法运行完毕,被中断等。
8.2 进入阻塞态的几种情况
- sleep函数
- wait函数
- 等待IO输入/输出
- 访问锁还未释放的方法
8.3中断
调用sleep方法而引起的阻塞可以被中断并且抛出InterruptedException异常。但等待IO和访问锁未释放的方法而导致的阻塞是不能被Interrupt方法中断的。对于这一类不可利用Interrupt方法中断的阻塞而言,可以通过关闭其底层资源的方式来达到中断的效果。例如:
System.in.close();
socket.close();
都可以达到中断阻塞。
另外,在没有抛出InterruptedException异常的情况下,可以利用interrupted方法来进行中断操作。
run(){
while(interrupted()){
}
}
8.4 executor的中断
- 中断所有由executor管理的线程:
executorService.shutdownNow();
- 中断单个线程
public class ExecInterrupt {
public static void main(String[] args) {
ExecutorService exec = Executors.newCachedThreadPool();
List<Future> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
list.add(exec.submit(new Callable<Object>() {
@Override
public Object call() throws Exception {
while (!Thread.interrupted()) {
}
System.out.println("over");
return null;
}
}));
}
list.get(2).cancel(true);
}
}
9 线程之间的协作
9.1 Join方法:
A线程调用B线程的Join会把A线程挂起直到B线程结束。
9.2 wait方法:
该方法将线程阻塞直到某一条件满足后调用notify或者notifyAll来唤醒。因为通常感兴趣的是这某一条件因此需要使用下面的方式。
while(condition) wait();
这似乎看起来有点像忙等待,但实际因为notifyAll可能会唤醒多个任务,因此添加一个限制条件是有必要的。
一个简单的物品拿起和放下的例子如下:
public class WaitNotifyAll {
boolean taken = true;
public synchronized void take() {
try {
System.out.println("want to take");
while (taken) wait();
taken = true;
} catch (InterruptedException e) {
System.out.println("take Interrupted");
} finally {
System.out.println("take shutdown");
}
}
public synchronized void drop() {
try {
TimeUnit.MILLISECONDS.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
taken = false;
notifyAll();
}
public static void main(String[] args) {
WaitNotifyAll waitNotifyAll = new WaitNotifyAll();
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(()->waitNotifyAll.take());
exec.execute(()->waitNotifyAll.drop());
exec.shutdown();
}
}
want to take1
want to take
notifyAll
release the lock
get to take
take shutdown
get to take
可以得出以下结论:
- wait和sleep都会造成线程的阻塞,但区别在于wait会释放当前对象的锁,而sleep不会。
- notifyAll会唤醒该对象上所有的wait,但需要注意的是即便是唤醒所有wait,但其仍旧只有一把锁,因此利用while(condition)的方式来判断谁有权利获得这把锁是有必要的。notifyAll虽然会唤醒所有的wait,但仍然需要线程释放锁(一般就是程序结束)后wait才能够获得锁。
使用wait和notify实现的消费者与生产者:
public class Meal {
private int orderNum = 0;
public Meal(int id) {
orderNum = id;
}
@Override
public String toString() {
return "Meal{" +
"orderNum=" + orderNum +
'}';
}
}
public class Chef implements Runnable {
private Restaurant restaurant;
private int count = 0;
public Chef(Restaurant restaurant) {
this.restaurant = restaurant;
}
@Override
public void run() {
try {
while (!Thread.interrupted()) {
synchronized (this) {
while (restaurant.meal != null) {
wait();
}
}
if (++count == 10) {
System.out.println("out of food closing");
restaurant.exec.shutdownNow();
}
synchronized (restaurant.waiter) {
System.out.println("Order up");
restaurant.meal = new Meal(count);
restaurant.waiter.notifyAll();
}
TimeUnit.MILLISECONDS.sleep(100);
}
} catch (InterruptedException e) {
System.out.println("Chef InterruptedException");
}
}
}
public class Waiter implements Runnable {
private Restaurant restaurant;
public Waiter(Restaurant restaurant) {
this.restaurant = restaurant;
}
@Override
public void run() {
try {
while (!Thread.interrupted()) {
synchronized (this) {
while (restaurant.meal == null) {
wait();
}
}
synchronized (restaurant.chef) {
System.out.println("waiter get " + restaurant.meal);
restaurant.meal = null;
restaurant.chef.notifyAll();
}
}
} catch (InterruptedException e) {
System.out.println("waiter InterruptedException");
}
}
}
public class Restaurant {
ExecutorService exec = Executors.newCachedThreadPool();
Meal meal;
Waiter waiter = new Waiter(this);
Chef chef = new Chef(this);
public Restaurant() {
exec.execute(chef);
exec.execute(waiter);
}
public static void main(String[] args) {
new Restaurant();
}
}
Order up
waiter get Meal{orderNum=1}
Order up
waiter get Meal{orderNum=2}
Order up
waiter get Meal{orderNum=3}
Order up
waiter get Meal{orderNum=4}
Order up
waiter get Meal{orderNum=5}
Order up
waiter get Meal{orderNum=6}
Order up
waiter get Meal{orderNum=7}
Order up
waiter get Meal{orderNum=8}
Order up
waiter get Meal{orderNum=9}
out of food closing
Order up
waiter InterruptedException
Chef InterruptedException
需要注意在wait和notify的时候都需要判断对应的锁是否能获得,即都需要加上synchronized关键字。
juc还提供了Lock & Condition来显示的实现协作
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();//获得锁,在前面的例子上使用的是synchronized
condition.await();//线程挂起并释放锁
condition.notifyAll();//唤醒挂起的任务
lock.unlock();//释放锁,前面的例子中是当程序结束则释放锁。