多线程与高并发编程之基础篇

1. 基本概念 1.1. 进程
进程即运行中的程序,比如当你双击QQ.exe这个程序时,操作系统就会启动一个进程。一个程序可以启动多个进程。(比如你可以运行多个QQ.exe程序,相当于启动了多个进程)
1.2. 线程
线程是进程中最小的执行单元。下面我们用一个程序来了解什么是线程:

package basic_concepts; import java.util.concurrent.TimeUnit; public class WhatIsThread { private static class Thread1 extends Thread { @Override public void run() { for (int i = 0; i < 10; i++) { try { TimeUnit.MICROSECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Thread1"); } } }public static void main(String[] args) { // 普通方法调用 new Thread1().run(); // 启动一个线程,并执行它的 run 方法 new Thread1().start(); for (int i = 0; i < 10; i++) { try { TimeUnit.MICROSECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("main"); } } }

在上面的程序中,线程的 run 方法相当于普通方法的调用,它会以程序流的顺序执行,而 start 方法相当于启动了一个线程,这个线程会和 main线程交替执行,因此我们可以在控制台上看到 “Thread1”和“main”交替进行打印,这也说明了不同的线程是进程中不同的执行路径,Thread1 线程和 main 线程代表了上述程序中两条不同的执行路径。
在 Java 中创建线程有三种方式,一种是继承 Thread 类,一种是实现 Runnable 接口,还有一种是使用线程池来创建线程:
package basic_concepts; public class HowToCreateThread { static class MyThread extends Thread { @Override public void run() { System.out.println("Hello MyThread!"); } } static class MyRun implements Runnable { @Override public void run() { System.out.println("Hello MyRun!"); } }public static void main(String[] args) { // 通过继承 Thread 类来创建线程 new MyThread().start(); // 通过实现 Runnable 接口来创建线程 new Thread(new MyRun()).start(); // 使用 Runnable 的 lambda 表达式来创建线程 new Thread(() -> { System.out.println("Hello Lambda!"); }).start(); // 使用线程池来创建线程 ExecutorService executorService = Executors.newCachedThreadPool(); executorService.execute(new MyRun()); executorService.shutdown(); } }

Thread 对象的常用方法有 sleep 、yield 和 join :
package basic_concepts; public class SleepYieldJoin { public static void main(String[] args) { //testSleep(); //testYield(); testJoin(); }static void testSleep() { new Thread(() -> { for (int i = 0; i < 100; i++) { System.out.println("A" + i); try { // 使线程休眠500毫秒 Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); }static void testYield() { new Thread(() -> { for (int i = 0; i < 100; i++) { System.out.println("A" + i); if (i % 10 == 0) { Thread.yield(); } } }).start(); new Thread(() -> { for (int i = 0; i < 100; i++) { System.out.println("B" + i); if (i % 10 == 0) { Thread.yield(); } } }).start(); }static void testJoin() { Thread t1 = new Thread(() -> { for (int i = 0; i < 100; i++) { System.out.println("A" + i); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } }); Thread t2 = new Thread(() -> { try { t1.join(); } catch (InterruptedException e) { e.printStackTrace(); }for (int i = 0; i < 100; i++) { System.out.println("B" + i); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } }); t1.start(); t2.start(); } }

其中 sleep 会使当前线程休眠,进入阻塞状态,如果线程在睡眠状态被中断,将会抛出 InterruptedException 中断异常,yield 会使当前线程暂停并允许其他线程执行,当一个线程执行了yield()方法之后,就会进入就绪状态,CPU 此时就会从就绪状态线程队列中选择与该线程优先级相同或者更高优先级的线程去执行(因此执行了yield()方法之后的线程仍有可能继续执行,如果此时没有和它优先级相同或者比它优先级更高的线程时),join 方法让一个线程等待另外一个线程完成才继续执行,比如在线程 A 执行体中调用 B 线程的join()方法,则 A 线程将会被阻塞,直到 B 线程执行完为止,A 才能得以继续执行。
线程状态
package basic_concepts; public class ThreadState { static class MyThread extends Thread { @Override public void run() { System.out.println(this.getState()); for (int i = 0; i < 10; i++) { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println(i); } } } public static void main(String[] args) { Thread t = new MyThread(); // 通过 getState 方法获取线程状态 System.out.println(t.getState()); t.start(); try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println(t.getState()); } }


多线程与高并发编程之基础篇
文章图片
线程状态迁移图
线程的上下文切换
用户态 - 内核态
  • int 0x80 - 128
  • sysenter cpu支持
  • 保存用户态现场
    • 寄存器压栈
    • 进行syscall
    • 内核态返回 eax
    • 恢复用户态现场
    • 用户程序继续执行
1.3. 纤程/协程
CPU - Ring0 - 1 2 - Ring3
  • Ring0 -> 内核态 Ring3 -> 用户态
  • 内核调用/系统调用 线程的操作
  • 用户态启动线程
    • 进入到内核态 - 保存用户态的线程
    • 用户态不经过内核态的线程 - 纤程 golang的go程
2. synchronized关键字 synchronized关键字用于对某个对象加锁,加锁的对象对资源是独占的。它用于解决多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。下面的代码展示了 synchronized 常见的应用场景:
package synchronize; /** * synchronized关键字 * 对某个对象加锁 */ public class Synchronize { private static int count = 10; private Object object = new Object(); public void lockObject() { /** * 不能用String常量 Integer Long */ synchronized (object) { count --; System.out.println(Thread.currentThread().getName() + " count = " + count); } }public void lockThis() { synchronized (this) { count --; System.out.println(Thread.currentThread().getName() + " count = " + count); } }/** * 等同于在方法的代码执行时要synchronized (this) */ public synchronized void synchronizedMethod() { count --; System.out.println(Thread.currentThread().getName() + " count = " + count); }/** * 等同于synchronized(Synchronize.class) */ public synchronized static void staticSynchronizedMethod() { count --; System.out.println(Thread.currentThread().getName() + " count = " + count); }public static void lockClass() { synchronized (Synchronize.class) { count --; System.out.println(Thread.currentThread().getName() + " count = " + count); } }static class MyRun implements Runnable {@Override public void run() { Synchronize synchronize = new Synchronize(); synchronize.lockObject(); } }public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(new MyRun()).start(); } } }

synchronized 关键字既能保证可见性,又能保证可见性,因此对于下面这个程序,它无论运行多少次,结果都会是 0 :
package synchronize; public class ThreadSafe implements Runnable { private int count = 100; @Override public synchronized void run() { count --; System.out.println(Thread.currentThread().getName() + " count = " + count); }public static void main(String[] args) { ThreadSafe threadSafe = new ThreadSafe(); for (int i = 0; i < 100; i++) { new Thread(threadSafe, "Thread" + i).start(); } } }

同步方法和非同步方法是可以同时调用的,在下面的程序中,非同步方法会穿插在同步方法中执行:
package synchronize; /** * 同时调用同步和非同步方法 */ public class SynchronizeCallNonSynchronize { public synchronized void synchronizedMethod() { System.out.println(Thread.currentThread().getName() + " synchronized method start..."); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " synchronized method end"); }public void nonSynchronizedMethod() { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " non-synchronized method call"); }public static void main(String[] args) { SynchronizeCallNonSynchronize synchronizeCallNonSynchronize = new SynchronizeCallNonSynchronize(); /*new Thread(() -> synchronizeCallNonSynchronize.synchronizedMethod(), "t1").start(); new Thread(() -> synchronizeCallNonSynchronize.nonSynchronizedMethod(), "t2").start(); */new Thread(synchronizeCallNonSynchronize::synchronizedMethod, "t1").start(); new Thread(synchronizeCallNonSynchronize::nonSynchronizedMethod, "t2").start(); } }

让我们来看一下 synchronized 另一个应用场景,有这样一个面试题:需要你模拟银行账户的存款取款操作,下面的程序模拟了这一过程,需要注意的是,对 getBalance 方法也需要使用 synchronized 关键字来修饰,否则会产生脏读问题:
package synchronize; import java.util.concurrent.TimeUnit; /** * 模拟银行账户 */ public class ThreadSafeAccount { private String name; private double balance; public synchronized void set(String name, double balance) { this.name = name; try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); }this.balance = balance; }/** * 如果对业务写方法加锁, * 而对业务读方法不加锁, * 会产生脏读问题(dirtyRead) * @param name * @return */ public synchronized double getBalance(String name) { return this.balance; }public static void main(String[] args) { ThreadSafeAccount account = new ThreadSafeAccount(); new Thread(() -> account.set("zhangsan", 100.0)).start(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println(account.getBalance("zhangsan")); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }System.out.println(account.getBalance("zhangsan")); } }

synchronized 是可重入的锁,它允许同一线程重复获得锁。下面的程序验证了 synchronized 是可重入的,如果 synchronized 不可重入,那么下面的程序将会产生死锁,但是事实证明,下面的程序能正常执行完,因此 synchronized 是可重入的:
package synchronize; import java.util.concurrent.TimeUnit; /** * 一个同步方法可以调用另一个同步方法, * 一个线程已经拥有某个对象的锁,再次申请的时候仍然会得到该对象的锁, * 也就是说synchronized获得的锁是可重入的 */ public class ReentrantSynchronized { synchronized void m() { System.out.println("m start"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m end"); }synchronized void m1() { System.out.println("m1 start"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } m2(); System.out.println("m1 end"); }synchronized void m2() { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m2"); }static class Child extends ReentrantSynchronized { @Override synchronized void m() { System.out.println("child m start"); super.m(); System.out.println("child m end"); } }public static void main(String[] args) { new ReentrantSynchronized().m1(); new Child().m(); } }

要特别注意的是,在同步业务逻辑中,要非常小心的处理异常,因为一旦产生异常,默认情况下锁会被释放。在下面的程序中,当 count = 5 时,程序产生的异常会导致线程 t1 的锁被释放了,从而让 t2 线程得到锁执行:
package synchronize; import java.util.concurrent.TimeUnit; /** * 程序在执行过程中,如果出现异常,默认情况锁会被释放 * 所以,在并发处理的过程中,有异常要多加小心,不然可能会发生不一致的情况。 * 比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适, * 在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。 * 因此要非常小心的处理同步业务逻辑中的异常。 */ public class SynchronizeCatchException { int count = 0; synchronized void m() { System.out.println(Thread.currentThread().getName() + " start"); while (true) { count ++; System.out.println(Thread.currentThread().getName() + " count = " + count); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } if (count == 5) { // 此处抛出异常,锁将被释放,要想不释放锁,可以在这里进行catch,然后让循环继续 int i = 1 / 0; System.out.println(i); } } }public static void main(String[] args) { SynchronizeCatchException synchronizeCatchException = new SynchronizeCatchException(); new Thread(synchronizeCatchException::m, "t1").start(); try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } new Thread(synchronizeCatchException::m, "t2").start(); } }

HotSpot虚拟机在对象头(64位)上拿出两位(mark word,这两位记录了锁的类型)来记录对象是否被锁定。
JDK早期的时候,synchronized的实现是重量级的(OS级,找操作系统申请锁),这样造成早期的时候synchronized的效率非常低。后来jdk针对synchronized做了一些改进,引入了锁升级的概念。
锁升级:
  • 只有一个线程访问的时候,先在对象头上的markword中记录这个线程的ID(偏向锁)
  • 如果有线程争用,升级为自旋锁
  • 如果线程自旋多次(默认10次)以后还是无法获取到锁,则会升级为重量级锁(OS)
  • 锁只能升级不能降级
【多线程与高并发编程之基础篇】执行时间短(加锁代码),线程数少,用自旋。
执行时间长,线程数多,用系统锁。

    推荐阅读