《Java并发编程实战》学习笔记(1)

第一章:介绍

知识点

  • 进程是资源(CPU、内存等)分配的基本单位
  • 线程是CPU调度和分派的基本单位
  • 一个进程包括一个或多个线程

1、为什么应用程序需要从单线程发展为多线程(从同步到异步)?

  • 资源利用。程序有时候需要等待外部的操作,比如输入和输出,并且在等待的时候不可能进行有价值的工作。在等待的时候,让其他的程序运行会提高效率。
  • 公平。多个用户或程序可能对系统资源有平等的优先级别。让他们通过更好的时间片方式来共享计算机,这要比结束一个程序后才开始下一个程序更可取。
  • 方便。写一些程序,让它们各自执行一个单独任务并进行必要的相互协调,这要比编写一个程序来执行所有的任务更容易,更让人满意。

第二章:线程安全

一个对象的状态就是它的数据。编写线程安全的代码,本质上就是管理对状态(state)的访问,而且通常都是共享的、可变的状态。

所谓共享,是指一个变量可以被多个线程访问;所谓可变,是指变量的值在其生命周期内可以改变。我们讨论的线程安全性好像是关于代码的,但是我们真正要做的,是在不可控制的并发访问中保护数据。

在没有正确同步的情况下,如果多个线程访问了同一个变量,你的程序就存在隐患。有3种方法修复它:

  • 不要跨线程共享变量;
  • 使状态变量为不可变的;或者
  • 在任何访问状态变量的时候使用同步。

线程安全的定义:

当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替执行,并且不需要额外的同步及在调用方代码不必作其他的协调,这个类的行为仍然是正确的,那么称这个类是线程安全的。

无状态对象永远是线程安全的。

线程不安全之复合操作

读-改-写(read-modify-write)

@NotThreadSafe
public class UnsafeCountingFactorizer 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);
    }
}

检查再运行(check-act)

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

为了确保线程安全,“检查再运行” 操作(如惰性初始化)和读-改-写操作(如自增)必须是原子操作。我们将“检查再运行”和“读-改-写”的全部执行过程看作是复合操作:为了保证线程安全,操作必须原子地执行。

我们会在下一节考虑用Java内置的原子性机制——锁。现在,我们先用其他方法修复这个问题——使用已有的线程安全类。

@ThreadSafe
public class CountingFactorizer 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);
    }
}

java.util.concurrent.atomic包中包括了原子变量(atomic variable) 类,这些类用来实现数字和对象引用的原子状态转换。把long类型的计数器替换为AtomicLong类型的,我们可以确保所有访问计数器状态的操作都是原子的。计数器是线程安全的了。

内部锁

Java提供了强制原子性的内置锁机制:synchronized。一个synchronized块有两部分:锁对象的引用,以及这个锁保护的代码块。

每个Java对象都可以隐式地扮演一个用于同步的锁的角色;这些内置的锁被称作内部锁(intrinsic locks)或监视器锁(monitor locks)。

执行线程进入synchronized块之前会自动获得锁;而无论通过正常控制路径退出,还是从块中抛出异常,线程都在放弃对synchronized块的控制时自动释放锁。

获得内部锁的唯一途径是:进入这个内部锁保护的同步块或方法。

内部锁在Java中扮演了互斥锁(mutual exclusion lock,也称作mutex)的角色,意味着至多只有一个线程可以拥有锁,当线程A尝试请求一个被线程B占有的锁时,线程A必须等待或者阻塞,直到B释放它。如果B永远不释放锁,A将永远等下去。

重进入(Reentrancy)

当一个线程请求其他线程已经占有的锁时,请求线程将被阻塞。然而内部锁是可重进入的,因此线程在试图获得它自己占有的锁时,请求会成功。

重进入意味着所的请求是基于“每线程(per-thread)”,而不是基于“每调用(per-invocation) ”的。

重进入的实现是通过为每个锁关联一个请求计数(acquisition count)和一个占有它的线程。当计数为0时,认为锁是未被占有的。

线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数置为1。如果同一线程再次请求这个锁,计数将递增;每次占用线程退出同步块,计数器值将递减。直到计数器达到0时,锁被释放。

/**
*	如果该锁不是可重入的,代码将死锁
*/
public class Widget {
	public synchronized void doSomething() {
		...
	}
}
public class LoggingWidget extends Widget {
	@Override
	public synchronized void doSomething() {
		super.doSomething();
	}
}

对于每个可被多个线程访问的可变状态变量,如果所有访问它的线程在执行时都占有同一个锁,这种情况下,我们称这个变量是由这个锁保护的。

每个共享的可变变量都需要由唯一一个确定的锁保护。

适当调整synchronized块的大小,使其在安全和性能之间达到平衡。

有些耗时的计算或操作,比如网络或控制台I/O,难以快速完成。执行这些操作期间不要占有锁。

发布了107 篇原创文章 · 获赞 88 · 访问量 26万+

猜你喜欢

转载自blog.csdn.net/Code_shadow/article/details/104275669
今日推荐