多线程常用模式总结一

[color=red]线程同步[/color]
线程同步其实就是线程排队访问资源。只有访问共享变量的情况下才需要同步。其一,要是共享的资源,比如一个文件,一个内存变量。其二,要是变量,如果是常量就不需要同步了。

[color=red]线程锁定[/color]
同步的实现方法,就是线程锁定。线程访问共享资源,此线程即持有锁,如果此线程不release这个锁,其他线程就无法访问。锁定通过synchronized或者volatile关键字来实现。如果是对代码段套上synchronized,则表示所有线程在执行到这段代码的时候,必须排队
如果给方法加上,就表示所有线程执行到此方法都得排队。
如果是对static方法加上synchronized, 就对这个类的所有实例对象起作用。
如果是对static方法中的代码块加上synchronized也是一样,其实对static方法的同步可以理解为对类的class对象的同步。
对于某些基本类型如char,int,它们是原子性的,即使被共享访问,不需要考虑同步的问题,但是在某些jvm上,long和double是非原子性的,这时候需要对它们的访问同步化,除了synchronized方法块外,还可以直接用volatile修饰。

[color=red]wait notify notifyAll[/color]
这三个方法在多线程开发中使用频率非常高,它们的实质都跟wait set有关。wait Set是一个执行该实例的wait方法时,所有停止的线程的集合,就像一个休息室。一旦发生如下几种动作:
1.有其他线程notify该线程。
2.有其他线程notifyAll。
3.有其他线程interrupt唤醒该线程。
4.wait方法到期。
线程都会退出wait Set。

[color=red]interrupt、InterruptedException与sleep,wait,join的关系[/color]
线程的interrupt方法只是改变线程的中断状态,而sleep,wait,join在执行的时候会去检查线程的中断状态,如果是中断状态就抛出InterruptedException,否则该怎么着还是怎么着。因此并不是调用了interrupt线程就马上抛InterruptedException。还有个interrupted方法是设置线程的中断状态的。


[color=red]多线程开发模式[/color]
1.共享互斥
多个线程访问同1个资源,对调用这个资源的代码块加上synchronized。但是,如果这个共享资源是类中的某个字段,它的子类却不知道这个字段是需要同步的,就会引发继承异常,子类对这个字段的访问不同步就会有问题。synchronized是不能被子类继承的,子类若要同步需要显式的指定。同步比异步执行要消耗更多的性能,我自己的机器上测试是2-4倍甚至更高的时间消耗,因此同步的范围应该尽可能的小,也就是同步粒度应该尽可能的细化,一般的原则就是,synchronized代码块尽可能的小。

2.常量对象
如果变量是只读性质的,可以让它常量化,这样就不需要互斥访问synchronized了,可以避免互斥带来的额外性能消耗。在只读共享对象被大量的线程并发访问的场景下,省下来的synchronized可以节省大量的性能。它一般要满足下面一些原则:
1.没有setter方法,因为这些方法会修改字段以及由字段引用的对象。
2.将字段声明为final和private访问类型。
3.不允许子类覆盖方法,最简单的办法是将类定义为final。稍微复杂点的方法是将构造函数定义为私有,并使用factory方法生成实例。
4.如果实例字段中包括可变对象的引用,则不允许这些对象被改变,也就是不要提供修该可变对象的方法。
5.不要将可变对象的引用共享;不要将可变对象的引用外部序列化;传递给构造函数的可变对象应复制备份赋给字段;不要返回原始对象,而应返回对象的clone。
Swing中大量使用了Immutable Object,比如Point、Rectangle等。由于用户界面属于输入响应交互式模式,因此存在大量的并发问题,尤其界面、数据一致性问题。Swing通过使用这些Immutable Object较好的避免了并发问题。
更灵活一点,可设计成2种类,一种可变,一种不可变,两者之间可以相互构造,就像StringBuffer和String。比如如下对象
public final class ImmutablePerson {
private final String name;
private final String address;
public ImmutablePerson(String name, String address) {
this.name = name;
this.address = address;
}
public ImmutablePerson(MutablePerson person) {
synchronized(person) {//去掉这个同步限制就会发现问题了
this.name = person.getName();
this.address = person.getAddress();
}
}
public MutablePerson getMutablePerson() {
return new MutablePerson(this);
}
public String getName() {
return name;
}
public String getAddress() {
return address;
}
public String toString() {
return "[ ImmutablePerson: " + name + ", " + address + " ]";
}
}
可以看到对象的字段全部是final的,而且没有set方法可更改值,跟它对比的有另外一个对象是可变的,提供了set方法但是有同步性能消耗。两者之间可相互构造。
public final class MutablePerson {
private String name;
private String address;
public MutablePerson(String name, String address) {
this.name = name;
this.address = address;
}
public MutablePerson(ImmutablePerson person) {
this.name = person.getName();
this.address = person.getAddress();
}
public synchronized void setPerson(String newName, String newAddress) {
name = newName;
address = newAddress;
}
public synchronized ImmutablePerson getImmutablePerson() {
return new ImmutablePerson(this);
}
String getName() { // Called only by ImmutablePerson
return name;
}
String getAddress() { // Called only by ImmutablePerson
return address;
}
public synchronized String toString() {
return "[ MutablePerson: " + name + ", " + address + " ]";
}
}
然后通过main来测试
public class Main {
public static void main(String[] args) {
MutablePerson mutable = new MutablePerson("start", "start");
new CrackerThread(mutable).start();
new CrackerThread(mutable).start();
new CrackerThread(mutable).start();
for (int i = 0; true; i++) {
mutable.setPerson("" + i, "" + i);
}
}
}

class CrackerThread extends Thread {
private final MutablePerson mutable;
public CrackerThread(MutablePerson mutable) {
this.mutable = mutable;
}
public void run() {
while (true) {
ImmutablePerson immutable = new ImmutablePerson(mutable);
if (!immutable.getName().equals(immutable.getAddress())) {
System.out.println(currentThread().getName() + " ***** BROKEN ***** " + immutable);
}
}
}
}
通过这个检测程序发现对象确实是不可变的,如果去掉注释,就会有问题了。

3.条件等待-唤醒/放弃
这个模式大致归纳成3种必须元素:有循环,有条件检查,有等待唤醒或放弃。线程执行一个操作,发现条件不允许的时候,等待,直到条件满足后被唤醒;或者放弃当前操作,下次再执行。根据不满足条件的场景,一般有几类做法:条件不满足,当前线程无限等待,直到被其他线程唤醒;条件不满足,yield当前线程并继续下一次条件检查;条件不满足,sleep当前线程,再循环检查等。

4.生产消费模式
如果需要线程A向线程B单向通信,而两者的处理速度差了很多,就可以使用这种方案。一般两者之间有一个缓冲队列或者缓冲区,生产者线程往队列里塞数据,消费者线程不断的从队列中拿数据执行。这样线程之间的共享访问冲突就集中在缓冲区上。下面是个简单的生产消费者例子。
用于缓冲数据的Box
public class Box {
private final String[] buffer;
private int tail; // 下一个put的地方
private int head; // 下一个take的地方
private int count;
public Box(int count) {
this.buffer = new String[count];
this.head = 0;
this.tail = 0;
this.count = 0;
}
public synchronized void put(String data) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " puts " + data);
while (count >= buffer.length) {
System.out.println(Thread.currentThread().getName() + " wait BEGIN");
wait();
System.out.println(Thread.currentThread().getName() + " wait END");
}
buffer[tail] = data;
tail = (tail + 1) % buffer.length;
count++;
notifyAll();
}
public synchronized String take() throws InterruptedException {
while (count <= 0) {
System.out.println(Thread.currentThread().getName() + " wait BEGIN");
wait();
System.out.println(Thread.currentThread().getName() + " wait END");
}
String data = buffer[head];
head = (head + 1) % buffer.length;
count--;
notifyAll();
System.out.println(Thread.currentThread().getName() + " takes " + data);
return data;
}
}

生产者线程
public class ProducerThread extends Thread {
private final Random random;
private final Box box;
private static int id = 0;
public ProducerThread(String name, Box box, long seed) {
super(name);
this.box = box;
this.random = new Random(seed);
}
public void run() {
try {
while (true) {
Thread.sleep(random.nextInt(1000));
String data = "[ Data No." + nextId() + " by " + getName() + " ]";
box.put(data);
}
} catch (InterruptedException e) {
}
}
private static synchronized int nextId() {
return id++;
}
}
消费者线程
public class ConsumerThread extends Thread {
private final Random random;
private final Box box;
public ConsumerThread(String name, Box box, long seed) {
super(name);
this.box = box;
this.random = new Random(seed);
}
public void run() {
try {
while (true) {
String data = box.take();
Thread.sleep(random.nextInt(1000));
}
} catch (InterruptedException e) {
}
}
}


5.读写锁
它将读取和写入分开处理,对于读取并发不做共享互斥,对于读写、写写并发做互斥,适用于读取频繁,写入较少的共享互斥情况,比如读远多于写的缓存。这种情况下,比直接的共享互斥有很大的性能提高。它的主要核心就是读写锁,做读操作就获取读的锁,写操作就获取写的锁.下面是个很简单的读写锁实现:
public final class ReadWriteLock {
private int readingReaders = 0; // 实际正在读取的线程数量
private int waitingWriters = 0; // 正在等待写入的线程数量
private int writingWriters = 0; // 实际正在写入的线程数量
private boolean preferWriter = true; // 写入优先的话,值为true

public synchronized void readLock() throws InterruptedException {
while (writingWriters > 0 || (preferWriter && waitingWriters > 0)) {
wait();
}
readingReaders++;
}

public synchronized void readUnlock() {
readingReaders--;
preferWriter = true;
notifyAll();
}

public synchronized void writeLock() throws InterruptedException {
waitingWriters++;
try {
while (readingReaders > 0 || writingWriters > 0) {
wait();
}
} finally {
waitingWriters--;
}
writingWriters++;
}

public synchronized void writeUnlock() {
writingWriters--;
preferWriter = false;
notifyAll();
}
}

然后是使用这个读写锁的共享数据
public class Data {
private final char[] buffer;
private final ReadWriteLock lock = new ReadWriteLock();
public Data(int size) {
this.buffer = new char[size];
for (int i = 0; i < buffer.length; i++) {
buffer[i] = '*';
}
}
public char[] read() throws InterruptedException {
lock.readLock();
try {
return doRead();
} finally {
lock.readUnlock();
}
}
public void write(char c) throws InterruptedException {
lock.writeLock();
try {
doWrite(c);
} finally {
lock.writeUnlock();
}
}
private char[] doRead() {
char[] newbuf = new char[buffer.length];
for (int i = 0; i < buffer.length; i++) {
newbuf[i] = buffer[i];
}
wating();
return newbuf;
}
private void doWrite(char c) {
for (int i = 0; i < buffer.length; i++) {
buffer[i] = c;
wating();
}
}
private void wating() {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
}
}
}

读写线程就分别调用read和write就可。
public class ReaderThread extends Thread {
private final Data data;
public ReaderThread(Data data) {
this.data = data;
}
public void run() {
try {
long begin = System.currentTimeMillis();
for (int i = 0; i < 20; i++) {
char[] readbuf = data.read();
System.out.println(Thread.currentThread().getName() + " reads " + String.valueOf(readbuf));
}
long time = System.currentTimeMillis() - begin;
System.out.println(Thread.currentThread().getName() + ": time = " + time);
} catch (InterruptedException e) {
}
}
}
public class WriterThread extends Thread {
private static final Random random = new Random();
private final Data data;
private final String filler;
private int index = 0;
public WriterThread(Data data) {
this.data = data;
this.filler = "hjshkjsfysfahfafaflkafsu7yyg";
}
public void run() {
try {
while (true) {
char c = nextchar();
data.write(c);
Thread.sleep(random.nextInt(3000));
}
} catch (InterruptedException e) {
}
}
private char nextchar() {
char c = filler.charAt(index);
index++;
if (index >= filler.length()) {
index = 0;
}
return c;
}
}

在jdk1.5里有ReadWriteLock接口以及实现类ReentrantReadWriteLock。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值