Java并发编程

什么是线程安全
当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。要编写线程安全的代码,其核心在于要对状态访问操作进行管理,特别是对共享的(Shared)和可变的(Mutable)状态的访问。

进程间交换数据
套接字,信号处理器,共享内存,信号量以及文件等。

竞态条件
在多线程环境下,getNext是否会返回唯一的值,要取决于运行时对线程中操作的交替执行方式。
读取-修改-写入

public class RaceCondition {
  private int value;

  public int getNext() {
    return value++;
  }
}

先检查后执行

public class LazyInitRace{
	private ExpensiveObject instance = null;
	public ExpensiveObject getInstance(){
		if(instance == null){
			instance = new ExpensiveObject();
		}
		return instance;
	}
}

:由于运行时可能将多个线程之间的操作交替执行,因此这两个线程可能同时执行读操作,从而使得他们得到相同的值

复合操作
为了确保线程安全,“先检查后执行”(例如延迟初始化)和"读取-修改-写入"(例如递增运算)等操作必须是原子的。我们将”先检查后执行“以及”读取-修改-写入“等操作统称为符合操作:包含了一组必须以原子方式执行的操作以确保线程安全。

要保持状态的一致性,就需要在单个原子操作中更新所有相关的状态变量

//非线程安全
public class UnsafeCachingFactoryizer impelemtns Servlet{

  private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();

  private final AtomicReference<BigInteger> lastFactors = new AtomicReference<BigInteger>();

  public void service(ServletRequest req, ServletResponse res) {
    BigInteger i = extractFromRequest(req);
    if(i.equals(lastNumber.get()) {
      encodeIntoResponse(res,lastFactors.get());
    }else {
      BigInteger[] factors = factor(i);
      lastNumber.set(i);
      lastFactors.set(factors);
      encodeIntoResponse(res,Factors);
    }
  }

}

注:在某种执行时序中,可能无法同时更新lastNumber和lastFactors。同样也无法保证会同时获取两个值。

线程切换
在多线程程序中,当线程调度器临时挂起活跃线程并转而运行另一个线程时,就会频繁的出现上下文 切换操作,这种操作将带来极大的开销:保存和恢复执行上下文,丢失局部性,并且CPU时间将更多地花在线程调度、而不是线程运行上。

Timer
Timer类的作用是使任务在稍后的时刻运行,或者运行一次,或者周期性地运行。TimerTask将在Timer管理的线程中执行,而不是由应用程序来管理。如果某个TimerTask访问了应用程序中其他线程访问的数据,那么TimerTask需要以线程安全的方式来访问数据,其他类也必须采用线程安全的方式来访问数据。通常,要实现这个目标,最简单的方式是确保TimerTask访问的对象本身是线程安全的,从而就能把线程安全性封装在共享对象内部。

JAVA同步机制
Java中主要同步机制是关键字synchronized,它提供了一种独占的加锁方式,但“同步”这个术语还包括volatile类型的变量,显示锁 (Explict Lock)以及原子变量。

加锁的含义
加锁的含义不仅仅局限于互斥行为,还包括内存可见性。为了确保所有线程都能看到共享变量的最新值,所有执行读操作或者写操作的线程都必须在同一个锁上同步。
重入
内置锁时可以重入的,因此某个线程试图获得一个已经由他自己持有的锁,那么这个请求就会成功。

重排序
假设线程A需要看到线程B更新了number,ready,但实际上却只看到了其中一个值更新。

关于锁住哪个对象的问题
无论List使用哪个锁来保护它的状态,可以肯定的是,这个锁并不是ListHelper上的锁。ListHelper只是带来了同步的假象,尽管所有的链表操作都被声明为synchronized,但却使用了不同的锁,这意味着putIfAbsent相对于List的其他操作来说并不是原子的,因此就无法确保当putIfAbsent执行时另一个线程不会修改链表。

public class ListHelper<E> {
  public List<E> list = Collections.synchronizedList(new ArrayList<E>());

  public synchronized boolean putIfAbsent(E e) {
    boolean absent = !list.contains(e);
    if(absent) {
      list.add(e);
    }
    return absent;
  }
}
  • 正确的实现方式
    要想使这个方法能正确的执行,必须使list在实现客户端加锁或外部加锁时使用同一个锁。客户端加锁是指,对于使用某个对象X的客户端端代码,使用X本身用于保护其状态的锁来保护这段代码。要使用客户端加锁,你必须知道对象X使用的是哪一个锁。
public class ListHelper<E> {
  public List<E> list = Collections.synchronizedList(new ArrayList<E>());

  public boolean putIfAbsent(E e) {
    synchronized (list) {
      boolean absent = !list.contains(e);
      if (absent) {
        list.add(e);
      }
      return absent;
    }
  }
}
  • synchronized
    java1.6之前重量级锁机制
    在这里插入图片描述

java1.6之后synchronized锁升级过程
在这里插入图片描述

并发编程三大特性:可见性,有序性,原子性
volatile
volatile可以保证可见性,有序性,但是无法保证原子性

  • 可见性
    volatile底层实现原理
  • 有序性
    计算机为了提高代码的执行效率,会对机器指令重排优化,volatile修饰的变量禁止指令重排。
    内存屏障技术:lock前缀指令禁止重排序

指令重排遵循as-if-serial,happens-before语义

  • as-if-serial

  • happens-before

public class VolatileSerialTest {
   
     volatile static int x = 0;
     volatile static int y = 0;
    public static void main(String[] args) throws InterruptedException {
   
        Set<String> resultSet = new HashSet<>();
        HashMap<String, Integer>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值