Java线程--Thread基础

目录

Thread基础

线程带来的风险 

安全性问题 

活跃性问题

性能问题

糟糕的样例:

分析并发语义后的样例:

进一步调优的样例:

一组相关状态的封装成immutable的不可变性

线程中的术语概念 

原子性操作

状态一致性

竞态条件

数据竞争

可见性

有序性

原子性

线程的同步手法

synchronized

volatile

lock

基于AQS的同步器 

线程的封闭性 

方法内的局部变量

ThreadLocal类的运用

样例:数据库连接connection的安全性

样例:频繁的操作临时对象

共享对象的安全发布

singleton

immutable

concurrent


死记:多线程业务代码核心思想:

获得锁

临界区:原子操作:一组语句访问一组相关的状态变量

释放锁


Vector、HashTable等所谓的线程安全类,以及java.util.concurrent包中的所谓的线程安全并发集合类,一定要切记,他们都是某某单一方法的调用时,是安全的。一旦牵扯到复合操作,就不是线程安全的。这也是我们编写多线程业务代码时,一定要注意和理解:临界区的含义,一组语句访问一组相关的状态变量

例如:

class MyClass {
	
	private final Vector<String> v = new Vector<String>();

	/**
	* 判断并执行
	*
	* v.contains(x) 和 v.add(x) 构成了复合操作
	*
	* 单独v.contains(x)是安全的,单独的v.add(x)也是安全的
	*
	* 但它俩一起构成了复合操作"判断并执行",这个"判断并执行"必须是一个完整的语义才是线程安全的
	*
	* 而现在的写法,v.contains(x) 和 v.add(x)构成的复合操作并不是完整语义的原子操作
	*/
	public void putIfAbsent(String x){
		if( !v.contains(x) )
			v.add(x);
	}
}

上述示例改造后的代码,就是线程安全的,如下:(但这种方式并不可取,对容器加锁,极有可能导致并发性能下降,因为容器加锁,导致所有的其它线程对该容器的任何操作操作都被阻塞在那里)

class MyClass {
	
	private final Vector<String> v = new Vector<String>();

	/**
	* JDK提供的线程安全类有的锁的机制是内部锁,所以下方改造是正确的
	*
	* 这种改造也有风险:
	*
	* 风险1:我们必须清楚本身Vector的锁机制到底是什么(如果下方代码比如写成synchronized(NyClass.class);仍然是线程不安全的,因为Vector本身的锁和此处提供的锁并不一致)
	*
	* 风险2:如果以后JDK版本变更升级,更改了其锁机制,那我们此处的改造就被破坏了安全性
	*/
	public void putIfAbsent(String x){
	    synchronized(v){
                if( !v.contains(x) )
                v.add(x);
            }
	}
}

Thread基础

Thread基础

线程带来的风险 

安全性问题 

一个进程内的多个线程是共享进程内的所有资源的,包括CPU、内存、IO通道等。操作系统一般都是调度线程来执行(每个程序片段对应到计算机内部其实可以理解为若干条指令,严格意义上来说,CPU是调度每条指令来执行的),线程与线程之间是并发执行的,所以对于同一块内存区域中的变量而言,就可能存在A线程访问并修改了变量i,同时B线程也访问并修改了变量i,那么此时变量i就不是线程安全的。变量i如何在多线程环境中是安全的保证策略,有诸如synchronized、volatile、lock、AQS同步器等机制来保证。

活跃性问题

由于多个线程的调度是操作系统随机调度并发执行的,就有可能碰到死锁和饥饿问题。

性能问题

性能问题包括:服务时间过长、响应不灵敏、吞吐率低下、资源消耗过高、可伸缩性低、任务的切分不符合并行语义等。

  • 糟糕的样例:

class MyServlet implements Servlet {

	private BigInteger lastNumber;		
	private BigInteger[] lastFactors;
	private long hits;
	private long cacheHits;

	public synchronized long getHits(){
		return hits;
	}

	public synchronized long getCacheHits(){
		return cacheHits;
	}

        /**
        * Servlet的每个请求都是独立的一个线程
        * synchronized修饰符导致串行了大量的并发请求,每次只能一个请求进入
        */
	public synchronized void service(ServletRequest req,ServletResponse res){
		
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = null;
						
		++hits;	
		if(i.equals(lastNumber)){			
			++cacheHits;					
			factors = lastFactors.clone();	
		}										
		
		if( null==factors ){
			factors = doFactor(i);				
			lastNumber = i;					
			lastFactors = factors.clone();
		}

		writeToResponse(res,factors);

	}
}

  • 分析并发语义后的样例:

class MyServlet implements Servlet {
	
	/**
	* 四个状态变量
	*
	* 单独的访问次数
	*
	* 匹配缓存命中率时的一组相关状态为:缓存命中率,缓存数,缓存因子数组
	*
	* 未缓存命中,计算请求结果并缓存的一组相关状态为:缓存数,缓存因子数组
	*/
	private BigInteger lastNumber;		//缓存数值
	private BigInteger[] lastFactors;	//缓存数的因子数组
	
	private long hits;	//访问次数
	private long cacheHits;	//缓存命中率

	//获得访问次数
	public long getHits(){
		synchronized(this){
			return hits;
		}
	}

	//获得缓存命中率
	public long getCacheHits(){
		synchronized(this){
			return cacheHits;
		}
	}

	/**
	* servlet访问入口:在内部,编写原子操作一组相关状态变量的代码
	*
	* synchronized是可以自动释放锁的,同时它还具备线程自身重入功能
	*
	* 基本的线程访问共享资源的代码结构为:
	* 
	* 获得锁
	* 进入临界区:编写原子操作(一组语句)一组相关状态变量的代码
	* 释放锁
	*
	*/
	public void service(ServletRequest req,ServletResponse res){
		
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = null;

		synchronized(this){				//获得锁
			++hits;					//临界区内一组语句操作相关状态:访问次数
		}						//释放锁

		
		synchronized(this){				//获得锁
					
			if(i.equals(lastNumber)){		//临界区内一组语句操作相关状态:缓存值
				++cacheHits;			//临界区内一组语句操作相关状态:缓存命中率
				factors = lastFactors.clone();	//临界区内一组语句操作相关状态:缓存因子数组
			}
		}						//释放锁

		
		if( null==factors ){

			/**
			* 这里要注意:servlet每次页面请求都是在单独的线程中执行的
			* 局部变量i的求因子方法也许会执行很长时间,
			* 局部变量是线程之间互相不可见的,
			* 所以这个大的局部变量的运算不在synchronized中,可避免活跃性和性能问题
			*/
			factors = doFactor(i);

			synchronized(this){			//获得锁

				lastNumber = i;			//临界区内一组语句操作相关状态:缓存值
				lastFactors = factors.clone();	//临界区内一组语句操作相关状态:缓存因子数组
			}
		}						//释放锁

		writeToResponse(res,factors);

	}
}

  • 进一步调优的样例:

/**
* synchronized会引起线程频繁的切换
* 所以在保证程序运行正确的结果下,要尽可能的少利用synchronized
*
* 注意该调优后的代码:减少了一个synchronized 并且保证了正确性
*/

class MyServlet implements Servlet {

	private BigInteger lastNumber;		
	private BigInteger[] lastFactors;
	private long hits;
	private long cacheHits;

	public long getHits(){
		synchronized(this){
			return hits;
		}
	}

	public long getCacheHits(){
		synchronized(this){
			return cacheHits;
		}
	}


	public void service(ServletRequest req,ServletResponse res){
		
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = null;
		
		synchronized(this){						
			++hits;			//临界区内一组语句操作相关状态:访问次数
			if(i.equals(lastNumber)){			
				++cacheHits;					
				factors = lastFactors.clone();	
			}
		}										
		
		if( null==factors ){
			factors = doFactor(i);
			synchronized(this){					
				lastNumber = i;					
				lastFactors = factors.clone();
			}
		}

		writeToResponse(res,factors);

	}
}

通过以上三例,我们可以总结出:

当访问相关的一组状态变量或者一组语句操作的执行期间,我们需要加锁。但在执行局部变量的求解因子方法时(有可能非常耗时)我们不能加锁。同时,分析完并发语义并且保证程序正确执行结果时,我们减少了synchronized的使用量进而避免过多的synchronized引起的线程切换的耗时。这样做即保证了线程安全性,也不会过多的影响并发性,在每个代码同步快中的代码都足够小运行足够快。


针对上述的第三个已经调优后的示例,我们简化掉状态变量访问次数和缓存命中率后,还有没有更好的办法呢?答案是:有

  • 一组相关状态的封装成immutable的不可变性

结合volatile修饰符和final修饰符的作用,让我们再进一步的给出样例4:

/**
* 以下示例中,我们简化掉了访问次数和缓存命中率,主要讲解volatile和final的作用
*/

/**
* final修饰的类/方法/8种基本类型变量不可变
* final修饰的数组或者对象,是指对象引用和数组引用不可变,
*      但是对象内部的数据仍然可以被改变,
*          数组内部的元素仍然可以被改变
*
* 我们把这组密切相关的缓存数以及缓存因子数组,
* 封装成immutable不可变的,用final修饰这些状态变量,
* final修饰符能保证变量的初始化的安全性
*
* 为什么不把四个状态变量都封装在NumberCache中呢?
* 原因是:
*       访问次数是每次请求到来都要时刻变化的
*       命中率是每次命中时都要累加变化的
*       而封装到NumberCache中的缓存数以及缓存因子数组,仅赋值一次(final修饰符)
*
* 由此可得出结论:
* 想封装状态变量到一个immutable不可变的类中,是有条件局限的(状态仅一次赋值才行)
*
* 在注意:为什么NumberCache构造函数中,必须使用Arrays.copyOf()呢?同理getFactors()方法
* 原因是:
*       形参factors是外界传递进来的,是数组引用传递,在外部我们更改factors进而破坏不变性
*       而用了Arrays.copyOf()后,lastFactors指向的一份独立的factors拷贝,
*       外部factors如何改变,并不会影响这份拷贝,进而保证了lastFactors的不变性
*/

class NumberCache{
    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;
    public NumberCache(BigInteger i,BigInteger[] factors){
        lastNumber = i;
        lastFactors = Arrays.copyOf(factors,factors.length);
    }
    
    public BigInteger[] getFactors(BigInteger i){
        if( null==lastNumber || !lastNumber.equals(i) )
            return null;
        else
            return Arrays.copyOf(lastFactors,lastFactors.length);
    }
}


/**
* volatile修饰的对象引用是可见的,也就是说
* 只要对象引用指向了新的对象内存空间,所有线程都能看到这块新的对象内存空间
*/

/**
* volatile不好理解的地方:我在这里简要说明下:
* 假设
* 线程A执行到@1处(拿到的factors为null),此时线程B执行到@4处(已将cache更新)
* 线程A被告知cache已经刷新,执行到@2处时,此时的factors已经不是null了,这就是可见性神奇的地方
*/

class MyServlet implements Servlet {

	private volatile NumberCache cache = new NumberCache(null,null);

	public void service(ServletRequest req,ServletResponse res){
		
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = cache.getFactors(i); //@1									
		if( null==factors ){ //@2
		    factors = doFactor(i);
		    cache = new NumberCache(i,factors); //@3
		}

		writeToResponse(res,factors); //@4
	}
}

 

线程中的术语概念 

原子性操作

与数据库事务原子性是一个概念:即指一组语句作为一个不可分割的单元被执行。

状态一致性

要保证对象的状态一致性,就必须在单个原子操作中更新所有相关的状态变量。

原子操作和状态一致性的伪码表示:

//获得锁

//临界区{一组语句访问相关的状态变量}

//释放锁

竞态条件

多线程并发时,由于不恰当的随机执行时序而导致的随机的执行结果的情况叫做“竞态条件”,换句话说,就是正确执行的结果取决于运气。例如:i=0;++i;线程A,B同时并发执行,线程A得到的结果是1,线程B得到的结果既有可能是1,也有可能是2

数据竞争

可见性

volatile稍弱的同步机制,保证了可见性。也就是说,对volatile修饰的变量的读/写具有同步行为,理解volatile的最简单的形式我用代码来表示即为:

/**
* 状态name是非线程安全的
*/
class MyClass1{

    private String name;
    
    public void setName(String name){
    	this.name = name;
    }

    public String getName(){
    	return name;
    }
}

/**
* 状态name是线程安全的
*/
class MyClass2{

    private String name;
    
    public synchronized void setName(String name){
    	this.name = name;
    }

    public synchronized String getName(){
    	return name;
    }
}

/**
* 状态name是线程安全的
*/
class MyClass3{

    private volatile String name;
    
    public void setName(String name){
    	this.name = name;
    }

    public String getName(){
    	return name;
    }
}

有序性

正常情况下,我们编写的代码都是从上至下的顺序执行。但JVM虚拟机环境里面,会在编译、运行等环节时进行优化重排序。而volatile修饰符出现后,能够很大程度的禁止JVM做这些重排序的优化。volatile修饰语句的前面的代码必定都会得到执行,然后才到volatile,然后才是volatile后方的代码得以执行。

原子性

volatile修饰符之所以说是一种弱的同步机制,因为其仅仅能保证变量的get读、set写这两个动作是原子性的。比如:来个复合操作,先读,读后加工出新值,然后在写回内存,这就是3步了,这样的复合操作如果没有同步机制,就不具备原子性。所以说,volatile修饰的变量通常用法是作为标志位的判断,例如:是?否的状态位变量;或者初始化时的一次性判断,例如:实现单例模式singleton时修饰单例对象;或者不可变类对象变量的修饰来使用。

线程的同步手法

synchronized

volatile

lock

基于AQS的同步器 

线程的封闭性 

  • 方法内的局部变量

方法的形参(非外部对象引用的形参),以及方法内部声明的变量,都是线程安全的

  • ThreadLocal类的运用

样例:数据库连接connection的安全性

数据库连接缓冲池技术是线程安全的,每次缓冲池颁发给当前线程的connection连接都是独享安全的

/**
* ThreadLocal可以理解为一种Map<currentThread,T>结构
*
* 直接使用DriverManager.getConnection()是非线程安全的
*
* 这里new ThreadLocal()时,调用了initialValue()方法
*
* 调用了initialValue()之后,相当于ThreadLocal调用了set()方法
*
* 所以getConnection()方法,总是能当前线程内获取一个独属的连接对象connection,保证线程安全
*/
private static ThreadLocal<Connection> connectionHolder = 
	new ThreadLocal<Connection>(){
		public Connection initialValue(){
			return DriverManager.getConnection(DB_URL);
		}
	};

public static connection getConnection(){
	return connectionHolder.get();
}

样例:频繁的操作临时对象

/**
* 用户登录后,将用户信息以及所属的机构信息,User对象放置在ThreadLocal中,以便随时取出访问
*/

class User
{
	private String name;
	private Integer age;
	public User(String name, Integer age){
		this.name = name;
		this.age = age;
	}

	public String getName(){ return name;}
	public Integer getAge(){ return age;}
}

class UserManager
{
	private static ThreadLocal<User> threadLocal = 
	new ThreadLocal<User>(){
		/**
                 * ThreadLocal没有被当前线程赋值时
		 * 或
		 * 当前线程刚调用remove方法后调用get方法,返回此方法值
                 */
		public User initialValue(){
		        return null;
		}
	};

	public static User getUser(){
		return threadLocal.get();
	}

	public static void setUser(){
		return threadLocal.set(User);
	}
}

class LoginServlet extends Servlet
{
	public void service(ServletRequest req,ServletReponse rep){
		User user;
		if(null == UserManager.threadLocal.get())
		{
			user = new User("dindoa",25);
			UserManager.threadLocal.set(User);
		}
		else
		{
			user = (User)UserManager.threadLocal.get();
		}
		System.out.println(user.getName()+":"+user.getAge());//dindoa:25
	}
}

共享对象的安全发布

 对象的安全发布的含义:

发布就是暴露出对象引用,可由任意代码进行 对象.method()方法 调用的意思。对象的安全发布,即指:多个线程同时访问该对象时是安全的。

要安全的发布一个对象,对象的引用以及对象的状态必须同时对所有线程可见。一个正确构造的对象可以通过以下方式来正确的发布:

  • singleton

私有构造函数形式的单例模式的运用(私有构造器以及volatile的结合使用)

  • immutable

不可变对象的运用(final修饰符以及volatile修饰符的结合使用)

  • concurrent

将对象压入并发集合类的运用(并发集合类、Vector等安全类中压入对象)

  • composition & entrust

组合与委托,也是一种简单的发布安全对象的手法,例如:

class MySafeList<T> implements List<T> {
	
	private final List<T> list;
	public MySafeList<List<T> list>(){ this.list = list; }

	//追加了我们自定义的一个操作
	public synchronized boolean putIfAbsent(T x){
		boolean containFlag = list.contains(x);
		if( !containFlag )
			list.add(x);
		return containFlag;
	}

	//......按照类似的方式委托List的其它方法
	public synchronized void clear(){
		list.clear();
	}

	//......List的其它方法也都要委托(类似上方的clear方法),此处省略没写完
}

猜你喜欢

转载自blog.csdn.net/mmlz00/article/details/83897963