Java多线程编程基础篇(二)-多线程同步关键字

一、多线程同步关键字-synchronized

1.概念

    synchronized保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证共享变量的内存可见性。当多个并发线程访问同一个对象object中的同步代码块时,一个时间内只有一个线程能够得到执行,另一个线程必须等到当前线程执行完这个代码块之后才能执行,但是其他的线程仍然能够访问该object中的非synchronized同步代码块。

2.使用方法

    java中每一个对象都可以作为锁,这是synchronized实现同步的基础:

  • 修饰实例方法:锁是当前实例对象
  • 修饰静态方法:锁是当前类的class对象
  • 修饰代码块:锁是括号里的对象

(1)修饰实例方法

    当synchronized作用于实例方法时,所示其实例对象。

    在下面的例子中定义了一个静态变量num,定义了一个incr()方法对num进行+1的操作,在run方法中是循环调用incr方法5次。最后在main方法中是创建了两个线程对num进行操作。在该方法中synchronized修饰的是实例方法incr,此时线程的锁便是实例对象instance。

    一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他的线程就无法获取该对象的锁,但是其他的对象还是可以访问该实例对象的其他非synchronized方法。

public class Test1 implements Runnable{
	
	public static int num = 0;
	
	public synchronized void incr() {
		num++;
		System.out.println("Thread-Id:"+Thread.currentThread().getId()+" num="+num);
	}
	
	@Override
	public void run() {
	    for(int i = 0; i< 5; i++) {
	    	incr();
	    }
	}
	
	public static void main(String[] args) throws InterruptedException {
		Test1 test1 = new Test1();
		Thread t1 = new Thread(test1);
		Thread t2 = new Thread(test1);
		
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println("num="+num);   /**num=10*/
		
	}
}

    Java多线程编程基础篇(二)-多线程同步关键字_第1张图片

在上面的例子中是两个线程共同操作一个对象,在下面的例子中可以看到两个线程操作的是不同的对象,这时候不同的对象有不同的锁,所以都会获取到各自的锁,这时候获取到的结果就不一定是10了,也许是比10小的数。这种情况下是不满足线程安全的。

public class Test1 implements Runnable{
	public static int num = 0;
	public synchronized void incr() {
		num++;
		System.out.println("Thread-Id:"+Thread.currentThread().getId()+" num="+num);
	}
	@Override
	public void run() {
	    for(int i = 0; i< 5; i++) {
	    	incr();
	    }
	}
	
	public static void main(String[] args) throws InterruptedException {
		Test1 test1 = new Test1();
		Test1 test2 = new Test1();
		Thread t1 = new Thread(test1);
		Thread t2 = new Thread(test2);
		
		t1.start();
		t2.start();
		//t1.join();
		//t2.join();
		//System.out.println("num="+num);   /**num=10??*/
		
	}
}
Java多线程编程基础篇(二)-多线程同步关键字_第2张图片

 (2)修饰静态方法

当synchronized作用与静态方法时,锁是当前类的class对象。

    由于静态方法是类成员,因此通过class对象锁可以控制静态成员的并发操作。如果一个线程A调用一个实例对象的非静态同步方法,线程B调用这个实例对象所属的类的静态同步方法,他们之间是不存在互斥的,因此两个线程占用的锁是不一样的。但是如果两个方法共同操作同一个静态变量num,那么还是需要考虑线程安全的问题。

public class Test1 implements Runnable{
	
	public static int num = 0;
	
	/**
	 * 非静态同步方法,占用的锁是当前实例对象的锁
	 */
	public synchronized void incr1() {
		num++;
	}
	
	/**
	 * 静态同步方法,占用的锁是当前类的锁
	 */
	public static synchronized void incr() {
		num++;
		System.out.println("Thread-Id:"+Thread.currentThread().getId()+" num="+num);
	}
	
	@Override
	public void run() {
	    for(int i = 0; i< 5; i++) {
	    	incr();
	    }
	}
	
	public static void main(String[] args) throws InterruptedException {
		Test1 test1 = new Test1();
		Test1 test2 = new Test1();
		Thread t1 = new Thread(test1);
		Thread t2 = new Thread(test2);
		
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println("num="+num);   /**num=10*/
	}
}
Java多线程编程基础篇(二)-多线程同步关键字_第3张图片

3.修饰静态块

    从下面的例子中可以看出,是将synchronized作用于一个给定的实例对象instance,当前实例对象就是锁对象,每次线程进入synchronized的代码块时就要求当前线程必须拿到instance实例的对象所,其他的线程就必须等待。

public class Test1 implements Runnable{
	
	public static int num = 0;
	
	static Test1 instance = new Test1();
	
	@Override
	public void run() {
	    synchronized(instance) {
	    	for(int i = 0; i< 5; i++) {
	    		num++;
	    		System.out.println("Thread-Id:"+Thread.currentThread().getId()+" num="+num);
	    	}
	    }
	}
	
	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(instance);
		Thread t2 = new Thread(instance);
		
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println("num="+num);   /**num=10*/
		
	}
}
Java多线程编程基础篇(二)-多线程同步关键字_第4张图片

二、多线程同步关键字-volatile

1.概念

    volatile是一个类型修饰符,他主要用于修饰被不同的线程访问和修改的变量,他是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。

    可以确保将变量的更新操作通知到其他的线程,当把变量声明为volatile类型,编译与运行时都会注意到这个变量是共享的,因此该变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

    在访问volatile变量时不会执行加锁操作,因此也就不会执行线程阻塞,他是一种比synchronized关键字更轻量级的同步机制。

2.volatile使用方法

class volatileTest implements Runnable {  
	  
    private volatile boolean flag = false;  
  
    public void run() {  
        for (int i = 0; i < 5; i++) {  
            try {
            	System.out.println("Thread-name:"+Thread.currentThread().getName()+" i:"+i+" flag="+flag);
            	//线程名为test1,则将flag值改为true,同时打印出新旧flag值
            	if("test1".equals(Thread.currentThread().getName())) {
            		boolean oldflag = flag;
            		flag = true;
            		System.out.println("test1 oldflag="+oldflag+" flag="+flag);
            	}else if("test2".equals(Thread.currentThread().getName())) {
            		//如果线程名为test2,则将flag值修改为false,同时打印出新旧flag值
            		boolean oldflag = flag;
            		flag = false;
            		System.out.println("test2 oldflag="+oldflag+" flag="+flag);
            	}
            	Thread.sleep(5000);
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
  
}  
  
public class Test {  
   
    public static void main(String[] args) {  
  
    	volatileTest test = new volatileTest();  
  
        Thread t1 = new Thread(test, "test1");  
        Thread t2 = new Thread(test, "test2");  
  
        t1.start();  
        t2.start();  
  
    }  
}  
Java多线程编程基础篇(二)-多线程同步关键字_第5张图片

在上面的例子中可以看到,当两个线程同时操作volatile修饰的变量flag时,两个线程每次拿到的都是flag的最新的值,但是这个时候对于获取到flag的值两个线程并没有一个互斥的操作,并不能保证操作的原子性。

3.volatile的特性

(1)volatile的可见性:当多个线程访问同一个变量时,一个线程修改了这个变量,其他得线程能够立即看的到值得改变,这一个特性对于volatile是满足的。

(2)volatile的原子性:不满足,对于下面的例子,正常期望的结果应该是每次运行都能得到值为10000,但是在实际的运行结果中发现每次得到的结果都不一样,因此volatile虽然保证了可见性,确保每次拿到的值都是最新的,但是不能确保操作的原子性。

public class Test {
    public volatile int inc = 0;
     
    public void increase() {
        inc++;
    }
     
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increase();
                };
            }.start();
        }
         
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}


三、synchronized与volatile的总结

1.synchronized关键字一次只允许一个线程持有某个特定的锁,因此可以使用该特性实现对共享数据的访问,一次只有一个线程能够使用该共享数据。

2.volatile主要是使变量的值在发生改变时能尽快的让其他的线程知道,使线程获取到数据是最新的。

3.volatile本质是告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他的线程被阻塞住。

4.volatile仅能够用在变量级别,synchronized则可以使用在变量和方法中。

5.volatile仅能够实现变量修改的可见性,而synchronized可以保证变量修改的可见性和原子性。

6.volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞。


你可能感兴趣的:(多线程)