java同步机制简单介绍

来源:互联网 发布:重度抑郁症的表现知乎 编辑:程序博客网 时间:2024/06/05 16:25

在java编程里经常听到类似的术语: 这个函数是不是同步的...

本文就简单介绍下什么是同步, java中同步的一些处理方法。


1.同步问题产生的原因

Java中同步问题是伴随这多线程而产生的, 也就是说如果一个程序是单线程的, 那么就没有同步的概念。


举1个最常见的例子:

假如1个售票程序支持多个线程同时售票。

它里面的核心代码大概是这样的:

void sellTickets{    买票者身份验证();    一些前期步骤();    if (剩余票数()>0){        卖一张票();        剩余票数减一;        交易额加上票的单价;    }}


这部分代码假如只有一条线程来执行是毫无问题的.

但是假如有A B C三条线程来同时执行. 也就是三条线程同时来售票. 就可能会出现问题了.


例如库存里共有50张票

cpu首先执行A线层, 执行完 "卖一张票()"时, cpu突然跳到另1个线层B去执行. 线程B检查剩余票数, 还是50张. 

这就出问题了, 因为A线程虽然卖出了一张票, 但是剩余票数还未减一,  那么第一张票可能被重复出售. 产生数据错误.


所以同步问题产生的根本原因是:

1. 多线程.

2. 多线程同时访问并修改同一段数据.


下面可以举个例子:

package Thread_kng.Td_synchronized;class Sell_ticket implements Runnable{public int tickets = 50; //count of ticketspublic int unit_price = 30; //unit_price of per ticketpublic int sum_price = 0; //total turnoverprivate int pre_counts;public Sell_ticket(){pre_counts = this.tickets;}    Thread cur_thrd;    //verify account of buyerprivate void accountVerify(){System.out.printf("Thread %s: account is valid!\n", cur_thrd.getName());}//previous preparationprivate void pre_Jobs(){System.out.printf("Thread %s: it's prepared!\n", cur_thrd.getName());}private void sellTicket(){this.accountVerify();this.pre_Jobs();if (this.tickets > 0){System.out.printf("Thread %s: sold the no.%d ticket!\n", cur_thrd.getName(),pre_counts - tickets + 1);tickets--;sum_price+=unit_price;}else{System.out.printf("Thread %s: all the tickets are sold out!\n", cur_thrd.getName(),pre_counts - tickets + 1);}}public void run(){    cur_thrd = Thread.currentThread();while(this.tickets > 0){this.sellTicket();}}}public class Td_syn_1{public static void st(){Sell_ticket s = new Sell_ticket();Thread a = new Thread(s);Thread b = new Thread(s);Thread c = new Thread(s);a.setName("A");b.setName("B");c.setName("C");a.start();b.start();c.start();try{Thread.currentThread().sleep(5000);}catch(Exception e){e.printStackTrace();}System.out.printf("Turnover is %d\n", s.sum_price);}}

上面定义了实现了Runnable 接口的售票类. 里面定义了票数, 单价, 总售价等.

其中run方法里调用了sellTicket()方法.


启动类里利用这个对象启动了3个线程同时执行


实际执行时, 就产生同步问题了: 标志就是最后的总交易额对不上, 例如上面售票50张总额应该是1500.

但是有些票被重复售出, 结果就错误了: 而且多次执行的结果并不一样.

执行结果(最后部分): 可以见到执行结果很混乱.最后交易额错误





二.什么是同步, 同步方法

如上面那个例子, 如果一个方法单线程中执行正常, 多线程由于同步问题出现数据错误.

那么我们就说这个方法不是同步的.

否则,我们就说这个是1个同步的方法, 也就是多线程执行也能正常执行.



三.同步方法的解决方法.

我们已经清楚同步问题的原因是: 多条线程同时访问同一段数据.

所以解决方法很简单: 就是令那1段数据在同1个时间点内只能有1个线程访问.

也就是A, B, C中, 如果B线程正在访问数据, 那么A 和 C线程就必须暂停. 直至B访问数据完毕!


我们这三条线程互斥


但是这样的话如果A在买票, 其他人都买不了吗? 多线程貌似失去意义.

实际上, 我们只需要令关键的部分代码互斥就可以了.


例如上面例子:  账户验证();前期准备();   等代码不是关键的, 不需要互斥

而关键代码部分:  获得剩余票数, 卖出一张票, 票数-1,交易额+=单价 这些部分是非常关键的, 需要互斥. 因为如果同时有多个线程执行这些代码就会数据错误.


所以我们又认为互斥是解决同步方法的必要条件.



四.关键字synchronized

Java里 提供了1个关键字synchronized来达到代码多线程互斥的目的.

synchronized 有两种方法,

1.就是用来修饰方法.

2.用来为对象加上同步锁.


4.1 synchronized 修饰方法

synchornized修饰一个方法, 意味着为同1个对象中(非静态方法)或同1个类中(静态方法)加上同步锁.

意识就是如果有多条线程执行该方法, 那么当其中一条线程执行该方法时, 就为该方法上锁, 其他线程就不能执行该方法. 而进入阻塞状态(暂停执行), 直至该方法执行完该方法.

也就是cpu绝不会执行synchronized的途中跳到另一条线程执行!


4.1.1 synchronized 修饰非static方法.

synchronized 修饰非静态方法与静态方法还是有点区别的.

我们知道非静态方法属于对象而不是类本身, 需要1个已实例化的对象才能执行.

而synchronized修饰1个非静态方法意味这个方法在同1个对象中, 多条线程执行该方法是互斥的. 也就是其中一条线程一旦执行该方法,其他线程就进入阻塞状态.

但是前提是同1个对象中.


举回上面的例子, 修改一下:

package Thread_kng.Td_synchronized;class Sell_ticket2 implements Runnable{public int tickets = 50; //count of ticketspublic int unit_price = 30; //unit_price of per ticketpublic int sum_price = 0; //total turnoverprivate int pre_counts;public Sell_ticket2(){pre_counts = this.tickets;}    Thread cur_thrd;    //verify account of buyerprivate void accountVerify(){System.out.printf("Thread %s: account is valid!\n", cur_thrd.getName());}//previous preparationprivate void pre_Jobs(){System.out.printf("Thread %s: it's prepared!\n", cur_thrd.getName());}private synchronized void sellTicket(){this.accountVerify();this.pre_Jobs();if (this.tickets > 0){System.out.printf("Thread %s: sold the no.%d ticket!\n", cur_thrd.getName(),pre_counts - tickets + 1);tickets--;sum_price+=unit_price;}else{System.out.printf("Thread %s: all the tickets are sold out!\n", cur_thrd.getName(),pre_counts - tickets + 1);}}public void run(){    cur_thrd = Thread.currentThread();while(this.tickets > 0){this.sellTicket();}}}public class Td_syn_2{public static void st(){Sell_ticket2 s = new Sell_ticket2();Thread a = new Thread(s);Thread b = new Thread(s);Thread c = new Thread(s);a.setName("A");b.setName("B");c.setName("C");a.start();b.start();c.start();try{Thread.currentThread().sleep(5000);}catch(Exception e){e.printStackTrace();}System.out.printf("Turnover is %d\n", s.sum_price);}}

实际上的改动只有一处, 就是在售票类中

用synchronized 关键字去修饰方法 sellTicket()

所以执行时一旦有1条线程开始执行 sellTicket(); 关于该对象的所有其他线程就进入阻塞状态..

执行结果:


可以见到售票次序很规律.

而且多次执行, 最后的交易额都是正确的.



4.1.2 synchronized 修饰static方法.

上面之所以强调同一个对象,是因为synchronized方法修饰静态方法只会影响同1个对象的线程.

而对同1个类而不同对象, 用synchornized 去修饰是无效的.


见下面的例子, 也是修改上面的程序:

package Thread_kng.Td_synchronized;class Sell_ticket3 extends Thread{public static int tickets = 50; //count of ticketspublic static int unit_price = 30; //unit_price of per ticketpublic static int sum_price = 0; //total turnoverprivate int pre_counts;public Sell_ticket3(String name){super(name);pre_counts = this.tickets;}    //verify account of buyerprivate void accountVerify(){System.out.printf("Thread %s: account is valid!\n", this.getName());}//previous preparationprivate void pre_Jobs(){System.out.printf("Thread %s: it's prepared!\n", this.getName());}private synchronized void sellTicket(){this.accountVerify();this.pre_Jobs();if (this.tickets > 0){System.out.printf("Thread %s: sold the no.%d ticket!\n", this.getName(),pre_counts - tickets + 1);tickets--;sum_price+=unit_price;}else{System.out.printf("Thread %s: all the tickets are sold out!\n", this.getName(),pre_counts - tickets + 1);}}public void run(){while(this.tickets > 0){this.sellTicket();}}}public class Td_syn_3{public static void st(){Sell_ticket3 a = new Sell_ticket3("A");Sell_ticket3 b = new Sell_ticket3("B");Sell_ticket3 c = new Sell_ticket3("C");a.start();b.start();c.start();try{Thread.currentThread().sleep(5000);}catch(Exception e){e.printStackTrace();}System.out.printf("Turnover is %d\n", Sell_ticket3.sum_price);}}


注意上面的售票类不在是实验Runnable 接口, 而是继承了Thread类.

那么在启动3个线程时, 就必须实例化3个对象,  而之前的例子是实例化一个对象而创建3个线程.


由于这3个对象a b c是不同的对象, 所以即使在非静态方法sellTicket()加上synchronized 关键字, 但是实际执行时.

3个线程分别执行自己对象的sellTicket(), 所以就不是互斥的.

执行结果:  可见最后交易额也是错误的. 而且有重复售票的现象:


解决方法也很简单, 就是把售票方法sellTickt() 改为1个静态方法.

静态方法只属于类本身.

如果用synchronized关键字修饰1个静态方法.

那么这个类或者任何对象调用这个方法, 在多线程中也是互斥的.


解决方法就是把sellTicket()改为1个静态方法, 注意的是静态方法不能调用非静态成员和方法.

修改后的例子:

package Thread_kng.Td_synchronized;class Sell_ticket4 extends Thread{public static int tickets = 50; //count of ticketspublic static int unit_price = 30; //unit_price of per ticketpublic static int sum_price = 0; //total turnoverprivate static int pre_counts;public Sell_ticket4(String name){super(name);pre_counts = this.tickets;}    //verify account of buyerprivate static void accountVerify(){System.out.printf("Thread %s: account is valid!\n", Thread.currentThread().getName());}//previous preparationprivate static void pre_Jobs(){System.out.printf("Thread %s: it's prepared!\n", Thread.currentThread().getName());}private synchronized static void sellTicket(){accountVerify();pre_Jobs();if (tickets > 0){System.out.printf("Thread %s: sold the no.%d ticket!\n", Thread.currentThread().getName(),pre_counts - tickets + 1);tickets--;sum_price+=unit_price;}else{System.out.printf("Thread %s: all the tickets are sold out!\n", Thread.currentThread().getName(),pre_counts - tickets + 1);}}public void run(){while(this.tickets > 0){this.sellTicket();}}}public class Td_syn_4{public static void st(){Sell_ticket4 a = new Sell_ticket4("A");Sell_ticket4 b = new Sell_ticket4("B");Sell_ticket4 c = new Sell_ticket4("C");a.start();b.start();c.start();try{Thread.currentThread().sleep(5000);}catch(Exception e){e.printStackTrace();}System.out.printf("Turnover is %d\n", Sell_ticket4.sum_price);}}

修改后的结果就变成正确同步的了:




4.2 利用synchronized关键字 为对象加上同步锁

我们回顾了上面的例子, 用synchronized 为售票方法设为同步方法, 貌似解决了问题.

但是还是有一些缺点.


因为售票方法sellTicket() 里面还调用了 账户验证() 前期准备()等方法.

现实中就意味着如果A在买票, 那么B连账户验证()这一步也做不了, 是不合适的.


根本原因是账户验证() 前期准备()等方法没有访问修改公共数据. 也就说这些方法不是需要同步的关键代码.

所以我们应该只为关键代码设为互斥.


方法无非两种:

1. 就是只把关键代码放进另1个方法, 为这个方法加上synchronized关键字.

2. 就是利用synchronized加上对象锁.


4.2.1 什么是对象锁

java编程中, 任何一个对象都具有1个对象锁, 包括在方法体内定义的局部变量对象.

某1个对象锁只能被1个线程占用,  如果一个线程占用了1个对象的锁, 那么其他线程尝试去占用这个对象锁时就会失败.


我们可以利用这个机制去达到某些代码在多线程中互斥的目的.


4.2.2 synchronized 为对象加锁的用法

用法如下

synchronized (object){

   代码...;

}


注意, object 是1个对象

那么当1个线程A执行这段代码时, 就尝试去获得这个对象object的锁, 如果获取成功, 则执行里面的代码.

如果这个对象的锁已经被其他某条线程占用, 则线程A进入阻塞状态,暂停执行. 直至这个对象的锁被其他线程释放并且线程A成功占用该对象的锁.


下面是例子, 还是上面的例子做下修改:

package Thread_kng.Td_synchronized;class Sell_ticket5 extends Thread{public static int tickets = 50; //count of ticketspublic static int unit_price = 30; //unit_price of per ticketpublic static int sum_price = 0; //total turnoverprivate int pre_counts;public Sell_ticket5(String name){super(name);pre_counts = this.tickets;}    //verify account of buyerprivate void accountVerify(){System.out.printf("Thread %s: account is valid!\n", this.getName());}//previous preparationprivate void pre_Jobs(){System.out.printf("Thread %s: it's prepared!\n", this.getName());}private static int i_flag;private static String s_flag = "flag_of_objectlock"; //assignmen is needed, otherwise will                                                      //throw exception when being synchronizedprivate void sellTicket(){this.accountVerify();this.pre_Jobs();//synchronized (i_flag) //error integer variable is not an objectsynchronized (s_flag){ //try to lock the object i_flagif (this.tickets > 0){System.out.printf("Thread %s: sold the no.%d ticket!\n", this.getName(),pre_counts - tickets + 1);tickets--;sum_price+=unit_price;}else{System.out.printf("Thread %s: all the tickets are sold out!\n", this.getName(),pre_counts - tickets + 1);}}}public void run(){while(this.tickets > 0){this.sellTicket();}System.out.printf("Turnover is %d\n", Sell_ticket5.sum_price);}}public class Td_syn_5{public static void st(){Sell_ticket5 a = new Sell_ticket5("A");Sell_ticket5 b = new Sell_ticket5("B");Sell_ticket5 c = new Sell_ticket5("C");a.start();b.start();c.start();try{Thread.currentThread().sleep(5000);}catch(Exception e){e.printStackTrace();}System.out.printf("Turnover is %d\n", Sell_ticket5.sum_price);}}


上面的例子中, sellTicket方法没有synchronized 修饰.

但是我定义了1个公共(静态)对象String s_flag.

在sellTicket方法把关键代码写进 synchronzied(s_flag)中.

这样的话sellTicket实际上也可以被称为同步的, 因为关键代码会在多线程中互斥.


执行结果:


可以见到结果正确, 而且执行状态是几种方法中最佳的.


4.2.3 synchronized 为对象加锁的要注意的地方

1. 只能为对象加锁,  synchronized 括号里面的只能是1个对象名, 而不能是类名.

2. int类型的变量不是对象. 所以不能为int类型的变量加锁

3. String类型的变量是对象, 一般情况下为1个String对象加锁, 比较方便.

4. 这个对象必须被实例化, 也就是有内存指向, 否则抛出异常,  对于String对象来讲, 如果要为其加锁, 则这个String变量必须被复制.

5. 如果多个线程由多个线程对象创建(如上面的例子), 则这个对象必须是1个类静态(公共)变量, 如果是类的非静态成员则达不到线程互斥的母的.

   因为多个线程里对象里的非静态成员的内存地址是不同的, 多个线程为各自的成员加锁肯定是成功的.

   所以上面的例子中的s_flag 必须是1个静态变量, 它只属于类本身.



4.3 synchronized 一些要点:

实在上, 如果为1个非静态的方法f() 加上synchronized 修饰

synchronized void f(){

   ....

}


就相当于

void f(){

 synchronized (this){

   ....

 }

}


只有该对象的线程调用该方法才是互斥的.


也就是获取对象本身的锁.


而为1个静态方法g()方法加上synchronize 修饰.

实际上就是为类本身上锁, 该类所有对象的线程调用该方法都是互斥的.



五.小结

关于java线程同步其实并不复杂. 个人觉得比oracle的锁还好理解一点. 关键就是熟悉synchronized 的用法.






0 0
原创粉丝点击