Java的多线程和线程同步

多线程主要设计两点:多线程如何正确使用,如何保证线程安全。

进程:操作系统上的一块独立的运行区域,有自己的数据管理,数据不共享。每隔进程内部是自己完整的程序逻辑,不能的程序之间本来就不应该共享资源;而同一个进程的多个不同线程,需要都能操作到这个进程的资源,程序才能正常运行。
线程: 运行在进程中,线程之间可以共享资源,所以线程安全的问题是重中之重。
操作系统线程和CPU线程是两回事,一般程序的线程是操作系统的线程。
Android APP,主线程不会结束,反复的刷新界面,AndroidUI线程为什么死循环不会卡界面的问题。UI线程是一个大循环,每一圈都是一次界面刷新操作,而不是对某一次界面刷新过程进行内部的死循环,所以不会卡死界面。

线程创建的几种方式

  1. 重写run方法 (不推荐)

    1. Thread thread = new Thread() {
    2. @Override
    3. public void run() {//重写run方法
    4. System.out.println("Thread start!");
    5. }
    6. };
    7. thread.start();
  2. runnable (不推荐)

    1. Runnable runnable = new Runnable() {
    2. @Override
    3. public void run() {
    4. System.out.println("Thread with Runnable started!");
    5. }
    6. };
    7. //赋值给target,在run中执行,runnable可以进行重用
    8. Thread thread = new Thread(runnable);
    9. thread.start();
  3. ThreadFactory 工厂方法创建线程,统一线程的初始化

    1. ThreadFactory factory = new ThreadFactory() {
    2. //AtomicInteger
    3. AtomicInteger count = new AtomicInteger(0);
    4. @Override
    5. public Thread newThread(Runnable r) {
    6. //统一线程的初始化操作
    7. return new Thread(r, "Thread-" + count.incrementAndGet());
    8. }
    9. };
    10. //runnable的复用
    11. Runnable runnable = new Runnable() {
    12. @Override
    13. public void run() {
    14. System.out.println(Thread.currentThread().getName() + "started!");
    15. }
    16. };
    17. //通过工厂模式 创建线程,统一线程的初始化操作
    18. Thread thread = factory.newThread(runnable);
    19. thread.start();
    20. Thread thread1 = factory.newThread(runnable);
    21. thread1.start();
  4. Executes 线程池,开发中常用的方式

    一个线程池的线程数定义为动态值:CPU核心数的1倍,CPU核心越多线程池越大,这样可以在高性能的手机上有更大的线程池,相当于按手机的能力来分配线程池的大小。

  1. Runnable runnable = new Runnable() {
  2. @Override
  3. public void run() {
  4. System.out.println("Thread with Runnable started!");
  5. }
  6. };
  7. //newCachedThreadPool 线程无限大,闲置60S回收
  8. Executor executor = Executors.newCachedThreadPool();
  9. executor.execute(runnable);
  10. executor.execute(runnable);
  11. executor.execute(runnable);
  1. 自带的几种线程池的创建方式

    创建线程池的参数说明:ThreadPoolExecutor(5,20,60,S) 默认线程数5个,最大线程数是20个,当线程执行完毕,60S后没有被使用则进行回收。 shutdown: 不会终止当前排队的任务,但是会阻止后面的任务进来,当前的任务队列执行完毕之前,后面都不会有任务排队进来。 shutdownNew: 结束所有的线程,调用线程的interrupt方法。

  1. Executors.newCachedThreadPool(); //线程无限大,闲置60s回收
  2. Executors.newSingleThreadExecutor();//只有1个线程,线程运行完毕,立即回收
  3. Executors.newFixedThreadPool(10);//固定线程数,使用完立即回收,用于集中处理多个瞬时爆发的任务
  4. //例如如下代码:处理20个bitmap
  5. // List<Bitmap> bitma = ...
  6. // ExecutorService executor1 = Executors.newFixedThreadPool(20);
  7. // for (Bitmap bitmap: bimaps) {
  8. // executor1.execute(processImagesRunnable);
  9. // }
  10. // //只允许现在的任务队列执行,不允许在添加排队任务
  11. // executor1.shutdown();
  12. // processImagesRunnable // processImage
  13. Executors.newScheduledThreadPool(10);//定时的线程池

自定义线程池:

  1. //自定义线程池
  2. ExecutorService myExecutor = new ThreadPoolExecutor(5, 100,
  3. 60L, TimeUnit.SECONDS,
  4. new SynchronousQueue<Runnable>());
  1. callable 可以有返回值

    Future.get() 会阻塞主线程,等待后台任务执行完毕,对于submit不会阻塞主线程。

  1. Callable<String> callable = new Callable<String>() {
  2. @Override
  3. public String call() throws Exception {
  4. Thread.sleep(1500);
  5. return "Done";
  6. }
  7. };
  8. ExecutorService executor = Executors.newCachedThreadPool();
  9. Future<String> future = executor.submit(callable);
  10. try {
  11. System.out.println(future.get());
  12. } catch (ExecutionException e) {
  13. e.printStackTrace();
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }

诶,我们使用线程的目的就是不阻塞主线程,但是这里阻塞了主线程,Java API层只能做到这一步,我们可以进行检测future.isDone子线程是否执行完毕了

  1. while (true) {
  2. System.out.println("其他指令,不会卡主线程");
  3. if (future.isDone()) {
  4. try {
  5. System.out.println("start");
  6. System.out.println(future.get());
  7. System.out.println("end");
  8. } catch (ExecutionException e) {
  9. e.printStackTrace();
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. break;
  14. }
  15. }

线程安全

线程安全的本质:某些资源被多个线程同时访问,导致资源在一个线程对它写到一半的途中被其他线程写或读,或在读到一半的途中被其他线程写,导致出现数据错误。

volatile

volatile 关键字可以保证数据的可见性,当一个线程进行写操作时,写完会立即同步数据;当一个线程读取数据时,会先进行同步数据再读取

如下代码:通过isRunning来控制子线程的结束,在主线程修改isRunning,然后来看子线程中的while循环是否结束?

  1. public class Synchronized1Demo implements TestDemo {
  2. private boolean isRunning = true;
  3. private void stop() {
  4. isRunning = false;
  5. }
  6. @Override
  7. public void runTest() {
  8. new Thread() {
  9. @Override
  10. public void run() {
  11. while (isRunning) {//=false 子线程就会结束
  12. }
  13. }
  14. }.start();
  15. try {
  16. Thread.sleep(1000);
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. }
  20. stop();
  21. }
  22. }

当执行了stop()方法,修改了isRunning = false,但是子线程并没有结束,还在执行,Why??
image.png
这就是典型的线程安全的问题,首先来看Java中的线程模型:主内存中是存储了资源数据,而每个线程有自己的工作内存,当要用到主内存中的资源,需要将资源拷贝到自己的工作内存中,当资源发生改变就需要同步到主内存中去。
多线程 | Android 必须掌握的多线程知识 - 图2

上述的代码中isRunning,主线程中的工作内存对isRunning进行了修改,但是并没有同步到主内存中,导致子线程工作内存isRunning一直是true. 通过volatile关键字可以将isRunning的变化及时的同步到主内存中,同时子线程读取数据也会从主内存中读取。voltaile保证了变量的可见性,当一个变量在一个线程发生变化,会同步到主内存,另一个线程读取时会立即进行同步,首先从主内存中读取数据。

  1. public class Synchronized1Demo implements TestDemo {
  2. private volatile boolean isRunning = true;
  3. private void stop() {
  4. isRunning = false;
  5. }
  6. @Override
  7. public void runTest() {
  8. new Thread() {
  9. @Override
  10. public void run() {
  11. while (isRunning) {//=false 子线程就会结束
  12. }
  13. }
  14. }.start();
  15. try {
  16. Thread.sleep(1000);
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. }
  20. stop();
  21. }
  22. }

synchronized

synchronized 保证了原子性,一组操作是不可分割的原子,不能被打断,同时也保证了可见性,在加锁前会清空工作内存变量值,重新从主内存中读取,在释放锁之前会将变量的值刷新回主内存中。

如下代码,来了解synchronized的本质:根据上面所学习的volatile关键字,保证变量值的在多线程下的同步性,那么下面代码看似没有问题,x的值最终应该为:2_000_000

  1. private volatile int x = 0;
  2. private void count() {
  3. x++;
  4. }
  5. @Override
  6. public void runTest() {
  7. new Thread() {
  8. @Override
  9. public void run() {
  10. for (int i = 0; i < 1_000_000; i++) {
  11. count();
  12. }
  13. System.out.println("final x from 1:" + x);
  14. }
  15. }.start();
  16. new Thread() {
  17. @Override
  18. public void run() {
  19. for (int i = 0; i < 1_000_000; i++) {
  20. count();
  21. }
  22. System.out.println("final x from 2:" + x);
  23. }
  24. }.start();
  25. }

但实际结果:这是为什么呢?volatile 不是保证了x的值具有同步性吗?
image.png
其实x++ 是分两步进行编译:int temp = x+ 1 x = temp 不是一个原子操作 而volatile只是保证了x具有同步性,但是temp不具备同步性了,如下图模拟执行的流程:

线程之间是共享主内存中的数据的,假如线程1,一直执行执行到temp = 10了这时候发生线程切换,注意x = temp没有执行,虽然x具有了同步性,但是x=temp没有执行,那么x并且没有同步到主内存中,x 的还是9,那么这时候线程2开始执行 temp = 9+1 = 10 x= 10,一直执行到x=15,这是切换到线程1,线程1继续上一次的操作,然而temp是在线程1工作内存中存储了值为10,那么下一步为x赋值 x=10,由于volatile的同步性,将x同步到了主内存中,这是在切换线程2拿到的x就是10了,这也是为什么最终打印的结果小于预期的值。

多线程 | Android 必须掌握的多线程知识 - 图4

这时候就需要有synchronized,而synchronized就是为了解决volatile不能处理的问题,volatile只能保证变量的值具有同步性,不能保证一组操作具有同步性,synchronized就实现了一组操作具有原子性,不可分割的。synchronized 会保证在每次执行时都会从主内存同步数据,执行完毕后将数据刷新回主内存中。

  1. //同步性问题 多个线程各自的内存拷贝
  2. //volatile 在这里不会起作用,X++是两步操作 int temp = x + 1 x = temp 导致不是一个原子操作(不可拆的操作)
  3. private int x = 0;
  4. //synchronized 保证count方法具有原子性,一个线程必须执行完count方法,下一个线程才能执行count
  5. //内部的变量 都会具有同步性 synchronized 保证了 同步性和原子性
  6. private synchronized void count() {
  7. x++;
  8. }
  9. @Override
  10. public void runTest() {
  11. new Thread() {
  12. @Override
  13. public void run() {
  14. for (int i = 0; i < 1_000_000; i++) {
  15. count();
  16. }
  17. System.out.println("final x from 1:" + x);
  18. }
  19. }.start();
  20. new Thread() {
  21. @Override
  22. public void run() {
  23. for (int i = 0; i < 1_000_000; i++) {
  24. count();
  25. }
  26. System.out.println("final x from 2:" + x);
  27. }
  28. }.start();
  29. }

这时候在执行代码就能拿到想要的结果了:
image.png
如下图来看一下synchronized的执行流程:

  1. 首先被synchronized包裹的一组操作只允许一个线程被访问,会上锁,其他线程要访问需要进行等待上一个线程执行完毕才可以访问
  2. 线程1 获取到synchronized锁,会先从主内存中同步数据,然后执行操作,当线程执行结束,会将数据刷新到主内存中,最后释放锁
  3. 然后其他的线程会公平竞争锁,拿到锁的线程执行,如此反复

多线程 | Android 必须掌握的多线程知识 - 图6

synchronized 的使用是不是所有的操作都在方法上加上synchronized关键字呢?这要做会有什么问题吗?

如下代码:看起来没有任何问题,一个线程访问count2(),一个线程访问setName2(),预期的执行结果就是:线程1输出,线程2输出,但是实际结果线程1执行完count2才会执行setName2方法,而不是线程1和线程2各执行自己的方法???这是为什么呢?完全是两个线程也没有共享一样的资源

  1. private int x = 0;
  2. private int y = 0;
  3. private String name;
  4. private synchronized void count2(int newValue) {
  5. x = newValue;
  6. y = newValue;
  7. System.out.println("newValue = " + newValue);
  8. try {
  9. Thread.sleep(5000);
  10. } catch (InterruptedException e) {
  11. e.printStackTrace();
  12. }
  13. }
  14. private synchronized void setName2(String name){
  15. System.out.println("name = " + name);
  16. }
  17. @Override
  18. public void runTest() {
  19. new Thread() {
  20. @Override
  21. public void run() {
  22. count2(10);
  23. }
  24. }.start();
  25. new Thread() {
  26. @Override
  27. public void run() {
  28. setName2("jakeprim");
  29. }
  30. }.start();
  31. }

这其实是synchronized的一个特性:synchronized 具有互斥访问的特性,会为方法提供一个监视器monitor,上述两个方法count2和setName2使用的是同一个监视器,所以当访问count2的时候,就不能访问setName2了,这个monitor的作用在于监视访问的线程,当一个线程访问了,这时候monitor就是设置一个标记,其他线程就不允许访问了。
多线程 | Android 必须掌握的多线程知识 - 图7

这种设定的意义:在于开发者可以指定设置的monitor,哪些操作共享一个monitor,哪些操作是其他的monitor进行监视, 这些操作是互不影响的

如下代码,synchronized(this) == synchronized void count2() 和作用在方法上是等价的

  1. synchronized (this) {//this 指定的monitor 他和在方法上的设置是一样的,默认是共享了一个monitor
  2. x = newvalue;
  3. y = newvalue;
  4. }

为setName2指定一个monitor,不使用默认的monitor

  1. private static final Object object2 = new Object();
  2. private final Object monitor1 = new Object();
  3. private void setName(String newName) {
  4. synchronized (monitor1) {//指定monitor
  5. name = newName;
  6. }

例如如下代码,使用一个monitor的情况:当一个线程访问count方法,那么肯定不希望其他的线程访问minus去改变x和y的值。

  1. /**
  2. * 当一个线程访问count,那么是不希望另一个线程访问minus的,这是为什么monitor要管理多个方法
  3. *
  4. * @param delta
  5. */
  6. private synchronized void minus(int delta) {
  7. x -= delta;
  8. y -= delta;
  9. }
  10. private void count(int newvalue) {
  11. synchronized (this) {//this 指定的monitor 他和在方法上的设置是一样的,默认是共享了一个monitor
  12. x = newvalue;
  13. y = newvalue;
  14. }
  15. }

这时候的monitor的分布情况如下图:精准的控制monitor
image.png
synchronized 经常会被问到死锁的问题,其实只有多重锁才会造成死锁的问题,如下代码死锁的情况:
monitor1 和 monitor2 两个监视器,假如线程1执行了count方法,持有了monitor2的锁,这时候线程2访问了setName方法,持有了monitor1的锁,线程1想要执行完毕,就需要拿到monitor1的锁执行,线程2要想执行完毕就要拿到monitor2的锁执行,两个线程都无法释放锁造成死锁的问题

  1. private void count(int newvalue) {
  2. synchronized (monitor2) {//this 指定的monitor 他和在方法上的设置是一样的,默认是共享了一个monitor
  3. x = newvalue;
  4. y = newvalue;
  5. //死锁的问题 当setName 被锁定,这里就不能执行 只有多重锁才会出现死锁
  6. synchronized (monitor1){
  7. name = "xxx";
  8. }
  9. }
  10. }
  11. private void setName(String newName) {
  12. synchronized (monitor1) {//指定monitor
  13. name = newName;
  14. //死锁的问题
  15. synchronized (monitor2){
  16. x = 1;
  17. y = 2;
  18. }
  19. }
  20. }

多线程 | Android 必须掌握的多线程知识 - 图9
synchronized 整体的执行流程如下图所示:
image.png
常用的单例模式:

  1. public class SingleMan {
  2. private static volatile SingleMan sInstance;
  3. private SingleMan() {
  4. }
  5. static SingleMan getInstance() {
  6. if (sInstance == null) {
  7. synchronized (SingleMan.class) { //等价于 static synchronized
  8. if (sInstance == null) {
  9. sInstance = new SingleMan();//volatile 保证构造方法 初始化完毕
  10. }
  11. }
  12. }
  13. return sInstance;
  14. }
  15. }

Lock
  1. public class ReadWriteLockDemo implements TestDemo {
  2. private int x = 0;
  3. ReentrantLock lock = new ReentrantLock();
  4. //将锁分成 读锁和写锁 更精细的控制 进行写操作的时候,不能进行读。在没有写操作的时候,就可以读
  5. //这样就不会因为写操作的冲突,或者写了一半的时候进行了读操作,或者写了一半的时候进行写操作等等这些问题
  6. //线程安全:主要是共享资源的安全问题
  7. ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
  8. //读锁
  9. Lock readLock = reentrantReadWriteLock.readLock();
  10. //写锁
  11. Lock writeLock = reentrantReadWriteLock.writeLock();
  12. private void count() {
  13. // lock.lock();//上锁
  14. writeLock.lock();
  15. try {
  16. x++;
  17. //... 如果中间抛异常 就无法解锁了
  18. } finally {
  19. // lock.unlock();//解锁
  20. writeLock.unlock();
  21. }
  22. }
  23. @Override
  24. public void runTest() {
  25. readLock.lock();
  26. try {
  27. System.out.println(x);
  28. } finally {
  29. readLock.unlock();
  30. }
  31. }
  32. }

正确停止线程

Thread.stop() 是非常危险的,因为它会直接杀死线程,有几率让线程在某些工作运行到一半的时候被结束,从而系统被强制停留在某种中间状态,进而导致问题。 Thread.interrupt() 来结束线程是安全的,因为它并不会直接杀死线程,而是告诉线程【外界希望你停止】,具体的结束工作由线程自己来完成,所以更加安全。

  1. public class ThreadInteractionDemo implements TestDemo {
  2. @Override
  3. public void runTest() {
  4. Thread thread = new Thread() {
  5. @Override
  6. public void run() {
  7. try {
  8. Thread.sleep(10000);
  9. //等待状态的线程终止
  10. } catch (InterruptedException e) {
  11. //不会改中断的标记的 还是false
  12. //擦屁股
  13. return;
  14. //在等待的过程中 中断了线程 会直接抛出一个异常 直接打断就可以 在等待过程中不会改资源的
  15. }
  16. // for (int i = 0; i < 1_000_000; i++) {
  17. // //Thread.interrupted()// 会把标记重置为false 会改标记的状态的
  18. // isInterrupted 不会改状态
  19. // if (isInterrupted()) {//如果中断状态被标记为true了 则结束线程 在哪里中断 需要根据业务来判断
  20. // return;
  21. // }
  22. // System.out.println("number:" + i);
  23. // }
  24. }
  25. };
  26. thread.start();
  27. try {
  28. Thread.sleep(1000);
  29. } catch (InterruptedException e) {
  30. e.printStackTrace();
  31. }
  32. //thread.stop();//停止线程 但是stop被弃用了,会导致线程不可预期的错误,stop强制中断线程
  33. // 假如线程正在修改两个变量,这是一个变量修改了 另一个变量被中断了a=100 b=50 导致中间状态 程序的状态是不可控的
  34. // 使用外力终止线程是非常危险的,本身是一个不靠谱的机制
  35. //interrupt 是打断,需要自己去程序中断线程,而Thread不会打断线程,只是把线程标记为中断状态,需要自己去中断
  36. //不是强制的,这样就能避免了stop导致的不可预期的错误
  37. thread.interrupt();
  38. }
  39. }

线程间通信

Object.wait() 经常需要外面包着while循环,因为线程在等待过程中被叫醒的原因是不确定的,所以醒来后需要重新判断条件是否达成。

  1. public class WaitDemo implements TestDemo {
  2. private String sharedString;
  3. private Object monitor = new Object();
  4. private void initString() {
  5. synchronized (monitor) {
  6. sharedString = "jakeprim";
  7. monitor.notify();//通知 等待区的出来一个,重新进行公平竞争锁
  8. // notifyAll();//通知 等待区的全部拿出来
  9. }
  10. }
  11. private void printString() {
  12. // if (sharedString != null) {
  13. // System.out.println("String:" + sharedString);
  14. // }
  15. //锁住了 initString 拿不到锁 永远在等待了
  16. // while (sharedString == null) {
  17. // }
  18. synchronized (monitor) {
  19. while (sharedString == null) {
  20. try {
  21. //wait 线程进入等待区 排队拿锁,会释放锁
  22. monitor.wait();
  23. } catch (InterruptedException e) {
  24. }
  25. }
  26. System.out.println("String:" + sharedString);
  27. }
  28. }
  29. @Override
  30. public void runTest() {
  31. final Thread thread2 = new Thread() {
  32. @Override
  33. public void run() {
  34. try {
  35. Thread.sleep(2000);
  36. } catch (InterruptedException e) {
  37. e.printStackTrace();
  38. }
  39. //初始化之后 通知另一个线程
  40. initString();
  41. }
  42. };
  43. thread2.start();
  44. final Thread thread1 = new Thread() {
  45. @Override
  46. public void run() {
  47. try {
  48. Thread.sleep(1000);
  49. } catch (InterruptedException e) {
  50. e.printStackTrace();
  51. }
  52. //等待2号线程执行完毕
  53. // try {
  54. // thread2.join();
  55. // } catch (InterruptedException e) {
  56. // e.printStackTrace();
  57. // }
  58. // yield(); 暂时让出 和自己同优先级的线程
  59. printString();
  60. }
  61. };
  62. thread1.start();
  63. //join 运行在主线程前面,后面的不在执行了
  64. try {
  65. //让线程变成串行的关系
  66. thread1.join();//等待状态 interrupt 中断
  67. } catch (InterruptedException e) {
  68. e.printStackTrace();
  69. }
  70. System.out.println("haha");
  71. }
  72. }

Android 的多线程机制

Handler 只能往HandlerThread插任务,而HandlerThread其实是无线循环,因此可以在每一环都做一次任务的检查与执行 AsyncTask 内部持有线程,而运行中的线程属于GC Root 可能会导致内存泄漏

  1. public class CustomThread extends Thread {
  2. Looper looper = new Looper();
  3. @Override
  4. public void run() {//注意不要在run 设置synchronized 否则锁处于一直锁定的状态
  5. //永不结束的线程
  6. looper.loop();
  7. }
  8. class Looper {
  9. //要执行的任务
  10. private Runnable task;
  11. //线程安全的AtomicBoolean 保证同步性
  12. private final AtomicBoolean quit = new AtomicBoolean(false);
  13. synchronized void setTask(Runnable task) {
  14. this.task = task;
  15. }
  16. void quit() {
  17. quit.set(true);
  18. }
  19. void loop() {
  20. while (!quit.get()) {
  21. synchronized (this) {
  22. if (task != null) {
  23. //执行任务
  24. task.run();
  25. //任务执行完毕清空
  26. task = null;
  27. }
  28. }
  29. }
  30. }
  31. }
  32. }

Service 后台线程的持续的空间 和 IntentService 后台任务