多线程(二)
- 线程同步
- 死锁
- 深入了解synchronized
线程同步
为了说明白什么是线程同步,我们来看一个小故事:
比如说你赚了点钱,在银行里存了点钱,不多也不少,刚好3000块钱,然后银行给你一个银行卡和一本存折。
有一天,你突然有急事想要用钱,你便拿着存折去银行柜台取钱,这时候工作人员问你打算取多少钱呀,不多,刚好取2000块钱,然后工作人员把这要求输入电脑,这时电脑会去检查你的账户够不够2000块钱,电脑一检查,诶,你有。正常情况下,工作人员便把钱给你,最后把你账户里的钱减为1000块钱。
但是,这时当电脑检查到你有2000块钱,现在已经准备把钱给你,然后更新账户里的钱。正在这个阶段
,你的老婆拿你的银行卡去ATM机上取钱,取的也是2000块钱,然后ATM机也得去检查你账户的钱,结果一检查,够2000块钱(前面你取钱的时候还没取到,账户的信息也没更新),接着ATM机就把钱吐出来了,然后账户里的钱更新为1000块钱。
然后你取钱的过程继续执行,工作人员给你两千,电脑更新账再次更新
为1000块钱。最后你和你老婆都取了2000,账户里还有1000。
这是怎么回事呢?你和你老婆好比是两个线程,现在这两个线程在执行一个取款方法的过程中,这两个线程同时访问同一个资源,如果协调不好,就会出现上面那种情况。所以,我们对线程访问同一个资源的多个线程之间来进行协调的这个东西,叫线程同步
。
那我们要如何解决这个问题呢?上面的问题就是,你在取款的时候,被你老婆打断了,还比你先取完了。所以解决办法就是,当你在取款的过程中,也就是在调用某个方法的过程中,对不起,在这段时间,谁也不能动我的账户信息(资源),这个账户归我独占,其他线程不能访问
,就好比两个人不能上同一个坑一样。
我们用一个小例子来看看:
TestSync.java
package Thread;
public class TestSync implements Runnable {
Timer timer = new Timer();
public static void main(String[] args) {
TestSync test = new TestSync();
Thread t1 = new Thread(test);
Thread t2 = new Thread(test);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
}
public void run(){
timer.add(Thread.currentThread().getName());
//拿到当前线程的名字,并传给add方法
}
}
class Timer{
private static int num = 0;//计数
public void add(String name){
num ++;//当add方法被调用的时候,num就往上增
try {
Thread.sleep(1);//哪个线程在执行就睡眠1ms
}
catch (InterruptedException e) { }
System.out.println(name+", 你是第"+num+"个使用timer的线程");
}
}
思考一下,输出来的结果是怎么样?是下面那样吗
其实不然, 上面程序输出的结果为:
这是怎么回事呢?这个执行过程是这样的。比方说第一个线程,已经开始访问timer对象的add方法了,执行到num++的时候,num原来是0,现在变成1,然后执行到sleep方法时,第一个线程睡眠了。
接着第二个线程开始执行,这时候num已经变成1了,当第二个线程执行到add方法,访问的是同一个对象,所以也是同一个num。则num由原来的1变成2
,然后第二个线程开始睡眠。
接着第一个线程醒了过来,然后他开始打印,“t1,你是第2个使用timer的线程”(这时候num变成了2),接着t2醒来,打印的也是第二个。
问题就出在这,问题就出在,第一个线程在执行add方法的过程中,被第二个线程给打断了。上面程序写了sleep方法,就是为了这个效果(一个线程的执行过程中被另外一个线程打断了
)被看的更清楚,如果不写sleep方法,可能打印出来的是正确的结果,但是,以后难免不会出问题。
那对于上述问题怎么解决呢?特别简单,在这行add方法的过程中,把对象锁住
就行了,怎么锁呢?看下面代码:
public synchronized void add(String name){
synchronized (this) {
num ++;
try { Thread.sleep(1);}
catch (InterruptedException e) { }
System.out.println(name+", 你是第"+num+"个使用timer的线程");
}
}
synchronized (this)锁定当前对象,意思就是,在执行synchronized下面语句的过程之中,
一个线程的执行过程之中,不会被另外一个线程打断
。当一个线程已经进入到锁定的区域里边了,你放心,不可能有另外一个线程也跑进来。
上面的 synchronized (this)是一个非常直接的写法,还有一种比较简便的写法:
public synchronized void add(String name) {
num++;
try {
Thread.sleep(1);
} catch (InterruptedException e) {
}
System.out.println(name + ", 你是第" + num + "个使用timer的线程");
}
}
直接将synchronized写到public后面,意思就是,
在执行add方法的过程中,锁定当前对象
。
来分析一下上面程序的执行过程。t1开始执行,调用add方法,num++,然后t1睡着了,睡着了没关系,他睡着了还抱着这把锁呢,别人进不来,必须等t1执行完了,才能轮到别人执行。
在Java语言中,引入对象互斥锁的概念,保证共享数据操作的完整性。每个对象都对应于一个可称为“互斥锁”的标记,这个标记保证在任一时刻,只能有一个线程访问该对象
。
关键字synchronized来与对象的互斥锁联系。当某个对象synchronized修饰时,表明该对象在任一时刻只能有一个线程访问
。关键字synchronized锁定某一段代码,当执行这段代码的过程之中,锁定当前对象,另外一个线程也想访问这段代码的话,他只能等着,等前面那个线程执行这段代码了,锁自然而然也就打开了,锁开了之后才能进的来。
synchronized的使用方法:
synchronize(this){
try { Thread.sleep(1);}
catch (InterruptedException e) { }
System.out.println(name+", 你是第"+num+"个使用timer的线程");
}
synchronize还可以放在方法声明中,表示整个方法为同步方法
。例如:
synchronize public void add(String name){ ...}
死锁
当我们讲了锁之后,多线程还会带来其他问题,一个典型的问题,就是死锁
。那么死锁的原理是怎么样的呢?
当线程a执行的过程之中,线程a需要锁定对象c,但是,线程a还得要锁住另外一个对象d才能继续往下执行。也就是说线程a需要锁定两个对象,才能够把整个操作完成。
此时,另外一个线程b也需要锁定两个对象才能往下执行,他首先锁定的是对象d。线程a锁住了对象c,他还需要拥有对象d的锁就能往下执行,而线程b首先锁住了对象d,如果再能拥有对象c的锁,他就能继续完成了。可是,最后这两个线程都执行不下去了,因为他们等的东西都被对方给锁住了。
那什么时候能释放锁呢,那就得等其中一个线程执行完了,但是这样就成了悖论了。你得等我执行完了放开锁,可是你不给我另外一个我也执行不完,我执行不完,你也别想执行完,这就是死锁
。
下面来看一个例子:
TestDeadLock.java
package Thread;
public class TestDeadLock implements Runnable {
public int flag = 1;
static Object o1 = new Object(), o2 = new Object();
public void run() {
System.out.println("flag=" + flag);
if(flag == 1) {
synchronized(o1) {
//把o1给锁定
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized(o2) {
//这要他再能锁住o2,就能继续完成了
System.out.println("1");
}
}
}
if(flag == 0) {
synchronized(o2) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized(o1) {
System.out.println("0");
}
}
}
}
public static void main(String[] args) {
TestDeadLock td1 = new TestDeadLock();
TestDeadLock td2 = new TestDeadLock();
td1.flag = 1;
td2.flag = 0;
Thread t1 = new Thread(td1);
Thread t2 = new Thread(td2);
t1.start();
t2.start();
}
}
上面代码能完成吗?完不成
输出下面这个之后就再也不动了
那我们要怎么解决这个问题呢?怎么避免死锁?其实很简单,
线程获取锁的顺序要一致
。即严格按照先获取o1,再获取o2的顺序,改写 if(flag == 0)方法如下:
if(flag == 0) {
synchronized(o1) {
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized(o2) {
System.out.println("0");
}
}
}
深入了解synchronized
为了深入了解一下synchronized关键字
,我们来看一个小程序:
public class TT {
int b = 100;
public synchronized void m1() throws Exception{
b = 10000;
Thread.sleep(5000);
System.out.println("b = " + b);
}
public void m2(){
System.out.println(b);
}
思考一下一个问题,当m1方法执行的过程之中,m2能够执行吗?就是说,比方有一个线程在执行m1方法,另外一个线程能够执行m2这个方法吗?是不是得m1执行完解锁之后才能执行呢?
那具体是不是呢?我们来把程序补全一下:
package Thread;
public class TT implements Runnable {
int b = 100;
public synchronized void m1() throws Exception{
//Thread.sleep(2000);
b = 1000;
Thread.sleep(5000);
System.out.println("m1方法的b = " + b);
}
public void m2(){
System.out.println("m2方法的b=" + 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();
//这个线程开始执行,然后会睡5s,在这个时间里
//我们在main主线程里边,我们去访问一下m2方法
Thread.sleep(1000);
tt.m2();
}
}
如果主线程执行出来b=100的话,那就说明在m1方法的执行过程中,m2不可以执行。为什么呢?因为m1方法执行过程中将b的值改成1000了,但是他没有解锁,m2不能执行,所以b看到的是100。
问题来了,m2方法中的b输出是多少呢?真的是100吗?还是1000呢?我们来看看结果:
注意
,synchronized 锁定当先对象,只是针对m1方法里边的代码,也就是说另外一个线程绝对不可能执行那段代码,但是有可能执行其他的代码。就是说,m1方法被锁定了,被同步了,他锁定当前对象;但是另外一个线程完完全全访问那种没有锁定的方法(m2)。否则的话,m2只能看到100而不是1000.
好好消化一下上面的代码,消化好了继续往下看,我把上面的程序改一下:
public class TT implements Runnable {
int b = 100;
public synchronized void m1() throws Exception{
b = 1000;
Thread.sleep(5000);
System.out.println("m1方法的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();
System.out.println("m2方法的b=" + tt.b);
}
}
思考一下,现在b又是多少呢?1000还是2000
我们先来再次理解一下synchronized关键字。他锁定了一个对象,但不是说完全的锁定了,不是说其他任何的线程,任何的方法都不能访问。保证同一时间只有一个线程进入到方法体里边,但是不保证其他线程会不会进到另外一个方法里边。
,像m2这个方法,他可以执行。好了,我们来看一下结果:
我们来分析一下他的执行过程:
过程:
main
方法开始执行,当main方法
执行到t.start();
的时候,另外一个线程开始执行,这个线程执行的是run()
方法,也就是m1
这个方法。m1
拿到这把锁,把b
设置成了1000。可是m2
不用得到这把锁就能执行,所以把b
设成了2000,既然m2
把b
设置成2000了,然后tt.m2()
,打印出来的tt.b
的值当然是2000.
接着m1
继续执行,然后睡眠,打印b
的值,打印出来的也是2000,刚刚m2
方法就把b变成了2000。
输出的是2000,m1里的b被改掉了。所以说,b = 100;是一个资源,这个资源能不能好好地被访问,能不能正确的上锁。就好比我们刚开始说的账户里的钱,能不能保证前后一致?我们就得
把访问这个资源的所有访问的方法都考虑到,每个方法是不是该设成同步的都要考虑到
。
上面程序,既然m1方法能改b的值,m2方法也能改b的值,两个方法都改了同一个值。他们就一定会产生冲突,你只给一个方法加了锁是不行的,必须把m2也加锁
。
public synchronized void m2() throws Exception { ...}
那给m2加锁之后的结果会是怎样的呢?你放心,这次绝对是1000
为什么m2也是1000呢?不应该是2000吗?我们来分析一下他执行的过程。
过程:
main方法
开始执行,当main方法
执行到t.start();
的时候,另外一个线程开始执行,这个线程执行的是run()
方法,也就是m1
这个方法。接下来main方法
继续往下执行,执行的是:
tt.m2();
System.out.println("m2方法的b=" + tt.b);
这两行代码。首先tt.m2()
,就是执行m2
方法,当m2
这个方法被执行的时候,他就锁定了当前这个对象,拿到锁之后自己睡眠2.5s,然后把b
设成2000。
接下来,tt.m2()
执行完了,m1
才有可能执行,因为m2
执行完了,那个锁才会被释放。这时候m1
执行,把b
设成1000,然后m1
开始睡眠,现在b
的值为1000。接下来才打印tt.b
,所以m2
打印出来的b
是1000。
总的来说,这个程序就是,m2
执行完了,m1
执行一句,然后才开始打印tt.b
。所以最后的结果都是1000
在强调一下:加锁这个东西,你写一个同步的东西,是挺困难的一件事。因为
每一个方法要不要同步
,你都需要考虑的非常清楚,如果一个方法做了同步,另外一个方法没做同步,那么,记住一点,别的线程可以自由的访问没有同步的方法,并且可能会对你同步的方法产生影响
。
如果你要保护好需要同步的对象的话,你必须对访问这个对象的所以的方法要仔细的考虑加不加同步
,加了同步,很有可能效率就会变低;不加同步,有可能产生数据不一致的现象。
现在我们回到最开始那个小程序的问题,当m1方法执行的过程之中,m2能够执行吗?答案是:能。但是在m2加了synchronize的话,m2就不能执行了。
多线程(二)就先写到这啦,不写不知道,一写吓一跳,要写的知识太多了,我还以为两个板块就能写完的,看来我还是太天真了,当然,我也不可能面面俱到,里面没写到的知识或者不懂的(不过我感觉应该都懂吧,感觉我已经写的够明白了),大家可以在评论区留言。
多线程(三)传送门