ThreadLocal、Synchronized针对多线程的处理

概述


       在java2后,提供了threadlocal。这样一个新的工具类在处理多线程时提供了另外一种与之前不同的解决方案,而且对于开发者来说更加的简洁。它为每个访问这个变量的线程提供一个线程副本,并将这个副本存入到map中。这样就相当于每个线程都拥有自己独立的变量,在多线程并发操作时就不会造成操作数据的不一致。而在单例模式中,使用到的synchronized。它的机制是控制变量只有单线程进行访问,这样对于变量每次只有一个线程来操作句柄就不会操作数据的不一致。

 

ThreadLocal


        ThreadLocal不是一个本地线程类,是一个提供线程的局部变量的工具类。对于每个访问该变量的线程都拥有初始化变量的副本,位于java.lang下。

 

常见方法



       Get()Set(),通过get(),set()来取得、设置当前线程的副本值。用的最多也就是这两个方法,另外需要构造相应的静态成员变量。


public final static ThreadLocal moneyLocal = new ThreadLocal(); 


       为了在之后所有的线程创建局部变量时不用一一去创建,在类装载时就加载这个静态变量,也是一种饿汉式加载吧!


创建

a.


//第一种声明线程,继承thread
public class ThreadSecond  extends Thread{
	private String ThreadName="";
	public ThreadSecond(){
			this.ThreadName=ThreadName;
		}
	public void run(){
			print();
		}
	public void print(){
		
		String currentThreadName = Thread.currentThread().getName(); 
		System.out.println(currentThreadName+" is running!"); 
		
		}
	public static void main(String[] args){
		
		
		ThreadSecond ts=new ThreadSecond();
		
		ts.start();

	}
} 

通过继承thread,并重写run()来创建threadlocal。

执行结果:

b.

import java.util.*;
import java.lang.*;


class teacher{
	private int salary=0;
	public int returnSalary(){
		return this.salary;
	}
	public void setSalary(int salary){
		this.salary=salary;
	}
}
//第一种创建方式
public	class ThreadTest implements Runnable {
		public void run(){
			setSalary();
			}
		teacher te=new teacher();
		public static void main(String[] args) { 
			
			ThreadTest td=new ThreadTest();
			Thread a=new Thread(td,"a");
			Thread b=new Thread(td,"b");					
	  
			a.start();
			b.start();
		}
	  
	

	public void setSalary(){

			String currentThreadName = Thread.currentThread().getName(); 
			System.out.println(currentThreadName+" is running!"); 
			Random rand =new Random();    
			    
			int salary = rand.nextInt(100); 
			System.out.println("thread "+currentThreadName +" set salary to:"+salary); 
			
			this.te.setSalary(salary); 
			System.out.println("thread "+currentThreadName+" first read salary is:"+this.te.returnSalary()); 
			 
			try { 
			   Thread.sleep(3000); 
			}catch(InterruptedException ex) { 
			       ex.printStackTrace(); 
			}  
			System.out.println("thread "+currentThreadName +" second read salary is:"+this.te.returnSalary()); 
			

	} 
}



执行结果



      如图,这个thread的创建是通过实现了接口来实现的。但在执行完后面一个设置后。线程在第二次访问该变量时出现了一致的情况,a、b线程读取的数据都成了a线程操作后的结果。这就是通常出现的多线程访问时,线程之间对于共享变量的操作出现了问题。

多线程并发操作的两种思路


从以上的出现的问题,最终还是由于在多个线程在访问一个对象的变量时出现的数据修改而导致之后数据访问时造成错误。稍微思考一下可以发现还是可以发现在生活中经常有这样的鲜活的实例的,比如盛饭这样一个问题。盆子里有4两饭,张三需要2两,在第一次盛了1两之后;李四需要3两,李四盛完了3两;这时候张三第二次来访问这个盆子时就没了。怎么解决这个问题呢?张三在盛饭时,保证盆子是张三的,这时候张三拿着这个盆子的句柄;这样无论何时来访问都能够对得上了。或者张三、李四都有这样一个盆子。每个人在自己的盆子里盛饭,谁也不会影响谁。


这样得到两个解决方案:


a.为每个线程单独创建自己的变量

b.保证变量的修改每次只有一个线程在操作


Threadlocal方式    


import java.util.*;
import java.lang.*;

class Money{
	
	private int money;
	
	public int returnMoney(){
		return this.money;
	}
	
	public void setCount(int moneyCount){
		this.money=moneyCount;
	}	

}
public	class ThreadSolve implements Runnable {
		//声明threadLocal静态常量
		public final static ThreadLocal moneyLocal = new ThreadLocal(); 
		
		//重写run方法
		public void run(){
			setMoney();
			}
		
			 
		public static void main(String[] args) { 
			
			ThreadSolve ts=new ThreadSolve();
			
			//声明三个线程
			Thread a=new Thread(ts,"a");
			Thread b=new Thread(ts,"b");		
			Thread c=new Thread(ts,"c");
			  	
			//启动线程
			a.start();
			b.start();
			c.start();
			
		}
	  
	

	public void setMoney(){
		
			//记录线程开始时间
			long startTime=System.currentTimeMillis(); 
			//得到当前线程名称
			String currentThreadName = Thread.currentThread().getName(); 
			System.out.println(currentThreadName+" is running!"); 
			Random rand =new Random();    
			    
			int moneyCount = rand.nextInt(100); 
			System.out.println("thread "+currentThreadName +" set moneyCount to:"+moneyCount); 
			
			//声明线程副本
			Money money=getMoney();
			money.setCount(moneyCount); 
			System.out.println("thread "+currentThreadName+" first read money is:"+money.returnMoney()); 
			 
			try { 
				 //中断当前线程3s
			   Thread.sleep(3000); 
			}catch(InterruptedException ex) { 
			       ex.printStackTrace(); 
			}  
			System.out.println("thread "+currentThreadName +" second read money is:"+money.returnMoney()); 
			//线程结束时间
			long endTime=System.currentTimeMillis(); 
			System.out.println("exuteTime is "+(endTime-startTime));
	}
/*
**得到实例副本
*/
protected Money getMoney() { 
		
		//得到线程副本,任何得到teacher对象都是通过ThreadLocal来get
  	Money money = (Money)moneyLocal.get(); 
  	if(money == null) { 
   		money= new Money(); 
   		moneyLocal.set(money); 
  	} 
  	return money; 
	} 

protected void setMoney(Money money) { 
  	moneyLocal.set(money); 
	}
	  
}
利用threadlocal的两个方法get和set,每次在现场访问时都是通过在ThreadSolve一开始加载时创建的静态变量初始得到的线程副本进行操作的。这样每个线程单独在自己的副本里面操作自己的变量,就不会出现问题。


执行结果



    两个线程执行的大致时间是3s左右。


Synchronized方式


     仔细看看这个类,可以发现出问题的地方就是对变量的修改这里出现问题。将问题的粒度缩小,单个控制对变量修改的线程访问也能够解决问题。至于在哪里控制,可以在访问的时候,或者在调用的时候,都可以。


import java.util.*;
import java.lang.*;



class  teacher{
	private int salary=0;
	public int returnSalary(){
		return this.salary;
	}
	public  void setSalary(int salary){
		this.salary=salary;
	}
}
public	class SyncSolve implements Runnable {
		public void run(){
			setSalary();
			}
		teacher te=new teacher();
		public  static void main(String[] args) { 
			
			SyncSolve td=new SyncSolve();
			Thread a=new Thread(td,"a");
			Thread b=new Thread(td,"b");	
				
	  
			a.start();
			b.start();
		}
	  
	

	public synchronized void setSalary(){
			long startTime=System.currentTimeMillis(); 
			String currentThreadName = Thread.currentThread().getName(); 
			System.out.println(currentThreadName+" is running!"); 
			Random rand =new Random();    
			    
			int salary = rand.nextInt(100); 
			System.out.println("thread "+currentThreadName +" set salary to:"+salary); 
			
			this.te.setSalary(salary); 
			System.out.println("thread "+currentThreadName+" first read salary is:"+this.te.returnSalary()); 
			 
			try { 
			   Thread.sleep(3000); 
			}catch(InterruptedException ex) { 
			       ex.printStackTrace(); 
			}  
			System.out.println("thread "+currentThreadName +" second read salary is:"+this.te.returnSalary()); 
			long endTime=System.currentTimeMillis(); 
			System.out.println("exuteTime is "+(endTime-startTime));
	} 
}




执行结果



      可以明显的看到这个图和上个图不一样的地方,这里每个线程是单独进行的。也就是当第一个线程访问setSalary()时,就对该方法加锁。另外对比两个图,在这种情况下两者的性能是差不太多的,在查阅资料时看到同步会带来巨大的性能开销,这也可以去理解,因为需要在多线程之间共享变量。


总结


      之上,threadlocal 与 Synchronized 都是针对多线程的一种解决方案。不同的是,threadlocal通过创建初始变量副本来为每个线程创建单独的变量;这样单独的线程副本数据之间就不会受影响。这种方式相对Synchronized来说占用内存上有点损耗,而多个线程同时运行的方式相对Synchronized来说性能上也是比较优越。而Synchronized线程操作的都是一个变量,这样利于多线程之间数据的共享。可以说是,各有千秋;没有说哪个好,哪个坏,特定时间特点的环境使用相应的解决方案就可以了。

 

原文地址:https://www.cnblogs.com/guziming/p/4232678.html