Java线程同步失效排查:同步方法为何无法实现线程等待?
首先咱们得先揪出最致命的一个错误:你在循环里反复提交的是同一个B t1实例!Thread类的实例只能被启动一次,虽然你用ExecutorService.submit提交它(因为Thread实现了Runnable),但第一次执行完后,后续的submit不会再触发它的run方法执行,这会导致你的线程逻辑根本没按预期跑10次。这是第一个要改的地方——每次循环都应该创建新的B实例:executorService.submit(new B(c)),而不是复用t1。
接下来咱们逐个看四个选项里的同步问题:
选项1的问题
public class A{ public static void main(String args[]){ C c = new C(); ExecutorService executorService = Executors.newFixedThreadPool(10); B t1 = new B(c); for(int i = 1; i <=10; i++) { executorService.submit(t1); } executorService.shutdown(); try{ executorService.awaitTermination(Long.MAX_VALUE,TimeUnit.NANOSECONDS); }catch(InterruptedException e){ System.out.println("Error while checking tread life: "+e); } } } public class B extends Thread{ C c; static final Object lockObject = new Object(); public B(C c) { this.c = c; } public void run(){ someProcess(); } synchronized public void someProcess(){ String something = c.C(); if(something == null || "".equalsIgnoreCase(something)){ c.A(); }else{ c.B(); } } } public class C { synchronized public void A(){ //insert the records } synchronized public void B(){ //update the records } synchronized public String C(){ // searching a record to get it's id if it exist return something; } }
这里的同步锁是完全分散的:
B.someProcess()用的是当前B实例作为锁(同步非静态方法的锁是this)- 而
C的三个同步方法用的是当前C实例作为锁
这就导致线程执行someProcess时,拿到的是B实例的锁,但调用c.C()、c.A()、c.B()时,又去拿C实例的锁,两个锁完全独立!多个线程可以同时进入someProcess(只要它们的B实例不同),而且即使进入了,调用C的方法时也可能和其他线程的C方法调用交叉执行,完全起不到同步整个“查询-判断-插入/更新”流程的作用。
选项2的问题
public class A { public static void main(String args[]) { C c = new C(); ExecutorService executorService = Executors.newFixedThreadPool(10); B t1 = new B(c); for(int i = 1; i <=10; i++) { executorService.submit(t1); } if(executorService !=null) { executorService.shutdown(); try { executorService.awaitTermination (Long.MAX_VALUE,TimeUnit.NANOSECONDS); }catch(InterruptedException e) { System.out.println("Error while checking tread life: "+e); } } } } public class B extends Thread { C c; static final Object lockObject = new Object(); public B(C c) { this.c = c; } public void run() { someProcess(); } public void someProcess() { synchronized(this) { String something = c.C(); if(something == null || "".equalsIgnoreCase(something)) { c.A(); }else{ c.B(); } } } } public class C { synchronized public void A() { //insert the records } synchronized public void B() { //update the records } synchronized public String C() { // searching a record to get it's id if it exist return something; } }
和选项1本质完全一致:synchronized(this)锁的是B实例,而C的方法锁的是C实例,两个锁不共享。而且如果你复用同一个B实例,所有线程会抢同一个B实例的锁,但调用C的方法时,其他线程如果用不同的B实例(如果改了复用问题),还是会同时调用C的方法,导致流程不同步。
选项3的问题
public class A { public static void main(String args[]) { C c = new C(); ExecutorService executorService = Executors.newFixedThreadPool(10); B t1 = new B(c); for(int i = 1; i <=10; i++) { executorService.submit(t1); } if(executorService !=null) { executorService.shutdown(); try { executorService.awaitTermination (Long.MAX_VALUE,TimeUnit.NANOSECONDS); }catch(InterruptedException e) { System.out.println("Error while checking tread life: "+e); } } } } public class B extends Thread { C c; static final Object lockObject = new Object(); public B(C c) { this.c = c; } public void run() { someProcess(); } public void someProcess() { synchronized(this) { String something = c.C(); if(something == null || "".equalsIgnoreCase(something)) { c.A(); }else{ c.B(); } } } } public class C { public void A() { synchronized(this) { //insert the records } } public void B() { synchronized(this) { //update the records } } public String C() { synchronized(this) { // searching a record to get it's id if it exist return something; } } }
这个选项只是把C方法的同步逻辑从方法级移到了方法内部,但锁还是C实例本身,和选项2的问题完全一致:B的someProcess用B实例锁,C的方法用C实例锁,锁不统一,整个查询-修改的流程还是会被打断。
选项4的问题
public class A { public static void main(String args[]) { C c = new C(); ExecutorService executorService = Executors.newFixedThreadPool(10); B t1 = new B(c); for(int i = 1; i <=10; i++) { executorService.submit(t1); } if(executorService !=null) { executorService.shutdown(); try { executorService.awaitTermination (Long.MAX_VALUE,TimeUnit.NANOSECONDS); }catch(InterruptedException e) { System.out.println("Error while checking tread life: "+e); } } } } public class B extends Thread { C c; static final Object lockObject = new Object(); public B(C c) { this.c = c; } public void run() { someProcess(); } public void someProcess() { synchronized(lockObject) { String something = c.C(lockObject); if(something == null || "".equalsIgnoreCase(something)) { c.A(lockObject); }else{ c.B(lockObject); } } } } public class C { public void A(Object lockObject) { synchronized(lockObject) { //insert the records } } public void B(Object lockObject) { synchronized(lockObject) { //update the records } } public String C(Object lockObject) { synchronized(lockObject) { // searching a record to get it's id if it exist return something; } } }
这个选项的思路是对的:用一个全局静态锁lockObject来同步整个流程,但存在冗余问题——你在someProcess里已经加了lockObject的锁,然后在C的方法里又加了一次同一个锁,这属于重复加锁(虽然不会死锁,但完全没必要)。而且同样存在前面说的复用B实例的问题,导致线程没按预期执行。
正确的实现方式
要解决这个问题,核心是让整个“查询C -> 判断 -> 执行A/B”的流程用同一个锁来同步,同时保证每个线程都是独立的B实例。修改后的代码可以这样:
方式一:用共享的C实例作为锁(推荐,因为所有线程共享同一个C实例)
public class A{ public static void main(String args[]){ C c = new C(); ExecutorService executorService = Executors.newFixedThreadPool(10); // 每次循环创建新的B实例,保证线程独立执行 for(int i = 1; i <=10; i++) { executorService.submit(new B(c)); } executorService.shutdown(); try{ executorService.awaitTermination(Long.MAX_VALUE,TimeUnit.NANOSECONDS); }catch(InterruptedException e){ System.out.println("Error while checking thread life: "+e); } } } // 没必要继承Thread,实现Runnable更灵活通用 public class B implements Runnable{ private C c; public B(C c) { this.c = c; } public void run(){ someProcess(); } public void someProcess(){ // 用共享的C实例作为锁,保证整个流程同步 synchronized(c) { String something = c.C(); if(something == null || "".equalsIgnoreCase(something)){ c.A(); }else{ c.B(); } } } } public class C { // 外面已经用c实例加锁了,这里不需要再同步 public void A(){ //insert the records } public void B(){ //update the records } public String C(){ // searching a record to get it's id if it exist return something; return ""; } }
方式二:用静态全局锁
public class B implements Runnable{ private C c; // 静态全局锁,所有B实例共享 private static final Object lock = new Object(); public B(C c) { this.c = c; } public void run(){ someProcess(); } public void someProcess(){ synchronized(lock) { String something = c.C(); if(something == null || "".equalsIgnoreCase(something)){ c.A(); }else{ c.B(); } } } } // A和C类和上面一致,C的方法不需要额外加锁
这样修改后,每个线程都会独立执行,而且整个查询-判断-修改的流程会被同一个锁保护,线程之间会互相等待,不会出现交叉执行的情况。
内容的提问来源于stack exchange,提问作者yatinbc

