【effective java】66~73.并发

第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

猜你喜欢

转载自blog.csdn.net/CharJay_Lin/article/details/81433493