实现线程同步的几种方式

为什么要使用同步

    Java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程调用,从而保证了该变量的唯一性和准确性。

    Java提供了一种内置的锁机制来支持原子性。每个Java对象都可以用作一个实现同步的锁,称为内置锁线程进入同步代码块之前自动获取到锁,代码块执行完成正常退出或代码块中抛出异常退出时会释放掉锁。

    内置锁为互斥锁,即线程A获取到锁后,线程B阻塞直到线程A释放锁,线程B才能获取到同一个锁;内置锁使用synchronized关键字实现。

同步的方式

1.同步方法

    即有synchronized关键字修饰的方法(所有访问状态变量的方法都必须进行同步),此时充当锁的对象为调用同步方法的对象在调用该方法前,需要获得内置锁,否则就处于阻塞状态。如下代码:

public class Test{
	private int count = 0;
	
	public synchronized int add(){
		count += 1;
		return count;
	}
	
	public synchronized int delete(){
		count -= 1;
		return count;
	}
}
    synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。

2.同步代码块

    即有synchronized关键字修饰的语句块。锁的粒度更细,并且充当锁的对象不一定是this,也可以是其它对象,使用起来更加灵活。

public class Test{
	private int count = 0;
	
	public int add(){
		synchronized(this){
			count += 1;
		}
		return count;
	}
	
	public int delete(){
		synchronized(this){
			count -= 1;
		}
		return count;
	}
}
    同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。下面看几个例子:
public class Status {
	private int num = 0;
	
	public void selfIncrease(){
		num = num + 1;
		System.out.println(Thread.currentThread().getName()   
				+ "|" + num);
	}
}
public class Task implements Runnable {
	private Status status;
	
	public Task(Status status){
		this.status = status;
	}
	
	public void run() {
		synchronized (status) {
			status.selfIncrease();
		}
	}
	
	public static void main(String[] args) {
		Status status = new Status();
		Task task = new Task(status);
		Thread t1 = new Thread(task);
		Thread t2 = new Thread(task);
		Thread t3 = new Thread(task);
		Thread t4 = new Thread(task);
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}
    从代码中可以看出,四个线程使用同一个Status对象。运行结果如下:

Thread-0|1
Thread-2|2
Thread-1|3
Thread-3|4
    修改一下上面的代码,每个线程运行时创建一个新的Status对象,如下:

public class Task implements Runnable {

    public void run() {
        Status status = new Status();

        synchronized (status) {
            status.selfIncrease();
        }
    }

    public static void main(String[] args) {
        Task task = new Task();

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        Thread t3 = new Thread(task);
        Thread t4 = new Thread(task);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}
   由于充当锁的对象实例不一定是同一个对象(hashcode不同),同步失效。 运行结果如下:

Thread-2|1
Thread-3|1
Thread-1|1
Thread-0|1
    由上面两段代码,可以看出 同步代码块中充当锁的对象必须为同一个对象

    我们再看一段代码,如下:

public class Task implements Runnable {
	private Status status;
	
	public Task(Status status){
		this.status = status;
	}
	
	public void run() {
		synchronized (status) {
			System.out.println("Thread lock");
			System.out.println("Thread:" + status.getNum());
			System.out.println("Thread over");

		}
	}
	
	public static void main(String[] args) {
		Status status = new Status();
		Task task = new Task(status);
		Thread t = new Thread(task);
		t.start();
		//synchronized(status){
			System.out.println("Main");
			status.setNum(1);
			System.out.println("Main:" + status.getNum());
		//}
	}
}
    执行结果如下:

Main
Thread lock
Main:1
Thread:1
Thread over
    从运行结果可以看出,在Thread线程锁定Status对象的时候,Main线程在Thread线程释放锁对象前依然能够修改Status对象的num域,说明锁没有生效。想一下这是为什么呢?

    在开始部分我们讲到内置锁也就是互斥锁的介绍时,线程A获得互斥锁后,线程B阻塞知道线程A释放互斥锁,线程B才能获取到同一个互斥锁。但是上面代码中Main线程并没有对Status对象进行同步,故在Thread线程锁定Status对象的时候不需要阻塞,可以直接操作Status对象,因此,所有使用同步对象的地方都必须进行同步

    修改方式是把注释掉的代码打开。

    如果锁对象为静态变量,或使用synchronized关键字修饰静态方法,则锁对象为Class对象。

    public class Status {  
        private static int num = 0;  
          
        public synchronized static void selfIncrease(){  
            num = num + 1;  
            System.out.println(Thread.currentThread().getName()     
                    + "|" + num);  
        }  
    }
    public class Task implements Runnable {  
          
        public void run() {  
            Status.selfIncrease();  
        }  
          
        public static void main(String[] args) throws Exception {  
            Task task = new Task();  
            Thread t1 = new Thread(task);  
            Thread t2 = new Thread(task);  
            Thread t3 = new Thread(task);  
            Thread t4 = new Thread(task);  
            t1.start();  
            t2.start();  
            t3.start();  
            t4.start();  
        }  
    }  
    运行结果如下:

Thread-0|1
Thread-2|2
Thread-1|3
Thread-3|4
    相当于:

public class Status {  
    private static int num = 0;  
      
    public static void selfIncrease(){  
        synchronized(Status.class){  
            num = num + 1;  
            System.out.println(Thread.currentThread().getName()     
                    + "|" + num);  
        }  
    }  
} 
或者:

    public class Status {  
        private static int num = 0;  
        private static Object lock = new Object();  
          
        public static void selfIncrease(){  
            synchronized(lock){  
                num = num + 1;  
                System.out.println(Thread.currentThread().getName()     
                        + "|" + num);  
            }  
        }  
    }

3.使用特殊域变量(volatile)实现线程同步

    (1).volatile关键字为域变量的访问提供了一种免锁机制

    (2).使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新

    (3).因此每次使用该域就要重新计算,而不是使用寄存器中的值

    (4).volatile不会提供任何原子操作,它也不能用来修饰final类型的变量

    示例如下:

  class Bank {
            //需要同步的变量加上volatile
            private volatile int account = 100;

            public int getAccount() {
                return account;
            }
            //这里不再需要synchronized 
            public void save(int money) {
                account += money;
            }
        }
    多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。

4.使用重入锁实现线程同步

    在Java1.5中新增了以个java.util.concurrent包来支持同步。

    ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它拥有synchronized相同的并发性和内存语义,此外还多了锁投票,定时锁等候和中断锁等候。

    线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定;

    如果使用synchronized,如果A不释放,B将一直等下去,不能被中断;

    如果使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情。

    ReentrantLock类常用方法有:

    ReentrantLock() : 创建一个ReentrantLock实例 
    lock() : 获得锁 
    unlock() : 释放锁 
    class Bank {

            private int account = 100;
            //需要声明这个锁
            private Lock lock = new ReentrantLock();
            public int getAccount() {
                return account;
            }
            //这里不再需要synchronized 
            public void save(int money) {
                lock.lock();
                try{
                    account += money;
                }finally{
                    lock.unlock();
                }

            }
        }
注意:Lock对象和synchronized关键字除了上面讲到的区别之外,还有下面不同的地方:

(1).synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行出现异常时,JVM会自动释放锁定;但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中。

(2).在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock;但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态。

5.使用局部变量实现线程同步

    如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

    ThreadLocal类的常用方法:

ThreadLocal() : 创建一个线程本地变量 
get() : 返回此线程局部变量的当前线程副本中的值 
initialValue() : 返回此线程局部变量的当前线程的"初始值" 
set(T value) : 将此线程局部变量的当前线程副本中的值设置为value
    上面的例子可改写为:
    public class Bank{
            //使用ThreadLocal类管理共享变量account
            private static ThreadLocal account = new ThreadLocal(){
                @Override
                protected Integer initialValue(){
                    return 100;
                }
            };
            public void save(int money){
                account.set(account.get()+money);
            }
            public int getAccount(){
                return account.get();
            }
        }
    注意:ThreadLocal与同步机制

    (1).都是为了解决多线程中相同变量的访问冲突问题。

    (2).ThreadLocal采用以“空间换时间”的方法,同步机制采用以“时间换空间”的方式。

你可能感兴趣的:(Java)