线程安全与锁优化(一)

时间记录:2020-2-5
之前学习到了java的线程与内存模型一例,其中最为关键的是在多线程下的操作如何保证线程安全,而其中最常用的手段就是加锁一保证数据一致性,但是锁的使用不合理会导致性能问题,所以我继续了解线程和锁的优化地方。锁在单独的机器上面是挺好用的,但是在分布式的系统中,如何保持数据的一致性,这里就衍生出了一个新的名词分布式锁。

线程安全

当多个线程访问一个对象的时候,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步操作,或者在调用方法进行任何其它的协调操作,调用这个对象的行为都是可以获得正确的结果,这个对象就是线程安全的。其实就是说在多线程下一个对象的数据获取是正确的那么就可以说这个是线程安全的,在API文档中经常会看见什么线程安全的名词,说的就是这个意思。

java中的线程安全
在java中线程安全最多的就是共享数据的正确性,也就是在任何时候获取对象的数据时都是正确的,主要包含:不可变绝对线程安全相对线程安全线程兼容线程独立

一:不可变
不可变的意思就是说java中一个对象的数据永远不会发生变化的,这样的数据是线程安全的,在java中的final对象被正确的构造出来的时候就是不可变的,也就是线程安全的。

二:绝对线程安全
绝对线程安全是说在不做任何的外部操作下,数据的获取都是正确的。java中很多标注自己线程安全的大部分都不是线程安全的,最常见的就是某容器的遍历,以其大小为判断依据操做带来的线程不安全问题。
例如:vector的遍历操做

package com.huo.thread;

import java.util.Vector;

public class TestVector 
{
	private static Vector<Integer> vector = new Vector<Integer>();

	public static void main(String[] args) 
	{
		while(true)
		{
			for(int i=0;i<10;i++)
			{
				vector.add(i);
			}

			Thread remove =  new Thread(new Runnable() {

				@Override
				public void run() {
					// TODO Auto-generated method stub
					for(int i=0;i<vector.size();i++)
					{
						try {
							vector.remove(i);
						} catch (Exception e) 
						{
							e.printStackTrace();
							System.exit(0);
						}
						
					}
				}
			});

			Thread print =  new Thread(new Runnable() {

				@Override
				public void run() {
					// TODO Auto-generated method stub
					for(int i=0;i<vector.size();i++)
					{
						try {
							System.out.println(vector.get(i));
						} catch (Exception e) 
						{
							e.printStackTrace();
							System.exit(0);
						}
						
					}
				}
			});
			
			remove.start();
			print.start();
		}
	}
}

输出: 存在异常

java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 3
	at java.util.Vector.remove(Vector.java:831)
	at com.huo.thread.TestVector$1.run(TestVector.java:26)
	at java.lang.Thread.run(Thread.java:745)

注意: 如果这里的依赖条件不以其大小为依赖条件的换,以其下一个值为依赖条件那么这种是不是不存在线程安全问题?

三:相对线程安全
相对线程安全就是我们通常说的线程安全,也就是在对这个对象单独的操作是线程安全的,以一些特定的顺序或者在调用的时候使用额外的同步手段来保证其的正确性。

四:线程兼容
就是说对象本身不是线程安全的,可以通过使用听不手段来保证对象在并发环境中可以安全的使用。

五:线程独立
值无论是否采用了同步手段,都无法在多线程环境中并发使用的,这里会造成死锁问题。
死锁: 死锁产生的原因在于引用成环。

package com.huo.thread;

public class TestThread
{
	public static void main(String[] args)
	{
		Thread t1 = new Thread(
				new DeadLock(true));
		Thread t2 = new Thread(
				new DeadLock(false));
		t1.start();
		t2.start();
	}
}
 
class DeadLock implements Runnable
{
	boolean lock;
	static Object object1 = new Object();
	static Object object2 = new Object();
	DeadLock(boolean lock)
	{
		this.lock = lock;
	}
	
	@Override
	public void run()
	{
		if(this.lock)
		{
			synchronized (object1) 
			{
				try
				{
					Thread.sleep(500);
				} catch (InterruptedException e) 
				{
					e.printStackTrace();
				}
				synchronized (object2) 
				{
					System.out.println("1");
				}
			}
		}else
		{
			synchronized (object2) 
			{
				try
				{
					Thread.sleep(500);
				} catch (InterruptedException e) 
				{
					e.printStackTrace();
				}
				synchronized (object1) 
				{
					System.out.println("2");
				}
			}
		}
		
	}
}

可以看出: 线程1获取到了object1的锁,线程2获得了object2的锁,而线程1里面需要获取object2的锁,而此锁被线程2获取了导致这个时候处于阻塞的状态,而线程2和线程1的情况相反,导致都在等对方释放锁,从而两个线程都处于阻塞状态,造成了死锁的发生。

线程安全的实现方法

在多线程的情况下如何保证线程安全是个问题,虚拟机在线程安全中也有很多的手段提供给用户使用。
一:互斥同步
互斥同步是最常见的一种并发正确性保障手段。同步是指在多个线程并发放稳共享数据是,保证共享数据在同一个时刻只被一个线程使用。而互斥是实现同步的一种手段,临界区、互斥量、信号量都是主要的互斥实现方式。因此互斥是因,同步是果,互斥是方法,同步是目的。
java中最基本的互斥同步手段就是synchronized关键字,在之前也介绍到了这个关键字会在同步代码块前后分别加上monitorentermonitorexit这两个字节码指令,这两个指令需要一个引用的非空参数来指明要锁定的对象。如果没有指明对象,指明的是方法,那就取对应的对象或class对象来作为锁定的对象。其实这个就是常用的方式,在对象和方法前加锁。
除了以上的关键字,还提供了juc包下的一些锁,不过这些锁属于代码层面的锁。其包含了等待可中断,可实现公平锁,以及锁可以绑定多个条件。
等待可中断: 是指当持有锁的线程长期不释放的现象,正在等待的线程可以选择放弃等待,改为处理其他事情,等待可中断对处理执行时间非常长的同步块很有帮助。
公平锁: 是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来一次获得锁;
非公平锁: 是指多个线程在等待同一个锁时,当锁被释放的时候任何一个等待锁的线程都有机会获得锁。synchronized是非公平锁,ReentrantLock默认情况下是非公平锁,其可以通过参数来调整为公平锁。
绑定多个条件: 是指一个ReentrantLock对象可以同时绑定对个Condition对象。
注意:

二:非阻塞同步
互斥同步带来的最大的影响就是在进行线程的唤醒和阻塞带来的性能消耗问题,而互斥同步属于一种悲观的并发策略,总认为不去做同步操作就会出现问题,但是有时候不是必须要进行同步的操作。而另一种就是乐观的并发策略,就是通过检测的方式来进行判断,意思就是说先对数据进行操作,如果没有其它的线程抢夺数据那么操作成功,如果操作失败会以再次尝试的方式来进行补偿,直到操作成功,这样的就是乐观的策略。
注意 这里的检测会涉及到CAS: 就是不停的比较交换,实际上就是对结果的预期进行估计,如果和估计的相同就认为是正确的,如果不正确就返回原先的值。cas指令需要三个操作数:旧的预期值A新值B内存位置V,在指令执行的时候当V值符合预期值A的时候会用B值更新V值,无论是否更新值都会返回V的旧值,而上面的操作都是指令级的也是原子操作。
注意: 上面的CAS操作有个ABA的问题:就是在读取V的值得时候是A,检查的时候也是A,那么对于其来说是正确的,但是其有些时候回从A->B->A,这样就不正正确的了。这时候采用传统的互斥同步会比较好点。在jdk中带有Atomic的一些类中都被称为原子类,也是通过CAS的方式来保证数据的准确性的。

三:无同步方案
要保证线程安全,并不是一定要进行同步操作,两者没有因果关系。只有在涉及到了使用共享数据的时候需要进行同步操作,才能够保证数据的正确性。

时间记录:2020-2-7

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值