第66条:同步访问共享的可变数据
private static boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
System.out.println(stopRequested); //false
//这里停不下来是因为主线程对stopRequest进行修改的时候,这个线程并不可见
while (!stopRequested) {
++i;
System.out.println(i);
}
}
});
//启动线程
backgroundThread.start();
//休眠1秒
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
System.out.println("stopRequested="+stopRequested);
}
以上这段程序会永远的运行下去,因为没有使用同步,无法保证后台进程可以看到stopRequested值的改变
怎么解决?
1)使用synchronized
2)使用volatile
3)解决这一问题的最好办法其实是尽量避免在线程间共享可变数据,将可变数据限制在单线程中。让线程短时间修改一个对象,并与其他线程共享,这是可以接受的,只需要同步修改的动作即可。不需要同步,其他线程也可以读取该对象,只要它以后没有再改变。总的来说,如果想要多个线程共享可变数据,那么读写都需要进行同步。
第67条:避免过度同步
依据情况不同,过度同步可能会导致性能降低,死锁,甚至不确定的行为
参考:https://www.cnblogs.com/cutter-point/p/5951046.html
第68条:executor和task优先于线程
new Thread的弊端如下:
a. 每次new Thread新建对象性能差。
b. 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
c. 缺乏更多功能,如定时执行、定期执行、线程中断。
相比new Thread,Java提供的四种线程池的好处在于:
a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。
b. 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
c. 提供定时执行、定期执行、单线程、并发数控制等功能。
Java通过Executors提供四种线程池,分别为:
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
参考:https://www.cnblogs.com/baizhanshi/p/5469948.html
第69条:并发工具优先于wait和notify
优先使用java.util.concurrent包中提供的更高级的语言来代替wait,notify.
如果非要用wait和notify,注意以下几点:
1)wait前的条件检查,当条件成立时,就跳过等待,可以保证不会死锁,
2)wait后的检查,条件不成立继续等待,可以保证安全
3)通常情况下都应该使用notifyAll,虽然从优化角度看,这样不好.
java.util.concurrent中更高级的工具分三类:
1)异步执行框架 Executor Framework
2)并发集合 Concurrent Collection
3)同步器 Synchronizer
参考:
https://blog.csdn.net/axi295309066/article/details/65665090
https://blog.csdn.net/zxm1306192988/article/details/59701101
第70条:线程安全性的文档化
“只要是加了synchronized关键字的方法或者代码块就一定是线程安全的,而没有加这个关键字的代码就不是线程安全的”。这种观点认为“线程安全要么全有要么全无”,事实上这是错误的。因为线程安全包含了几种级别:
1)不可变的(Immutable):类的实例不可变(不可变类),一定线程安全,如String、Long、BigInteger等。
2)无条件的线程安全(Unconditionally ThreadSafe):该类的实例是可变的,但是这个类有足够的的内部同步。所以,它的实例可以被并发使用,无需任何外部同步,如Random和ConcurrentHashMap。
3)有条件的线程安全(Conditionally ThreadSafe):某些方法需要为了安全的并发而在外部进行同步,其余方法与无条件的线程安全一致。如Collection.synchronized返回的集合,对它们进行迭代时就需要外部同步。如下代码,当对4)synchronizeColletcion返回的 collection进行迭代时,用户必须手工在返回的 collection 上进行同步,不遵从此建议将导致无法确定的行为。
Collection c = Collections.synchronizedCollection(myCollection);
synchronized(c) {
Iterator i = c.iterator(); // Must be in the synchronized block
while (i.hasNext())
foo(i.next());
}
4)非线程安全(UnThreadSafe):该类是实例可变的,如需安全地并发使用,必须外部手动同步。如HashMap和HashSet。
5)线程对立的(thread-hostile):即便所有的方法都被外部同步保卫,这个类仍不能安全的被多个线程并发使用。这种情况的类很少,不常用。
以上是常用的5种线程安全性的级别,这些级别应该认真编写在类的线程安全注解中,以让用户清楚的知道某个类的线程安全性。synchronized关键字与这个文档毫无关系。
如果正在编写的是无条件的线程安全类,就应该考虑使用私有的锁对象来代替同步方法,这样可以防止客户端程序和子类的不同步干扰。
参考:https://blog.csdn.net/yizhenn/article/details/70224782
第71条:慎用延迟初始化
延迟初始化是延迟到需要域的值时才将它初始化的这种行为。
对于延迟初始化,最好建议“除非绝对必要,否则就不要那么做”。延迟化降低了初始化类或者创建实例的开销,却增加了访问被延迟初始化的域的开销。
如果域只是在类的实例部分被访问,并且初始化这个域的开销很高,可能就值得进行延迟初始化。
如果出于性能的考虑而需要对静态域使用延迟初始化,就使用lazy initialization holder class 模式。保证在被用时初始化。
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
public static FieldType getField() {
return FieldHolder.field;
}
如果出于性能的考虑而需要对实例域使用延迟初始化,就使用双重检查模式。这种模式避免了在域被初始化之后访问这个域时的锁定开销。
private volatile FieldType field;
public FieldType getField() {
FieldType result = field;
if (result == null) {
synchronized (this) {
result = field;
if (result == null) {
field = result = computeFieldValue();
}
}
}
return result;
}
第一次检查时没有锁定,看看这个域是否被初始化;第二次检查时又锁定。只有当第二次检查时标明这个域没有被初始化,才进行初始化。如果域已经被初始化就不会有锁定,域被声明为volatile很重要。
result局部变量的使用,是为了保证在已经被初始化的情况下,原来的变量只被读取一次到局部变量result中,否则在比较的时候需要读取一次,返回的时候还需要读取一次。虽然这不是严格要求,但是可以提升性能。
第72条:不要依赖于线程调度器
1)当有多个线程可以运行时,由线程调度器决定哪些线程将会执行.以及运行多长时间。
2)任何依赖于线程调度器来达到正确性或者性能要求的程序,很有可能都是不可移植的。
3)要确保可运行线程的平均数量不明显多于处理器的数量。
4)要编写健壮,响应良好的,可移植的多线程应用程序,最好的办法是确保可运行线程的平均数量不明显多于处理器的数量。
5)线程优先级是Java平台中移植性最差的部分,所以也不要用。
对于大多数程序员来说,Thread.yield的唯一用途,就是在测试期间人为的增加程序的并发性。
在Java语言规范中,Thread.yield根本不做实质性工作,只是将控制权返回给它的调用者。
第73条:避免使用线程组
线程组并没有提供太多有用的功能,而且他们提供的许多功能还都有缺陷的。
如果你正在设计的一个类需要处理线程的逻辑组,或许就应该使用线程池executor。
事例代码
https://gitee.com/charjay/effective.git