Java复习(十二)—-多线程(二)

线程同步

为了说明白什么是线程同步,我们来看一个小故事:

比如说你赚了点钱,在银行里存了点钱,不多也不少,刚好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的线程");
    }
}

coding-Java复习(十二)----多线程(二)-

思考一下,输出来的结果是怎么样?是下面那样吗
coding-Java复习(十二)----多线程(二)-
其实不然, 上面程序输出的结果为:
coding-Java复习(十二)----多线程(二)-

  这是怎么回事呢?这个执行过程是这样的。比方说第一个线程,已经开始访问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){...}

死锁

当我们讲了锁之后,多线程还会带来其他问题,一个典型的问题,就是死锁。那么死锁的原理是怎么样的呢?
coding-Java复习(十二)----多线程(二)-

当线程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();

    }
}

上面代码能完成吗?完不成
输出下面这个之后就再也不动了coding-Java复习(十二)----多线程(二)-

那我们要怎么解决这个问题呢?怎么避免死锁?其实很简单,
线程获取锁的顺序要一致。即严格按照先获取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呢?我们来看看结果:
coding-Java复习(十二)----多线程(二)-

注意,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这个方法,他可以执行。好了,我们来看一下结果:
coding-Java复习(十二)----多线程(二)-
我们来分析一下他的执行过程:

过程
main方法开始执行,当main方法执行到t.start();的时候,另外一个线程开始执行,这个线程执行的是run()方法,也就是m1这个方法。m1拿到这把锁,把b设置成了1000。可是m2不用得到这把锁就能执行,所以把b设成了2000,既然m2b设置成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
coding-Java复习(十二)----多线程(二)-
为什么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就不能执行了。

多线程(二)就先写到这啦,不写不知道,一写吓一跳,要写的知识太多了,我还以为两个板块就能写完的,看来我还是太天真了,当然,我也不可能面面俱到,里面没写到的知识或者不懂的(不过我感觉应该都懂吧,感觉我已经写的够明白了),大家可以在评论区留言。

多线程(一)传送门
多线程(三)传送门

匿名

发表评论

匿名网友