Java多线程之同步

Java多线程之同步

 

很多时候我们用多线程去操作共享资源,有可能会让共享资源出现意想不到的结果,为了避免这种情况,我们需要使用同步。

 

多线程的三个特性

 

  • 原子性:线程一旦开始,就不会被其他线程干扰
  • 有序性:程序执行的顺序按照代码的先后顺序执行
  • 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值

 

 

多线程的同步方法

 

  • synchronized:同步方法、同步块
  • lock:最常用的是ReentrantLock,可重入锁
  • object.wait()和object.notify()同样能实现同步
  • Volatile:修饰变量,用于同步变量

线程之间通信

 

      其实线程之间就是通过共享数据来进行通信的,但是要保证共享数据的安全,就要想好线程安全的做法,一般是通过上面的同步放来来实现线程安全的。

 

 

 

synchronized的实现原理

 

  • 获得互斥锁
  • 清空工作内存
  • 从主内存拷贝共享变量新的值到工作内存做副本
  • 执行代码
  • 将修改后副本的值刷新到主内存中
  • 线程释放锁

代码

 

public class Thread2 {  
     public void m4t1() {  
          synchronized(this) {  
               int i = 5;  
               while( i-- > 0) {  
                    System.out.println(Thread.currentThread().getName() + " : " + i);  
                    try {  
                         Thread.sleep(500);  
                    } catch (InterruptedException ie) {  
                    }  
               }  
          }  
     }  
     public void m4t2() {  
          int i = 5;  
          while( i-- > 0) {  
               System.out.println(Thread.currentThread().getName() + " : " + i);  
               try {  
                    Thread.sleep(500);  
               } catch (InterruptedException ie) {  
               }  
          }  
     }  
     public static void main(String[] args) {  
          final Thread2 myt2 = new Thread2();  
          Thread t1 = new Thread(  new Runnable() {  public void run() {  myt2.m4t1();  }  }, "t1"  );  
          Thread t2 = new Thread(  new Runnable() {  public void run() { myt2.m4t2();   }  }, "t2"  );  
          t1.start();  
          t2.start();  
     } 
}
 

结果

     t1 : 4  

     t2 : 4  

     t1 : 3  

     t2 : 3  

     t1 : 2  

     t2 : 2  

     t1 : 1  

     t2 : 1  

     t1 : 0  

     t2 : 0

 

 

 

lock实现原理

 

获得锁

  • 从线程中读取表示锁状态的变量
  • 如果状态为0,就改为1,如果有多个线程,只会有一个成功
  • 如果修改成功就获得了锁,进入维护队列
  • 如果失败,则进入等待队列并阻塞自身,此时线程一直被阻塞在lock方法中,没有从该方法中返回
  • 如果表示状态的变量的值为1,那么将当前线程放入等待队列中,然后将自身阻塞(被唤醒后仍然在lock方法中,并从下一条语句继续执行,这里又会回到第1步重新开始)

注意: 唤醒并不表示线程能立刻运行,而是表示线程处于就绪状态,仅仅是可以运行而已

 

释放锁

  • 释放锁的线程将状态变量的值从1设置为0,并唤醒等待(锁)队列中的队首节点,释放锁的线程从就从unlock方法中返回,继续执行线程后面的代码
  • 被唤醒的线程(队列中的队首节点)和可能和未进入队列并且准备获取的线程竞争获取锁,重复获取锁的过程

注意:可能有多个线程同时竞争去获取锁,但是一次只能有一个线程去释放锁,队列中的节点都需要它的前一个节点将其唤醒,例如有队列A<-B-<C ,即由A释放锁时唤醒B,B释放锁时唤醒C

 

代码

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Lockers
 * 在多线程编程里面一个重要的概念是锁定,如果一个资源是多个线程共享的,为了保证数据的完整性,
 * 在进行事务性操作时需要将共享资源锁定,这样可以保证在做事务性操作时只有一个线程能对资源进行操作,
 * 从而保证数据的完整性。在5.0以前,锁定的功能是由Synchronized关键字来实现的。
 */
public class Lockers {
	
	/**
	 * 测试Lock的使用。在方法中使用Lock,可以避免使用Synchronized关键字。
	 */
	public static class LockTest {

		Lock lock = new ReentrantLock();// 锁
		double value = 0d; // 值
		int addtimes = 0;

		/**
		 * 增加value的值,该方法的操作分为2步,而且相互依赖,必须实现在一个事务中
		 * 所以该方法必须同步,以前的做法是在方法声明中使用Synchronized关键字。
		 */
		public void addValue(double v) {
			lock.lock();// 取得锁
			System.out.println("LockTest to addValue: " + v + "   "
					+ System.currentTimeMillis());
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
			}
			this.value += v;
			this.addtimes++;
			lock.unlock();// 释放锁
		}

		public double getValue() {
			return this.value;
		}
	}
	public static void testLockTest() throws Exception{
		final LockTest lockTest = new LockTest();
		// 新建任务1,调用lockTest的addValue方法
		Runnable task1 = new Runnable(){
			public void run(){
				lockTest.addValue(55.55);
			}
		};
		// 新建任务2,调用lockTest的getValue方法
		Runnable task2 = new Runnable(){
			public void run(){
				System.out.println("value: " + lockTest.getValue());
			}
		};
		// 新建任务执行服务
		ExecutorService cachedService = Executors.newCachedThreadPool();
		Future future = null;
		// 同时执行任务1三次,由于addValue方法使用了锁机制,所以,实质上会顺序执行
		for (int i=0; i<3; i++){
			future = cachedService.submit(task1);
		}
		// 等待最后一个任务1被执行完
		future.get();
		// 再执行任务2,输出结果
		future = cachedService.submit(task2);
		// 等待任务2执行完后,关闭任务执行服务
		future.get();
		cachedService.shutdownNow();
	}
	
	/**
	 * ReadWriteLock内置两个Lock,一个是读的Lock,一个是写的Lock。
	 * 多个线程可同时得到读的Lock,但只有一个线程能得到写的Lock,
	 * 而且写的Lock被锁定后,任何线程都不能得到Lock。ReadWriteLock提供的方法有:
	 * readLock(): 返回一个读的lock 
	 * writeLock(): 返回一个写的lock, 此lock是排他的。
	 * ReadWriteLockTest很适合处理类似文件的读写操作。
	 * 读的时候可以同时读,但不能写;写的时候既不能同时写也不能读。
	 */
	public static class ReadWriteLockTest{
		// 锁
		ReadWriteLock lock = new ReentrantReadWriteLock();
		// 值
		double value = 0d;
		int addtimes = 0;
		
		/**
		 * 增加value的值,不允许多个线程同时进入该方法
		 */
		public void addValue(double v) {
			// 得到writeLock并锁定
			Lock writeLock = lock.writeLock();
			writeLock.lock();
			System.out.println("ReadWriteLockTest to addValue: " + v + "   "
					+ System.currentTimeMillis());
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
			}
			try {
				// 做写的工作
				this.value += v;
				this.addtimes++;
			} finally {
				// 释放writeLock锁
				writeLock.unlock();
			}
		}
		/**
		 * 获得信息。当有线程在调用addValue方法时,getInfo得到的信息可能是不正确的。
		 * 所以,也必须保证该方法在被调用时,没有方法在调用addValue方法。
		 */
		public String getInfo() {
			// 得到readLock并锁定
			Lock readLock = lock.readLock();
			readLock.lock();
			System.out.println("ReadWriteLockTest to getInfo   "
					+ System.currentTimeMillis());
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
			}
			try {
				// 做读的工作
				return this.value + " : " + this.addtimes;
			} finally {
				// 释放readLock
				readLock.unlock();
			}
		}
	}
	
	public static void testReadWriteLockTest() throws Exception{
		final ReadWriteLockTest readWriteLockTest = new ReadWriteLockTest();
		// 新建任务1,调用lockTest的addValue方法
		Runnable task_1 = new Runnable(){
			public void run(){
				readWriteLockTest.addValue(55.55);
			}
		};
		// 新建任务2,调用lockTest的getValue方法
		Runnable task_2 = new Runnable(){
			public void run(){
				System.out.println("info: " + readWriteLockTest.getInfo());
			}
		};
		// 新建任务执行服务
		ExecutorService cachedService_1 = Executors.newCachedThreadPool();
		Future future_1 = null;
		// 同时执行5个任务,其中前2个任务是task_1,后两个任务是task_2
		for (int i=0; i<2; i++){
			future_1 = cachedService_1.submit(task_1);
		}
		for (int i=0; i<2; i++){
			future_1 = cachedService_1.submit(task_2);
		}
		// 最后一个任务是task_1
		future_1 = cachedService_1.submit(task_1);
		// 这5个任务的执行顺序应该是:
		// 第一个task_1先执行,第二个task_1再执行;这是因为不能同时写,所以必须等。
		// 然后2个task_2同时执行;这是因为在写的时候,就不能读,所以都等待写结束,
		// 又因为可以同时读,所以它们同时执行
		// 最后一个task_1再执行。这是因为在读的时候,也不能写,所以必须等待读结束后,才能写。
		
		// 等待最后一个task_2被执行完
		future_1.get();
		cachedService_1.shutdownNow();
	}

	public static void main(String[] args) throws Exception{
		Lockers.testLockTest();
		System.out.println("---------------------");
		Lockers.testReadWriteLockTest();
	}
}

 

 

 

Volatile实现原理

 

      Volatile主要用来修饰变量,每次被线程访问时,都强迫从主内存的共享变量中读取最新值,而每次修改变量,求强迫将最新的值刷新到主内存中。

 

代码

package  mythread;

public   class  JoinThread  extends  Thread
{
     public   static volatile int  n  =   0 ;
    public   void  run()
    {
         for  ( int  i  =   0 ; i  <   10 ; i ++ )
             try 
        {
                n  =  n  +   1 ;
                sleep( 3 );  //  为了使运行结果更随机,延迟3毫秒 

            }
             catch  (Exception e)
            {
            }
    }

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

        Thread threads[]  =   new  Thread[ 100 ];
         for  ( int  i  =   0 ; i  <  threads.length; i ++ )
             //  建立100个线程 
            threads[i]  =   new  JoinThread();
         for  ( int  i  =   0 ; i  <  threads.length; i ++ )
             //  运行刚才建立的100个线程 
            threads[i].start();
         for  ( int  i  =   0 ; i  <  threads.length; i ++ )
             //  100个线程都执行完后继续 
            threads[i].join();
        System.out.println( " n= "   +  JoinThread.n);
    }
}

 

 

 

 

保证多线程三个特性的方法

 

  • 保证原子性:Lock、Synchronized
  • 保证可见性:Lock、Synchronized、Volatile
  • 保证顺序性:Lock、Synchronized、Volatile

 

 

为何Volatile不能保证原子性?

 

例如:i++,这个可以分为三个操作

          1. 获取i的值

          2. 值+1

          3. 最后把值付给i

 

如果有两个线程执行,就会这样

Thread1             Thread2
r1 = i;             r3 = i;               
r2 = r1 + 1;        r4 = r3 + 1;
i = r2;             i = r4;

 这样会造成的问题就是 r1, r3读到的值都是 0, 最后两个线程都将 1 写入 i, 最后 i 等于 1, 但是却进行了两次自增操作

 

注意:可以用atomic代替Volatile

 

 

 

Synchronized和Lock的区别

 

  • Lock会跟灵活,用lcok()加锁,用unLock()解锁;Synchronized是关键字,用来修饰同步方法和同步块
  • Lock可以超时放弃等待
  • Lock需要我们手动释放锁,Synchronized不需要
  • Lock是公平锁,按照申请锁的顺序获得锁

 

 

Synchronized和Volatile的区别

 

  • Volatile不要同步操作,所以效率高,不会阻塞线程,但由于不能保证原子性,所以使用情况比较窄
  • Synchronized既能保证共享数据的可见性,又能保证其原子性
  • Volatile读变量相当于加锁(进入Synchronized块),写变量相当于解锁(退出Synchronized块)

保证线程可见性的关键

  • 线程工作内存中的副本,怎样更新到主内存中
  • 其他线程,怎样能够将主内存中的共享数据更新到自己的工作内存中

猜你喜欢

转载自youyu4.iteye.com/blog/2351339