java多线程(九)

在两个线程之间共享数据

  Java里面进行多线程通信的主要方式就是共享内存的方式,共享内存主要的关注点有三个:可见性和有序性、原子性。Java内存模型(JMM)解决了可见性和有序性的问题,而锁解决了原子性的问题,理想情况下我们希望做到“同步”和“互斥”。有以下常规实现方法:

将数据抽象成一个类,并将数据的操作作为这个类的方法

  将数据抽象成一个类,并将对这个数据的操作作为这个类的方法,这么设计可以和容易做到同步,只要在方法上加”synchronized“

1
2
3
4
5
6
7
8
9
public class AddRunnable implements Runnable {
MyData data;
public AddRunnable(MyData data) {
this.data = data;
}
public void run() {
data.add();
}
}

1
2
3
4
5
6
7
8
9
public class DecRunnable implements Runnable {
MyData data;
public DecRunnable(MyData data) {
this.data = data;
}
public void run() {
data.dec();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MyData {
private int j = 0;
public synchronized void add() {
j++;
System.out.println("线程" + Thread.currentThread().getName() + "j 为:" + j);
}
public synchronized void dec() {
j--;
System.out.println("线程" + Thread.currentThread().getName() + "j 为:" + j);
}
public int getData() {
return j;
}
public static void main(String[] args) {
MyData data = new MyData();
Runnable add = new AddRunnable(data);
Runnable dec = new DecRunnable(data);
for (int i = 0; i < 2; i++) {
new Thread(add).start();
new Thread(dec).start();
}
}
}

Runnable 对象作为一个类的内部类

  将Runnable对象作为一个类的内部类,共享数据作为这个类的成员变量,每个线程对共享数据的操作方法也封装在外部类,以便实现对数据的各个操作的同步和互斥,作为内部类的各个Runnable对象调用外部类的这些方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyData {
private int j = 0;
public synchronized void add() {
j++;
System.out.println("线程" + Thread.currentThread().getName() + "j 为:" + j);
}
public synchronized void dec() {
j--;
System.out.println("线程" + Thread.currentThread().getName() + "j 为:" + j);
}
public int getData() {
return j;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestThread {
public static void main(String[] args) {
final MyData data = new MyData();
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
public void run() {
data.add();
}
}).start();
new Thread(new Runnable() {
public void run() {
data.dec();
}
}).start();
}
}
}

ThreadLocal作用(线程本地存储)

  ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

ThreadLocalMap(线程的一个属性)

  1. 每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
  2. 将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
  3. ThreadLocalMap 其实就是线程里面的一个属性,它在Thread类中定义ThreadLocal.ThreadLocalMap threadLocals = null;

synchronized和ReentrantLock的区别

两者的共同点

  1. 都是用来协调多线程对共享对象、变量的访问
  2. 都是可重入锁,同一线程可以多次获得同一个锁
  3. 都保证了可见性和互斥性

两者的不同点

  1. ReentrantLock显示的获得、释放锁,synchronized隐式获得释放锁
  2. ReentrantLock可响应中断、可轮回,synchronized是不可以响应中断的,为处理锁的不可用性提供了更高的灵活性
  3. ReentrantLock是API级别的,synchronized是JVM级别的
  4. ReentrantLock可以实现公平锁
  5. ReentrantLock通过Condition可以绑定多个条件
  6. 底层实现不一样,synchronized是同步阻塞,使用的是悲观并发策略,lock是同步非阻塞,采用的是乐观并发策略
  7. Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现。
  8. synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁。
  9. Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断。
  10. 通过Lock可以知道有没有成功获取锁,而 synchronized 却无法办到。
  11. Lock可以提高多个线程进行读操作的效率,既就是实现读写锁等。

ConcurrentHashMap 并发

减小锁粒度

  减小锁粒度是指缩小锁定对象的范围,从而减小锁冲突的可能性,从而提高系统的并发能力。减小锁粒度是一种削弱多线程锁竞争的有效手段,这种技术典型的应用是ConcurrentHashMap(高性能的HashMap)类的实现。对于HashMap而言,最重要的两个方法是get与set方法,如果我们对整个HashMap加锁,可以得到线程安全的对象,但是加锁粒度太大。Segment的大小也被称为ConcurrentHashMap的并发度。

ConcurrentHashMap 分段锁

  ConcurrentHashMap,它内部细分了若干个小的HashMap,称之为段(Segment)。默认情况下一个ConcurrentHashMap被进一步细分为16个段,既就是锁的并发度。
  如果需要在ConcurrentHashMap中添加一个新的表项,并不是将整个HashMap加锁,而是首先根据hashcode得到该表项应该存放在哪个段中,然后对该段加锁,并完成put操作。在多线程环境中,如果多个线程同时进行put操作,只要被加入的表项不存放在同一个段中,则线程间可以做到真正的并行。

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成

  ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构,一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。