--------------android培训、java培训、期待与您交流! --------------
一、多线程概述
现实生活中许多事情并不是按照顺序发生的,很多时候都是同时发生,例如人体中可以同时进行呼吸、血液流动、思考问题这些活动,用户操作电脑可以同时听音乐、玩游戏和打印文件。这些问题投射在java中就引出了并发执行的问题,而java就靠多线程机制来解决这些问题。
(1)进程:
是一个正在执行中的程序。
每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元。
(2)线程:
就是进程中的一个独立的控制单元。
线程在控制着进程的执行。一个进程中至少有一个线程。
java中体现:java JVM启动的时候,会有一个进程java.exe。该进程中至少有一个线程在负责java程序的执行。
而且这个线程运行的代码存在于main方法中。该线程称为主线程。
扩展知识:jvm启动不止一个线程,因为还有负责垃圾回收机制的线程。
二、线程类及线程实现
1、Thread(线程类)
java体系中,任何机制都是基于类的,线程机制也是基于Thread类。
Thread类的对象实例就表示java程序中的线程,通过它可以对线程采取多种操作。
Thread类常见方法:
yield();引起当前执行线程暂停,允许其他线程执行。
sleep(long millis);
start();使线程由新建状态变成可运行,并自动调用线程中的run方法
run();线程在启动时执行的功能
stop();停止当前线程
wait();使当前线程等待直到被唤醒或者超过指定的等待时间
isAlive();测试线程是否是活动的
setPriority(int new);设置线程优先级
notify();唤起一个等待的线程
notifyAll();唤起所有等待的线程
线程运行状态示例图:
2、实现线程的两种方式
(1)继承Thread类
Thread类位于java.lang包中,且java.lang包自动的被导入每个java程序中,省去了建立线程还要进行导入类的操作,说明java对线程进行了彻底支持。
继承Thread类实现线程步骤及代码实例:
①建立一个类使其继承Thread类。
②在类中复写Thread类的run方法,就是将希望运行的代码写进run中,让其在线程启动的时候执行。
③新建一个该类的对象,即一个线程。
④使用该对象调用start方法启动线程。
实例:
//Demo继承Thread类
class Demo extends Thread{
//复写了Thread类的run方法
public void run(){
for(int i=0;i<10;i++){
System.out.println("Demo run "+i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
Demo d =new Demo();//创建Demo的对象,即新建了一个线程
d.start();//调用start方法,此线程进入可运行状态
for(int i=0;i<10;i++){
System.out.println("main run "+i);
}//main函数的执行代码,其实主程序也是线程,而且一般都是主线程。
}
}
将代码运行多次发现每一次的结果都不相同。
原因:因为多个线程都获取cpu的执行权。cpu执行到谁,谁就运行。
在上述实例中,Demo的线程和主程序的主线程在抢夺cpu执行权,
由于每次运行时的cpu运行环境不同,就造成了每次运行时它们之间抢夺cpu执行权的结果也不同,就造成了程序运行结果不同。
明确一点,在某一个时刻,只能有一个程序在运行。(多核除外)
cpu在做着快速的切换,以达到看上去是同时运行的效果。
我们可以形象把多线程的运行行为在互相抢夺cpu的执行权。
这就是多线程的一个特性:随机性。谁抢到谁执行,至于执行多长,cpu说的算。
(2)实现Runnable接口
通过继承Thread类来实现线程确实很方便,但又出现了一个问题,如果类已经继承了一个类,由于java不允许多继承,类想要实现线程就无法继承Thread类,这时候又不想创建别的类,这时候就应该使该类通过实现Runnable接口来实现线程。
实现Runnable接口实现线程步骤及代码实例:
①建立一个类使其实现Runnable接口。
②在类中实现Runnable接口的run方法,就是将希望运行的代码写进run中,让其在线程启动的时候执行。
③新建一个该类的对象,并使用参数为该Runnable对象的构造方法创建Thread实例
④使用Thread实例调用start方法启动线程。
实例:
//Demo类实现Runnable接口
class Demo implements Runnable{
//实现了Runnable接口的run方法
public void run(){
for(int i=0;i<10;i++){
System.out.println("Demo run "+i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
Demo d =new Demo();//创建Demo的对象,即新建了一个Runnable对象
Thread t = new Thread(d);
/*调用Thread类中的Thread(Runnable target)构造方法建立线程实例,
*就是将实现Runnable接口的类对象传递给Thread类的对象即传递给新线程,为新线程提供方法和数据。
*/
t.start();//Thread类实例调用start方法,此线程进入可运行状态
for(int i=0;i<10;i++){
System.out.println("main run "+i);
}//main函数的执行代码,其实主程序也是线程,而且一般都是主线程。
}
}
继承Thread和实现Runnable的区别:
继承Thread:线程代码存放在Thread子类run方法中。无法多继承。
实现Runnable:线程代码存放在接口的子类的run方法中。可以多实现。
实现方式好处:避免了单继承的局限性。
在定义线程时,建立使用实现方式。
为什么要覆盖run方法呢?
Thread类用于描述线程。
该类就定义了一个功能,用于存储线程要运行的代码。该存储功能就是run方法。也就是说Thread类中的run方法,用于存储线程要运行的代码。
class ThreadA implements Runnable{
//实现了Runnable接口的run方法
public void run(){
for(int i=0;i<10;i++){
System.out.println("ThreadA run "+i);
}
}
}
class ThreadB implements Runnable{
//实现了Runnable接口的run方法
public void run(){
for(int i=0;i<10;i++){
System.out.println("ThreadB run "+i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
Thread t1= new Thread(new ThreadA());
Thread t2= new Thread(new ThreadB());
t1.start();
t2.start();
//t1.run();
//t2.run();
for(int i=0;i<10;i++){
System.out.println("main run "+i);
}
}
}
通过实例发现:
start可以实现多线程,run就不行。
run():是Thread类和Runnable接口中的方法,对其进行复写或实现后,其中存储的是线程启动时要执行的代码。
直接调用run方法只是执行了其中的代码,并不能启动线程。
start():此方法并没有执行代码,它的作用就是使一个新建线程开启并处于就绪状态,等待cpu执行权并获得后自动调用run方法。
run方法可以看成只要一运行就获得了cpu执行权,没有其他方法与之争抢。
例如A线程的run()和B线程的run()。先让A线程的run方法开始执行,它会一直执行下去直到完毕,接着才执行B线程的run方法。
start方法可以看成多个线程在争抢cpu执行权,获得了cpu执行权的方法才能执行。
例如A线程的start()和B线程的start()。两个线程开启后,它们会开始争抢cpu执行权,A线程的run方法和B线程的run方法会根据哪个线程抢到了cpu的执行权而交替执行。
4、获取线程对象和名称
currentThread() 返回对当前正在执行的线程对象的引用。
setName() 设置线程名称。
getName():获取线程名称。
System.currentTimeMillis(); 获取当前毫秒数。
System.nanoTime(); 获取当前毫微秒(1毫秒=1000微秒=1000000毫微秒)
(1)怎么获取主线程的名称?
Thread类有一个静态方法currentThread(),直接类名调用得到主线程的对象,该对象调用getName()获得名称。
String mainThreadName = Thread.currentThread().getName();
(2)为什么在主函数中无法用this调用getName()方法获取线程名?
因为主函数是静态的,类一加载,函数就存在了,但还没有对象,所以不能使用this。
class Test extends Thread
{ //构造函数,设置线程名称
Test(String name)
{
super(name);//访问父类的设置名称构造函数
}
public void run()
{
for(int x=0; x<10; x++)
{
System.out.println(this.getName()+" run..."+x+" 当前毫秒"+System.currentTimeMillis());
//getName可以获取当前线程名称,这里的this就相当于Thread.currentThread()
//Thread.currentThread()返回的是当前执行的线程对象引用
}
}
}
class ThreadDemo
{
public static void main(String[] args)
{
Test t1 = new Test("线程A");
Test t2 = new Test("线程B");
t1.start();
t2.start();
Thread.currentThread().setName("主线程");//将当前线程的名称设为主线程
for(int x=0; x<10; x++)
{
System.out.println(Thread.currentThread().getName()+"run"+x);
//主线程中不能用this来调用getName获取名称
//因为主函数是静态的,其随着类的加载而加载而加载,不存在对象,所以不能使用this
}
}
}
5、多线程实例需求:简单的卖票程序,多个窗口同时卖票。
class TicketWindow implements Runnable
{
private int ticket_num=100;//设置总票数
public void run()
{
while(true)
{
if(ticket_num>0)
{
try{Thread.sleep(10);}catch(Exception e){}//让该线程休眠10毫秒,目的:为了显现出线程中的安全问题
System.out.println(Thread.currentThread().getName()+"....sale : "+ ticket_num--);
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
//新建4个线程并调用start方法启动线程
TicketWindow t = new TicketWindow();
Thread t1=new Thread(t);
Thread t2=new Thread(t);
Thread t3=new Thread(t);
Thread t4=new Thread(t);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t4.setName("窗口4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
通过分析,发现程序可能会打印出0,-1,-2等错票, 多线程的运行出现了安全问题。
问题的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,
另一个线程参与进来执行。导致共享数据的错误。
解决办法:对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。
Java对于多线程的安全问题提供了专业的解决方式,这就引入了线程的同步。
三、线程的同步
由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。
Java语言为解决这种冲突提供了专门的机制,有效避免了同一个数据对象被多个线程同时访问,这就是线程同步。
1、同步代码块
(1)格式:
synchronized(对象)
{
需要被同步的代码
}
对象:如同锁
持有锁的线程可以在同步中执行。
没有锁的线程即使获取CPU的执行权,也进不去,因为没有获取锁。
(2)同步的前提:
1:必须要有两个或者两个以上的线程。
2:必须是多个线程使用同一个锁。
3:必须保证同步中只能有一个线程在运行。
(3)好处:解决了多线程的安全问题。
弊端:多个线程需要判断锁,较为消耗资源。
例如公共卫生间问题:
公共卫生间的门关上并锁死,表示里面有人使用,外面的人进不来,只能等里面的人使用完出来才能进入;
公共卫生间的门开着,表示里面没有人,外面的人可以进来使用,进来一个人把门关上锁死后,外面的人就又进不来了。
实例:
class TicketWindow implements Runnable
{
private int ticket_num=100;//设置总票数
Object obj = new Object();
public void run()
{
while(true)
{
//给访问共享数据的代码加锁,当有进程正在访问共享数据的时候,其他进程不能访问,保证了数据安全性。
synchronized (obj)//obj对象标志位为0,线程不能运行同步块代码,为1,则可以运行。
{
if(ticket_num>0)
{
try{Thread.sleep(10);}catch(Exception e){}//让该线程休眠10毫秒
System.out.println(Thread.currentThread().getName()+"....sale : "+ ticket_num--);
}
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
//新建4个线程并调用start方法启动线程
TicketWindow t = new TicketWindow();
Thread t1=new Thread(t);
Thread t2=new Thread(t);
Thread t3=new Thread(t);
Thread t4=new Thread(t);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t4.setName("窗口4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
2、同步函数
同步函数就是在方法前面修饰synchronized关键字的方法。
当某个对象调用了同步方法时,该对象上的其他同步方法必须等待该同步方法执行完毕后才能执行,必须将每个能访问共享资源的方法修饰为同步方法,否则会出错。
需求:
银行有一个金库。
有两个储户分别存300员,每次存100,存3次。
目的:该程序是否有安全问题,如果有,如何解决?
如何在程序中解决同步问题:
(1)明确哪些代码是多线程运行代码。
(2)明确共享数据。
(3)明确多线程运行代码中哪些语句是操作共享数据的。
class Bank
{
private int sum;//银行储金总数
public synchronized void add(int n)//同步方法
{
sum = sum + n;
try{Thread.sleep(10);}catch(Exception e){}
System.out.println("sum="+sum);
}
}
class Custom implements Runnable
{
private Bank b = new Bank();
public void run()
{
for(int x=0; x<3; x++)
{
b.add(100);//调用同步方法,其他线程必须等待该线程的同步方法执行完毕才能调用。
}
}
}
class BankDemo
{
public static void main(String[] args)
{
Custom c = new Custom();
//建立两个线程开始运行,即两个顾客开始存钱
Thread t1 = new Thread(c);
Thread t2 = new Thread(c);
t1.start();
t2.start();
}
}
3、同步函数的锁是this
同步代码块中都有一个对象作为锁,可是同步函数没有指明的对象也能进行锁的操作。
这是因为同步函数都将当前所属的对象作为锁,即this。
class TicketWindow implements Runnable
{
private int ticket_num=100;//设置总票数
Object obj = new Object();
public void run()
{
while(true)
{
sell();
}
}
public synchronized void sell(){ //同步函数的锁是this
if(ticket_num>0)
{
try{Thread.sleep(10);}catch(Exception e){}//让该线程休眠10毫秒
System.out.println(Thread.currentThread().getName()+"....sale : "+ ticket_num--);
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
//新建4个线程并调用start方法启动线程
TicketWindow t = new TicketWindow();
Thread t1=new Thread(t);
Thread t2=new Thread(t);
Thread t3=new Thread(t);
Thread t4=new Thread(t);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t4.setName("窗口4");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
4、静态同步函数的锁是Class对象
如果同步函数被静态修饰后,使用的锁是什么呢?
通过实例验证,发现不再是this。因为静态方法中也不可以定义this。
静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象:类名.class 该对象的类型是Class
静态的同步方法,使用的锁是该方法所在类的字节码文件对象。 类名.class。
实例验证:
class Ticket implements Runnable
{
private static int tick = 100;
boolean flag = true;
public void run()
{
if(flag)//如果标记flag为真,则线程运行同步代码块
{
while(true)
{
synchronized(Ticket.class)
{
if(tick>0)
{
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"....code : "+ tick--);
}
}
}
}
else//如果标记flag为真,则线程运行同步函数
while(true)
sell();
}
public static synchronized void sell()//静态同步函数使用的锁是Class对象
{
if(tick>0)
{
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"....show.... : "+ tick--);
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try{Thread.sleep(10);}catch(Exception e){}
//让主线程在开启t1线程后休眠10毫秒,目的是让t1有运行的时间,防止两个线程只运行同步函数
t.flag = false;//在t1线程运行同步代码块后,设置标记为flase,用来让线程访问同步函数
t2.start();
}
}
从实例得出:
两个线程同时在卖票,且run方法中使用了两个同步机制:同步代码块和静态同步函数。
若让同步代码块中使用一个对象锁,则代码运行结果中会出现卖出0票,表明不安全,就是线程同步中用的锁不一样。
若让同步代码块中使用当前class对象锁,则代码运行结果中不会出现卖出0票,表明安全,就是说静态同步函数使用的锁就是class对象锁。
5、单例设计模式-懒汉式
class Single{
private Single(){}
private static Single s=null;
public static Single getInstance(){
if(s==null){
s=new Single();
return s;
}
}//向外提供本类对象。
}
例如A程序和B程序,A程序判断完s==null,cpu切换到另一程序,A程序挂起;接着cpu开始执行B程序,B程序判断完s==null,cpu又切换到另一程序,B程序挂起。等到A程序恢复执行,则建立一个对象s,接下来当B程序开始执行,B又会进行创建对象s,由于这是单例设计,所以这里两个对象会冲突报错,产生安全问题。
class Single{
private Single(){}
private static Single s=null;
public static Single getInstance(){//或者在函数上修饰synchronized关键字
//判断对象是否为空
if(s==null)
{
//判断锁,无锁,则执行该同步代码块中代码,建立对象
synchronized(Single.class)//用到的是class对象锁
{
//再次判断对象是否为空
if(s==null){
s=new Single();//新建Single类对象
return s;//返回该对象
}
}
}
}//向外提供本类对象。
}
6、死锁
死锁是指发生在线程之间相互阻塞的现象,这种现象导致同步线程相互等待,以致每个线程都不能往下执行。
例如:两个人各持有一根筷子,想要吃饭,但吃饭需要两根筷子,所以两个人都想要对方的筷子,这时候就产生了死锁,谁也得不到所需的资源。
死锁:同步中嵌套同步。
死锁实例1:多窗口同时售票,这个程序会造成死锁。
死锁问题发生:
若线程1开始访问同步代码块,并把obj锁锁死,准备访问同步函数;
与此同时,线程2开始访问同步函数,并把class锁锁死,准备访问同步代码块;
紧接着,线程1发现同步函数被线程2锁死,不能运行,线程2发现同步代码块被线程1锁死,也不能运行;
同时两个线程都想访问对方所占用的资源,且都不放弃自己的资源,就发生了阻塞,产生了死锁现象。
class Ticket implements Runnable
{
private int tick = 1000;
Object obj = new Object();
boolean flag = true;
public void run()
{
if(flag)
{
while(true)
{
synchronized(obj)
{
sell();
}
}
}
else
while(true)
sell();
}
public synchronized void sell()//this
{
synchronized(obj)
{
if(tick>0)
{
try{Thread.sleep(10);}catch(Exception e){}
System.out.println(Thread.currentThread().getName()+"....code : "+ tick--);
}
}
}
}
class DeadLockDemo
{
public static void main(String[] args)
{
Ticket t = new Ticket();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
try{Thread.sleep(10);}catch(Exception e){}
t.flag = false;
t2.start();
}
}
死锁实例2:
class Test implements Runnable
{
private boolean flag;
Test(boolean flag)
{
this.flag = flag;
}
public void run()
{
if(flag)
{
while(true)
{
synchronized(MyLock.locka)
{
System.out.println(Thread.currentThread().getName()+"...if locka ");
synchronized(MyLock.lockb)
{
System.out.println(Thread.currentThread().getName()+"..if lockb");
}
}
}
}
else
{
while(true)
{
synchronized(MyLock.lockb)
{
System.out.println(Thread.currentThread().getName()+"..else lockb");
synchronized(MyLock.locka)
{
System.out.println(Thread.currentThread().getName()+".....else locka");
}
}
}
}
}
}
//定义锁
class MyLock
{
static Object locka = new Object();
static Object lockb = new Object();
}
class DeadLockDemo
{
public static void main(String[] args)
{
Thread t1 = new Thread(new Test(true));
Thread t2 = new Thread(new Test(false));
t1.start();
t2.start();
}
}
死锁问题发生:
线程1有a锁,线程2有b锁;线程1想要访问b锁,线程2想要访问a锁,双方还不放自己的锁,于是产生死锁。
四、线程操作
1、线程通信
(1)线程通信
多线程一个重要特点就是它们之间可以相互通信,线程通信是线程之间可以相互交流和等待,可以通过经常共享的数据使线程相互交流,也可以通过线程控制方法使线程之间相互等待。
线程间通信:其实就是多个线程在操作同一个资源,但是操作的动作不同。
(2)等待唤醒机制
线程通信可以通过线程控制方法使线程互相等待。
Object类提供了3个方法:wait()、notify()和notifyAll()。
都使用在同步中,因为要对持有监视器(锁)的线程操作。
所以要使用在同步中,因为只有同步才具有锁。
为什么这些操作线程的方法要定义Object类中呢?
因为这些方法在操作同步中线程时,都必须要标识它们所操作线程只有的锁,
只有同一个锁上的被等待线程,可以被同一个锁上notify唤醒。
不可以对不同锁中的线程进行唤醒。
也就是说,等待和唤醒必须是同一个锁。
而锁可以是任意对象,所以可以被任意对象调用的方法定义Object类中。
wait()和sleep()的区别:wait() 释放资源,释放锁;sleep() 释放资源,不释放锁。
线程通信实例:
//资源
class Res
{
String name;
String sex;
boolean flag = false;//默认资源标识为false,没有资源
}
//输入
class Input implements Runnable
{
private Res r ;//资源锁
Input(Res r)
{
this.r = r;
}
public void run()
{
int x = 0;
while(true)
{
synchronized(r)//同步锁
{
if(r.flag)//若有资源,则调用wait等待输出者取走资源
try{r.wait();}catch(Exception e){}
//放入资源
if(x==0)
{
r.name="小明";
r.sex="男";
}
else
{
r.name="小丽";
r.sex = "女";
}
x = (x+1)%2;//对输入的资源进行切换
r.flag = true;//将资源标识设为true,表明资源有了
r.notify();//唤醒输出线程,让其取走资源
}
}
}
}
//输出
class Output implements Runnable
{
private Res r ;//资源锁
Output(Res r)
{
this.r = r;
}
public void run()
{
while(true)
{
synchronized(r)//同步锁
{
if(!r.flag)//若没有资源,则调用wait等待输入者放入资源
try{r.wait();}catch(Exception e){}
System.out.println(r.name+"...."+r.sex);//输出资源
r.flag = false;//将资源标识设为flase,表明资源没有了
r.notify();//唤醒输入线程,让其放入资源
}
}
}
}
class InputOutputDemo
{
public static void main(String[] args)
{
Res r = new Res();//建立资源对象
Input in = new Input(r);
Output out = new Output(r);
//建立两个线程并开启线程,一个输入线程,一个输出线程
new Thread(in).start();
new Thread(out).start();
}
}
(3)生产者与消费者
生产者和消费者问题:是一个典型的线程通信模型。
该问题和上文的输入输出同步问题相似,并对其进行优化,将同步代码块封装成同步函数。
代码:
//商品仓库
class Res
{
private String name;
private int num=1;//仓库商品编号
private boolean flag = false;//仓库中是否有商品的标识,初始化为没有商品。
public synchronized void put()//生产函数,同步函数
{
if(flag)//若有商品,则调用wait等待消费者消费商品
try{this.wait();}catch (Exception e){}
//生产产品
this.name="商品"+num++;
System.out.println(Thread.currentThread().getName()+"生产者生产:"+name);
flag = true;//将商品标识设为true,表明仓库中已有商品
this.notify();//通知消费者消费商品
}
public synchronized void get()//消费函数,同步函数
{
if(!flag)//若没有商品,则调用wait等待生产者生产商品
try{this.wait();}catch(Exception e){}
//消费产品
System.out.println(Thread.currentThread().getName()+"消费者消费:"+name);
flag = false;//将商品标识设为false,表明仓库中没商品
this.notify();//通知生产者生产商品
}
}
//生产者
class Producer implements Runnable
{
private Res r ;
Producer(Res r)
{
this.r = r;
}
public void run()
{
for(int i=0;i<100;i++){
r.put();
}
}
}
//消费者
class Customer implements Runnable
{
private Res r ;
Customer(Res r)
{
this.r = r;
}
public void run()
{
for(int i=0;i<100;i++){
r.get();
}
}
}
class ProducerCustomerDemo
{
public static void main(String[] args)
{
Res r = new Res();
new Thread(new Producer(r)).start();
new Thread(new Customer(r)).start();
}
}
上述是一个生产者线程和一个消费者线程的代码,如果使其开启两个生产者线程和两个消费者线程,则可能会出现异常情况。
出现的情况:
序号为线程执行顺序,F、T指商品标识
生产一次,消费两次的异常情况:
F T F F T F
生产者1 (1)生产商品186,等待
生产者2 (4)生产商品187,等待
消费者1 (2)消费商品186,等待 (5)消费商品187,等待
消费者2 (3)等待 (6)不再判断标识,仍然消费商品187,问题出现
生产两次,消费一次的异常情况:
F T T F T F
生产者1 (1)生产商品169,等待 (5)生产商品170,等待
生产者2 (2)等待 (6)不再判断标识,继续生产商品171,导致商品170没有被消费
消费者1 (3)消费商品169,等待
消费者2 (4)等待
下面解决这个问题并说明原因。
下面是两个生产者线程和两个消费者线程的代码:
//商品仓库
class Res
{
private String name;
private int num=1;//仓库商品编号
private boolean flag = false;//仓库中是否有商品的标识,初始化为没有商品。
public synchronized void put()//生产函数,同步函数
{
while(flag)//若多次判断商品标识
try{this.wait();}catch (Exception e){}
//生产产品
this.name="商品"+num++;
System.out.println(Thread.currentThread().getName()+"生产者生产:"+name);
flag = true;//将商品标识设为true,表明仓库中已有商品
this.notifyAll();//唤醒全部线程
}
public synchronized void get()//消费函数,同步函数
{
while(!flag)//若没有商品,则调用wait等待生产者生产商品
try{this.wait();}catch(Exception e){}
//消费产品
System.out.println(Thread.currentThread().getName()+"消费者消费:"+name);
flag = false;//将商品标识设为false,表明仓库中没商品
this.notifyAll();
}
}
//生产者
class Producer implements Runnable
{
private Res r ;
Producer(Res r)
{
this.r = r;
}
public void run()
{
for(int i=0;i<100;i++){
r.put();
}
}
}
//消费者
class Customer implements Runnable
{
private Res r ;
Customer(Res r)
{
this.r = r;
}
public void run()
{
for(int i=0;i<100;i++){
r.get();
}
}
}
class ProducerCustomerDemo
{
public static void main(String[] args)
{
Res r = new Res();
//两个生产线程生产
new Thread(new Producer(r)).start();
new Thread(new Producer(r)).start();
//两个消费线程消费
new Thread(new Customer(r)).start();
new Thread(new Customer(r)).start();
}
}
对于多个生产者和消费者:
为什么要定义while判断标记?
原因:让被唤醒的线程再一次判断标记。
为什么定义notifyAll?
因为需要唤醒对方线程。只用notify,容易出现只唤醒本方线程的情况。导致程序中的所有线程都等待。
生产者消费者问题中怎么只唤醒对方线程呢?
JDK1.5 中提供了多线程升级解决方案。
将同步Synchronized替换成现实Lock操作。
将Object中的wait,notify notifyAll,替换了Condition对象。
该对象可以Lock锁 进行获取。
该示例中,实现了本方只唤醒对方操作。
Lock:替代了Synchronized
lock
unlock
newCondition()
Condition:替代了Object wait notify notifyAll
await();
signal();
signalAll();
import java.util.concurrent.locks.*;
//商品仓库
class Res
{
private String name;
private int num=1;//仓库商品编号
private boolean flag = false;//仓库中是否有商品的标识,初始化为没有商品。
private Lock lock = new ReentrantLock();
private Condition condition_put = lock.newCondition();
private Condition condition_get= lock.newCondition();
public void put()throws InterruptedException//生产函数
{
lock.lock();//获取锁
try{
while(flag)//若多次判断商品标识
condition_put.await();
//生产产品
this.name="商品"+num++;
System.out.println(Thread.currentThread().getName()+"生产者生产:"+name);
flag = true;//将商品标识设为true,表明仓库中已有商品
condition_get.signalAll();//唤醒消费线程
}
finally{
lock.unlock();//一定要释放锁
}
}
public void get()throws InterruptedException//消费函数
{
lock.lock();//获取锁
try{
while(!flag)//若没有商品,则调用wait等待生产者生产商品
condition_get.await();
//消费产品
System.out.println(Thread.currentThread().getName()+"消费者消费:"+name);
flag = false;//将商品标识设为false,表明仓库中没商品
condition_put.signalAll();//唤醒生产线程
}
finally{
lock.unlock();//一定要释放锁
}
}
}
//生产者
class Producer implements Runnable
{
private Res r ;
Producer(Res r)
{
this.r = r;
}
public void run()
{
for(int i=0;i<100;i++){
try{
r.put();
}catch (InterruptedException e){}
}
}
}
//消费者
class Customer implements Runnable
{
private Res r ;
Customer(Res r)
{
this.r = r;
}
public void run()
{
for(int i=0;i<100;i++){
try{
r.get();
}catch (InterruptedException e){}
}
}
}
class ProducerCustomerDemo
{
public static void main(String[] args)
{
Res r = new Res();
//两个生产线程生产
new Thread(new Producer(r)).start();
new Thread(new Producer(r)).start();
//两个消费线程消费
new Thread(new Customer(r)).start();
new Thread(new Customer(r)).start();
}
}
2、线程的控制
(1)停止线程
stop方法已经过时
如何停止线程?
只有一种方法,run方法结束。开启多线线程,运行代码通常是循环结构。
只要控制住循环,就可以让run方法结束,也就是线程结束。
特殊情况:
当线程处于冻结状态,就不会读取到标记,那么线程就不会结束。
当没有指定的方式让冻结的线程恢复到运行状态时,这时需要对冻结进行清除。
强制让线程恢复到运行状态中来,这样就可以操作标记让线程结束。
一个例子:一个人(线程)正处于冻结状态(wait),当没有指定的方式(notify)让其苏醒,可以对这个人糊过去一个砖头( interrupt),这个人就醒了,恢复了活动(运行)。
Thread类提供该方法 interrupt();
class StopThread implements Runnable{
private boolean flag = true;
public synchronized void run(){
while(flag){
try{
wait();
}
catch(InterruptedException e){
System.out.println(Thread.currentThread().getName()+"....Exception");
flag = false;
}
System.out.println(Thread.currentThread().getName()+"....run");
}
}
public void changeFlag(){
flag = false;
}
}
class StopThreadDemo{
public static void main(String[] args){
StopThread st = new StopThread();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
t1.start();
t2.start();
int num = 0;
while(true){
if(num++ ==60){
t1.interrupt();
t2.interrupt();
break;
}
System.out.println(Thread.currentThread().getName()+"...."+num);
}
System.out.println("over");
}
}
(2)守护线程
守护线程 setDaemon
特点:开启后和前台线程共同抢劫CPU的执行权运行
当所有的前台线程都结束后,后台线程会自动结束。
public final void setDaemon(boolean on)将该线程标记为守护线程或用户线程。
当正在运行的线程都是守护线程时,Java 虚拟机退出。
该方法必须在启动线程前调用。
该方法首先调用该线程的 checkAccess 方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。
class StopThread implements Runnable
{
private boolean flag =true;
public void run()
{
while(flag)
{
System.out.println(Thread.currentThread().getName()+"....run");
}
}
public void changeFlag()
{
flag = false;
}
}
class StopThreadDemo
{
public static void main(String[] args)
{
StopThread st = new StopThread();
Thread t1 = new Thread(st);
Thread t2 = new Thread(st);
t1.setDaemon(true);
t2.setDaemon(true);
t1.start();
t2.start();
int num = 0;
while(true)
{
if(num++ == 60)
{
break;
}
System.out.println(Thread.currentThread().getName()+"......."+num);
}
System.out.println("over");
}
}
(3)加入线程(join)
public final void join()
throws InterruptedException等待该线程终止。
当A线程执行到了B线程的join()方法时,A就会等待,等B线程都执行完后,A线程才执行。
join可以用来临时加入线程执行。
class Demo implements Runnable{
public void run(){
for (int x=0;x<=10 ;x++ ){
System.out.println(Thread.currentThread().getName()+"...."+x);
}
}
}
class JoinDemo{
public static void main(String[] arsg) throws Exception{
Demo d = new Demo();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t1.join();//主线程执行到t1线程的join方法后,主线程停止,等到t1线程run方法执行结束,主线程才会运行。也就是t1线程加入了主线程。
t2.start();
for (int x=0;x<=10 ;x++ ){
System.out.println("main...."+x);
}
System.out.println("over");
}
}
3.、线程的优先级
yield() 暂停当前正在执行的线程对象,并执行其他线程。
setPriority(int newPriority) 更改线程的优先级。newPriority用1-10表示
java已经给出了常用的三个等级:
MAX_PRIORITY 线程可以具有的最高优先级。最高是10
MIN_PRIORITY 线程可以具有的最低优先级。最低是1
NORM_PRIORITY 分配给线程的默认优先级。默认是5
toString():
返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
class Demo implements Runnable
{
public void run()
{
for(int x=0; x<10; x++)
{
System.out.println(Thread.currentThread().toString()+"....."+x);
//Thread.yield();//暂停当前正在执行的线程对象,并执行其他线程
}
}
}
class JoinDemo
{
public static void main(String[] args) throws Exception
{
Demo d = new Demo();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
t1.start();
t1.setPriority(Thread.MAX_PRIORITY);//使t1线程拥有最高的优先级,但t1并不一定先运行。
t2.start();
for(int x=0; x<10; x++)
{
System.out.println("main....."+x);
}
}
}