Java中多线程编程
一、进程与线程?并行与并发?
进程:
代表一个正在执行中的程序(直译),一个程序一旦在内存中开辟空间就代表程序正在运行,程序一旦运行就是进程。进程有三大特征:
- 独立性:独立的资源,私有的地址空间,进程之间互不影响。进程是系统进行资源分配的独立实体, 且每个进程拥有独立的地址空间。一个进程无法直接访问另一个进程的变量和数据结构。
- 动态性:进程具有生命周期。
- 并发性:多个进程可以在单核cpu上运行。对于进程A,B而言,cpu可以执行进程A 30ms转而执行进程B 20ms,再继续回去执行进程A 30ms,执行B 20ms,依次执行。
线程:
线程就是进程中一个负责程序执行的控制单元(执行路径),一个线程中可以有多个执行路径,称之为多线程。但是一个进程中至少有一个线程。线程是系统运行的基本单位,CPU的执行是在线程之间切换。
多线程:
开启多个线程就是为了同时运行多部分代码,每一个线程都有自己运行的内容,这个内容可以称之为线程要执行的任务。
多线程的好处:
- 解决掉了多部分同时运行的问题。
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
- 并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求。
- 创建进程进行资源分配的代价较创建线程要大得多,所以多线程在高并发环境中效率更高。
- 进程之间不能共享内存,线程之间共享内存更容易,多线程可协作完成进程工作。
并行与并发的概念:
并行是指:多核多CPU或多机器处理同一段处理逻辑的时候,同一时刻多个执行流共同执行。
并发是指:通过CPU的调度算法,使用户感觉像是同时处理多个任务,但同一时刻只有一个执行流占用CPU执行。即使多核多CPU环境还是会使用并发,以提高处理效率。
主要的CPU调度算法有如下两种:
1、分时调度:每个线程轮流获取CPU使用权,各个线程平均CPU时间片。比如有线程A,B,C,线程A获取CPU使用权执行20ms后释放CPU资源,然后线程B获取CPU使用权执行20ms后释放CPU资源,线程C获取CPU使用权执行20ms后释放CPU资源。
2、抢占式调度:Java虚拟机使用的就是这种调度模型。这种调度方式会根据线程优先级,先调度优先级高的线程,如果线程优先级相同,会随机选取线程执行。
针对于分时调用计算CPU的利用率:比如A进程占用10ms,然后B进程占用30ms,然后空闲60ms,再又是A进程占10ms,B进程占30ms,空闲60ms;如果在一段时间内都是如此,那么这段时间内的占用率为40%。
二、PCB是什么?
进程控制块,是进程最重要的数据结构,记录着进程的标识、调度信息、控制信息、以及处理机状态。PCB会在进程创建时创建、进程消亡时消亡,伴随PCB整个生命周期。
其中需要了解的有以下几点:
1、进程标识符用于唯一的标识一个进程。一个进程通常有两种标识符:内部标识符和外部标识符。
2、处理机状态信息主要是由处理机的各种寄存器中的内容组成的。处理机在运行中,许多信息都是放在寄存器中的。当处理机被中断时,所有的这些信息都保存在PCB中,以便在该进程重新执行时,能从断点继续执行。
3、进程调度信息包括进程状态、进程优先级、进程调度所需的其他信息,事件(阻塞原因)。
4、进程控制信息包括程序和数据的地址、进程同步和通信机制、资源清单、链接指针。
5、进程控制块的组织方式为链接方式、索引方式。
三、Java中创建线程的方式
创建线程的目的:为了开启一条执行路径运行指定的代码和其他代码实现同时运行。而运行指定路径的代码就是这个执行路径的任务。
jvm创建的主线程的任务都定义在主函数中。
创建线程方式一:
- 定义一个类继承Thread类。
- 覆盖Thread类中run方法。
- 直接创建Thread类的子类对象创建线程。
- 调用start方法开启线程并调用线程的任务run方法执行。
对于自定义的线程,他的任务在哪呢?为什么创建线程要继承Thread类呢?
Thread类用于描述线程,而线程是需要任务的,所以Thread类也是对任务进行描述。这个任务就是通过Thread类中的run方法来体现的,也就是说,run方法就是封装自定义线程运行任务的函数,run方法中定义的就是线程要运行的任务代码。开启线程是为了运行指定代码,所以只有继承Thread类,并覆写run方法,将要运行的代码定义在run方法中。
直接调用run方法与调用start方法的区别?
直接调用run方法就相当于子类调用自己定义的run方法在主程序中运行。
调用start方法就相当于程序通过子类对象调用父类的start方法,并在start方法中调用run方法。由于子类覆写了父类中的run方法,产生多态则开启了一个新的线程,在新的线程中调用子类中定义的run方法。
一些小函数介绍:
程序可以通过Thread的getName获取线程的名称Thread-编号(从0开始)。子类继承Thread类并创建对象的时候,在执行子类的构造函数时会先执行父类的super()此时父类构造函数就会给创建的对象创建一个名字Thread-编号。所以每个子类对象的名字在Thread类中初始化对象的时候就给对象加上了名字。
Thread.current()获取当前cpu正在执行的线程,主线程的名字就是main。
多次启动一个线程是非法的,会抛出异常。看异常的方法包括四部分信息:异常发生的线程名称,异常名称,异常信息,异常位置。
分析如下代码:
class Demo extends Thread
{
private String name;
Demo(String name)
{
super();
this.name=name;
}
public void run()
{
show();
}
public void show()
{
for(int x=0;x<10;x++)
{
System.out.println(name+"...x="+x+getName());
}
}
}
class MultithreadingDemo2
{
public static void main(String[] args)
{
Demo d1=new Demo("张三");
Demo d2=new Demo("李四");
d1.start();//开启线程,调用run方法。
d2.run();
System.out.println("Hello World!"+Thread.currentThread().getName());
}
}
如果不调用Thread.currentThread()获取当前线程的,直接使用getName()则程序中输出的是d2对象所对应的线程的名字就相当于d2.getName()。getName()函数就相当于获取对象所对应线程的名字而已,虽然d2对象是在主线程中运行但是并没有获取当前线程,直接是getName()就相当于获取d2.getName()的名字并不是执行该程序的当前线程主线程main。对于d1.start()中是开启了一条线程并调用了run方法。对于d1对象所对应的线程而言获取的只是d1对象所对应线程的名字跟d2对象一样只是碰巧d1对象调用的run方法在d1对象所对应开辟的线程中执行。而d2对象的run方法只在主函数中执行。而最后一句System.out.println("Hello World!"+Thread.currentThread().getName())这段线程是获取执行这条代码当前线程的名字main。
创建线程的第二种方法:
- 定义类实现的Runnable接口。
- 覆盖接口中的run方法,将线程的任务代码封装到run方法中。
- 通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。
- 为什么?因为线程的任务都封装在Runnable接口子类对象的run方法中,所以要在线程对象创建时就必须明确要运行的任务。
- 调用线程对象的start方法开启线程。
实现Runnable接口的好处:
- 将线程的任务从线程的子类中分离出来,进行了单独的封装。按照面向对象的思想将任务封装成了对象。
- 避免了java单继承的局限性。
所以,创建线程的第二种方式比较常用。
通过如下伪代码来解释创建线程的第二种方式的原因:
class Thread implements Runnable
{
private Runnable r;
Thread()
{}
Thread(Runnable r)
{
this.r=r;
}
public void run()
{
if(r!=null)
r.run;
}
public void start()
{
run();
}
}
//线程创建方法二
class ThreadImpl implements Runnable
{
public void run()
{
System.out.println("RUNRUN");
}
}
ThreadImpl ti=new ThreadImpl();
Thread t=new Thread(ti);
t.start();
首先明白API中定义的Thread类的方式,上面代码大致写出了Thread类的结构。当使用创建线程的第二种方法时,我们首先定义了一个类实现了Runnable接口,并覆写了接口中的run方法。接着创建了一个Runnable的子类对象和Thread类的对象。并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。传进去之后程序调用Thread类带参数的构造函数:
Thread(Runnable r)
{
this.r=r;
}
将Runnable接口的子类对象的地址给Thread类中定义的私有的Runnable引用,则私有的Runnable的引用指向外部Runnable接口的子类对象。然后使用Thread类通过Runnable接口的子类对象作为Thread类的构造函数的参数进行传递建立的对象调用start函数,然后在start函数类调用Thread类的run函数,然后对r进行判断是否为null,如果不为空则调用r对象的run函数。由于多态的原因,当r不为空时调用外部传进来的Runnable接口的子类对象的run函数进而在开启的新线程中执行外部Runnable接口的子类覆写run函数。
根据以上Thread类的伪代码在强调一下创建线程方式一的原理:
以下代码就是创建线程的方式一的代码
class SubThread extends Thread
{
public void run()
{
System.out.println("Hello");
}
}
SubThread s=new SubThread();
s.start();
当定义一个子类继承Thread类之后,覆写了Thread中的run函数。子类定义一个对象,通过子类对象调用父类中的start函数,然后程序进入start函数中调用run函数。由于多态的原因程序在开启的新线程中执行子类的run函数。
四、多线程中的安全问题
线程安全问题产生的原因:
- 多个线程在操作共享数据。
- 操作共享数据的线程代码有多条。
- 当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生。
解决线程安全问题的思路:
就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程可以不参与运算。必须要当前线程把这些代码都执行完毕以后,其他线程才可以参与运算。
解决线程安全的方法:
在java中,用同步代码块和同步函数就可以解决这个问题。
同步的好处:解决了线程的安全问题。
同步的弊端:相对降低了效率,因为同步外的线程都会判断同步锁,会降低执行效率。
同步的前提:同步中必须有多个线程并使用同一个锁
同步代码块的格式:
synchronized(对象)
{
需要同步的代码;
}
同步代码块实现多线程卖票例子:
class Ticket implements Runnable
{
private int num=100;
public void run()
{
while(true)
{
synchronized(this)
{
if(num>0)
{
try{Thread.sleep(20);}catch(Exception e){};
System.out.println(Thread.currentThread().getName()+"...num..."+num--);
}
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t=new Ticket();
Thread t1=new Thread(t);
Thread t2=new Thread(t);
Thread t3=new Thread(t);
Thread t4=new Thread(t);
t1.start();
t2.start();
t3.start();
t4.start();
System.out.println("Hello World!");
}
}
根据上述程序分析多线程执行的顺序,在主函数中当线程0,1,2,3启动之后,假设线程0获取CPU的执行资格跟CPU的执行权的时候。执行到同步代码处synchronized(this)发现同步代码块没上锁,进入同步代码当中。当线程0执行同步代码块中的程序时候会给同步代码块中的程序上锁。与此同时当线程1执行到同步代码块处synchronized(this)的时候,会发现同步代码块中的程序已经被上锁,线程1不能进入同步代码块中。直到线程0执行完同步代码块中的程序之后,解锁同步代码块并释放CPU的执行权与CPU的执行资格后,线程1获得CPU的执行资格与CPU的执行权后进入解锁的同步代码块中,执行程序并同时给同步代码块上锁。
简而言之:只有当同步代码块处于未上锁或者当前线程执行完同步代码块中的程序后给同步代码块解锁之后,接下的线程获得CPU的执行资格与CPU的执行权后才能执行同步代码块中的内容。
通过两个客户在同一家银行分别进行三次100元存款的例子来理解同步代码块:
class Bank
{
private int sumMoney;
public /*synchronized*/ void add(int num)
{
sumMoney=sumMoney+num;
try{Thread.sleep(10);}catch(Exception e){}
System.out.println("Money...."+sumMoney);
}
}
class Custom implements Runnable
{
//由于是两个客户,因此要使用同一个锁就将对象定义成静态的
static private Bank b=new Bank();
public void run()
{
for(int i=0;i<3;i++)
{
//使用同步代码块
synchronized(b)
{
b.add(100);
}
}
}
}
class BankDemo
{
public static void main(String[] args)
{
Custom c1=new Custom();
Custom c2=new Custom();
Thread t1=new Thread(c1);
Thread t2=new Thread(c2);
t1.start();
t2.start();
// System.out.println("Hello World!");
}
}
同步代码块介绍完之后介绍同步函数:
同步函数:
- 同步函数的锁是this。
- 同步函数的锁是固定的this。
- 同步代码块的锁是任意的对象。
- 静态的同步函数使用的锁是 该函数所属字节码文件对象 。可以用getClass()方法获取,也可以用当前类名.class表示。
同步函数例子,该例子为100张票通过两个线程卖直到卖完为止:
class Ticket implements Runnable
{
private int num=100;
public void run()
{
while(true)
showTicket();
}
public synchronized void showTicket()
{
if(num>0)
{
try{Thread.sleep(20);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"...function num..."+num--);
}
}
}
class SynFunctionLockDemo
{
public static void main(String[] args)
{
Ticket t=new Ticket();
Thread t1=new Thread(t);
Thread t2=new Thread(t);
t1.start();
t2.start();
}
}
同步函数表示当一个线程没有执行完同步函数中的内容的时候其他线程不可以调用执行该同步函数。
五、多线程的单例设计模式:
单例设计模式分为饿汉式与懒汉式设计模式:
单例设计模式请看链接:https://mp.youkuaiyun.com/postedit/82020373。
饿汉式单例设计模式:(开发时用的比较多),类一加载,对象就已经存在了。
class Single
{
private static Single s=new Single();
private Single(){}
public static Single getInstance()
{
return s;
}
}
如果多个线程共享这个对象资源的,不会产生线程安全性问题。
懒汉式单例设计模式:(面试时问的比较多),类延迟加载进来,没有对象,只有调用了getInstance方法时,才会创建对象。
class Single
{
private static Single s=null;
private Single(){}
public static Single getInstance()//getclass()方法是非静态的
{
if(s==null)//1
s=new Single();//2
return s;
}
}
但是懒汉式单例设计模式会产生线程访问的安全性问题:
当主函数中创建两个线程并启动后,两个线程中分别调用Single s1=Single.getInstance()方法时。假设线程0调用getInstance()方法执行到函数中1处,对s进行判断发现s=null进入if语句中。此时线程1获得CPU的执行资格与执行权,线程0释放CPU的执行资格与执行权。线程1调用getInstance()方法执行到函数中1处,对s进行判断发现s=null也进入if语句中。然后执行语句2创建并返回了一个对象。此时线程0获取CPU的执行资格与执行权,执行语句2创建并返回一个对象。此时线程0与线程1都创建并返回了不同的对象。当使用这个对象中的资源的时候,由于产生了两个不同的对象,我们所使用的并不是同一个对象的共享资源,从而产生了线程的安全性问题。
如何解决在懒汉式单例模式中解决线程访问的安全性问题呢?
我们使用同步代码块的方式解决,懒汉式单例设计模式中线程访问的安全性问题。对上述代码做如下修改:
class Single
{
private static Single s=null;
private Single(){}
public static Single getInstance()//getclass()方法是非静态的
{
if(s==null)//1
{
synchronized(Single.class)//2
{
if(s==null)//3
s=new Single();//4
}
}
return s;
}
}
为什么使用同步代码块的方式来解决线程访问的安全性问题呢?
单例设计模式中将类中的对象与对象的获取方法都定义成静态的是因为其他类中不能创建该类的对象,但是又为了调用该类的方法返回该类的对象,所以将其都定义成静态的方便使用类名调用。但是同步函数的同步锁用的是this,在静态函数中是不可以使用this作为同步锁的。所以我们使用同步代码块来解决这个问题。但是同步代码块中需要用一个对象作为同步锁,但是不同线程要使用同一个同步锁。所以我们使用Single.class作为同步锁。但是当线程访问同步代码块的时候每一个线程都要访问一次同步锁,这样会大大降低程序运行的效率。所以我们给同步代码块外面加上一个if语句进行判断。当线程0调用getInstance()方法时,程序执行到1处对s进行判断是否为null,如果为null对同步锁进行判断,并执行同步代码块中的程序。然后执行程序运行到3处。假设此时线程1获取CPU的执行权与执行资格,调用getInstance()方法,程序执行到1处对s进行判断是否为null,此时s为null,程序对同步锁进行判断发现程序上锁。此时线程0获得CPU的执行资格与执行权创建一个对象,但是程序并没有退出同步代码块。此时线程2获得CPU的执行资格与执行权,调用getInstance()方法对s是否为null进行判断,发现s不为null退出程序,此时线程2并不用对同步锁进行判断。此时线程3获得CPU的执行资格与执行权,调用getInstance()方法直接对s是否为null进行判断,发现s不为null退出程序,此时线程3不对同步锁进行判断。如果不加if判断语句,线程2,3都要对同步锁进行判断,这样大大降低了程序的运行效率。所以在同步代码块外部加上if语句是为了提高程序的运行效率。
六、死锁与死锁的程序设计
死锁常见情况之一:同步的嵌套。
当线程0持有同步锁A,但是在同步锁A中还有同步锁B,此时线程0在持有同步锁A的时候还需要请求获得同步锁B。但是线程1持有同步锁B在请求获取同步锁A。此时线程0与线程1都不放开手中持有的资源,但是都等待其他线程释放手中的资源,所以造成了线程0与线程1都无法继续执行下去。如果用生活中的例子解释:当一群哲学家在一起共同进餐,每个哲学家手中只持有一根筷子。只有当哲学家获取两根筷子的时候才能进餐。如果一个哲学家肯把筷子借给其他人,等其他人吃完后再交出自己的两根筷子,自己在进餐就可以了。但是由于哲学家都不肯交出自己手中的筷子,都等待别人交出手中的筷子给自己进餐,这样造成了所有哲学家都无法进餐,造成了死锁。
简短的死锁代码如下:
class Mylock
{
}
class LockDemo implements Runnable
{
public boolean flag;
public static Mylock locka=new Mylock();
public static Mylock lockb=new Mylock();
public void run()
{
if(flag)
{
synchronized(locka)
{
System.out.println(Thread.currentThread().getName()+"...flag true...locka...");
synchronized(lockb)
{
System.out.println(Thread.currentThread().getName()+"...flag true...lockb...");
}
}
}
else
{
synchronized(lockb)
{
System.out.println(Thread.currentThread().getName()+"...flag false...lockb...");
synchronized(locka)
{
System.out.println(Thread.currentThread().getName()+"...flag false...locka...");
}
}
}
}
}
class DeadLockDemo
{
public static void main(String[] args)
{
LockDemo l1=new LockDemo();
LockDemo l2=new LockDemo();
Thread t1=new Thread(l1);
Thread t2=new Thread(l2);
l1.flag=true;
l2.flag=false;
t1.start();
t2.start();
}
}
这个程序就是死锁的程序,该程序有两个同步锁,同步锁locka,lockb。在同步锁locka中有同步锁lockb,同步锁lockb中有locka。然后新建两个线程任务,每个任务都持有同步锁locka与同步锁lockb。我们使两个任务分别一个从同步锁locka开始执行,一个从同步锁lockb开始执行。当线程0获取同步锁locka时,请求获取同步锁lockb。但是线程1获取了同步锁lockb,请求获取同步锁locka。此时线程0与线程1都等待获取另一个锁的资源,但是线程0与线程1都不肯释放手中的资源,因此导致线程0与线程1都无法继续执行,导致死锁。
七、等待与唤醒机制
通过多生产者与多消费者案例来介绍等待与唤醒机制。
分析如下代码:
class Resource
{
private String name;
private int count=1;
private boolean flag=false;
public synchronized void set(String name)
{
if(flag)
try{this.wait();}catch(Exception e){}
this.name=name+count;
count++;
System.out.println(Thread.currentThread().getName()+"...生产者..........."+this.name);
flag=true;
notify();
}
public synchronized void out()
{
if(!flag)
try{this.wait();}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
flag=false;
notify();
}
}
class Producer implements Runnable
{
private Resource r;
Producer(Resource r)
{
this.r=r;
}
public void run()
{
while(true)
r.set("A");
}
}
class Consumer implements Runnable
{
private Resource r;
Consumer(Resource r)
{
this.r=r;
}
public void run()
{
while(true)
r.out();
}
}
class ProducerConsumerDemo2
{
public static void main(String[] args)
{
Resource r=new Resource();
Producer pro=new Producer(r);
Consumer c1=new Consumer(r);
Thread t0=new Thread(pro);
Thread t1=new Thread(pro);
Thread t2=new Thread(c1);
Thread t3=new Thread(c1);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
运行结果:


以上程序会出现生产者连续生产几件物品但是消费者只消费最后生产的一件物品,还会出现消费者把生产者生产的一件物品重复消费。
原因如下:
线程0,1是生产者线程,线程2,3为消费者线程。假设当线程0获得CPU的执行资格与执行权,调用run函数执行set函数执行到flag判断,发现flag为假,生产者生产一件商品。再将flag标记为置为真,由于没有线程等待此时没有唤醒的线程。假设此时线程0还持有CPU的执行资格与执行权,对flag进行判断,发现flag为真则线程0进入等待,释放CPU的执行资格与执行权。此时线程1,2,3都是处于临时阻塞状态。假设线程1获得CPU的执行资格与执行权,线程1对flag进行判断,发现flag为真则线程1进入等待,释放CPU的执行资格与执行权。此时线程0,1处于冻结状态,线程2,3处于临时阻塞状态。假设线程2获得CPU的执行资格与执行权,线程2对flag进行判断发现flag为真,消费者消费掉生产出来的一件产品,然后将flag置位假,并唤醒线程0,1中任意一个线程。但是此时线程2还持有CPU的执行资格与执行权,并再次对flag进行判断flag为假线程2进入等待状态,释放CPU的执行资格与执行权。此时线程0,3处于临时阻塞状态,线程1,2处于冻结状态。假设线程3获得CPU的执行资格与执行权,对flag判断为假进入冻结状态。此时线程1,2,3处于冻结状态,线程0处于临时阻塞状态。线程0获得CPU的执行资格与执行权,从阻塞处直接向下执行生产出一件商品,并唤醒一个线程,假设此时唤醒线程1。此时线程线程0还持有CPU的执行资格与执行权,对flag进行判断为真进入等待状态。此时线程0,2,3处于冻结状态,线程1处于临时阻塞状态。线程1从阻塞处直接向下执行生产出一件商品,并唤醒一个线程,这样生产者线程0,1就连续生产出来了两件产品。如果线程1唤醒的线程为线程0则生产者又在没有消费的情况下继续生产产品。循环往复,直到生产者线程唤醒消费者线程为止。假设线程1唤醒了消费者线程2,此时线程0,1,3处于冻结状态,线程2处于临时阻塞状态。线程2获取CPU的执行资格与执行权,线程2从阻塞处向下执行消费一件产品,并唤醒一个线程。假设此时线程3被唤醒,则线程0,1,2处于冻结状态,线程3处于临时阻塞状态。线程3从阻塞处向下执行消费一件产品,并唤醒一个线程。此时消费者线程2,3消费同一件产品。如果消费者线程继续唤醒消费者线程,则会出现在生产者没有生产产品时,一件商品被反复消费的情况。
对上述代码进行如下更改:
class Resource
{
private String name;
private int count=1;
private boolean flag=false;
public synchronized void set(String name)
{
while(flag)
try{this.wait();}catch(Exception e){}
this.name=name+count;
count++;
System.out.println(Thread.currentThread().getName()+"...生产者..........."+this.name);
flag=true;
notify();
}
public synchronized void out()
{
while(!flag)
try{this.wait();}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
flag=false;
notify();
}
}
class Producer implements Runnable
{
private Resource r;
Producer(Resource r)
{
this.r=r;
}
public void run()
{
while(true)
r.set("A");
}
}
class Consumer implements Runnable
{
private Resource r;
Consumer(Resource r)
{
this.r=r;
}
public void run()
{
while(true)
r.out();
}
}
class ProducerConsumerDemo2
{
public static void main(String[] args)
{
Resource r=new Resource();
Producer pro=new Producer(r);
Consumer c1=new Consumer(r);
Thread t0=new Thread(pro);
Thread t1=new Thread(pro);
Thread t2=new Thread(c1);
Thread t3=new Thread(c1);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
运行结果如下:

程序会卡主,请问是什么原因?
原因如下:
线程0,1是生产者线程,线程2,3为消费者线程。假设当线程0获得CPU的执行资格与执行权,调用run函数执行set函数执行到flag判断,发现flag为假,生产者生产一件商品。再将flag标记为置为真,由于没有线程等待此时没有唤醒的线程。假设此时线程0还持有CPU的执行资格与执行权,对flag进行判断,发现flag为真则线程0进入等待,释放CPU的执行资格与执行权。此时线程0处于冻结状态,线程1,2,3都是处于临时阻塞状态。假设线程1获得CPU的执行资格与执行权,线程1对flag进行判断,发现flag为真则线程1进入等待,释放CPU的执行资格与执行权。此时线程0,1处于冻结状态,线程2,3处于临时阻塞状态。假设线程2获得CPU的执行资格与执行权,线程2对flag进行判断发现flag为真,消费者消费掉生产出来的一件产品,然后将flag置位假,并唤醒线程0,1中任意一个线程。但是此时线程2还持有CPU的执行资格与执行权,并再次对flag进行判断flag为假线程2进入等待状态,释放CPU的执行资格与执行权。此时线程0,3处于临时阻塞状态,线程1,2处于冻结状态。假设线程3获得CPU的执行资格与执行权,对flag判断为假进入冻结状态。此时线程1,2,3处于冻结状态,线程0处于临时阻塞状态。线程0获得CPU的执行资格与执行权,对flag进行判断为假,生产者线程0生产出一件商品,并唤醒一个线程,假设此时唤醒线程1。此时线程线程0还持有CPU的执行资格与执行权,对flag进行判断为真进入等待状态。此时线程0,2,3处于冻结状态,线程1处于临时阻塞状态。线程1获得CPU的执行资格与执行权,对flag进行判断为真进入等待状态。此时线程0,1,2,3处于冻结状态,程序死锁无法继续向下执行。
为了解决上述问题,我们将程序中的notify()换成notifyAll()。程序如下:
class Resource
{
private String name;
private int count=1;
private boolean flag=false;
public synchronized void set(String name)
{
while(flag)
try{this.wait();}catch(Exception e){}
this.name=name+count;
count++;
System.out.println(Thread.currentThread().getName()+"...生产者..........."+this.name);
flag=true;
notifyAll();
}
public synchronized void out()
{
while(!flag)
try{this.wait();}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
flag=false;
notifyAll();
}
}
class Producer implements Runnable
{
private Resource r;
Producer(Resource r)
{
this.r=r;
}
public void run()
{
while(true)
r.set("A");
}
}
class Consumer implements Runnable
{
private Resource r;
Consumer(Resource r)
{
this.r=r;
}
public void run()
{
while(true)
r.out();
}
}
class ProducerConsumerDemo2
{
public static void main(String[] args)
{
Resource r=new Resource();
Producer pro=new Producer(r);
Consumer c1=new Consumer(r);
Thread t0=new Thread(pro);
Thread t1=new Thread(pro);
Thread t2=new Thread(c1);
Thread t3=new Thread(c1);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
运行结果:

程序正常运行,不会产生死锁。
分析上述程序:
线程0,1是生产者线程,线程2,3为消费者线程。假设当线程0获得CPU的执行资格与执行权,调用run函数执行set函数执行到flag判断,发现flag为假,生产者生产一件商品。再将flag标记为置为真,由于没有线程等待此时没有唤醒的线程。假设此时线程0还持有CPU的执行资格与执行权,对flag进行判断,发现flag为真则线程0进入等待,释放CPU的执行资格与执行权。此时线程0处于冻结状态,线程1,2,3都是处于临时阻塞状态。假设线程1获得CPU的执行资格与执行权,线程1对flag进行判断,发现flag为真则线程1进入等待,释放CPU的执行资格与执行权。此时线程0,1处于冻结状态,线程2,3处于临时阻塞状态。假设线程2获得CPU的执行资格与执行权,线程2对flag进行判断发现flag为真,消费者消费掉生产出来的一件产品,然后将flag置为假,并唤醒所有线程。但是此时线程2还持有CPU的执行资格与执行权,并再次对flag进行判断flag为假线程2进入等待状态,释放CPU的执行资格与执行权。此时线程0,1,3处于临时阻塞状态,线程2处于冻结状态。假设线程3获得CPU的执行资格与执行权,对flag判断为假进入冻结状态。此时线程2,3处于冻结状态,线程0,1处于临时阻塞状态。线程0获得CPU的执行资格与执行权,对flag进行判断为假,生产者线程0生产出一件商品,并唤醒所有线程,假设此时唤醒线程1。此时线程线程0还持有CPU的执行资格与执行权,对flag进行判断为真进入等待状态。此时线程0处于冻结状态,线程1,2,3处于临时阻塞状态。线程1获得CPU的执行资格与执行权,对flag进行判断为真进入等待状态。此时线程0,1处于冻结状态,线程2,3处于临时阻塞状态。此时消费者线程将获得CPU的执行资格与执行权,程序将不会发生死锁,会使生产者线程与消费者线程相互交替执行。
但是这样坐每次都会唤醒所有线程,会大大浪费CPU的资源。
针对上述三种情况进行一下小的总结:
多生产者,多消费者。
if判断标记,只有一次,会导致不该运行的线程运行了,出现了数据错误的情况。
while判断标记,解决了线程获得执行权后,是否要运行!解决了数据错误的情况。
notify:只唤醒了一个线程,如果本方唤醒本方没有意义。而且while判断标记+notify会导致死锁。
notifyAll解决了,本方线程一定会唤醒对方线程的问题。但是大大浪费了CPU的资源。
针对notifyAll()的问题我们提出解决方案:
Lock接口:出现替代了同步代码块或者同步函数。将同步的隐式锁操作变成了显示锁操作。同时更为灵活。可以一个锁上加上多组监视器。lock(): 获取锁、unlock():释放锁,通常需要定义在finally代码块当中。Condition接口:出现替代了Object中的wait notify notifyAll方法。将这些监视器方法单独进行了封装,变成Condition监视器对象可以和任意的锁进行组合。
实现代码如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
class Resource
{
private String name;
private int count=1;
private boolean flag=false;
//通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者。
Condition pro_con=lock.newCondition();
Condition con_con=lock.newCondition();
public void set(String name)
{
lock.lock();
try
{
while(flag)
try{pro_con.await();}catch(Exception e){}
this.name=name+count;
count++;
System.out.println(Thread.currentThread().getName()+"...生产者..........."+this.name);
flag=true;
con_con.signal();
}
catch(Exception e)
{}
finally
{
lock.unlock();
}
}
public void out()
{
lock.lock();
try
{
while(!flag)
try{con_con.await();}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
flag=false;
pro_con.signal();
}
catch(Exception e)
{}
finally
{
lock.unlock();
}
}
}
class Producer implements Runnable
{
private Resource r;
Producer(Resource r)
{
this.r=r;
}
public void run()
{
while(true)
r.set("A");
}
}
class Consumer implements Runnable
{
private Resource r;
Consumer(Resource r)
{
this.r=r;
}
public void run()
{
while(true)
r.out();
}
}
class ProducerConsumerDemo
{
public static void main(String[] args)
{
Resource r=new Resource();
Producer pro=new Producer(r);
Consumer c1=new Consumer(r);
Thread t0=new Thread(pro);
Thread t1=new Thread(pro);
Thread t2=new Thread(c1);
Thread t3=new Thread(c1);
t0.start();
t1.start();
t2.start();
t3.start();
}
}
这个与notifyAll()的区别就是我们定义两个监视器,一组监视生产者,一组监视消费者。当生产者生产完毕后,我们通过消费者的监视器唤醒消费者线程。当消费者消费完毕后我们通过生产者的监视器唤醒生产者线程。
八、多线程编程两道面试题
面试题1:
下述代码是否有错误,错误发生在哪一行?
class Test implements Runnable
{
public void run(Thread t)
{
}
}
分析:在Test类中定一个了一个自己的特有方法 public void run(Thread t),并且实现了Runnable接口。由于Test不是抽象类,实现了Runnable接口但是并未覆盖其中的run方法。所以程序会在第一行报错,Test不是抽象类并且未覆盖Rannabel中的抽象方法run()。
面试题2:
分析下述代码的输出结果:
class ThreadInterviewDemo {
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
System.out.println("runnable run");
}
}) {
public void run() {
System.out.println("SubThread run");
}
}.start();
分析:上述代码中定义了一个子类的匿名对象并且覆写了父类的run方法,但是在构造子类对象的时候向Thread类的构造函数中传入了一个Runnable任务,此时子类对象调用start()函数由于子类没有start()函数直接调用Thread类的start()函数。而start函数中调用run方法,由于子类覆写了Thread类中的run方法,则调用子类的run方法,输出SubThread run。
在此处给出Thread类中的构造函数,run方法,start方法实现的伪代码。
class Thread implements Runnable
{
private Runnable r;
Thread()
{}
Thread(Runnable r)
{
this.r=r;
}
public void run()
{
if(r!=null)
r.run;
}
public void start()
{
run();
}
}

被折叠的 条评论
为什么被折叠?



