多线程编程(1)(共享内存与锁)

1、什么是共享内存
共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址,就好像它们是由用C语言函数malloc分配的内存一样。而如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。
多线程编程(1)(共享内存与锁)
文章图片





2、竞态条件
由于共享内存的存在,当多个线程同时对同一内存地址进行读写操作时,最终结果取决于线程的执行顺序。

package com.paulbutcher; public class Counting { public static void main(String[] args) throws InterruptedException { long start = System.currentTimeMillis(); System.out.println(start); class Counter{ private int count; public int getCount(){ return count; } public void increment(){ ++count; } }final Counter counter = new Counter(); class CountingThread extends Thread{ public void run(){ for(int x = 0 ; x < 10000 ; x++){ counter.increment(); } } }CountingThread thread1 = new CountingThread(); CountingThread thread2 = new CountingThread(); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println(counter.getCount()); } }

上面这个列子,最终的打印结果是不固定的,肯定是小于20000的结果,其原因就是竞态条件


3、锁
为了避免竞态条件引起的不确定性,我们可以使用锁来达到线程互斥的目的,即某一时间至多有一个线程持有锁。
/** * 对于共享变量,在多线程操作时需要添加锁,保证不能有多个线程同时操作该变量 */ public synchronized void increment(){ ++count; }

我们只需要给count的自增方法添加同步,这样运行的结果就是我们期望的20000了。 4、内存可见性
java内存模型定义了何时一个线程对内存的修改对另一个线程的可见性。其基本原则是:如果读线程和写线程不进行同步,就不能保证可见性。


5、死锁
当多线程程序中存在多个锁的情况下,这样不但效率低下,线程之间也有可能发生死锁。
死锁发生的情况,当所有锁被多个线程持有,并且所有持有锁的线程都在等待其他锁的释放,这个时候就会发生死锁
死锁产生的条件:1、互斥条件;2、请求和保持条件;3、不剥夺条件;4、环路等待条件(自行百度)
可以通过科学家进餐问题,来阐述死锁
有5为科学家做成一个圆形的桌子上进餐,左手边各有一只筷子(仅有左手边),
哲学家的状态可能是“ 思考” 或者“ 饥饿”。 如果饥饿, 哲学家将拿起他 边的筷子并进餐 一段时间。 进餐结束, 哲学家就会放回筷子。




import java.util.Random; public class Philosopher extends Thread{ private Chopstick left, right; private Random random; public Philosopher( Chopstick left, Chopstick right) { this. left = left; this. right = right; random = new Random(); } public void run(){ try{ while(true){ Thread.sleep(random.nextInt(1000)); //科学家随机考虑一段时间 synchronized (left) {//拿起左侧筷子 synchronized (right) {//拿起右侧筷子 Thread.sleep(random.nextInt(1000)); //开始吃饭一段时间 } } } }catch(Exception e){ e.printStackTrace(); } } public class Chopstick{ } }


这5为科学家可以愉快的进餐一段时间(最长一周),直到某个时刻停止下来。分析其原因,就是5个科学家同时拿起左侧筷子,然后等待拿右手边筷子。这是死锁就出现了。 一个线程想使用多把锁时,就需要考虑死锁的问题。幸运的是,有一个简单的规则可以避开死锁------总是按照一个全局的固定的顺序获取多把锁。
package com.paulbutcher; import java.util.Random; public class Philosopher extends Thread{ private Chopstick first, second; private Random random; public Philosopher( Chopstick left, Chopstick right) { if( left. getId() < right. getId()) { first = left; second = right; }else { first = right; second = left; }random = new Random(); } public void run(){ try{ while(true){ Thread.sleep(random.nextInt(1000)); //科学家随机考虑一段时间 synchronized (first) {//拿起筷子1 synchronized (second) {//拿起筷子2 Thread.sleep(random.nextInt(1000)); //开始吃饭一段时间 } } } }catch(Exception e){ e.printStackTrace(); } } public class Chopstick{ private Integer id; public Integer getId() { return id; }public void setId(Integer id) { this.id = id; } } }

科学家不再按照左手边和右手边的顺序拿筷子了,而是根据编号,获取编号1和编号2的筷子。(只需要保证编号唯一和有序)现在科学家可以一直进行下去了。

问:可以使用对象的散列值做为锁的全局顺序吗?
有一个常用的技巧是使用对象的散列值作为锁的顺序,类似于以下的代码:

if( System. identityHashCode( left) < System. identityHashCode( right)) { first = left; second = right; } else { first = right; second = left; }

这个技巧的好处是适用于所有的java对象,不用为锁对象单独定义并维护一个顺序。但是对象的散列值不能保证唯一(虽然重复的概率很小,但确实可能发生重复)。 【多线程编程(1)(共享内存与锁)】除非迫不得已,尽量不要使用对象散列值作为锁全局顺序

    推荐阅读