同步阻塞
每一个 Java 对象有一个锁。线程可以通过调用同步方法获得锁。还有另一种机制可以获得锁,通过进入一个同步阻塞。当线程进入如下形式的阻塞
synchronized (obj) // this is the syntax for a synchronized block
{
critical section
}
有时会发现“ 特殊的” 锁,例如:
public class Bank{
private doublet] accounts;private Object lock = new Object。;
public void transfer(int from, int to, int amount){
synchronized (lock) // an ad-hoc lock
{
accounts[from] -= amount;accounts[to] += amount;}
System.out.print
在此,lock 对象被创建仅仅是用来使用每个 Java 对象持有的锁。例子:
Object a = new Object();
//同步阻塞
new Thread(() -> {
synchronized (a) {
for (int b = 0; b <= 1000; b++) {
System.out.println(b + "-----");
}
}
}).start();
new Thread(() -> {
synchronized (a) {
for (int b = 0; b <= 1000; b++) {
System.out.println(b + "-----");
}
}
}).start();
}
监视器
刚看完这一段,又去百度了一下监视器,没想到打成了监听器,感觉和线程没什么关系,才发现把监视器和监听器当成了一种东西,很傻。。
锁和条件是线程同步的强大工具,但是,严格地讲,它们不是面向对象的。多年来,研究人员努力寻找一种方法,可以在不需要程序员考虑如何加锁的情况下,就可以保证多线程的安全性。最成功的解决方案之一是监视器(monitor), 这一概念最早是由 PerBrinchHansen和 Tony Hoare 在 20 世纪 70 年代提出的。用 Java 的术语来讲,监视器具有如下特性:
•监视器是只包含私有域的类。•使用该锁对所有的方法进行加锁。换句话说,如果客户端调用 obj.meth0d(), 那 么 obj对象的锁是在方法调用开始时自动获得, 并且当方法返回时自动释放该锁。因为所有的域是私有的,这样的安排可以确保一个线程在对对象操作时, 没有其他线程能访问该域。
•每个监视器类的对象有一个相关。
•该锁可以有任意多个相关条件。
Java 设计者以不是很精确的方式采用了监视器概念, Java 中的每一个对象有一个内部的锁和内部的条件。如果一个方法用 synchronized 关键字声明,那么,它表现的就像是一个监视器方法。通过调过调用 wait/notifyAU/notify 来访问条件变量。
在下述的 3 个方面 Java 对象不同于监视器, 从而使得线程的安全性下降:
•域不要求必须是 private。
•方法不要求必须是 synchronized。
•内部锁对客户是可用的。
Volatile域
多并发编程的三个特别重要的概念:可见性,原子性,有序性
volatile 关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为 volatile ,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
但是volatile关键字只实现了可见性和有序性,并没有实现原子性。
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
以下是volatile关键字的使用场景:
1.状态标记量
volatile boolean flag = false;
while(!flag){
doSomething();
}
public void setFlag() {
flag = true;
}
2.双重检查
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instance==null) {
synchronized (Singleton.class) {
if(instance==null)
instance = new Singleton();
}
}
return instance;
}
}
关于volatile关键字的详细介绍与内部原理,查看 http://www.importnew.com/18126.html,非常的不错。
线程局部变量
要为每个线程构造一个实例,可以使用以下代码:
要访问具体的格式化方法,可以调用
public static final ThreadLocal<SimpleDateFormat> dateFormat =ThreadLocal.withInitial(() -> new SimpleDateF
String dateStamp = dateFormat.get().format(new DateO);
ThreadLocal<T> 在一个给定线程中首次调用 get 时, 会调用 initialValue 方法。在此之后, get 方法会返回
属于当前线程的那个。
读/写锁
// 下面是使用读 / 写锁的必要步骤:
// 1 ) 构 造 一 个 ReentrantReadWriteLock 对象:
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock():
// 2 ) 抽取读锁和写锁:
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();
// 3 ) 对所有的获取方法加读锁:
public double getTotalBalance()
{
readLock.lock();
try {
//...
} finally {
readLock.unlock();
}
}
// 4 ) 对所有的修改方法加写锁:
public void transfer() {
writeLock.lock();
try {
// . . .
} finally {
writeLock.unlock();
}
}
上面的代码很清楚的展示了读/写锁的用法,在一个写线程与多个读线程的操作时,读/写锁和volatile关键字修饰是一样的,因为volatile关键字能保证可见性,但是在多个写线程与多个读线程时,volatile就不行了,因为不能保证原子性,而读/写锁可以互斥,同时保证了尽可能的并行。 synchronized的修饰一般限制这个区域是不可重入的。每次只有一个线程可以访问。这种强烈的互斥性使得每次不管是读数据还是写数据都只能有一个线程可以操作。在希望有多个读线程可以并行执行的情况下,它并不是一个理想的选择。