在java编程里经常听到类似的术语: 这个函数是不是同步的...
本文就简单介绍下什么是同步, java中同步的一些处理方法。
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 tickets
public int unit_price = 30; //unit_price of per ticket
public int sum_price = 0; //total turnover
private int pre_counts;
public Sell_ticket(){
pre_counts = this.tickets;
}
Thread cur_thrd;
//verify account of buyer
private void accountVerify(){
System.out.printf("Thread %s: account is valid!\n", cur_thrd.getName());
}
//previous preparation
private 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,交易额+=单价 这些部分是非常关键的, 需要互斥. 因为如果同时有多个线程执行这些代码就会数据错误.
所以我们又认为互斥是解决同步方法的必要条件.
Java里 提供了1个关键字synchronized来达到代码多线程互斥的目的.
synchronized 有两种方法,
1.就是用来修饰方法.
2.用来为对象加上同步锁.
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 tickets
public int unit_price = 30; //unit_price of per ticket
public int sum_price = 0; //total turnover
private int pre_counts;
public Sell_ticket2(){
pre_counts = this.tickets;
}
Thread cur_thrd;
//verify account of buyer
private void accountVerify(){
System.out.printf("Thread %s: account is valid!\n", cur_thrd.getName());
}
//previous preparation
private 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 tickets
public static int unit_price = 30; //unit_price of per ticket
public static int sum_price = 0; //total turnover
private int pre_counts;
public Sell_ticket3(String name){
super(name);
pre_counts = this.tickets;
}
//verify account of buyer
private void accountVerify(){
System.out.printf("Thread %s: account is valid!\n", this.getName());
}
//previous preparation
private 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 tickets
public static int unit_price = 30; //unit_price of per ticket
public static int sum_price = 0; //total turnover
private static int pre_counts;
public Sell_ticket4(String name){
super(name);
pre_counts = this.tickets;
}
//verify account of buyer
private static void accountVerify(){
System.out.printf("Thread %s: account is valid!\n", Thread.currentThread().getName());
}
//previous preparation
private 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);
}
}
我们回顾了上面的例子, 用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 tickets
public static int unit_price = 30; //unit_price of per ticket
public static int sum_price = 0; //total turnover
private int pre_counts;
public Sell_ticket5(String name){
super(name);
pre_counts = this.tickets;
}
//verify account of buyer
private void accountVerify(){
System.out.printf("Thread %s: account is valid!\n", this.getName());
}
//previous preparation
private 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 synchronized
private void sellTicket(){
this.accountVerify();
this.pre_Jobs();
//synchronized (i_flag) //error integer variable is not an object
synchronized (s_flag){ //try to lock the object i_flag
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();
}
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个静态变量, 它只属于类本身.
实在上, 如果为1个非静态的方法f() 加上synchronized 修饰
synchronized void f(){
....
}
就相当于
void f(){
synchronized (this){
....
}
}
只有该对象的线程调用该方法才是互斥的.
也就是获取对象本身的锁.
而为1个静态方法g()方法加上synchronize 修饰.
实际上就是为类本身上锁, 该类所有对象的线程调用该方法都是互斥的.
关于java线程同步其实并不复杂. 个人觉得比oracle的锁还好理解一点. 关键就是熟悉synchronized 的用法.