第66条 同步访问共享的可变数据
1.同步仅仅意味着互斥访问吗?
不是,同步一方面意味着互斥访问,这保证了同步操作的原子性,可以防止线程访问到对象的不一致状态;另一方面,同步意味着保证可见性,他保证进入同步方法或者同步代码快的每个线程,都看到由同一个锁保护的之前所有的修改效果.
2.为了提高性能,在读或者写原子数据的时候,应该避免使用同步,对吗?
不对,虽然原子数据的读或写是原子的..,但是没有保证可见性,也就是说一个线程对原子数据的读或写对另一个线程可能不可见
3.
public class StopThread {
private static boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(new Runnable() {
public void run() {
int i = 0;
while (!stopRequested)
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
上面这段代码有什么问题?
答:可能永远不会停止,因为这里没有同步,而在循环体内又没有对stopRequested的修改操作,所以虚拟机会将
while (!stopRequested)
i++;
优化为
if(!stopRequested)
while (true)
i++;
这种优化称为提升(hoisting),结果是个活性失败:这个程序无法前进
4.上面问题如何修正
第一种方法是对stopRequested的读写操作进行同步
第二种方法是将stopRequested域定义为violate
5.什么是活性失败,什么是安全性失败
(自己猜的):活性失败就是虚拟机/编译器在基于单线程的语义不变的前提下进行活性分析并优化原程序导致在多线程环境下的程序失败
(自己猜的):安全性失败就是,举个例子吧:
private static violate int nextSerialNumber=0;
public static int generateSerialNumber(){
return nextSerialNumber++;
}
这段程序中nextSerialNumber是violate的,可是nextSerialNumber++这个操作并不是原子的,他会先读取nextSerialNumber然后+1再返回,这样的话就有可能有这样的问题了:一个线程A执行了nextSerialNumber++中的读取nextSerialNumber操作,然后另一个线程B马上也执行读取nextSerialNumber操作,他们读取的nextSerialNumber是相同的,然后他们分别对nextSerialNumber+1并返回,这样就和需求不符了,这是一种安全性失败(safty failure)
6.集合中什么是快速失败(fail-fast)和安全失败(fail-safe)
快速失败:当你在迭代一个集合的时候,如果有另一个线程正在修改你正在访问的那个集合时,就会抛出一个ConcurrentModification异常。在java.util包下的都是快速失败。
安全失败:你在迭代的时候会去底层集合做一个拷贝,所以你在修改上层集合的时候是不会受影响的,不会抛出ConcurrentModification异常。
在java.util.concurrent包下的全是安全失败的。
(来自牛克网:https://www.nowcoder.com/questionTerminal/95e4f9fa513c4ef5bd6344cc3819d3f7?pos=101&mutiTagIds=570&orderByHotValue=1)
第67条 避免过度同步
为了避免活性失败和安全性失败,在一个被同步的方法或者代码块中,永远不要放弃对客户端的控制,什么意思呢,就是在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者是由客户端以对象的形式提供的方法
第68条 executor和task优先于线程
第69条 并发工具优先于wait和notify
1.JUC中的高级工具分为三类:
Executor Framework,并发集合,同步器
2.为了提高并发性,这些实现(并发集合)在内部自己管理同步.因此,并发集合中不可能排除并发活动;将它锁定没有什么作用,只会使程序的速度变慢.这意味着<em>客户无法原子地对并发集合进行方法调用</em>(因为这些实现(并发集合)在内部自己管理同步,所以你不一定拿得到这些并发集合内部状态的守护锁).因此有些集合接口已经通过依赖状态的修改操作进行了扩展,他将几个基本操作合并到了单个原子操作中.例如,ConcurrentMap扩展了Map接口,并添加了几个方法,包括putIfAbsent(key,value)
3.同步器是一些使线程能够等待另一个线程的对象,允许它们协调动作.有CountDownLatch,Semaphore,CyclicBarrier和Exchanger
4.始终应该使用wait循环模式来调用wait方法,永远不要在循环之外调用wait方法.循环会在等待之前和之后测试条件.为什么??
分两点:在等待之前测试条件和等待之后测试条件.
在等待之前测试条件,当条件已经成立时就跳过等待,这对于确保活性(liveness)是必要的.如果条件已经成立,并且在线程的等待之前,notify(或者notifyAll)方法已经被调用,则无法保证该线程将会从等待中苏醒过来.
在等待之后测试条件,如果条件不成立的话继续等待,这对于确保安全性(safety)是必要的.当条件不成立的时候,如果线程继续执行,则可能会破坏被锁保护的约束关系.当条件不成立时,有下面一些理由可使一个线程苏醒过来:
另一个线程可能已经得到了锁,并且从一个线程调用notify那一刻起,到等待线程苏醒过来的这段时间中,得到锁的线程已经改变了受保护的状态
条件并不成立,但是另一个线程可能意外地或恶意地调用了notify.(尤其在公有可访问对象上等待)
通知线程(notifying thread)在唤醒等待线程时可能会过度"大方".例如,即使只有某一些等待线程的条件已经被满足,但是通知线程可能仍然调用notifyAll
在没有通知的情况下,等待线程也可能(但很少)会苏醒过来.这被称为"伪唤醒"
第70条 线程安全性的文档化
线程安全性的几种级别:
不可变的(immutable)
无条件的线程安全:如Random和ConcurrentHashMap
有条件的线程安全:如Collections.synchronized包装返回的集合,他们的迭代器要求外部同步
非线程安全:为了并发地使用他们,客户必须利用自己选择的外部同步包围每个方法调用(或者调用序列)
线程对立:这个类不能安全地被多个线程并发使用,即使所有的方法调用都被外部同步包围.线程对立的根源通常在于没有同步地修改静态数据
第71条 慎用延迟初始化
public class Initialization {
1.除非绝对必要,否则就不要使用延迟初始化
private final FieldType field1 = computeFieldValue();
2.使用同步方法的延迟初始化:
private FieldType field2;
synchronized FieldType getField2() {
if (field2 == null)
field2 = computeFieldValue();
return field2;
}
3.对静态域,使用Lazy initialization holder class模式(也称作initialize-on-demand holder class模式)
<em>现代的VM将在初始化该类的时候,同步域的访问.一旦这个类被初始化,VM将修补代码,以便后续对该域的访问不会导致任何测试或者同步</em>
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
static FieldType getField3() {
return FieldHolder.field;
}
4.双重检查
private volatile FieldType field4;
FieldType getField4() {
FieldType result = field4;
if (result == null) {
synchronized (this) {
result = field4;
if (result == null)
field4 = result = computeFieldValue();
}
}
return result;
}
5.可以接受重复初始化的情况下可以使用单检查
private volatile FieldType field5;
private FieldType getField5() {
FieldType result = field5;
if (result == null)
field5 = result = computeFieldValue();
return result;
}
private static FieldType computeFieldValue() {
return new FieldType();
}
}
第72条 不要依赖于线程调度器
1.当有多个线程可以运行时,由线程调度器决定哪些线程将会运行.
2.任何依赖线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的.
3.怎么做到不依赖线程调度器呢?最好的方法就是确保<em>可运行线程</em>(等待的线程不是可运行的)的平均数量不明显多于处理器的数量,这使得线程调度器没有更多的选择:他只需要运行这些可运行的线程.
4.那要怎么减少可运行线程的数量呢?让每个线程做些有意义的工作,然后等待更多有意义的工作.如果线程没有在做有意义的工作,就不应该运行(线程不应该一直处于忙-等(busy-wait)的状态,即反复地检查一个共享对象,以等待某些事情发生).
5.如果某一个程序不能工作,是因为某些线程无法像其他程序那样获得足够的CPU时间,那么不要企图通过调用Thread.yield来"修正"程序,因为不同JVM可能对yield的实现不同.
6.还有类似的:线程优先级是java平台上最不可移植的特性了
第73条 避免使用线程组(ThreadGroup)