Java线程(三)
浅谈synchronized应何时使用
?????? 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。由于synchronized关键词实现方式的缘故,常常导致无谓的同步控制,造成并发度(concurrency)的降低。
public class HelloRunnable implements Runnable {public void run() {System.out.println("hello");}public static void main(String[] args) {HelloRunnable run = new HelloRunnable();Thread thread = new Thread(run);thread.start();}}
????? 实际上,main方法中的thread.start()后,某一时刻线程分配到时间片,会启动并自动运行对象run的run()方法,但是run()方法执行且仅执行一次,即只打印一次"hello",然后程序运行结束。因此对于多线程的run()方法,一般在其run()方法体内首先会是while(true)死循环。
public class HelloRunnable implements Runnable {public void run() {while(true){ //注意这里的while(true)System.out.println("hello");}}public static void main(String[] args) {HelloRunnable run = new HelloRunnable();Thread thread = new Thread(run);thread.start();}}
?????? 我们来看一下java.lang.Thread的start()方法和run()方法
public synchronized void start() {if (threadStatus != 0)throw new IllegalThreadStateException();group.add(this);start0();if (stopBeforeStart) {stop0(throwableFromStop);}}public void run() {if (target != null) {target.run();}}
?????? 举例1.模拟火车售票程序,有100张票需要售出
public class SaleTicketRunnable implements Runnable {private int ticket = 100;private boolean flag = true;@Overridepublic void run() {while(flag) {sale();}}//private void sale () {/** 不使用同步控制,出现票为负的情况 */private synchronized void sale () {/** 使用同步控制 */if (ticket > 0) {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " : sale ticket " + (ticket--));} if (ticket <= 0) {flag = false;}}public static void main(String[] args) {SaleTicketRunnable run = new SaleTicketRunnable();Thread one = new Thread(run, "一号窗口");one.start();Thread two = new Thread(run, "二号窗口");two.start();Thread three = new Thread(run, "三号窗口");three.start();Thread four = new Thread(run, "四号窗口");four.start();}}
?????? (1).synchronzied关键字不可用来修饰run()方法,因为同一个对象的synchronized方法只能由一个线程来访问,其他线程访问时,必须等到上一个线程执行完毕(这里是指把100张票卖完)将对象锁释放。在这里的意思是说,对于四个线程,若有一个线程首先分到了时间片,那么它就获得了对象(run)的lock,其他的三个线程是没有机会再去卖票的。
?????? (2).这里就定义了另外一个方法sale()用来售票,当一个线程启动后,它会自动调用目标对象run的run()方法,此时的run()方法是四个线程都可同时访问的。比如说线程one先获得时间片(具体哪个线程先获得要看哪个先获得CPU分配的时间片,这里为了方便就假设是线程one了),它将调用sale()方法,由于sale()方法是synchronized的,线程one将获得该对象(run)的lock。线程one获得lock后,由于此时使用了sleep()方法,则线程one将让出时间片,其他三个线程中的某一个开始运行,同样运行到sale()方法的时候,发现该对象的lock已经被线程one占用,该线程等待。
?????? (3).线程one经过了500ms的sleep后,在某一时刻再次获得时间片,它将接着上一次的断点运行。由于sale()方法仅仅是判断然后打印,打印后判断ticket的数量,至此线程one所持有的对象lock将被释放;若此时仍然由它占用时间片,而run()方法中又有while(true),则线程one将一直调用sale()方法来售票,其他线程只能等待,直到其时间片用完为止。线程one时间片用完后,其他线程可以运行sale()方法并获得对象run的lock,进行售票。
??????? 举例2.线程死锁
public class DeadLockOneRunnable implements Runnable {private byte [] bytes;public DeadLockOneRunnable (byte [] bytes) {this.bytes= bytes;}@Overridepublic void run() {synchronized (bytes) {while(true) {System.out.println(Thread.currentThread().getName() + "is running...");}}}public static void main(String[] args) {byte [] bytes = new byte[0];DeadLockOneRunnable run = new DeadLockOneRunnable(bytes);Thread thread = new Thread(run, "thread");thread.start();synchronized (bytes) {try {thread.join();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("main thread run over .");}}
??????? 说明:在main线程中获得了对象bytes的lock,然后线程thread通过join()方法,则主线程main必须要等到thread线程终止后才可以继续往下运行。但是,当线程thread运行后,发现在其run()方法内部synchronized(bytes),那么线程thread也需要持有对象bytes的lock才可以运行。此时的情况为main线程持有对象bytes的lock等待线程thread运行终止后运行,而线程thread又需要对象bytes的lock被释放;两个线程相互等待彼此的资源,产生死锁。
??????? 举例3.线程死锁
public class DeadLockTwoRunnable implements Runnable {private byte[] source;private byte[] dest;public DeadLockTwoRunnable(byte[] source, byte[] dest) {this.source = source;this.dest = dest;}@Overridepublic void run() {synchronized (source) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " is running...");synchronized (dest) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " is running...");}}}public static void main(String[] args) {byte[] bytesOne = new byte[0];byte[] bytesTwo = new byte[0];DeadLockTwoRunnable runOne = new DeadLockTwoRunnable(bytesOne, bytesTwo);Thread threadOne = new Thread(runOne, "threadOne");DeadLockTwoRunnable runTwo = new DeadLockTwoRunnable(bytesTwo, bytesOne);Thread threadTwo = new Thread(runTwo, "threadTwo");threadOne.start();threadTwo.start();}}
?????? 尽管举例2和举例3说的是线程死锁,但是也不是说死锁比如会出现,只不过这中情况下很容易就出现。若synchronized(source)和synchronized(dest)不是包含关系而是并列关系的话,可能会出现,出现的几率很小。
?????? 在举例1中我们提到如果不使用同步控制会出现的情况,而在举例2和举例3中我们又说使用同步控制会出现的情况。那么同步机制到底该如何使用呢?
public class TestSyncLock {private int position = 0;/** 记录下一个数据存放的位置 */private int[] nums;private String[] strs;public synchronized void methodOne() {}public synchronized void methodTwo() {}public synchronized void methodThree() {}public synchronized void methodFour() {}}
?????? 这个class是线程安全(thread safe)的,是为确保数组不因多线程并发访问而遭受损坏,每个方法都必须声明为synchronized。例如由于methodOne()和methodTwo()都访问(并可能修改)数组num,所以必须同步控制。methodThree()和methodFour()都访问数组strs,也是如此。
?? ??? 尽管methodOne()与methodTwo()彼此之间必须进行同步控制,但它们却不需要与methodThree()或methodFour()进行同步控制。这是因为methodOne()与methodTwo()并不操控methodThree()或methodFour()所操控的数据。反之亦然。
?? ??? 实际上,在某些classes中,这种instance方法的同步控制方式并不少见。Java同步控制机制其实并不粒度化(granular),同步机制对每个对象只提供一个lock。如果你创建了TestSyncLock 的一个对象(lockObj),在主线程中访问对象(lockObj)的methodOne(),在次线程中访问对象(lockObj)的methodThree(),就付出了非必要的性能代价——这些方法彼此同步控制(尽管它们其实不需要如此)。
?? ??? 当一个方法声明为synchronized,所获得的lock乃是隶属于调用此方法的那个对象。因此两个方法都试图争取同一个lock。为了修正这个问题,需要为每个对象配备多个locks。Java没有提供这项功能,因此需要设立自己的机制。—种做法是创建一些对象作为instance数据,这些对象仅仅用来提供locks。
public class TestSyncLock {private int position = 0;/** 记录下一个数据存放的位置 */private int[] nums;private String[] strs;private byte[] bytesOne = new byte[0];private byte[] bytesTwo = new byte[0];public void methodOne() {synchronized (bytesOne) {/** */}}public void methodTwo() {synchronized (bytesTwo) {/** */}}public void methodThree() {synchronized (bytesOne) {/** */}}public void methodFour() {synchronized (bytesTwo) {/** */}}}
????? 这段代码不再拥有synchronized方法,取而代之的是方法内的synchronized代码块。这使得同步控制得以发生在不同的对象身上,并让安全并发(safely run concurrently)的方法做到这一点。例如methodOne()可以和methodThree()或methodFour()并发执行,因为它们争取的是不同对象的locks。
????? 但是使用这项技术时你必须确信,无需同步机制操控的方法,不会造成数据的不一致性。实际上methodOne()和methodTwo()也可同时锁定nums,不过那样容易出错。当你锁定多个对象的时候,必须确保在代码中自始至终使用同样的顺序锁定它们,否则可能导致死锁(deadlock)。
?
synchronized关键字是不能继承的,也就是说,基类的方法synchronized f(){} 在继承类中并不自动是synchronized f(){},而是变成了f(){}。继承类需要你显式的指定它的某个方法为synchronized方法。
? <<To Be Continued>>