同步语法synchronized

发表时间:2017-09-05 14:42:50 浏览量( 21 ) 留言数( 0 )

学习目标:

1、了解同步的方法的使用

2、什么情况下需要考虑线程安全

3、如何保证线程安全


教学过程

1、我们来看一下我们昨天例子。

public class Num {

	private int mycount = 0;

	//多个线程操作这个方法,默认没有做同步机制的。
	public  int add() {
		return mycount++;
	}

	public int getCount() {
		return mycount;
	}

}

    

线程类:

public class Thread1 extends Thread {

	public Num num;

	public Thread1(Num num) {
		this.num = num;
	}

	public void run() {

		for (int i = 0; i < 100; i++) {
			System.out.println("i="+num.add());
		}

	}

}

运行类:

public class Run1 {
	
	public static void main(String[] args) throws InterruptedException {
		
	    Num num=new Num();
		
		Thread1 thread1=new Thread1(num);
		Thread1 thread2=new Thread1(num);
		Thread1 thread3=new Thread1(num);
		
		thread1.start();
		thread2.start();
		thread3.start();
		
		Thread.sleep(2000);
		
		System.out.println(num.getCount());
		
		
	}

}


 多运行几次,观察结果,会发现结果并不是每次都是一样,所以我们可以说这个方法不是线程安全的。

attcontent/7de8245c-34a1-4a92-9801-0e6a17f72ae0.png

    为什么这个类最后得到的结果会产生不一致呢?之前我们解析过了counter++方法事实上时分为三个步骤的,再第一个线程取值的时候,也许这个值已经给另外一个线程修改了,但是第一个线程并不知道值已经改变了,操作了加一之后,把这个值保存到内存,这时就会覆盖了另外一个线程的值。

   那么我们如果解决呢。那么如果我们只需要一个线程进入这个方法,其他线程必须再外面等待他处理完毕后才能进入继续处理。

	//添加了synchronized就可以了,一次只能一个线程进入
	public synchronized  int add() {
		return mycount++;
	}

    因为其他线程在临界区中的时候每条线程对该临界区的访问都会互斥地执行,这种同步属性就称为互斥。由于这个原因,线程获取到的锁经常称为互斥锁。

同步也表现出可见性,该属性能够保证一条线程在临界区执行的时候总是能看到共享变量最近的修改。当进入临界区时,它从主存中读入这些变量,离开时把这些变量的值写入主存。每一次都只能进入一个线程,而且必须等这个线程处理完毕后你才能看到这个值,因为线程会把主存的变量拷贝一份,修改完毕后才会再一次的写入到内存中。

实例代码,一个线程执行i++,最后需要计算完毕后才能看到这个值的变化。


二、锁定代码块

   JVM会给每一个对象都有一个锁,那么再上面的例子中写在方法前面,表示锁定的时当前这个对象,我们也可以使用synchronized的另外一种写法,直接锁定某一个对象,

public class Run2 {

	public static void main(String[] args) throws InterruptedException {

		Object object = new Object();

		Num num = new Num();

		Runnable r = () -> {

			synchronized (object) {
				for (int i = 0; i < 100; i++) {
					num.add();
				}
			}

		};

		Thread thread1 = new Thread(r);
		Thread thread2 = new Thread(r);
		Thread thread3 = new Thread(r);

		thread1.start();
		thread2.start();
		thread3.start();

		Thread.sleep(1000);

		System.out.println(num.getCount());

	}
}


三、锁的可重入性

   可重入锁:自己可以再次获取自己的内部的锁。比如有线程A获得了某对象的锁,此时这个时候锁还没有释放,当其再次想获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。

   可重入性表明了锁的分配机制:基于线程的分配,而不是基于方法调用的分配。举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。可重入锁也支持在父子类继承的环境中。

实例代码:

public class Child extends Father{  

    public static void main(String[] args) {  

        new Child().doSomething();      

    }  

      

    public synchronized void doSomething(){  

        System.out.println("child");  

        super.doSomething();  

    }  

  

}  

  

class Father{  

    public synchronized void doSomething(){  

        System.out.println("Father");  

    }  

}  

    上述代码,子类调用doSomething方法时,除了获得自身类锁的同时还会获得父类的锁,如果内置锁不可重入,那调用super.doSomething时就会发生死锁。所以重入就是,你拿了锁,再调用该锁包含的代码可以不用再次等待拿锁。


四、总结

   1、写在静态方法前面的区别如果这个方法是一个普通方法,那么锁定的是当前的对象,如果是一个静态方法,那么锁定的是这个类对象。

   2、锁是可以重入的,而且支持继承,还需要注意的时如果程序发生了异常,那么锁时会自动释放的,这一点和之后我们学习的lock时不一样的。

   最后我们对锁做一个总结:每一个Java 对象都和一个监听器相关联,监听器是一个互斥(每次只允许一条线程在临界区中执行构造,它阻止多条线程同时在临界区中并发执行。在线程可以进入临界区之前,它需要锁住监听器。如果这个监听器已经上锁,在监听器释放之前这条线程会一直阻塞(因为其他线程正在使用临界区)。