Java并发编程实战笔记

如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题:

i、不在线程之间共享该状态变量

ii、将状态变量修改为不可变的变量

iii、在访问状态变量时使用同步


 

什么是线程安全性?

A:我们可以将单线程的正确性近似定义为“所见即所知”。在对“正确性”给出了一个较为清晰的定义后,就可以定义线程安全性 :当多个线程访问某个类时,这个类始终都能表现出正确的行为,那么就称这个类是线程安全的。

当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

在线程安全类中封装了必要的同步机制,因此客户端无须进一步采取同步措施。


 

什么是数据竞争?

A:如果在访问共享的非final类型的域时没有采用同步来进行协同,那么就会出现数据竞争。当一个线程写入一个变量而另一个线程接下来读取这个变量,或者读取一个之前由另一个线程写入的变量时,并且在这两个线程之间没有使用同步,那么就可能出现数据竞争。在JAVA 内存模型中,如果在代码中出现数据竞争,那么这段代码就没有确定的语义。并非所有的竞态条件都是数据竞争,同样,并非所有的数据竞争都是竞态条件,但二者都有可能使并发程序失败。


 

竞态条件的本质:基于一种可能失效的观察结果来做出判断或者执行某个计算。

 这种类型的竞态条件称为“先检查后执行”:首先观察到某个条件为真(例如文件X不存在)。然后根据这个观察结果采用相应的动作(创建文件X),但事实上,在你观察到这个结果以及开始创建文件之间,观察结果可能变得无效(另一个线程在这期间创建了文件X),从而导致各种问题(未预期的异常、数据被覆盖、文件被破坏等)

 


 

volatile:

如果在验证正确性时需要对可见性进行复杂的判断,那么就不要使用volatile变量。volatile的正确使用方式包括:确保它们自身状态的可见性,确保它们所引用对象的状态的可见性,以及标识一些重要的程序生命周期事件的发生(例如:初始化或关闭)

volatile的典型用法:检查某个状态标记以判断是否退出循环。在下列这个栗子中,线程试图通过类似数绵羊的传统方法进入休眠状态。为了使这个示例可以正确执行,asleep必须为volatile变量,否则,当asleep被另一个线程修改时,执行判断的线程却发现不了。我们也可以用锁来确保asleep更新操作的可见性,但这将使代码变得更加复杂。

volatile boolean asleep;
...
while(!asleep) {
      countSomeSheep();  
}

虽然volatile很方便但也存在一些局限性。volatile变量通常用做某个操作完成、发生中断或者状态的标志,例如asleep标志。尽管volatile也可以用于表示其他的状态信息,但在使用时要非常小心。例如,volatile的语义不足以确保递增操作(count++)的原子性,除非你能确保只有一个线程对变量执行写操作。(原子变量提供了读-改-写的原子操作,并且常常用作一种更好的volatile变量)

加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。

 


 

在并发程序中使用和共享对象时,可以使用一些实用的策略,包括:

线程封闭:线程封闭的对象只能由一个线程拥有。对象被封闭在该线程中,并且只能由这个线程修改。

只读共享:在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改它。共享的只读对象包括不可变对象和事实不可变对象。

线程安全共享:线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问而不需要进一步的同步。

保护对象:被保护的对象只能通过持有的锁来访问。保护对象包括封装在其他线程安全对象中的对象,以及已发布的并且由某个特定锁保护的对象。

 


 

 

同步机制

1.synchronized类:synchronized关键字允许你在某个代码块或者某个完整的方法中定义一个临界段

2.Lock接口:Lock提供了比synchronized关键字更加灵活的同步操作。Lock接口有多种不同类型:ReentrantLock用于实现一个可与某种条件相关联的锁;ReentrantReadWriteLock将读写操作分离开来;StampedLock是Java8中增加的一种新特性,它包括三种控制读写的访问模式。

3.Semaphore类:该类通过实现经典的信号量机制来实现同步。Java支持二进制信号量和一般信号量。

4.CountDownLatch类:该类允许一个任务等待多项操作的结束

5.CyclicBarrier类:该类允许多线程在某一共同点上进行同步

6.Phaser类:该类允许你控制那些分割成多个阶段的任务的执行。在所有任务都完成当前阶段之前,任何任务都不能进入下一个阶段

 

执行器:将线程的创建和管理分割开来的一种机制

1.Executor接口和ExecutorService类:它们包含了所有执行器共有的execute()方法。

2.ThreadPoolExecutor类:该类允许你获取一个含有线程池的执行器,而且可以定义并行任务的最大数目。

3.ScheduledThreadPoolExecutor类:这是一种特殊的执行器,可以使你在某段延迟之后执行任务或者周期性执行任务。

4.Callable接口:这是Runnable接口的替代接口——可返回值的一个单独的任务。

5.Future接口:该接口包含了一些能获取Callable接口返回值并且控制其状态的方法。

 

Fork/Join框架:尤其针对分治法进行求解的问题。

1.ForkJoinPool:该类实现了要用于运行任务的执行器。

2.ForkJoinTask接口:这是一个可以在ForkJoinPool类中执行的任务。

3.ForkJoinWorkerThread:这是一个准备在ForkJoinPool类中执行任务的线程。

 

并发数据结构:Java并发API分为 阻塞型数据结构、非阻塞型数据结构

ConcurrentLinkedDeque:非阻塞型列表

ConcurrentLinkedQueue:非阻塞型队列

LinkedBlockingDeque:阻塞型列表

LinkedBlockingQuque:阻塞型队列

PriorityBlockingQueue:基于优先级对元素排列的阻塞型队列

ConcurrentSkipListMap:非阻塞型NavigableMap

ConcurrentHashMap:非阻塞型哈希表

AtomicBoolean、AtomicInteger、AtomicLong和AtomicReference:基本Java数据类型的原子实现

 


 

不可变对象的好处:

1.不需要任何同步机制来保护这些类的方法。如果两个任务要修改同一对象,它们将创建新的对象,因此绝不会出现两个任务同时修改同一对象的情况。

2.不会有任何数据不一致问题,因为这是第一点的必然结果。

不可变对象的缺点:

1.如果你创建了太多对象,可能会影响应用程序的吞吐量和内存使用。如果你有一个没有内部数据结构的简单对象,将其作为不可变对象通常没问题。然而,构造由其他对象集合整合而成的复杂不可变对象通常会导致严重的性能问题。


 

编号:

在并发程序中避免死锁的最佳机制之一就是强制要求任务总是以相同顺序获取资源。实现这种机制的一种简单方式是为每个资源都分配一个编号。当一个任务需要多个资源时,它需要按照顺序来请求。


使用原子变量代替同步:

当你在两个或者多个任务之间共享数据时,必须使用同步机制来保护对该数据的访问,并且避免任何数据不一致的问题。某些情况下,你可以使用volatile关键字而不是用同步机制。如果只有一个任务修改数据而其他任务都读取数据,那么你可以使用volatile关键字而无需任何同步机制。在其他场合,你需要使用锁、synchronized关键字或者其他同步方法。

在Java 5中,并发API中一种新的变量,叫做原子变量。这些变量都是在单个变量上支持原子操作的类。它们含有一个名为compareAndSet(oldValue,newValue)的方法,该方法具有一种机制,可用于探测某个步骤中将新值赋给变量的操作是否完成。如果变量的值等于oldValue,那么该方法将变量的值改为newValue并且返回true。否则,该方法返回false。

以类似方式工作的方法还有很多,例如getAndIncrement()和getAndDecrement()等。这些方法也是原子的。

该解决方法是免锁的,也就是说不需要使用锁或者任何同步机制,因此它的性能比任何采用同步机制的解决方案要好。.

在Java中可用的最重要的原子变量有如下几种:

AtomicInteger

AtomicLong

AtomicReference

AtomicBoolean

LongAdder

DoubleAdder


 

synchronized 和 ReentrantLock 比较

1. 锁的实现

synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。

2. 性能

新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,因此性能因素不再是选择 ReentrantLock 的理由。synchronized 有更大的性能优化空间,应该优先考虑 synchronized。

3. 功能

ReentrantLock 多了一些高级功能。

4. 使用选择

除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。


 

同步/异步/阻塞/非阻塞 场景举例与总结

最后,再来举一个我们日常的例子来加深对这几个概念的理解。

假设小明需要在网上下载一个软件:

  • 如果小明点击下载按钮之后,就一直干瞪着进度条不做其他任何事情直到软件下载完成,这是同步阻塞;
  • 如果小明点击下载按钮之后,就一直干瞪着进度条不做其他任何事情直到软件下载完成,但是软件下载完成其实是会「叮」的一声通知的(但小明依然那样干等着),这是异步阻塞;(不常见)
  • 如果小明点击下载按钮之后,就去做其他事情了,不过他总需要时不时瞄一眼屏幕看软件是不是下载完成了,这是同步非阻塞;
  • 如果小明点击下载按钮之后,就去做其他事情了,软件下载完之后「叮」的一声通知小明,小明再回来继续处理下载完的软件,这是异步非阻塞。

相信看完以上这个案例之后,这几个概念已经能够分辨得很清楚了。

总的来说,同步和异步关注的是任务完成消息通知的机制,而阻塞和非阻塞关注的是等待任务完成时请求者的状态。


老张爱喝茶,废话不说,煮开水。 出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。

1 老张把水壶放到火上,立等水开。(同步阻塞) 老张觉得自己有点傻

2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞) 老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。

3 老张把响水壶放到火上,立等水开。(异步阻塞) 老张觉得这样傻等意义不大

4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞) 老张觉得自己聪明了。

所谓同步异步,只是对于水壶而言。 普通水壶,同步;响水壶,异步。 虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。 同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。

所谓阻塞非阻塞,仅仅对于老张而言。 立等的老张,阻塞;看电视的老张,非阻塞。 情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。

 

作者:未央
链接:https://www.zhihu.com/question/26393784/answer/513257548
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
线程状态转换:
1.NEW
2.RUNNABLE
3.WAITING
4.TIME_WAITNG
5.BLOCKED
6.TERMINATED
 

对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。

sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

在调用sleep()方法的过程中,线程不会释放对象锁。

而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备

获取对象锁进入运行状态。

posted @ 2019-02-21 11:11  Roni_i  阅读(220)  评论(0编辑  收藏  举报