Java支持多个线程同时访问一个对象或者对象的成员变量,由于每个线程可以拥有这个变量的拷贝(虽然对象以及成员变量分配的内存是在共享内存中的,但是每个执行的线程还是可以拥有一份拷贝,这样做的目的是加速程序的执行,这是现代多核处理器的一个显著特性),所以程序在执行过程中,一个线程看到的变量并不一定是最新的。

一、了解volatile 和 synchronized关键字

1.1. volatile关键字

关键字volatile可以用来修饰字段(成员变量),就是告知程序任何对该变量的访问均需要从共享内存(主内存)中获取,忽视(设置无效)工作内存中的变量,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
**

1.2. synchronized 关键字

synchronized 是提供同步锁 关键字,可以修饰方法 或 以同步块的方式使用,synchronized 实现的锁是排他锁,非公平锁。主要采用monitorenter和monitorexit指令 来保持线程同步, synchronized 可以保证数据的一致性:即原子性、可见性和有序性

关键字volatile 和 synchronized简单了解一下,后面着重深入了解。

二、等待通知机制

2.1 Object 类中等待通知的方法

  • notify() : 用于唤醒正在等待对象监视器的单一线程,使其从wait()方法中返回,返回的前提是该线程必须获取了对象锁
  • notifyAll(): 唤醒等待对象监视器的所有线程
  • wait() : 调用该方法线程进入等待状态WAITING,只用等其他线程唤醒或者中断操作才能返回,注意在调用wait() 方法后,线程会释放锁
  • wait(long timeout) : 超时等待返回,超时参数为毫秒,所以说在等待n毫秒后,没有被唤醒,则超时返回
  • wait(long timeout, int nanos) : 用于控制超时参数的单位,可以达到纳秒级

    2.2 实现交替打印1,2

  1. //交替打印1 2 1 2
  2. public class WaitNotifyThread {
  3. //定义对象锁
  4. private static Object lock = new Object();
  5. private static boolean flag = false;
  6. public static void main(String[] args) {
  7. WaitThread waitThread = new WaitThread();
  8. NotifyThread notifyThread = new NotifyThread();
  9. waitThread.setName("WaitThread");
  10. notifyThread.setName("notifyThread");
  11. waitThread.start();
  12. notifyThread.start();
  13. }
  14. static class WaitThread extends Thread {
  15. @Override
  16. public void run() {
  17. //synchronized代码块,锁住lock
  18. synchronized (lock) {
  19. while (true) {
  20. //如果flag == true ,则挂起线程等待,释放锁
  21. while (flag) {
  22. try {
  23. lock.wait();
  24. } catch (InterruptedException e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. System.out.println(Thread.currentThread().getName() + " 1");
  29. try {
  30. TimeUnit.SECONDS.sleep(1);
  31. } catch (InterruptedException e) {
  32. e.printStackTrace();
  33. }
  34. //设置flag = true ,挂起线程
  35. flag = true;
  36. //通知其他线程执行
  37. lock.notifyAll();
  38. }
  39. }
  40. }
  41. }
  42. static class NotifyThread extends Thread {
  43. @Override
  44. public void run() {
  45. synchronized (lock) {
  46. while (true) {
  47. //如果flag == false ,则挂起线程等待,释放锁
  48. while (!flag) {
  49. try {
  50. lock.wait();
  51. } catch (InterruptedException e) {
  52. e.printStackTrace();
  53. }
  54. }
  55. System.out.println(Thread.currentThread().getName() + " 2");
  56. try {
  57. TimeUnit.SECONDS.sleep(1);
  58. } catch (InterruptedException e) {
  59. e.printStackTrace();
  60. }
  61. //设置flag = false ,挂起线程
  62. flag = false;
  63. //通知其他线程执行
  64. lock.notifyAll();
  65. }
  66. }
  67. }
  68. }
  69. }

注意:

  • 1)使用wait()、notify()和notifyAll()时需要先对调用对象加锁。
  • 2)调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列,释放对象锁。
  • 3)notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifAll()的线程释放锁之后,等待线程才有机会从wait()返回。
  • 4)notify()方法将等待队列中的任意一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED

2.3 等待通知图示

image.png

三、管道输入/输出流

3.1 定义

管道流用于输送一个线程的输出流到另一个线程的输入流,即可用于线程间的通信

3.2 管道流相关类

3.3 例子

  1. public class SendAndReceivePips {
  2. PipedReader reader = new PipedReader();
  3. PipedWriter writer = new PipedWriter();
  4. public SendAndReceivePips() {
  5. try {
  6. //管道连接
  7. reader.connect(writer);
  8. } catch (IOException e) {
  9. e.printStackTrace();
  10. }
  11. }
  12. public void sender() {
  13. new Thread(new Runnable() {
  14. @Override
  15. public void run() {
  16. try {
  17. int a = 10;
  18. while (a > 0) {
  19. a = a - 1;
  20. Thread.sleep(1000);
  21. int num = (int) (Math.random() * 255);
  22. System.out.println(Thread.currentThread().getName() + " 生产者生产了一个数字,该数字为: " + num);
  23. writer.write(num);
  24. writer.flush();
  25. }
  26. } catch (Exception e) {
  27. e.printStackTrace();
  28. }finally {
  29. try {
  30. writer.close();
  31. } catch (IOException e) {
  32. e.printStackTrace();
  33. }
  34. }
  35. }
  36. }).start();
  37. }
  38. public void receiver() {
  39. new Thread(new Runnable() {
  40. @Override
  41. public void run() {
  42. try {
  43. int a = 10;
  44. while (a > 0) {
  45. a = a - 1;
  46. Thread.sleep(2000);
  47. int num = reader.read();
  48. System.out.println(Thread.currentThread().getName() + " 消费者消费了一个数字,该数字为:" + num);
  49. }
  50. } catch (Exception e) {
  51. e.printStackTrace();
  52. }finally {
  53. try {
  54. reader.close();
  55. } catch (IOException e) {
  56. e.printStackTrace();
  57. }
  58. }
  59. }
  60. }).start();
  61. }
  62. public static void main(String[] args) {
  63. SendAndReceivePips pips = new SendAndReceivePips();
  64. pips.sender();
  65. pips.receiver();
  66. }
  67. }
  1. Thread-0 生产者生产了一个数字,该数字为: 150
  2. Thread-0 生产者生产了一个数字,该数字为: 118
  3. Thread-1 消费者消费了一个数字,该数字为:150
  4. Thread-0 生产者生产了一个数字,该数字为: 232
  5. Thread-0 生产者生产了一个数字,该数字为: 250
  6. Thread-1 消费者消费了一个数字,该数字为:118
  7. Thread-0 生产者生产了一个数字,该数字为: 141
  8. Thread-0 生产者生产了一个数字,该数字为: 103
  9. Thread-1 消费者消费了一个数字,该数字为:232
  10. Thread-0 生产者生产了一个数字,该数字为: 24
  11. Thread-0 生产者生产了一个数字,该数字为: 111
  12. Thread-1 消费者消费了一个数字,该数字为:250
  13. Thread-0 生产者生产了一个数字,该数字为: 142
  14. Thread-0 生产者生产了一个数字,该数字为: 12
  15. Thread-1 消费者消费了一个数字,该数字为:141
  16. Thread-1 消费者消费了一个数字,该数字为:103
  17. Thread-1 消费者消费了一个数字,该数字为:24
  18. Thread-1 消费者消费了一个数字,该数字为:111
  19. Thread-1 消费者消费了一个数字,该数字为:142
  20. Thread-1 消费者消费了一个数字,该数字为:12

四、 Thread.join 的使用

4.1 介绍

如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。线程Thread除了提供join()方法之外,还提供了join(long millis)和join(longmillis,int nanos)两个具备超时特性的方法。这两个超时方法表示,如果线程thread在给定的超时时间里没有终止,那么将会从该超时方法中返回

4.2 例子

  1. public class Join {
  2. public static void main(String[] args) throws Exception {
  3. Thread previous = Thread.currentThread();
  4. for (int i = 0; i < 10; i++) {
  5. // 每个线程拥有前一个线程的引用,需要等待前一个线程终止,才能从等待中返回
  6. Thread thread = new Thread(new Domino(previous), String.valueOf(i));
  7. thread.start();
  8. previous = thread;
  9. }
  10. TimeUnit.SECONDS.sleep(5);
  11. System.out.println(Thread.currentThread().getName() + " terminate.");
  12. }
  13. static class Domino implements Runnable {
  14. private Thread thread;
  15. public Domino(Thread thread) {
  16. this.thread = thread;
  17. }
  18. public void run() {
  19. try {
  20. //当前线程挂起,进入阻塞状态
  21. thread.join();
  22. } catch (InterruptedException e) {
  23. }
  24. System.out.println(Thread.currentThread().getName() + " terminate.");
  25. }
  26. }
  27. }

输出

  1. main terminate.
  2. 0 terminate.
  3. 1 terminate.
  4. 2 terminate.
  5. 3 terminate.
  6. 4 terminate.
  7. 5 terminate.
  8. 6 terminate.
  9. 7 terminate.
  10. 8 terminate.
  11. 9 terminate.

4.3 join 源码

  1. public final synchronized void join(long millis)
  2. throws InterruptedException {
  3. long base = System.currentTimeMillis();
  4. long now = 0;
  5. if (millis < 0) {
  6. throw new IllegalArgumentException("timeout value is negative");
  7. }
  8. if (millis == 0) {
  9. // 条件不满足,继续等待
  10. while (isAlive()) {
  11. wait(0);
  12. }
  13. } else {
  14. // 条件不满足,继续等待
  15. while (isAlive()) {
  16. long delay = millis - now;
  17. if (delay <= 0) {
  18. break;
  19. }
  20. wait(delay);
  21. now = System.currentTimeMillis() - base;
  22. }
  23. }
  24. // 条件满足,方法返回
  25. }

五、ThreadLocal

5.1 介绍

ThreadLocal,即线程本地变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。

5.2 ThreadLocal 、Thread和ThreadLocalMap 关系 图示(来自google搜索)

image.png

5.3 ThreadLocal 使用总结

  • ThreadLocal 并不解决线程间共享数据的问题
  • ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
  • 个线程持有一个 Map 并维护了 ThreadLocal 对象与具体实例的映射,该 Map 由于只被持有它的线程访问,故不存在线程安全以及锁的问题
  • ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题
  • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景

    六、并发工具类

  • CountDownLatch 并发工具

  • CyclicBarrier 并发工具

线程之间的通信可按照实际场景,选择合适的方法。

参考

  • 《Java并发编程的艺术》