什么是线程同步,先来举一个小例子吧:
我现在银行账户里有3000块,一天我去柜台机那里想取款2000元,我向柜台机完成了操作后,机器自动检查我的账户,看看够不够钱,发现够,于是准备吐钱(但又还没吐)。就在这个时候,我的老婆在另一个银行也准备取款,她也想取2000,她输入后,机器也去检查我的账户够不够钱,(因为银行还没吐)发现也够,于是也准备吐钱。就这样,我们在3000元的账户中取了4000元。
我和老婆就相当于两个线程,我们在用同一个方法访问相同的资源。这个时候,就很容易出现数据前后不一致的问题。
所以,我们对线程访问同一份资源的多个线程之间来进行协调的东西,叫做线程同步。
那么要怎么解决这个问题呢?
当我在取款的过程中,也就是在调用方法的过程中,这个账户归我独占。就好比两个人不能占一个坑的时候。
现在我们用程序来体现两个线程同一个方法访问相同的资源:
public class TestSync implements Runnable { //主类直接实现runnable接口 Timer timer = new Timer(); //注意,这个是个引用类型的成员变量 public static void main(String[] args) { TestSync test = new TestSync(); //只new了一个TestSync对象,所以这个Timer对象也只有一个 Thread t1 = new Thread(test); Thread t2 = new Thread(test); t1.setName("t1"); t2.setName("t2"); t1.start(); t2.start(); } public void run() { //既然实现了runnable接口就要重写run()方法 timer.add(Thread.currentThread().getName()); }}class Timer { //类Timer private static int num = 0; //定义一个静态的成员变量 public void add(String name) { num++; try { Thread.sleep(1); } catch (InterruptedException e) { } System.out.println(name+"你是第"+num+"个使用Timer的线程"); }}
内存图:
得出来的答案是:
t1你是第2个使用Timer的线程
t2你是第2个使用Timer的线程显然和想象的不一样,因为比如t1这个线程先开始,它num++之后,sleep了一下下,就这一下下,t2也开始了,然后num++,此时num(注意num位static,是公用的内存)已经为2了,可见,线程在执行这个add方法的过程之中,被另外一个线程打断了。注意,即使不写sleep,这个线程也可能被打断,sleep只是放大这个效果。就像检查账户和吐钱这个操作中间被打断了。
那么怎么解决呢?在执行这句话的过程之中,把当前的对象锁住就行了。
用关键字synchronized(this) 叫锁定当前对象
在执行这个关键字后面两个大括号里面的内容的过程之中,一个线程执行的过程之中,不会被另一个线程打断。当然这时这个成员变量num也锁定了。
还有一种写法:
public synchronized void add(String name) {
}
意思是在执行这个方法的过程中,锁定当前对象。
也就是说,synchronized这个关键字会锁定某一段代码,它的内部含义是当执行这段代码的时候锁定当前对象,如果另外一个人如果也想访问我这个对象的话,他只能等着。
这就是锁
讲了锁之后,多线程还会带来其他的问题,比如一个典型的死锁
啥叫死锁?
现在你有一个线程,左边的那个,有个方法,它在执行的过程中需要锁定某个对象,除此以外,它还要锁定另外一个对象,它要锁定两个对象才能把整个操作完成。 这个时候,另外一个线程,它也需要锁定两个对象,然后它首先锁定的是第二个(下面那个对象)。然后只要再拥有上面那个对象的锁,它就能继续完成了。显然,现在这两个个线程都完成不下去了,你得等我执行完了,我才可以放开锁,可是你不给我另一个锁,我也执行不完,我执行不完,你也执行不完。
现在用代码体现:
public class TestDeadLock implements Runnable { public int flag; static Object o1 = new Object(); static Object o2 = new Object();//两个静态对象,专门用来锁的 public void run() { //重写run() System.out.println("flag = "+flag); if(flag == 1) { synchronized(o1) { //锁o1 try { Thread.sleep(500); }catch (InterruptedException e) { e.printStackTrace(); } synchronized(o2) { //又锁o2,所以这个线程的这个方法一定要锁定两个对象才能结束 System.out.println("1"); } } } else if(flag == 2){ synchronized(o2) { //锁o2 try { Thread.sleep(500); }catch (InterruptedException e) { e.printStackTrace(); } synchronized(o1) { //又锁o1,所以这个线程的这个方法一定要锁定两个对象才能结束 System.out.println("2"); } } } } public static void main(String[] args) { TestDeadLock td1 = new TestDeadLock(); TestDeadLock td2 = new TestDeadLock(); td1.flag = 1; td2.flag = 2; Thread thread1 = new Thread(td1); Thread thread2 = new Thread(td2); thread1.start(); thread2.start(); }}
如何避免呢~所以尽量只锁定一个对象,或者你干脆一次锁起所有的对象。
锁定了一个对象,锁定了一个方法,另外一个线程绝对不能执行这段话但是它可以访问没有锁定的方法。
public class TT implements Runnable { int b = 100; public synchronized void m1() throws Exception { b = 1000; Thread.sleep(5000); System.out.println("b = "+b); } public void m2() { System.out.println(b); } public void run() { try { m1(); } catch(Exception e) { e.printStackTrace(); } } public static void main(String[] args) throws Exception { TT tt = new TT(); Thread t = new Thread(tt); t.start(); Thread.sleep(1000);//保证线程tt已经开始 tt.m2(); }}
如果主线程在t线程执行m1()的时候不能访问m2(),那么结果应该是100.(其实还不太明白),因为它那个t线程的m1()给锁住了,还没结束,所以m2()看到的是100.但是,结果是1000,说明主线程可以访问没有被锁定的方法m2()。
我们再改一下这个例子,更好得理解:
public class TT implements Runnable { int b = 100; public synchronized void m1() throws Exception { b = 1000; Thread.sleep(5000); System.out.println("b = "+b); } public void m2() throws Exception { Thread.sleep(2500); b = 2000; } public void run() { try { m1(); } catch(Exception e) { e.printStackTrace(); } } public static void main(String[] args) throws Exception { TT tt = new TT(); Thread t = new Thread(tt); t.start(); tt.m2(); }}
那么会输出什么呢?—————b = 2000;
这也说明m1()执行的过程中被打断了。
如果你在m2()前面也上锁:public synchronized void m2() 那么输出的就是2000了,因为在m1()执行的过程中,加锁了的m2()也想锁定这个对象,但是它锁不住,所以不能够改变b,只能能m1()执行完了。
所以,别的线程可以自由访问非同步的方法,并且可能对你已经同步的方法产生影响。 一般就是比如说有个账户,有读的方法也有写的方法,你看读的方法它就不用加锁,因为你可以很多人一起读,这没事,但写的方法就要加锁了,他在写的时候,别人不能写。