66.同步访问共享的可变数据
基本数据类型除了 long 与 double 都是原子性的
java虽然保证了在读取原子数据时不会看到任意的数值,但它并不能保证一个线程的写入,对例外一个线程是可见的,考察如下代码:
private static boolean canAdd = false;
public static void main(String[] args)
throws InterruptedException
{
new Thread(() -> {
int i = 0;
while (!canAdd)
{
i++;
}
}).start();
TimeUnit.SECONDS.sleep(1);
canAdd = true;
}
其实这块代码将永远不会停止,而不是预想的那样在1s 左右停止,这是由于虚拟机在运行时将代码优化成了 if(!canAdd) while(true){…}
修正这个问题应该同步访问canAdd
private static boolean canAdd = false;
public synchronized static boolean getCanAdd(){
return canAdd;
}
public synchronized static void stop(){
canAdd = true;
}
public static void main(String[] args)
throws InterruptedException
{
new Thread(() -> {
int i = 0;
while (!getCanAdd())
{
i++;
}
}).start();
TimeUnit.SECONDS.sleep(1);
stop();
}
注意读写方法都需要同步,如果只同步了一个方法,同步是不起作用的
如果同步方法不为了互斥访问,以上例子可以使用volatile 来修饰 canAdd,保证每个线程在读取该域时都将看到最近刚刚被写入的值
注意++操作符不会原子的,可能存在第二个线程在第一个线程读取旧值与返回新值之间读取这个域,可以使用 synchronized 来保证++操作符的互斥,其实可以使用 AtomicLong 类来实现自增
不共享可变的数据,或者共享不可变的数据就不存在需要考虑线程同步的问题
当多个线程共享可变数据时每个读或者写的线程都必须执行同步
67.避免过度同步
在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者是由客户端以函数对象的形式提供的方法
为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法
68.executor和 task 优先于线程
尽量不要编写自己的工作队列,尽量不要直接使用线程
工作队列Executors 类中提供了各种线程池(单例,多例,固定数量,可配置数量等的) ThreadPoolExecutor
任务有两种 Runnable与 Callable
任务的通用机制: executor service
ScheduledThreadPoolExecutor 来代替 timer执行定时任务
69.并发工具优先于wait 和notify
并发集合,不可能排除并发活动,将它锁定没有什么作业,只会使程序变慢
ConcurrentHashMap BlockingQueue
同步器(Synchronizer),最常用的同步器CountDownLatch和Semaphore,较少使用CyclicBarrier Exchanger
倒计数锁存器(CountDownLatch),构造方法 int 值只所有在等待的线程在被处理之前必须在锁存器上调用countDown方法的次数
定时操作应该使用 System.nanoTime而不是System.currentTimeMills,前者更加精准,不受系统的实时时钟影响
永远应该在 while 循环中使用 wait 方法,循环可以在等待之前和之后测试条件
synchronized(obj){
while(...obj){
obj.wait();
}
....
}
在 while 中检查条件的原因:其他线程获取锁后调用 notify 但已经改变了 obj 的状态,此时需要重新检查条件,条件并不成立,但某个方法恶意调用了 notify,通知线程过度大方 notifyAll,在没有通知的情况下线程也有可能会苏醒(伪唤醒)
正常来说不应该使用 wait 与 notify
优先使用 notifyAll,如果使用 notify,请小心保持程序的活性
70.线程安全的文档性
线程安全级别:不可变的,无条件线程安全的,有条件线程安全,非线程安全的,线程对立的(几乎不存在)
私有锁对象模式(类内部持有锁,保证线程安全)只能用于无条件线程安全的类,可以避免子类和客户端程序的干扰
71.慎用延迟初始化
就像绝大数优化一样,延迟初始化除非绝对必要,否则不要这样做
大多数情况正常初始化都要优于延迟初始化
如果使用延迟初始化来破坏初始化循环就需要使用同步方法
如果出于性能考虑需要对静态域使用延迟初始化,就使用如下方式(lazy initialization holder class):
private TestLazyInit()
{
}
private static class TestLazyInitHolder
{
static final TestLazyInit field = new TestLazyInit();
}
public static TestLazyInit getInstance()
{
return TestLazyInitHolder.field;
}
如果出于性能考虑对实例域做延迟加载,就使用双重检查模式
//注意 field 需要 volatile 修饰
if(null==field){
synchronized(this){
if(null==field){
....
}
}
}
可以使用局部变量检查的方式(性能更好)
虽然也可以对静态域使用双重检查模式,但没有必要这样做,前一种方法是更好的选择
72.不要依赖于线程调度器
任何依赖线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的
线程优先级是 java 平台上最不可移植的特征
大多数情况Thread.yield的唯一用途是在测试期间人为的增加程序的并发性,应该使用 Thread.sleep(1)代替 Thread.yield,千万不要使用 Thread.sleep(0)它会立即返回
73.避免使用线程组
使用 executor