何谓同步问题呢?就是比如银行开了一个账户,那么如果我在ATM机上执行两个操作,分别是存入了200元,然后取出100元,原来账户中假设有1000元,那么此时操作完上述两步骤之后账户中总共有1100元。在银行账户上操作只有等当前步骤操作完才能进行下一个步骤,这就是同步。如果不同步呢,那么可能存入钱的时候同时取了钱,可能取完钱账户数据更新为900元,那么这就是因为不同步产生的脏数据。接下来我们通过举例的方式进行讲解同步问题以及相应的解决方法。首先我们本文采用的账户类Student类,该类中分别有存钱取钱两个操作。代码:
package person;
public class Student {
public int k=0;
public int total;
public int perMonth;
public String name;
public void saveMoney(Student s) {
// TODO Auto-generated method stub
try {
Thread.sleep(0);;
}
catch(InterruptedException e) {
e.printStackTrace();
}
s.total=s.total+2*(s.perMonth);
System.out.println(s.name+"存了"+2*s.perMonth+"当前总共拥有"+s.total+"元");
}
public void getMoney(Student s)
{
try {
Thread.sleep(0);;
}
catch(InterruptedException e) {
e.printStackTrace();
}
s.total=s.total-s.perMonth;
System.out.println(s.name+"取了"+s.perMonth+"当前总共拥有"+s.total+"元");
}
}
1.脏数据的产生
假如有多个存钱的线程以及多个取钱的线程,那么账户的余额便是线程间的共享数据,如果线程间没有同步,而是乱序进行,可能出现几个线程同时对数据进行改变,那么就会产生脏数据。具体代码如下:
package waytobuildthread;
import person.Student;
public class RunnableTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
final Student s=new Student();
s.total=1000;
s.perMonth=200;
s.name="Tom";
System.out.println("Tom最开始拥有"+s.total+"元");
Thread[] save=new Thread[10];
Thread[] get=new Thread[10];
for(int i=0;i<10;i++)
{
Thread t1= new Thread(){
public void run(){
s.saveMoney(s);
try {
Thread.sleep(1000);;
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
save[i]=t1;
}
for(int i=0;i<10;i++)
{
Thread t2= new Thread(){
public void run(){
s.getMoney(s);
try {
Thread.sleep(1000);;
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
};
t2.start();
get[i]=t2;
}
//等待线程结束
for(Thread t:save)
{
try
{
t.join();
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
//等待线程结束
for(Thread t:get)
{
try
{
t.join();
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
System.out.println("最后Tom总共拥有"+s.total+"元");
}
}
运行程序,结果如下:
Tom最开始拥有1000元
Tom存了400当前总共拥有1800元
Tom存了400当前总共拥有1800元
Tom存了400当前总共拥有1800元
Tom存了400当前总共拥有2200元
Tom存了400当前总共拥有2600元
Tom存了400当前总共拥有3000元
Tom存了400当前总共拥有3400元
Tom存了400当前总共拥有4200元
Tom存了400当前总共拥有3800元
Tom取了200当前总共拥有4400元
Tom取了200当前总共拥有4200元
Tom存了400当前总共拥有4600元
Tom取了200当前总共拥有3600元
Tom取了200当前总共拥有3800元
Tom取了200当前总共拥有4000元
Tom取了200当前总共拥有3400元
Tom取了200当前总共拥有3200元
Tom取了200当前总共拥有3000元
Tom取了200当前总共拥有2800元
Tom取了200当前总共拥有2600元
最后Tom总共拥有2600元
根据上述结果可以知道本来应该余额是3000元,结果只剩2600元,出现了脏数据。从第一组数据就是1800元,可能出现的情况就是一个存400的线程正在进行,另一个存400的线程紧接其后,还没来得及显示余额,第二笔款便存进去了。
很显然,脏数据的存在是不可取的,因此应该使用相应的解决方法将线程间设置为同步问题,接下来就讲述几个解决同步问题的方法。
2.synchronized代码块
使用同步代码块即可保证代码块内的程序是同步的,即当前有线程在执行代码块中的程序,其它程序只能处于等待状态。这样就不会出现因多个线程同时修改数据而产生脏数据。以下是代码:
package waytobuildthread;
import person.Student;
public class RunnableTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
final Object obj=new Object();
final Student s=new Student();
final Student jack=new Student();
s.total=1000;
s.perMonth=200;
s.name="Tom";
System.out.println("Tom最开始拥有"+s.total+"元");
Thread[] save=new Thread[10];
Thread[] get=new Thread[10];
for(int i=0;i<10;i++)
{
Thread t1= new Thread(){
public void run(){
synchronized(obj)
{
s.saveMoney(s);
}
try {
Thread.sleep(1000);;
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
save[i]=t1;
}
for(int i=0;i<10;i++)
{
Thread t2= new Thread(){
public void run(){
synchronized(obj)
{
s.getMoney(s);
}
try {
Thread.sleep(1000);;
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
};
t2.start();
get[i]=t2;
}
//等待线程结束
for(Thread t:save)
{
try
{
t.join();
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
//等待线程结束
for(Thread t:get)
{
try
{
t.join();
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
System.out.println("最后Tom总共拥有"+s.total+"元");
}
}
以上代码是先创建Object对象,注意标明对象不可变,然后该对象作为修饰代码块的关键字synchronized括号中的参数。运行上述程序,得到如下结果:
Tom最开始拥有1000元
Tom存了400当前总共拥有1400元
Tom存了400当前总共拥有1800元
Tom存了400当前总共拥有2200元
Tom存了400当前总共拥有2600元
Tom存了400当前总共拥有3000元
Tom存了400当前总共拥有3400元
Tom存了400当前总共拥有3800元
Tom存了400当前总共拥有4200元
Tom取了200当前总共拥有4000元
Tom取了200当前总共拥有3800元
Tom取了200当前总共拥有3600元
Tom存了400当前总共拥有4000元
Tom存了400当前总共拥有4400元
Tom取了200当前总共拥有4200元
Tom取了200当前总共拥有4000元
Tom取了200当前总共拥有3800元
Tom取了200当前总共拥有3600元
Tom取了200当前总共拥有3400元
Tom取了200当前总共拥有3200元
Tom取了200当前总共拥有3000元
最后Tom总共拥有3000元
根据上述结果可以看出,每个线程都是有条理进行的,不会出现几个线程抢着修改数据的情况,因此最终结果也是与预计的相同,达到了同步的效果。
3.synchronized方法
还可以使用关键字synchronized修饰方法,这样当有线程正在使用该方法时,会使其它线程进入等待状态,从而达到同步,修改Student类如下:
package person;
public class Student {
public int k=0;
public int total;
public int perMonth;
public String name;
public synchronized void saveMoney(Student s) {
// TODO Auto-generated method stub
try {
Thread.sleep(0);;
}
catch(InterruptedException e) {
e.printStackTrace();
}
s.total=s.total+2*(s.perMonth);
System.out.println(s.name+"存了"+2*s.perMonth+"当前总共拥有"+s.total+"元");
}
public synchronized void getMoney(Student s)
{
try {
Thread.sleep(0);;
}
catch(InterruptedException e) {
e.printStackTrace();
}
s.total=s.total-s.perMonth;
System.out.println(s.name+"取了"+s.perMonth+"当前总共拥有"+s.total+"元");
}
}
测试程序如同普通线程测试程序一样:
package waytobuildthread;
import person.Student;
public class RunnableTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
final Object obj=new Object();
final Student s=new Student();
final Student jack=new Student();
s.total=1000;
s.perMonth=200;
s.name="Tom";
System.out.println("Tom最开始拥有"+s.total+"元");
Thread[] save=new Thread[10];
Thread[] get=new Thread[10];
for(int i=0;i<10;i++)
{
Thread t1= new Thread(){
public void run(){
s.saveMoney(s);
try {
Thread.sleep(1000);;
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
};
t1.start();
save[i]=t1;
}
for(int i=0;i<10;i++)
{
Thread t2= new Thread(){
public void run(){
s.getMoney(s);
try {
Thread.sleep(1000);;
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
};
t2.start();
get[i]=t2;
}
//等待线程结束
for(Thread t:save)
{
try
{
t.join();
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
//等待线程结束
for(Thread t:get)
{
try
{
t.join();
}
catch(InterruptedException e)
{
e.printStackTrace();
}
}
System.out.println("最后Tom总共拥有"+s.total+"元");
}
}
运行结果如下:
Tom最开始拥有1000元
Tom存了400当前总共拥有1400元
Tom存了400当前总共拥有1800元
Tom存了400当前总共拥有2200元
Tom存了400当前总共拥有2600元
Tom存了400当前总共拥有3000元
Tom存了400当前总共拥有3400元
Tom存了400当前总共拥有3800元
Tom存了400当前总共拥有4200元
Tom存了400当前总共拥有4600元
Tom存了400当前总共拥有5000元
Tom取了200当前总共拥有4800元
Tom取了200当前总共拥有4600元
Tom取了200当前总共拥有4400元
Tom取了200当前总共拥有4200元
Tom取了200当前总共拥有4000元
Tom取了200当前总共拥有3800元
Tom取了200当前总共拥有3600元
Tom取了200当前总共拥有3400元
Tom取了200当前总共拥有3200元
Tom取了200当前总共拥有3000元
最后Tom总共拥有3000元
4.Lock锁
JDK5新增加了一个Lock接口以及它的一个实现类ReentrantLock(重入锁),Lock也可以用来实现多线程的同步。下面便将该例子通过Lock来实现同步,将Student类修改如下,测试程序不变动:
package person;
import java.util.concurrent.locks.*;
public class Student {
public int k=0;
public int total;
public int perMonth;
public String name;
Lock lock=new ReentrantLock();//先建立一个lock对象
public void saveMoney(Student s) {
// TODO Auto-generated method stub
try {
Thread.sleep(0);;
}
catch(InterruptedException e) {
e.printStackTrace();
}
try
{
lock.lock();
s.total=s.total+2*(s.perMonth);
System.out.println(s.name+"存了"+2*s.perMonth+"当前总共拥有"+s.total+"元");
}
finally
{
lock.unlock();
}
}
public void getMoney(Student s)
{
try {
Thread.sleep(0);;
}
catch(InterruptedException e) {
e.printStackTrace();
}
try
{
lock.lock();
s.total=s.total-s.perMonth;
System.out.println(s.name+"取了"+s.perMonth+"当前总共拥有"+s.total+"元");
}
finally
{
lock.unlock();//释放锁
}
}
}
运行测试程序结果如下:
Tom最开始拥有1000元
Tom存了400当前总共拥有1400元
Tom存了400当前总共拥有1800元
Tom存了400当前总共拥有2200元
Tom存了400当前总共拥有2600元
Tom存了400当前总共拥有3000元
Tom存了400当前总共拥有3400元
Tom存了400当前总共拥有3800元
Tom存了400当前总共拥有4200元
Tom存了400当前总共拥有4600元
Tom取了200当前总共拥有4400元
Tom存了400当前总共拥有4800元
Tom取了200当前总共拥有4600元
Tom取了200当前总共拥有4400元
Tom取了200当前总共拥有4200元
Tom取了200当前总共拥有4000元
Tom取了200当前总共拥有3800元
Tom取了200当前总共拥有3600元
Tom取了200当前总共拥有3400元
Tom取了200当前总共拥有3200元
Tom取了200当前总共拥有3000元
最后Tom总共拥有3000元
5.比较synchronized和Lock
1.用法不一样:在需要同步的对象上加入synchronized控制,synchronized既可以加在方法上,也可以加在特定代码块中。而Lock需要显示地指定起始位置和终止位置。synchronized是托管给JVM执行的,而Lock的锁定是通过代码实现的。
2.性能不一样:synchronized会随着竞争越来越激烈的情况下性能下降很快,而Lock性能基本保持不变。
3.锁机制不同:synchronized获得锁和释放的方式都是在块结构中,当获取多个锁的时候,必须以相反的顺序释放,并且是自动解锁,不会因为出了异常导致锁没有释放从而引发死锁现象。而Lock需要手动释放,并且必须在finally块中释放,否则会产生死锁的发生。Lock还提供了更强大的功能,它的tryLock()方法可以采用非阻塞的方式去获取锁。
注意:两种锁不要一起使用。