在这篇文章里,我们首先阐述什么是同步,不同步有什么问题,然后讨论可以采取哪些措施控制同步,接下来我们会仿照回顾网络通信时那样,构建一个服务器端的“线程池”,JDK为我们提供了一个很大的concurrent工具包,最后我们会对里面的内容进行探索。
为什么要线程同步?
说到线程同步,大部分情况下, 我们是在针对“单对象多线程”的情况进行讨论,一般会将其分成两部分,一部分是关于“共享变量”,一部分关于“执行步骤”。
共享变量
当我们在线程对象(Runnable)中定义了全局变量,run方法会修改该变量时,如果有多个线程同时使用该线程对象,那么就会造成全局变量的值被同时修改,造成错误。我们来看下面的代码:
1 class MyRunner implementsRunnable2 {3 public int sum = 0;4
5 public voidrun()6 {7 System.out.println(Thread.currentThread().getName() + " Start.");8 for (int i = 1; i <= 100; i++)9 {10 sum +=i;11 }12 try{13 Thread.sleep(500);14 } catch(InterruptedException e) {15 e.printStackTrace();16 }17 System.out.println(Thread.currentThread().getName() + " --- The value of sum is " +sum);18 System.out.println(Thread.currentThread().getName() + " End.");19 }20 }21
22
23 private static void sharedVaribleTest() throwsInterruptedException24 {25 MyRunner runner = newMyRunner();26 Thread thread1 = newThread(runner);27 Thread thread2 = newThread(runner);28 thread1.setDaemon(true);29 thread2.setDaemon(true);30 thread1.start();31 thread2.start();32 thread1.join();33 thread2.join();34 }
这个示例中,线程用来计算1到100的和是多少,我们知道正确结果是5050(好像是高斯小时候玩过这个?),但是上述程序返回的结果是10100,原因是两个线程同时对sum进行操作。
执行步骤
我们在多个线程运行时,可能需要某些操作合在一起作为“原子操作”,即在这些操作可以看做是“单线程”的,例如我们可能希望输出结果的样子是这样的:
1 线程1:步骤12 线程1:步骤23 线程1:步骤34 线程2:步骤15 线程2:步骤26 线程2:步骤3
如果同步控制不好,出来的样子可能是这样的:
线程1:步骤1
线程2:步骤1
线程1:步骤2
线程2:步骤2
线程1:步骤3
线程2:步骤3
这里我们也给出一个示例代码:
1 class MyNonSyncRunner implementsRunnable2 {3 public voidrun() {4 System.out.println(Thread.currentThread().getName() + " Start.");5 for(int i = 1; i <= 5; i++)6 {7 System.out.println(Thread.currentThread().getName() + " Running step " +i);8 try
9 {10 Thread.sleep(50);11 }12 catch(InterruptedException ex)13 {14 ex.printStackTrace();15 }16 }17 System.out.println(Thread.currentThread().getName() + " End.");18 }19 }20
21
22 private static void syncTest() throwsInterruptedException23 {24 MyNonSyncRunner runner = newMyNonSyncRunner();25 Thread thread1 = newThread(runner);26 Thread thread2 = newThread(runner);27 thread1.setDaemon(true);28 thread2.setDaemon(true);29 thread1.start();30 thread2.start();31 thread1.join();32 thread2.join();33 }
如何控制线程同步
既然线程同步有上述问题,那么我们应该如何去解决呢?针对不同原因造成的同步问题,我们可以采取不同的策略。
控制共享变量
我们可以采取3种方式来控制共享变量。
将“单对象多线程”修改成“多对象多线程”
上文提及,同步问题一般发生在“单对象多线程”的场景中,那么最简单的处理方式就是将运行模型修改成“多对象多线程”的样子,针对上面示例中的同步问题,修改后的代码如下:
1 private static void sharedVaribleTest2() throwsInterruptedException2 {3 Thread thread1 = new Thread(newMyRunner());4 Thread thread2 = new Thread(newMyRunner());5 thread1.setDaemon(true);6 thread2.setDaemon(true);7 thread1.start();8 thread2.start();9 thread1.join();10 thread2.join();11 }
我们可以看到,上述代码中两个线程使用了两个不同的Runnable实例,它们在运行过程中,就不会去访问同一个全局变量。
将“全局变量”降级为“局部变量”
既然是共享变量造成的问题,那么我们可以将共享变量改为“不共享”,即将其修改为局部变量。这样也可以解决问题,同样针对上面的示例,这种解决方式的代码如下:
1 class MyRunner2 implementsRunnable2 {3 public voidrun()4 {5 System.out.println(Thread.currentThread().getName() + " Start.");6 int sum = 0;7 for (int i = 1; i <= 100; i++)8 {9 sum +=i;10 }11 try{12 Thread.sleep(500);13 } catch(InterruptedException e) {14 e.printStackTrace();15 }16 System.out.println(Thread.currentThread().getName() + " --- The value of sum is " +sum);17 System.out.println(Thread.currentThread().getName() + " End.");18 }19 }20
21
22 private static void sharedVaribleTest3() throwsInterruptedException23 {24 MyRunner2 runner = newMyRunner2();25 Thread thread1 = newThread(runner);26 Thread thread2 = newThread(runner);27 thread1.setDaemon(true);28 thread2.setDaemon(true);29 thread1.start();30 thread2.start();31 thread1.join();32 thread2.join();33 }
我们可以看出,sum变量已经由全局变量变为run方法内部的局部变量了。
使用ThreadLocal机制
ThreadLocal是JDK引入的一种机制,它用于解决线程间共享变量,使用ThreadLocal声明的变量,即使在线程中属于全局变量,针对每个线程来讲,这个变量也是独立的。
我们可以用这种方式来改造上面的代码,如下所示:
1 class MyRunner3 implementsRunnable2 {3 public ThreadLocal tl = new ThreadLocal();4
5 public voidrun()6 {7 System.out.println(Thread.currentT