为什么要用到并发
1.并发理论:并发编程要解决的三大问题;介绍可见性与有序性问题的根源重排序;学习Java内存模型(JMM),理解JMM如何解决这些问题以实现并发编程的。
2.并发关键字:深入volatile、synchronized、final关键字的作用,都解决了什么问题,以及其实现原理。
3.并发基础:并发编程中用到的一些基本概念,如:死锁、饥饿与公平等;线程的创建、运行、调度。
4.CAS原子操作:并发编程的基础与核心CAS的实现原理,以及Java中的CAS原子操作。
5.Lock体系:JDK的Lock对于synchronized有哪些优势;Lock如何通过AQS与LockSupport工具实现的;Lock的使用。
6.并发工具:介绍java.util.concurrent包下提供的并发工具倒计时器CountDownLatch、循环栅栏CyclicBarrier、资源访问控制Semaphore、数据交换Exchanger的用法,并深入分析实现原理。
7.并发容器:介绍java.util.concurrent包下提供的并发容器ConcurrentHashMap、CopyOnWriteArrayList、ConcurrentLinkedQueue、ThreadLocal、及阻塞队列的用法,并深入分析实现原理。
8.线程池(Executor体系):介绍线程池ThreadPoolExecutor和ScheduledThreadPoolExecutor的用法,分析线程池执行流程和原理。
并发编程有哪些缺点
1. 频繁的上下文切换
-
无锁并发编程:可以参照concurrentHashMap锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。
-
CAS算法,利用Atomic下使用CAS算法来更新数据,使用了乐观锁,可以有效的减少一部分不必要的锁竞争带来的上下文切换
-
使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多的线程,这样会造成大量的线程都处于等待状态
-
协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换
2. 线程安全
public class DeadLockDemo { private static String resource_a = "A"; private static String resource_b = "B"; public static void main(String[] args) { deadLock(); } public static void deadLock() { Thread threadA = new Thread(new Runnable() { @Override public void run() { synchronized (resource_a) { System.out.println("get resource a"); try { Thread.sleep(3000); synchronized (resource_b) { System.out.println("get resource b"); } } catch (InterruptedException e) { e.printStackTrace(); } } } }); Thread threadB = new Thread(new Runnable() { @Override public void run() { synchronized (resource_b) { System.out.println("get resource b"); synchronized (resource_a) { System.out.println("get resource a"); } } } }); threadA.start(); threadB.start(); } }在上面的这个demo中,开启了两个线程threadA, threadB,其中threadA占用了resource_a, 并等待被threadB释放的resource _b。threadB占用了resource _b正在等待被threadA释放的resource _a。因此threadA,threadB出现线程安全的问题,形成死锁。同样可以通过jps,jstack证明这种推论:
如上所述,完全可以看出当前死锁的情况。
那么,通常可以用如下方式避免死锁的情况:
- 避免一个线程同时获得多个锁;
- 避免一个线程在锁内部占有多个资源,尽量保证每个锁只占用一个资源;
- 尝试使用定时锁,使用lock.tryLock(timeOut),当超时等待时当前线程不会阻塞;
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况
所以,如何正确的使用多线程编程技术有很大的学问,比如如何保证线程安全,如何正确理解由于JMM内存模型在原子性,有序性,可见性带来的问题,比如数据脏读,DCL等这些问题(在后续篇幅会讲述)。而在学习多线程编程技术的过程中也会让你收获颇丰。
3. 相关术语概念
3.1 同步VS异步
同步和异步通常用来形容一次方法调用。同步方法调用一开始,调用者必须等待被调用的方法结束后,调用者后面的代码才能执行。而异步调用,指的是,调用者不用管被调用方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者。比如,在超时购物,如果一件物品没了,你得等仓库人员跟你调货,直到仓库人员跟你把货物送过来,你才能继续去收银台付款,这就类似同步调用。而异步调用了,就像网购,你在网上付款下单后,什么事就不用管了,该干嘛就干嘛去了,当货物到达后你收到通知去取就好。
3.2 并发与并行
并发和并行是十分容易混淆的概念。并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在拥有多个CPU的系统中。
3.3 阻塞和非阻塞
阻塞和非阻塞通常用来形容多线程间的相互影响,比如一个线程占有了临界区资源,那么其他线程需要这个资源就必须进行等待该资源的释放,会导致等待的线程挂起,这种情况就是阻塞,而非阻塞就恰好相反,它强调没有一个线程可以阻塞其他线程,所有的线程都会尝试地往前运行。
3.4 临界区
临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界区资源被一个线程占有,那么其他线程必须等待。
线程的状态转换以及基本操作:
新建线程
-
通过继承Thread类,重写run方法;
-
通过实现runable接口;
-
通过实现callable接口这三种方式,下面看具体demo。
public class CreateThreadDemo { public static void main(String[] args) { //1.继承Thread Thread thread = new Thread() { @Override public void run() { System.out.println("继承Thread"); super.run(); } }; thread.start(); //2.实现runable接口 Thread thread1 = new Thread(new Runnable() { @Override public void run() { System.out.println("实现runable接口"); } }); thread1.start(); //3.实现callable接口 ExecutorService service = Executors.newSingleThreadExecutor(); Future<String> future = service.submit(new Callable() { @Override public String call() throws Exception { return "通过实现Callable接口"; } }); try { String result = future.get(); System.out.println(result); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } }
三种新建线程的方式具体看以上注释,需要主要的是:
- 由于java不能多继承可以实现多个接口,因此,在创建线程的时候尽量多考虑采用实现接口的形式;
- 实现callable接口,提交给ExecutorService返回的是异步执行的结果,另外,通常也可以利用FutureTask(Callable callable)将callable进行包装然后FeatureTask提交给ExecutorsService。
另外由于FeatureTask也实现了Runable接口也可以利用上面第二种方式(实现Runable接口)来新建线程;
- 可以通过Executors将Runable转换成Callable,具体方法是:Callable callable(Runnable task, T result), Callable callable(Runnable task)。
线程状态转换
- 线程创建之后调用start()方法开始运行
- 当调用wait(),join(),LockSupport.lock()方法线程会进入到WAITING状态,而同样的wait(long timeout),sleep(long),join(long),LockSupport.parkNanos(),LockSupport.parkUtil()增加了超时等待的功能
- 当超时等待时间到达后,线程会切换到Runable的状态,另外当WAITING和TIMED _WAITING状态时可以通过Object.notify(),Object.notifyAll()方法使线程转换到Runable状态。
- 当线程出现资源竞争时,即等待获取锁的时候,线程会进入到BLOCKED阻塞状态,当线程获取锁时,线程进入到Runable状态。
- 线程运行结束后,线程进入到TERMINATED状态,状态转换可以说是线程的生命周期。
- 当线程进入到synchronized方法或者synchronized代码块时,线程切换到的是BLOCKED状态,而使用java.util.concurrent.locks下lock进行加锁的时候线程切换的是WAITING或者TIMED_WAITING状态,因为lock会调用LockSupport的方法。
线程状态的基本操作
interrupted
中断可以理解为线程的一个标志位,它表示了一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了一个招呼。其他线程可以调用该线程的interrupt()方法对其进行中断操作,同时该线程可以调用 isInterrupted()来感知其他线程对其自身的中断操作,从而做出响应。另外,同样可以调用Thread的静态方法 interrupted()对当前线程进行中断操作,该方法会清除中断标志位。public class InterruptDemo { public static void main(String[] args) throws InterruptedException { //sleepThread睡眠1000ms final Thread sleepThread = new Thread() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } super.run(); } }; //busyThread一直执行死循环 Thread busyThread = new Thread() { @Override public void run() { while (true) ; } }; sleepThread.start(); busyThread.start(); sleepThread.interrupt(); busyThread.interrupt(); while (sleepThread.isInterrupted()) ; System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted()); System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted()); } } //输出结果:sleepThread isInterrupted: false busyThread isInterrupted: true开启了两个线程分别为sleepThread和BusyThread, sleepThread睡眠1s,BusyThread执行死循环。然后分别对着两个线程进行中断操作,可以看出sleepThread抛出InterruptedException后清除标志位,而busyThread就不会清除标志位。
同样可以通过中断的方式实现线程间的简单交互, while (sleepThread.isInterrupted()) 表示在Main中会持续监测sleepThread,一旦sleepThread的中断标志位清零,即sleepThread.isInterrupted()返回为false时才会继续Main线程才会继续往下执行。因此,中断操作可以看做线程间一种简便的交互方式。一般在结束线程时通过中断标志位或者标志位的方式可以有机会去清理资源,相对于武断而直接的结束线程,这种方式要优雅和安全。
join
public final synchronized void join(long millis) public final synchronized void join(long millis, int nanos) public final void join() throws InterruptedExceptionThread类除了提供join()方法外,另外还提供了超时等待的方法,如果线程threadB在等待的时间内还没有结束的话,threadA会在超时之后继续执行。
while (isAlive()) { wait(0); }可以看出来当前等待对象threadA会一直阻塞,直到被等待对象threadB结束后即isAlive()返回false的时候才会结束while循环,当threadB退出时会调用notifyAll()方法通知所有的等待线程。
public class JoinDemo { public static void main(String[] args) { Thread previousThread = Thread.currentThread(); for (int i = 1; i <= 10; i++) { Thread curThread = new JoinThread(previousThread); curThread.start(); previousThread = curThread; } } static class JoinThread extends Thread { private Thread thread; public JoinThread(Thread thread) { this.thread = thread; } @Override public void run() { try { thread.join(); System.out.println(thread.getName() + " terminated."); } catch (InterruptedException e) { e.printStackTrace(); } } } }在上面的例子中一个创建了10个线程,每个线程都会等待前一个线程结束才会继续运行。可以通俗的理解成接力,前一个线程将接力棒传给下一个线程,然后又传给下一个线程......
sleep
public static native void sleep(long millis)方法显然是Thread的静态方法,很显然它是让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器。sleep vs wait
两者主要的区别:
- sleep()方法是Thread的静态方法,而wait是Object实例方法
- wait()方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而sleep()方法没有这个限制可以在任何地方种使用。另外,wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁;
- sleep()方法在休眠时间达到后如果再次获得CPU时间片就会继续执行,而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。
public static native void yield();这是一个静态方法,一旦执行,它会是当前线程让出CPU,但是,需要注意的是,让出的CPU并不是代表当前线程不再运行了,如果在下一次竞争中,又获得了CPU时间片当前线程依然会继续运行。
sleep()和yield()方法,同样都是当前线程会交出处理器资源,而它们不同的是,sleep()交出来的时间片其他线程都可以去竞争,也就是说都有机会获得当前线程让出的时间片。而yield()方法只允许与当前线程具有相同优先级的线程能够获得释放出来的CPU时间片。
守护线程Daemon
守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地守护一些系统服务,比如垃圾回收线程,JIT线程就可以理解守护线程。与之对应的就是用户线程,用户线程就可以认为是系统的工作线程,它会完成整个系统的业务操作。用户线程完全结束后就意味着整个系统的业务任务全部结束了,因此系统就没有对象需要守护的了,守护线程自然而然就会退。public class DaemonDemo { public static void main(String[] args) { Thread daemonThread = new Thread(new Runnable() { @Override public void run() { while (true) { try { System.out.println("i am alive"); Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println("finally block"); } } } }); daemonThread.setDaemon(true); daemonThread.start(); //确保main线程结束前能给daemonThread能够分到时间片 try { Thread.sleep(800); } catch (InterruptedException e) { e.printStackTrace(); } } }上面的例子中daemodThread run方法中是一个while死循环,会一直打印,但是当main线程结束后daemonThread就会退出所以不会出现死循环的情况。main线程先睡眠800ms保证daemonThread能够拥有一次时间片的机会,也就是说可以正常执行一次打印“i am alive”操作和一次finally块中"finally block"操作。紧接着main 线程结束后,daemonThread退出,这个时候只打印了"i am alive"并没有打印finnal块中的。因此,这里需要注意的是守护线程在退出的时候并不会执行finnaly块中的代码,所以将释放资源等操作不要放在finnaly块中执行,这种操作是不安全的