什么是线程安全性
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么称这个类是线程安全的。
线程安全管理的核心
对状态访问操作进行管理,主要是共享的和可变的状态的访问。
线程安全常见几种情景
- 原子性
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);
}
- 竞态条件
//延迟加载的例子
@NotThreadSafe
public class LazyInitRace {
private ExpensiveObject instance = null;
public ExpensiveObject getInstance() {
if (instance == null) //多个线程竞争这一条件
instance = new ExpensiveObject();
return instance;
}
}
这里延迟加载就是一种竞态条件,可能会出现线程安全问题
加锁机制—内置锁
java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。同步代码块包含两部分:一个作为锁的对象引用,一个作为由这个锁的保护的代码块。主要有以下三种用法
- 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