Java多线程编程中synchronized线程同步的教程
0.关于线程同步
(1)为什么需要同步多线程?
线程的同步是指让多个运行的线程在一起良好地协作,达到让多线程按要求合理地占用释放资源。我们采用Java中的同步代码块和同步方法达到这样的目的。比如这样的解决多线程无固定序执行的问题:
publicclassTwoThreadTest{ publicstaticvoidmain(String[]args){ Threadth1=newMyThread1(); Threadth2=newMyThread2(); th1.start(); th2.start(); } } classMyThread2extendsThread{ @Override publicvoidrun(){ for(inti=0;i<10;i++) System.out.println("thread1counter:"+i); } } classMyThread1extendsThread{ @Override publicvoidrun(){ for(inti=0;i<10;i++) System.out.println("thread2counter:"+i); } }
这种状态下多线程执行的结果是随机地去任意插入执行,这完全取决于JVM对于线程的调度,在很多要求定序执行的情况下,这种随机执行的状态显然是不合要求的。
publicclassThreadTest{ publicstaticvoidmain(String[]args){ MyThreadthread=newMyThread(); Threadth1=newThread(thread); Threadth2=newThread(thread); th1.start(); th2.start(); } } classMyThreadimplementsRunnable{ @Override publicsynchronizedvoidrun(){ for(inti=0;i<10;i++) System.out.println(Thread.currentThread().getName()+"counter:"+i); } }
使用了同步方法后我们就可以控制线程独占执行体对象,这样在执行的过程中就可以使得线程将执行体上的任务一次性执行完后退出锁定状态,JVM再调度另一个线程进来一次性运行执行体内的任务。
(2)线程创建运行的范式:
在以前我们也有自己的线程创建和运行的编程范式,一般是定义一个执行类重写run()方法,但是这种方式将执行体和执行的任务放在了一起,从软件工程的角度来看不利于解耦。一个线程的执行的意思是说线程通过执行对象执行了某个对象的某个任务,从这个角度来说,将任务的规定者从执行类中分离出来可以使得多线程编程的各个角色明晰出来,进而获得良好地解耦,以下就是线程创建和执行的编程范式:
publicclassFormalThreadClass{ publicstaticvoidmain(String[]args){ Threadthread=newThread(newMyRunnable()); thread.start(); } } classMyRunnableimplementsRunnable{ MyTaskmyTask=newMyTask(); @Override publicvoidrun(){ myTask.doTask(); } } classMyTask{ publicvoiddoTask(){ System.out.println("ThisisrealTasking"); } }
1.synchronized原理
在java中,每一个对象有且仅有一个同步锁。这也意味着,同步锁是依赖于对象而存在。
当我们调用某对象的synchronized方法时,就获取了该对象的同步锁。例如,synchronized(obj)就获取了“obj这个对象”的同步锁。
不同线程对同步锁的访问是互斥的。也就是说,某时间点,对象的同步锁只能被一个线程获取到!通过同步锁,我们就能在多线程中,实现对“对象/方法”的互斥访问。例如,现在有两个线程A和线程B,它们都会访问“对象obj的同步锁”。假设,在某一时刻,线程A获取到“obj的同步锁”并在执行一些操作;而此时,线程B也企图获取“obj的同步锁”——线程B会获取失败,它必须等待,直到线程A释放了“该对象的同步锁”之后线程B才能获取到“obj的同步锁”从而才可以运行。
2.synchronized基本规则
我们将synchronized的基本规则总结为下面3条,并通过实例对它们进行说明。
第一条:当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
第二条:当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程仍然可以访问“该对象”的非同步代码块。
第三条:当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的其他的“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
(1)第一条:
当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。下面是“synchronized代码块”对应的演示程序。
classMyRunableimplementsRunnable{ @Override publicvoidrun(){ synchronized(this){ try{ for(inti=0;i<5;i++){ Thread.sleep(100);//休眠100ms System.out.println(Thread.currentThread().getName()+"loop"+i); } }catch(InterruptedExceptionie){ } } } } publicclassDemo1_1{ publicstaticvoidmain(String[]args){ Runnabledemo=newMyRunable();//新建“Runnable对象” Threadt1=newThread(demo,"t1");//新建“线程t1”,t1是基于demo这个Runnable对象 Threadt2=newThread(demo,"t2");//新建“线程t2”,t2是基于demo这个Runnable对象 t1.start();//启动“线程t1” t2.start();//启动“线程t2” } }
运行结果:
t1loop0 t1loop1 t1loop2 t1loop3 t1loop4 t2loop0 t2loop1 t2loop2 t2loop3 t2loop4
结果说明:run()方法中存在“synchronized(this)代码块”,而且t1和t2都是基于"demo这个Runnable对象"创建的线程。这就意味着,我们可以将synchronized(this)中的this看作是“demo这个Runnable对象”;因此,线程t1和t2共享“demo对象的同步锁”。所以,当一个线程运行的时候,另外一个线程必须等待“运行线程”释放“demo的同步锁”之后才能运行。
如果你确认,你搞清楚这个问题了。那我们将上面的代码进行修改,然后再运行看看结果怎么样,看看你是否会迷糊。修改后的源码如下:
classMyThreadextendsThread{ publicMyThread(Stringname){ super(name); } @Override publicvoidrun(){ synchronized(this){ try{ for(inti=0;i<5;i++){ Thread.sleep(100);//休眠100ms System.out.println(Thread.currentThread().getName()+"loop"+i); } }catch(InterruptedExceptionie){ } } } } publicclassDemo1_2{ publicstaticvoidmain(String[]args){ Threadt1=newMyThread("t1");//新建“线程t1” Threadt2=newMyThread("t2");//新建“线程t2” t1.start();//启动“线程t1” t2.start();//启动“线程t2” } }
代码说明:比较Demo1_2和Demo1_1,我们发现,Demo1_2中的MyThread类是直接继承于Thread,而且t1和t2都是MyThread子线程。
幸运的是,在“Demo1_2的run()方法”也调用了synchronized(this),正如“Demo1_1的run()方法”也调用了synchronized(this)一样!
那么,Demo1_2的执行流程是不是和Demo1_1一样呢?运行结果:
t1loop0 t2loop0 t1loop1 t2loop1 t1loop2 t2loop2 t1loop3 t2loop3 t1loop4 t2loop4
结果说明:
如果这个结果一点也不令你感到惊讶,那么我相信你对synchronized和this的认识已经比较深刻了。否则的话,请继续阅读这里的分析。
synchronized(this)中的this是指“当前的类对象”,即synchronized(this)所在的类对应的当前对象。它的作用是获取“当前对象的同步锁”。
对于Demo1_2中,synchronized(this)中的this代表的是MyThread对象,而t1和t2是两个不同的MyThread对象,因此t1和t2在执行synchronized(this)时,获取的是不同对象的同步锁。对于Demo1_1对而言,synchronized(this)中的this代表的是MyRunable对象;t1和t2共同一个MyRunable对象,因此,一个线程获取了对象的同步锁,会造成另外一个线程等待。
(2)第二条:
当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程仍然可以访问“该对象”的非同步代码块。
下面是“synchronized代码块”对应的演示程序。
classCount{ //含有synchronized同步块的方法 publicvoidsynMethod(){ synchronized(this){ try{ for(inti=0;i<5;i++){ Thread.sleep(100);//休眠100ms System.out.println(Thread.currentThread().getName()+"synMethodloop"+i); } }catch(InterruptedExceptionie){ } } } //非同步的方法 publicvoidnonSynMethod(){ try{ for(inti=0;i<5;i++){ Thread.sleep(100); System.out.println(Thread.currentThread().getName()+"nonSynMethodloop"+i); } }catch(InterruptedExceptionie){ } } } publicclassDemo2{ publicstaticvoidmain(String[]args){ finalCountcount=newCount(); //新建t1,t1会调用“count对象”的synMethod()方法 Threadt1=newThread( newRunnable(){ @Override publicvoidrun(){ count.synMethod(); } },"t1"); //新建t2,t2会调用“count对象”的nonSynMethod()方法 Threadt2=newThread( newRunnable(){ @Override publicvoidrun(){ count.nonSynMethod(); } },"t2"); t1.start();//启动t1 t2.start();//启动t2 } }
运行结果:
t1synMethodloop0 t2nonSynMethodloop0 t1synMethodloop1 t2nonSynMethodloop1 t1synMethodloop2 t2nonSynMethodloop2 t1synMethodloop3 t2nonSynMethodloop3 t1synMethodloop4 t2nonSynMethodloop4
结果说明:
主线程中新建了两个子线程t1和t2。t1会调用count对象的synMethod()方法,该方法内含有同步块;而t2则会调用count对象的nonSynMethod()方法,该方法不是同步方法。t1运行时,虽然调用synchronized(this)获取“count的同步锁”;但是并没有造成t2的阻塞,因为t2没有用到“count”同步锁。
(3)第三条:
当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的其他的“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
我们将上面的例子中的nonSynMethod()方法体的也用synchronized(this)修饰。修改后的源码如下:
classCount{ //含有synchronized同步块的方法 publicvoidsynMethod(){ synchronized(this){ try{ for(inti=0;i<5;i++){ Thread.sleep(100);//休眠100ms System.out.println(Thread.currentThread().getName()+"synMethodloop"+i); } }catch(InterruptedExceptionie){ } } } //也包含synchronized同步块的方法 publicvoidnonSynMethod(){ synchronized(this){ try{ for(inti=0;i<5;i++){ Thread.sleep(100); System.out.println(Thread.currentThread().getName()+"nonSynMethodloop"+i); } }catch(InterruptedExceptionie){ } } } } publicclassDemo3{ publicstaticvoidmain(String[]args){ finalCountcount=newCount(); //新建t1,t1会调用“count对象”的synMethod()方法 Threadt1=newThread( newRunnable(){ @Override publicvoidrun(){ count.synMethod(); } },"t1"); //新建t2,t2会调用“count对象”的nonSynMethod()方法 Threadt2=newThread( newRunnable(){ @Override publicvoidrun(){ count.nonSynMethod(); } },"t2"); t1.start();//启动t1 t2.start();//启动t2 } }
运行结果:
t1synMethodloop0 t1synMethodloop1 t1synMethodloop2 t1synMethodloop3 t1synMethodloop4 t2nonSynMethodloop0 t2nonSynMethodloop1 t2nonSynMethodloop2 t2nonSynMethodloop3 t2nonSynMethodloop4
结果说明:
主线程中新建了两个子线程t1和t2。t1和t2运行时都调用synchronized(this),这个this是Count对象(count),而t1和t2共用count。因此,在t1运行时,t2会被阻塞,等待t1运行释放“count对象的同步锁”,t2才能运行。
3.synchronized方法和synchronized代码块
“synchronized方法”是用synchronized修饰方法,而“synchronized代码块”则是用synchronized修饰代码块。
synchronized方法示例
publicsynchronizedvoidfoo1(){ System.out.println("synchronizedmethoed"); } synchronized代码块 publicvoidfoo2(){ synchronized(this){ System.out.println("synchronizedmethoed"); } }
synchronized代码块中的this是指当前对象。也可以将this替换成其他对象,例如将this替换成obj,则foo2()在执行synchronized(obj)时就获取的是obj的同步锁。
synchronized代码块可以更精确的控制冲突限制访问区域,有时候表现更高效率。下面通过一个示例来演示:
//Demo4.java的源码 publicclassDemo4{ publicsynchronizedvoidsynMethod(){ for(inti=0;i<1000000;i++) ; } publicvoidsynBlock(){ synchronized(this){ for(inti=0;i<1000000;i++) ; } } publicstaticvoidmain(String[]args){ Demo4demo=newDemo4(); longstart,diff; start=System.currentTimeMillis();//获取当前时间(millis) demo.synMethod();//调用“synchronized方法” diff=System.currentTimeMillis()-start;//获取“时间差值” System.out.println("synMethod():"+diff); start=System.currentTimeMillis();//获取当前时间(millis) demo.synBlock();//调用“synchronized方法块” diff=System.currentTimeMillis()-start;//获取“时间差值” System.out.println("synBlock():"+diff); } }
(某一次)执行结果:
synMethod():11 synBlock():3
4.实例锁和全局锁
实例锁--锁在某一个实例对象上。如果该类是单例,那么该锁也具有全局锁的概念。
(1)实例锁对应的就是synchronized关键字。
(2)全局锁--该锁针对的是类,无论实例多少个对象,那么线程都共享该锁。
全局锁对应的就是staticsynchronized(或者是锁在该类的class或者classloader对象上)。
关于“实例锁”和“全局锁”有一个很形象的例子:
pulbicclassSomething{ publicsynchronizedvoidisSyncA(){} publicsynchronizedvoidisSyncB(){} publicstaticsynchronizedvoidcSyncA(){} publicstaticsynchronizedvoidcSyncB(){} }
假设,Something有两个实例x和y。分析下面4组表达式获取的锁的情况。
(1)x.isSyncA()与x.isSyncB()
(2)x.isSyncA()与y.isSyncA()
(3)x.cSyncA()与y.cSyncB()
(4)x.isSyncA()与Something.cSyncA()
(1)不能被同时访问。
因为isSyncA()和isSyncB()都是访问同一个对象(对象x)的同步锁!
//LockTest1.java的源码 classSomething{ publicsynchronizedvoidisSyncA(){ try{ for(inti=0;i<5;i++){ Thread.sleep(100);//休眠100ms System.out.println(Thread.currentThread().getName()+":isSyncA"); } }catch(InterruptedExceptionie){ } } publicsynchronizedvoidisSyncB(){ try{ for(inti=0;i<5;i++){ Thread.sleep(100);//休眠100ms System.out.println(Thread.currentThread().getName()+":isSyncB"); } }catch(InterruptedExceptionie){ } } } publicclassLockTest1{ Somethingx=newSomething(); Somethingy=newSomething(); //比较(01)x.isSyncA()与x.isSyncB() privatevoidtest1(){ //新建t11,t11会调用x.isSyncA() Threadt11=newThread( newRunnable(){ @Override publicvoidrun(){ x.isSyncA(); } },"t11"); //新建t12,t12会调用x.isSyncB() Threadt12=newThread( newRunnable(){ @Override publicvoidrun(){ x.isSyncB(); } },"t12"); t11.start();//启动t11 t12.start();//启动t12 } publicstaticvoidmain(String[]args){ LockTest1demo=newLockTest1(); demo.test1(); } }
运行结果:
t11:isSyncA t11:isSyncA t11:isSyncA t11:isSyncA t11:isSyncA t12:isSyncB t12:isSyncB t12:isSyncB t12:isSyncB t12:isSyncB
(2)可以同时被访问
因为访问的不是同一个对象的同步锁,x.isSyncA()访问的是x的同步锁,而y.isSyncA()访问的是y的同步锁。
//LockTest2.java的源码 classSomething{ publicsynchronizedvoidisSyncA(){ try{ for(inti=0;i<5;i++){ Thread.sleep(100);//休眠100ms System.out.println(Thread.currentThread().getName()+":isSyncA"); } }catch(InterruptedExceptionie){ } } publicsynchronizedvoidisSyncB(){ try{ for(inti=0;i<5;i++){ Thread.sleep(100);//休眠100ms System.out.println(Thread.currentThread().getName()+":isSyncB"); } }catch(InterruptedExceptionie){ } } publicstaticsynchronizedvoidcSyncA(){ try{ for(inti=0;i<5;i++){ Thread.sleep(100);//休眠100ms System.out.println(Thread.currentThread().getName()+":cSyncA"); } }catch(InterruptedExceptionie){ } } publicstaticsynchronizedvoidcSyncB(){ try{ for(inti=0;i<5;i++){ Thread.sleep(100);//休眠100ms System.out.println(Thread.currentThread().getName()+":cSyncB"); } }catch(InterruptedExceptionie){ } } } publicclassLockTest2{ Somethingx=newSomething(); Somethingy=newSomething(); //比较(02)x.isSyncA()与y.isSyncA() privatevoidtest2(){ //新建t21,t21会调用x.isSyncA() Threadt21=newThread( newRunnable(){ @Override publicvoidrun(){ x.isSyncA(); } },"t21"); //新建t22,t22会调用x.isSyncB() Threadt22=newThread( newRunnable(){ @Override publicvoidrun(){ y.isSyncA(); } },"t22"); t21.start();//启动t21 t22.start();//启动t22 } publicstaticvoidmain(String[]args){ LockTest2demo=newLockTest2(); demo.test2(); } }
运行结果:
t21:isSyncA t22:isSyncA t21:isSyncA t22:isSyncA t21:isSyncA t22:isSyncA t21:isSyncA t22:isSyncA t21:isSyncA t22:isSyncA
(3)不能被同时访问
因为cSyncA()和cSyncB()都是static类型,x.cSyncA()相当于Something.isSyncA(),y.cSyncB()相当于Something.isSyncB(),因此它们共用一个同步锁,不能被同时反问。
//LockTest3.java的源码 classSomething{ publicsynchronizedvoidisSyncA(){ try{ for(inti=0;i<5;i++){ Thread.sleep(100);//休眠100ms System.out.println(Thread.currentThread().getName()+":isSyncA"); } }catch(InterruptedExceptionie){ } } publicsynchronizedvoidisSyncB(){ try{ for(inti=0;i<5;i++){ Thread.sleep(100);//休眠100ms System.out.println(Thread.currentThread().getName()+":isSyncB"); } }catch(InterruptedExceptionie){ } } publicstaticsynchronizedvoidcSyncA(){ try{ for(inti=0;i<5;i++){ Thread.sleep(100);//休眠100ms System.out.println(Thread.currentThread().getName()+":cSyncA"); } }catch(InterruptedExceptionie){ } } publicstaticsynchronizedvoidcSyncB(){ try{ for(inti=0;i<5;i++){ Thread.sleep(100);//休眠100ms System.out.println(Thread.currentThread().getName()+":cSyncB"); } }catch(InterruptedExceptionie){ } } } publicclassLockTest3{ Somethingx=newSomething(); Somethingy=newSomething(); //比较(03)x.cSyncA()与y.cSyncB() privatevoidtest3(){ //新建t31,t31会调用x.isSyncA() Threadt31=newThread( newRunnable(){ @Override publicvoidrun(){ x.cSyncA(); } },"t31"); //新建t32,t32会调用x.isSyncB() Threadt32=newThread( newRunnable(){ @Override publicvoidrun(){ y.cSyncB(); } },"t32"); t31.start();//启动t31 t32.start();//启动t32 } publicstaticvoidmain(String[]args){ LockTest3demo=newLockTest3(); demo.test3(); } }
运行结果:
t31:cSyncA t31:cSyncA t31:cSyncA t31:cSyncA t31:cSyncA t32:cSyncB t32:cSyncB t32:cSyncB t32:cSyncB t32:cSyncB
(4)可以被同时访问
因为isSyncA()是实例方法,x.isSyncA()使用的是对象x的锁;而cSyncA()是静态方法,Something.cSyncA()可以理解对使用的是“类的锁”。因此,它们是可以被同时访问的。
//LockTest4.java的源码 classSomething{ publicsynchronizedvoidisSyncA(){ try{ for(inti=0;i<5;i++){ Thread.sleep(100);//休眠100ms System.out.println(Thread.currentThread().getName()+":isSyncA"); } }catch(InterruptedExceptionie){ } } publicsynchronizedvoidisSyncB(){ try{ for(inti=0;i<5;i++){ Thread.sleep(100);//休眠100ms System.out.println(Thread.currentThread().getName()+":isSyncB"); } }catch(InterruptedExceptionie){ } } publicstaticsynchronizedvoidcSyncA(){ try{ for(inti=0;i<5;i++){ Thread.sleep(100);//休眠100ms System.out.println(Thread.currentThread().getName()+":cSyncA"); } }catch(InterruptedExceptionie){ } } publicstaticsynchronizedvoidcSyncB(){ try{ for(inti=0;i<5;i++){ Thread.sleep(100);//休眠100ms System.out.println(Thread.currentThread().getName()+":cSyncB"); } }catch(InterruptedExceptionie){ } } } publicclassLockTest4{ Somethingx=newSomething(); Somethingy=newSomething(); //比较(04)x.isSyncA()与Something.cSyncA() privatevoidtest4(){ //新建t41,t41会调用x.isSyncA() Threadt41=newThread( newRunnable(){ @Override publicvoidrun(){ x.isSyncA(); } },"t41"); //新建t42,t42会调用x.isSyncB() Threadt42=newThread( newRunnable(){ @Override publicvoidrun(){ Something.cSyncA(); } },"t42"); t41.start();//启动t41 t42.start();//启动t42 } publicstaticvoidmain(String[]args){ LockTest4demo=newLockTest4(); demo.test4(); } }
运行结果:
t41:isSyncA t42:cSyncA t41:isSyncA t42:cSyncA t41:isSyncA t42:cSyncA t41:isSyncA t42:cSyncA t41:isSyncA t42:cSyncA