java并发编程-线程安全

什么是线程安全性

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么称这个类是线程安全的

线程安全管理的核心

对状态访问操作进行管理,主要是共享的和可变的状态的访问。

线程安全常见几种情景

  1. 原子性
    cout++看上去很紧凑。实际上包含了读取-修改-写入的复合操作,多线程访问肯定不安全。
@NotThreadSafe
public class UnsafeCountingFactorizer extends GenericServlet implements Servlet {
    private long count = 0;

    public long getCount() {
        return count;
    }

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        ++count; //这一步并不是原子操作,包含了读取-修改-写入三个操作
        encodeIntoResponse(resp, factors);
    }
}

我们可以采用juc的原子类来原子操作来将这些操作复合。

@ThreadSafe
public class CountingFactorizer extends GenericServlet implements Servlet {
    private final AtomicLong count = new AtomicLong(0);

    public long getCount() { return count.get(); }

    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        count.incrementAndGet();
        encodeIntoResponse(resp, factors);
    }
  1. 竞态条件
//延迟加载的例子
@NotThreadSafe
public class LazyInitRace {
    private ExpensiveObject instance = null;

    public ExpensiveObject getInstance() {
        if (instance == null)  //多个线程竞争这一条件
            instance = new ExpensiveObject();
        return instance;
    }
}

这里延迟加载就是一种竞态条件,可能会出现线程安全问题

加锁机制—内置锁

java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。同步代码块包含两部分:一个作为锁的对象引用,一个作为由这个锁的保护的代码块。主要有以下三种用法

  1. synchronized关键字同步代码块,clock就是一个对象
synchronized(clock){
	//需要同步的内容
}

2.synchronized关键字加在方法体前面,锁对象为方法调用所在的对象

public class Test(){
	public synchronized void ervice(){
		//这里使需要同步的方法体
	}
}

3.synchronized关键字加在静态方法前面,锁对象为方法体所在的Class对象

public class Test(){
	public static synchronized void ervice(){
		//这里使需要同步的方法体
	}
}

锁的重入

每个Java对象都可以用做一个实现同步的锁,这些锁被称为内置锁或监视器锁。线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时会自动释放锁。获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。
然而,由于内置锁是可重入的,因此如果摸个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。“重入”意味着获取锁的操作的粒度是“线程”,而不是调用。

public class Child extends Father {
   public static void main(String[] args) {
       Child child = new Child();
       child.doSomething();
   }
   public synchronized void doSomething() {
       System.out.println("child.doSomething()");
       doAnotherThing(); // 调用自己类中其他的synchronized方法
   }
   private synchronized void doAnotherThing() {
       super.doSomething(); // 调用父类的synchronized方法
       System.out.println("child.doAnotherThing()");
   }
}

class Father {
   public synchronized void doSomething() {
       System.out.println("father.doSomething()");
   }
}

运行结果

child.doSomething()
father.doSomething()
child.doAnotherThing()

这里的对象锁只有一个,就是child对象的锁,当执行child.doSomething()时,该线程获得child对象的锁,在doSomething()方法内执行doAnotherThing()时再次请求child对象的锁,因为synchronized是重入锁,所以可以得到该锁,继续在doAnotherThing()里执行父类的doSomething()方法时第三次请求child对象的锁,同理可得到,如果不是重入锁的话,那这后面这两次请求锁将会被一直阻塞,从而导致死锁。

在java内部,同一线程在调用自己类中其他synchronized方法/块或调用父类的synchronized方法/块都不会阻碍该线程的执行,就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入

因为java线程是基于“每线程(per-thread)”,而不是基于“每调用(per-invocation)”的(java中线程获得对象锁的操作是以每线程为粒度的,per-invocation互斥体获得对象锁的操作是以每调用作为粒度的)

重入的实现原理

每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为0,则释放该锁。

引用:
《java并发编程实战》
https://blog.csdn.net/aigoogle/article/details/29893667#commentsedit

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值