多线程编程总结
2016-12-10 14:22
120 查看
实现多线程编程一:继承Thread类,复写父类的run方法,父类引用调用的是子类方法
1.继承Thread类
2.重写run方法(线程要执行的任务)
3.创建并启动(通过start方法)
下面以售票例子说明:
用这种方法大家可能会发现有些问题,就是原本我们是希望100张票开四个窗口买,可现在变成四个窗口各有100张票。解决方案有我们可以用静态关键字修饰ticket 或者用单例模式解决。但这都不是最佳的因为这种实现继承方式本身有些不好的地方,一、类不符合继承思想,什么时候用继承?当类是某个类的一种,符合is a 关系,而为了实现继承而去继承一个类不符合这个思想。二、Java不支持多继承,如果某个类是子类,那不是无法实现多线程。三、耦合性问题,像这个例子每个线程都包含资源,结果每个线程都用自己的资源,不符合本意,当然是可以修改的,但即使修改了线程与资源耦合性也太高,没有实现线程与资源分离,这与线程思想(轻装上阵,最小的执行单元)不符。
因此推出第二种继承方式,将资源与线程分离,独立封装,更符合oo思想,那就是实现Runnable接口,将实现的类的示例作为参数传给Thread(查阅API文档可知有个Thread(Runnable)构造方法)
上述的多线程存在安全问题,因为你对共享资源进行了并发操作,举个例子,你银行卡有500块,又存了200,又花了200,应该是不变的,但如果同时并发操作,一个算300,另一个算700,这两个数据互相覆盖,无论是300还是700都是错的,要出大问题。这个例子也是,在票数为一时,让一个线程进入了减一操作,但还没减一时,又切换了线程,此时的值还是一,于是多个线程进入ticket--操作,有可能结果为负数。可以修改下程序,停顿一会,就能看出效果
因此,我们应该保证对于共享数据同一时间段只有一个线程进行操作,于是引入同步代码块synchronized(对象){需要被同步的代码块;}。就好像一个锁,进入之后把它开关关上,阻止后面的线程进入,等离开后将开关打开允许后续进入。这就是同步的锁机制,不过要重复判断,降低了效率
不直接new Object(),因为这样锁不住,每次进去都新建一个对象,拿的是不一样的锁。加同步代码块的原则是寻找共享数据,在操作共享数据的位置加同步代码块。
要同步还可以用同步函数,加synchronized关键字,那么同步函数用的什么锁呢?其实就是this,我们可以验证一下
由代码可知如果在这时候输出结果还能保持正确,就说明它们持有的锁是一致的,否则无法达到同步效果。但是运行之后却发现问题了
发现运行结果全都是线程0,执行同步函数,之所以这样是因为线程t1(Thread-0)开启后立刻切换到主线程执行,主线程将flag改为false,所以线程执行的是同步函数。修改如下:
结果大家去试试吧。接下来引入新问题,同步函数要是被static修饰呢?那就没有this指针。一个函数是被对象调用的,如果是静态可以被类直接调用。有点类似这边的锁为类名.class(字节码对象,静态方法随类加载,没有该类的对象但有该类的字节码文件对象。)。同样可以验证一下:
截屏大小有限看不出,自己去试试吧,结果应该是可以同步的
同步升级:生产者消费者问题
在还没加同步前会出现未生产先消费和重复消费的问题,这是由于生产者还没来得及输出就切换线程,消费者多次得到执行权,但是加了同步之后,消费者生产者操作都封闭了。生产者拼命加一,输出,消费者就拼命输出相同的面包(连续重复消费)
这很明显不是我们想要的效果,我们要的是生产一个消费一个交替协调。我们可以使用等待--唤醒机制,应该生产的时候却去消费就让那个线程wait(),让生产线程顺利生产,然后去唤醒(notify)被阻塞的线程。具体我们可以看代码:
wait()和notify()方法要指明相应的锁对象,默认为this(同步函数)。唤醒也是有针对的唤醒,比如locka.notify()唤醒的一定是locka.wait()的等待线程。notify()是唤醒一个,notifyAll()是唤醒所有。
问题升级:多生产多消费
出现问题:重复生产重复消费。原因:唤醒的不确定性,比如生产者线程可能唤醒了己方原本等待的生产者线程,生产者线程继续执行生产覆盖了还未被消费的面包,这就是重复生产。重复消费也是这样原因。因此,被唤醒的线程应该同样去判断标记flag。代码修改只需把两处if判断改成while循环。结果却很尴尬的发现死锁了
还是上述的原因,唤醒己方线程,判断标记后wait(),导致所有线程都进入等待状态,就陷入死锁状态。
解决方案一:用notifyAll()全部唤醒,即使本方再一次抢到也会进入wait()状态,让另一方执行。用notifyAll()替换notify()就修改好代码了,测试结果发现是理想的结果。但这种效率低。
解决方案二:根据先前说的针对性唤醒我们可以用locka.wait()....lockb.notify() lockb.wait()......locka.notify()模式去唤醒对方线程而不是己方,但这个程序同步嵌套明显容易发生死锁。我们需要引入新东西,那就是同步的那套设备全都升级了,我们有新的方法去替代旧的。锁不再是任意对象,而是被封装成对象Lock,更符合面向对象思想。
wait(),notify(),notifyAll()也升级成await(),signal()和signalAll(),不再直接关联在锁上,而是把这些监视器方法封装到Condition对象中,Condition通过lock.newConfition()获得
上面的代码效果是与先前代码效果完全一样。而且还把方法从锁转移到Condition对象,就没有先前的顾虑,我们创建多个Condition,代码如下:
这就是好的解决方案。不过我们可以发现生产一个面包就消费一个,明显不符合实际情况,所以代码可以进一步升级,在API文档Condition示例恰好给出了代码
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
1.继承Thread类
2.重写run方法(线程要执行的任务)
3.创建并启动(通过start方法)
下面以售票例子说明:
class SaleTicket extends Thread{//继承Thread方法 private int ticket=100; public void run(){//重写run方法 while(true){ if(ticket>0) System.out.println(Thread.currentThread().getName()+"....."+ticket--); } } } public class TicketDemo { public static void main(String[] args) { //创建并启动四个线程 SaleTicket st1=new SaleTicket(); SaleTicket st2=new SaleTicket(); SaleTicket st3=new SaleTicket(); SaleTicket st4=new SaleTicket(); st1.start();//有可能执行权先给st1,先不继续往下 st2.start(); st3.start(); st4.start(); } }
用这种方法大家可能会发现有些问题,就是原本我们是希望100张票开四个窗口买,可现在变成四个窗口各有100张票。解决方案有我们可以用静态关键字修饰ticket 或者用单例模式解决。但这都不是最佳的因为这种实现继承方式本身有些不好的地方,一、类不符合继承思想,什么时候用继承?当类是某个类的一种,符合is a 关系,而为了实现继承而去继承一个类不符合这个思想。二、Java不支持多继承,如果某个类是子类,那不是无法实现多线程。三、耦合性问题,像这个例子每个线程都包含资源,结果每个线程都用自己的资源,不符合本意,当然是可以修改的,但即使修改了线程与资源耦合性也太高,没有实现线程与资源分离,这与线程思想(轻装上阵,最小的执行单元)不符。
因此推出第二种继承方式,将资源与线程分离,独立封装,更符合oo思想,那就是实现Runnable接口,将实现的类的示例作为参数传给Thread(查阅API文档可知有个Thread(Runnable)构造方法)
class SaleTicket implements Runnable{//实现Runnable接口 private int ticket=100; public void run(){//重写run方法 while(true){ if(ticket>0) System.out.println(Thread.currentThread().getName()+"....."+ticket--); } } } public class TicketDemo { public static void main(String[] args) { //给多个线程传共享资源 SaleTicket st=new SaleTicket(); Thread t1=new Thread(st); t1.start(); Thread t2=new Thread(st); t2.start(); Thread t3=new Thread(st); t3.start(); Thread t4=new Thread(st); t4.start(); } }
上述的多线程存在安全问题,因为你对共享资源进行了并发操作,举个例子,你银行卡有500块,又存了200,又花了200,应该是不变的,但如果同时并发操作,一个算300,另一个算700,这两个数据互相覆盖,无论是300还是700都是错的,要出大问题。这个例子也是,在票数为一时,让一个线程进入了减一操作,但还没减一时,又切换了线程,此时的值还是一,于是多个线程进入ticket--操作,有可能结果为负数。可以修改下程序,停顿一会,就能看出效果
if(ticket>0){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"....."+ticket--); }
因此,我们应该保证对于共享数据同一时间段只有一个线程进行操作,于是引入同步代码块synchronized(对象){需要被同步的代码块;}。就好像一个锁,进入之后把它开关关上,阻止后面的线程进入,等离开后将开关打开允许后续进入。这就是同步的锁机制,不过要重复判断,降低了效率
synchronized(obj){//Object obj=new Object();
if(ticket>0){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"....."+ticket--); }
}
}
不直接new Object(),因为这样锁不住,每次进去都新建一个对象,拿的是不一样的锁。加同步代码块的原则是寻找共享数据,在操作共享数据的位置加同步代码块。
要同步还可以用同步函数,加synchronized关键字,那么同步函数用的什么锁呢?其实就是this,我们可以验证一下
class SaleTicket implements Runnable {// 实现Runnable接口 private int ticket = 100; boolean flag = true; public void run() {// 重写run方法 if (flag) while (true) { synchronized (this) { if (ticket > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "..block..." + ticket--); } } } else { while (true) sale(); } } public synchronized void sale() { while (true) { if (ticket > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "....func..." + ticket--); } } } } public class TicketDemo { public static void main(String[] args) { SaleTicket st = new SaleTicket(); Thread t1 = new Thread(st); t1.start(); Thread t2 = new Thread(st); st.flag = false; t2.start(); } }
由代码可知如果在这时候输出结果还能保持正确,就说明它们持有的锁是一致的,否则无法达到同步效果。但是运行之后却发现问题了
发现运行结果全都是线程0,执行同步函数,之所以这样是因为线程t1(Thread-0)开启后立刻切换到主线程执行,主线程将flag改为false,所以线程执行的是同步函数。修改如下:
public class TicketDemo { public static void main(String[] args) throws InterruptedException { SaleTicket st = new SaleTicket(); Thread t1 = new Thread(st); Thread t2 = new Thread(st); t1.start(); Thread.sleep(10);//主线程休眠,执行权给t1,防止主线程得到直接改了flag st.flag = false; t2.start(); } }
结果大家去试试吧。接下来引入新问题,同步函数要是被static修饰呢?那就没有this指针。一个函数是被对象调用的,如果是静态可以被类直接调用。有点类似这边的锁为类名.class(字节码对象,静态方法随类加载,没有该类的对象但有该类的字节码文件对象。)。同样可以验证一下:
class SaleTicket implements Runnable {// 实现Runnable接口 private static int ticket = 100; boolean flag = true; public void run() {// 重写run方法 if (flag) while (true) { synchronized (SaleTicket.class) { if (ticket > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "..block..." + ticket--); } } } else { while (true) sale(); } } public static synchronized void sale() { while (true) { if (ticket > 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "....func..." + ticket--); } } } }
截屏大小有限看不出,自己去试试吧,结果应该是可以同步的
同步升级:生产者消费者问题
class Resource{ private String name; private int cou 4000 nt=1; public void set(String name){ this.name=name+"...."+count++; System.out.println(Thread.currentThread().getName()+"。。。。生产者。。。"+this.name); } public void get(){ System.out.println(Thread.currentThread().getName()+"。。。。消费者。。。"+this.name); } } class Producer implements Runnable{ private Resource res; public Producer(Resource res){ this.res=res; } public void run(){ while(true) res.set("面包"); } } class Consumer implements Runnable{ private Resource res; public Consumer(Resource res){ this.res=res; } public void run(){ while(true) res.get(); } } public class ProducerConsumerDemo { public static void main(String[] args) { Resource res=new Resource(); Producer pro=new Producer(res); Consumer con=new Consumer(res); new Thread(pro).start(); new Thread(con).start(); } }
在还没加同步前会出现未生产先消费和重复消费的问题,这是由于生产者还没来得及输出就切换线程,消费者多次得到执行权,但是加了同步之后,消费者生产者操作都封闭了。生产者拼命加一,输出,消费者就拼命输出相同的面包(连续重复消费)
这很明显不是我们想要的效果,我们要的是生产一个消费一个交替协调。我们可以使用等待--唤醒机制,应该生产的时候却去消费就让那个线程wait(),让生产线程顺利生产,然后去唤醒(notify)被阻塞的线程。具体我们可以看代码:
class Resource { private String name; private int count = 1; private boolean flag = false; public synchronized void set(String name) { if (flag)// 为true说明生产者已经生产了,要让生产者去等待,消费者去消费 try { wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } this.name = name + "...." + count++; System.out.println(Thread.currentThread().getName() + "。。。。生产者。。。" + this.name); flag = true;// 生产了要消费,置为true notify();// 唤醒等待的线程 } public synchronized void get() { if (!flag)// 没生产不能消费,等待 try { this.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "。。。。消费者。。。" + this.name); flag = false;// 消费完了,去生产 this.notify();// 唤醒等待的线程 } }
wait()和notify()方法要指明相应的锁对象,默认为this(同步函数)。唤醒也是有针对的唤醒,比如locka.notify()唤醒的一定是locka.wait()的等待线程。notify()是唤醒一个,notifyAll()是唤醒所有。
问题升级:多生产多消费
public class ProducerConsumerDemo { public static void main(String[] args) { Resource res = new Resource(); Producer pro = new Producer(res); Consumer con = new Consumer(res); new Thread(pro).start(); new Thread(con).start(); new Thread(pro).start(); new Thread(con).start(); } }
出现问题:重复生产重复消费。原因:唤醒的不确定性,比如生产者线程可能唤醒了己方原本等待的生产者线程,生产者线程继续执行生产覆盖了还未被消费的面包,这就是重复生产。重复消费也是这样原因。因此,被唤醒的线程应该同样去判断标记flag。代码修改只需把两处if判断改成while循环。结果却很尴尬的发现死锁了
还是上述的原因,唤醒己方线程,判断标记后wait(),导致所有线程都进入等待状态,就陷入死锁状态。
解决方案一:用notifyAll()全部唤醒,即使本方再一次抢到也会进入wait()状态,让另一方执行。用notifyAll()替换notify()就修改好代码了,测试结果发现是理想的结果。但这种效率低。
解决方案二:根据先前说的针对性唤醒我们可以用locka.wait()....lockb.notify() lockb.wait()......locka.notify()模式去唤醒对方线程而不是己方,但这个程序同步嵌套明显容易发生死锁。我们需要引入新东西,那就是同步的那套设备全都升级了,我们有新的方法去替代旧的。锁不再是任意对象,而是被封装成对象Lock,更符合面向对象思想。
wait(),notify(),notifyAll()也升级成await(),signal()和signalAll(),不再直接关联在锁上,而是把这些监视器方法封装到Condition对象中,Condition通过lock.newConfition()获得
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class Resource { private String name; private int count = 1; private boolean flag = false; Lock myLock = new ReentrantLock(); Condition con = myLock.newCondition(); public void set(String name) { myLock.lock(); try { while (flag)// 为true说明生产者已经生产了,要让生产者去等待,消费者去消费 try { con.await(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } this.name = name + "...." + count++; System.out.println(Thread.currentThread().getName() + "。。。。生产者。。。" + this.name); flag = true;// 生产了要消费,置为true con.signalAll();// 唤醒等待的线程 } finally { myLock.unlock(); } } public void get() { myLock.lock(); try { while (!flag)// 没生产不能消费,等待 try { con.await(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "。。。。消费者。。。" + this.name); flag = false;// 消费完了,去生产 con.signalAll();// 唤醒等待的线程 } finally { myLock.unlock(); } } }
上面的代码效果是与先前代码效果完全一样。而且还把方法从锁转移到Condition对象,就没有先前的顾虑,我们创建多个Condition,代码如下:
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class Resource { private String name; private int count = 1; private boolean flag = false; Lock myLock = new ReentrantLock(); Condition producer_con = myLock.newCondition(); Condition consumer_con = myLock.newCondition(); public void set(String name) { myLock.lock(); try { while (flag)// 为true说明生产者已经生产了,要让生产者去等待,消费者去消费 try { producer_con.await(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } this.name = name + "...." + count++; System.out.println(Thread.currentThread().getName() + "。。。。生产者。。。" + this.name); flag = true;// 生产了要消费,置为true consumer_con.signal();// 唤醒等待的线程 } finally { myLock.unlock(); } } public void get() { myLock.lock(); try { while (!flag)// 没生产不能消费,等待 try { consumer_con.await(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "。。。。消费者。。。" + this.name); flag = false;// 消费完了,去生产 producer_con.signal();// 唤醒等待的生产者线程 } finally { myLock.unlock(); } } }
这就是好的解决方案。不过我们可以发现生产一个面包就消费一个,明显不符合实际情况,所以代码可以进一步升级,在API文档Condition示例恰好给出了代码
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}