java(五)

多线程


进程:可以理解为正在进行中的程序。(直译)
      进程会在内存中开辟空间,不直接执行,线程负责执行。
线程:就是进程中的一个负责程序执行的控制单元(也叫执行路径)。
多线程:一个进程中可以有多个执行路径,称之为多线程。
一个进程中至少要有一个线程。开启多个线程是为了同时运行多部分代码。
每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务。
多线程的弊端:线程太多会导致效率的降低。
     其实,应用程序的执行都是cpu在做着快速的切换完成的,不是真正的同时,只是切换的很快。这个切换是随机的。


jvm启动时就启动了多个线程,至少有两个线程,这是可以分析出来的。
第一个是执行main函数的主线程,该线程的任务代码都定义在main函数中。
第二个是负责垃圾回收的线程。


如何创建一个线程呢?
方式一:继承thread类。
      步骤:1.定义一个类继承thread类。
   2.覆盖thread类中的run方法。
   3.直接创建thread类的子类对象。
   4.调用start方法,开启线程,并调用线程的任务,run方法执行。
      为什么这么做?
      答:创建线程的目的是为了开启除主线程外的另一条执行路径,去运行指定的代码,和其他代码同时运行。
 而运行的指定代码就是这个执行路径的任务。
 JVM创建的主线程的任务都定义在了主函数中。
 而自定义的线程他的任务在哪儿呢?
 Thread用于描述线程,线程是需要任务的,这个任务就通过Thread类中的run方法来体现。
 
 也就是说,run方法中定义的就是线程要运行的任务代码。
 开启线程是为了运行指定代码,所以继承Thread类,并复写run方法,将运行的代码定义在run方法中,
 最后,调用start方法,开启线程,start方法会调用run方法,就完成了线程的创建。




例:此例中,不用自定义线程时,由于y的影响,运行特别慢,而且要看到xiaoqiang必须等到旺财运行完毕才能看到。
    要想让旺财和xiaoqiang同时运行,就要用到自定义线程了。
    注意:使用线程后,输出的顺序是随机的,每次运行的结果都有可能不同,因为CPU在线程之间的切换是随机的。
 可以通过Thread的getName方法获取线程的名称  Thread-编号(从0开始),主线程的名字就是main。
 通过Thread.currentThread().getname()获取的就是当前执行对象的线程名称。
class Demo extends Thread
{
private String name;
Demo(String name)
{
// super(name);    要给线程自己起名字就加这句就行
this.name=name;
}
public void run()
{
for(int x=0;x<10;x++)
{
for(int y=-99999;y<99999;y++)
{}
System.out.println(name+"...x="+x+","+getName()+","+Thread.currentThread().getName());
}
}
}
class ThreadDemo
{
public static void main(String[] args)
{
Demo d1=new Demo("旺财");
Demo d2=new Demo("xiaoqiang");

d1.start();                    //开启线程,调用run方法。
d2.start();
d1.run();                      //通过这个就能看出Thread.currentThread().getname()和getname()的区别。
System.out.println("haha!!!             "+Thread.currentThread().getName());
}
}


多线程运行,栈内存加载时,有几个线程就加载几个并列的函数,和前面所讲的异常有点不同,这里,那个线程有异常,就结束那个线程。、
其他的线程继续执行。之前讲的异常因为都只有一个线程,所以说有异常就直接挂了。、








包(package)
对类文件进行分类管理,给类提供多层命名(名称)空间,写在程序的第一行。
包名所有字母都小写,包也是一种封装形式。  类名的全称是    包名.类名


包与包之间的类进行访问时,长犯错误:
1.建立另一个包里的类的对象时,类名要写完整(包名.类名)
2.当两个包不在同一个目录下时,要建立临时变量(set classpath:\路径)
3.包中的类和包中类的方法权限都要足够大,即都要加上public。


protected关键字:
包与包之间的类不用继承就能访问,如果想要继承才能访问就用到了protected关键字。
它的权限比public小,可以看成封装,不让和它本身没关系的类访问,只让他的子类访问。






四种权限:(default是默认权限,啥都不写时就是这种权限)


public   protected default      private
同一类中  ok           ok          ok           ok      
同一包中  ok       ok          ok           
子类中  ok       ok          
不同包中  ok       




import关键字
创建对象需要访问其他包的类时,包名.类名太复杂。
这时,在包名下面导入另一个包之后,就可以不用写包名,直接写类名了。
而导入所用到的关键字就是import,一个类中可以导入多个类,如:
import package.DemoA;       //导入了package包中的DemoA类
import package.DemoAA;      
import package.DemoAAA;
但这样太复杂,可以使用通配符:*。
import package.*;          //这就代表导入了package包中所有的类。
导入了package包中所有的类后,并不会导入这个包的子包中的类。
比如package包还有一个子包abc,要使用abc中的类,就要重新导入。
import package.abc.*;
导包的原则:用到那个类,尽量就导入那个类,别用通配符。
   实际开发时,通配符并不会用到太多的。

注意:import导入的是包中的类,而不是包。



线程的四种状态:
                 临时阻塞状态<------------------|
        | |
      | |
    双|向 |
      | |
         start()       |   sleep(t)    wait()   |
被创建-------------->运行----------------------->冻结
   |                   | <----------------------|
   |               |   sleep(t)   notify()  |
   |       | |
   |               | |
   |          run()结束| |
   |       | |
   |       | |
   | stop() | |
   |       | |
   |       | |
   |       | |
   |  stop()       | |
   |----------------->消亡<---------------------|


    cpu的执行资格:可以被cpu处理,在处理队列中排队。
    cpu的执行权:正在被cpu处理。
运行状态的线程具备cpu的执行资格和cpu的执行权。
冻结状态的线程既没有cpu的执行资格,也没有cpu的执行权。
而具备执行资格,但不具备执行权,正在等待执行权的线程的状态,就叫临时阻塞状态。


创建线程的第一种方式:继承thread类。
但是万一这个类已经继承了其他的类时,还想要多线程运行,然而java又不能多继承
此时的需求是实现这个类的功能扩展,让其可以作为线程的任务执行,这就可以通过接口来实现。
于是,就有了创建线程的第二种方式:实现runnable接口。


创建线程的第二种方式:实现runnable接口。
步骤:1.定义类实现runnable接口。
      2.覆盖接口中的run方法,将线程的任务代码封装到run方法中。
      3.通过thread类创建对象,并将runnable接口的子类对象作为thread类构造函数的参数进行传递。
这一步是为什么呢?
因为线程的任务都封装在runnable接口子类对象的run方法中,所以要在线程对象创建时明确要运行的任务。
      4.调用线程对象的start方法开启线程。
runnable的出现仅仅是将线程的任务进行了对象的封装。




实现runnable的好处(也就是两种方式的区别):
1.将线程的任务从线程的子类中分离出来,进行了单独的封装。
  按照面向对象的思想将任务封装成了对象。
2.避免了java单继承的局限性。
所以,创建线程的第二种方式比较常用。




线程的安全问题
产生原因:1.多个线程在操作共享数据。
 2.操作共享数据的线程代码有多条。
当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生。
比如下面的例题就会出现卖出票号超出1-100,票号为负的情况
解决思路:就是将多条操作共享数据的代码封装起来,当有线程在执行这些代码的时候,其他线程不能参与运算。
 必须等到当前线程把这些代码都执行完毕后,其他线程才可以参与运算。


在java中,用同步代码块或同步代码块就可以解决这个问题。(同步函数直接在函数声明加synchronized,参考下面第二个例子)
同步代码块的格式:
synchronized(对象)      //这里的对象随便,写obj方便,记得先去创建一个obj对象。对象相当于一个锁,一个线程进入代码块后就给代码块枷锁了,此时别的线程就不能进来了,这就解决了安全问题
{
需要被同步执行的代码;
}
同步的好处:解决了线程的安全问题。
同步的弊端:相对降低了效率,因为同步外的线程都会判断同步锁。
同步的前提:同步中必须有多个线程并使用同一个锁。(就是别把synchronized后的对象创建在run方法里,这样就等于让每个线程都有一把锁,就不能解决线程的安全问题了)
注意:同步只把需要同步的同步就行,例如下面的例子不用同步代码块,直接把run变成同步函数就不行,就会出现所有的票全是1号窗口卖的。
      而下面第二个例子就可以直接把add函数同步。


同步函数的锁是什么?
同步函数不用像同步代码块那样在后面写出一个对象obj作为锁,同步函数自身带锁的,这个锁(对象)就是this,下面的例子中就是t      
同步静态函数是静态的,没有对象,他用的是当前文件的字节码文件对象。就是this.getclass()
this.getclass()也可以换成Ticket.class,效果是一样的,都获取了当前文件的字节码文件对象。
建议尽量使用同步代码块,少用同步函数。






实例:
/*
需求:卖票,四个窗口卖100张票。
*/


class Ticket implements Runnable
{
private int num=100; 
Object obj=new Object();
public void run()
{
while(true)
{
synchronized(obj)
{
if(num>0)                        //票的数量不可能出现负数。
{  
try{Thread.sleep(10);}
catch(InterruptedException e){}      //sleep方法可能出现中断异常,必须声明或捕捉,但是run方法的父类都没有异常,所以不能声明,只能捕捉。   这里涉及到了线程的安全问题,详情看上课的视频1:40分钟处。
System.out.println(Thread.currentThread().getName()+"..sale.."+num--);
}
}
}
}
}
class TicketDemo
{
public static void main(String[] args)
{
Ticket t=new Ticket();      //创建一个线程任务对象。  注意:这里指创建了一个对象,若用继承thread的方式,就要创建四个对象,四个窗口会各自卖各自的票,
//就会出现四个窗口都卖了某一张票,总共卖出400张票的情况。即使将num定义成静态的,也有局限性,比如8个窗口同时卖100张汽车票和100张火车票,就无法实现。

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();
}
}












/*
需求:两个银行储户,每个都到银行存钱,每次存100,共存3次。
分析该例是否存在线程的安全问题,并修改。
答:从线程安全问题的原因入手,多个线程共享sum,操作sum的代码表面是一句,但是调用了add方法,所以实际操作sum的代码也是多句。
       所以这就有了安全问题了,(不信自己多运行几次试试,或者在add方法的输出语句前加sleep)
       解决方法:封装sum=sum+num和输出语句,同步,加锁。解决方法就是注释掉的那几行。
同步代码块起了一个封装的作用,而函数本身也是封装的功能,所以可以直接同步函数,在函数声明里加synchronized即可。

*/
class Bank
{
private int sum;     //银行金库总钱数
// private Object obj=new Object();
public synchronized void add(int num)
{
// synchronized(obj)             //别想着直接在括号里写new Object(),原因看上面那道题
// {
sum=sum+num;
System.out.println("sum="+sum);
// }
}
}
class Cus implements Runnable
{
private Bank b=new Bank();
public void run()
{
//Bank b=new Bank();         这句话不能放这里,这里相当于储户存钱存在了两个不同的银行
for(int x=0;x<3;x++)
{
b.add(100);              //访问另一个类的方法,要先创建对象
}
}
}
class BankDemo
{
public static void main(String[] args)
{
Cus c=new Cus();
Thread t1=new Thread(c);
Thread t2=new Thread(c);
t1.start();
t2.start();

}
}






多线程下的单例


饿汉式(单例设计模式):
class Single
{
private static final Single s=new Single();
private single(){}
public static Single getInstance()         
  
{
return s;       
}
}
懒汉式(单例设计模式的延迟加载形式)
class Single
{
private static Single s=null;
private single(){}
public static Single getInstance()           
{
if(s==null)
s=new Single();
return s;
}
}
饿汉式不存在安全问题,但懒汉式存在安全问题。
为了解决懒汉式的安全问题并让它更高效,修改方法:
if(s==null)
{
synchronized(Single.class)
{
if(s==null)
s=new Single();
return s;
}
}
懒汉式技术含量高一点,面试一般会考这种,但饿汉式更简单方便,更常用。






同步中的死锁问题
死锁常见的情景之一:同步的嵌套
class Test implements Runnable
{
private boolean flag;
Test(boolean flag)
{
this.flag=flag;
}
public void run()
{
if(flag)
{
   while(true)      //加这个是因为cpu随机切换,有时候可能锁不上,让他多循环几次,就肯定锁上了
   {
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.locka)
{
System.out.println(Thread.currentThread().getName()+"else locka...");
synchronized(MyLock.lockb)
{
System.out.println(Thread.currentThread().getName()+"else lockb...");
}
}
    }
}
}
}
class MyLock
{
public static final object locka=new Object();
public static final object lockb=new Object();
}
class DeadLockDemo
{
public static void main(String[] args)
{
Test a=new Test(true);
Test b=new Test(false);
Thread t1=new Thread(a);
Thread t2=new Thread(b);
t1.start;
t2.start;
}
}



线程间通信
多个线程在处理同一资源,但是任务却不同。
等待唤醒机制:
    涉及的方法:
1.wait();让线程处于冻结状态,被wait的线程会存储到线程池中。
2.notify();唤醒线程池中的一个线程(任意)
3.notifyAll();唤醒线程池中的所有线程。
这些方法都必须定义在同步中,因为这些方法是用于操作线程状态的方法,必须要明确到底操作的是那个锁上的线程。






需求:循环输出一条丽丽女,一条mike男(用到等待唤醒)
class Resource
{
private String name;
private String sex;
private boolean flag=false;
public synchronized void set(String name,String sex)
{
if(flag)
try{this.wait();}catch(InterruptedException e){}
this.name=name;
this.sex=sex;
flag=true;
this.notify();
}
public synchronized void out()
{
if(!flag)
try{this.wait();}catch(InterruptedException e){}
System.out.println(name+"..."+sex);
flag=false;
this.notify();
}
}
class Input implements Runnable
{
Resource r;
Input(Resource r)
{
this.r=r;
}
public void run()
{
int x=0;
while(true)
{
if(x==0)
{
r.set("mike", "nan");
}
else
{
r.set("丽丽", "女女女女女女女女");
}
x=(x+1)%2;
}
}
}
class Output implements Runnable
{
Resource r;
Output(Resource r)
{
this.r=r;
}
public void run()
{
while(true)
{
synchronized(r)
{
r.out();
}
}
}
}
class ResourceDemo
{
public static void main(String[] args) 
{
//创建资源
Resource r=new Resource();
//创建任务
Input in=new Input(r);
Output out=new Output(r);

//创建线程,执行路径
Thread t1=new Thread(in);
Thread t2=new Thread(out);

t1.start();
t2.start();
}
}










等待唤醒经典案例
生产者消费者(多生产者多消费者)
区别就是:if变while               if判断标记,只有一次,会导致不该运行的线程运行了,while解决了线程获取执行权后,是否要运行的问题
 notify变notifyAll       notify只能唤醒一个线程,如果本方唤醒了本方,没有意义,而且while判断标记+notify会导致死锁。
 notifyAll解决了,本方线程一定会唤醒对方线程
但是这样明显导致效率降低了,因为唤醒了所有,本方又要判断,能否只唤醒对方呢?
答案是可以的。


Lock接口:jdk1.5之后新增了Lock接口,将同步和锁封装成了对象。lock接口的出现替代了同步代码块和同步函数,将同步的隐式操作变成显示操作,
 同时更加灵活,可以一个锁上加多组监视器。
          lock():获取锁
 unlock():释放锁,通常需要定义在finally代码块中。
 以前的synchronized代码就可以写成下面这种形式
         (先导包别忘了)
import java.util.concurrent.locks.*;
Lock lock=new ReentrantLock();
void show()
{
lock.lock();         //获取锁
code....
lock.unlock();        //释放锁(如果代码里有异常,释放锁必须放在finally中)
}
Conditon接口:Condition接口的出现替代了object中的wait notify notifyAll方法。
     将这些监视器方法单独进行了封装,变成Condition监视器对象,可以和任意锁进行组合。
              这也就实现了可以在一个锁上加多组监视器,为我们前边提出的想要提高效率,只唤醒对方提供了可能。
     await():
     signal():
     signalAll():














import java.util.concurrent.locks.*;




class Resource
{
private String name;      //商品名称
private int count;        //商品编号
private boolean flag=false;
//创建一个锁对象
Lock lock=new ReentrantLock();
//通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者
Condition producer_con=lock.newCondition();
Condition consumer_con=lock.newCondition();
public void set(String name)
{
lock.lock();
try
{
while(flag)           //这里本来是if(flag),由于多生产者,而且唤醒时,程序会直接从下面this开始执行,不会再判断,安全问题就再次出现了。
 //但是只需把if换成while,就可以解决,while每次唤醒都会再次判断flag。然而同时另一个问题出现了,死锁,4个线程全都wait了,没人唤醒。
   //解决这个死锁的方法:notifyAll()   全部唤醒,如果cpu切到本方线程,会回到while判断,就会继续wait,不影响。
try{producer_con.await();}catch(InterruptedException e){}
/*用condition替换obj
try{this.wait();}catch(InterruptedException e){}
*/
this.name=name+count;
count++;
System.out.println(Thread.currentThread().getName()+".生产者."+this.name);
flag=true;
consumer_con.signal();
//notifyAll();
}
finally
{
lock.unlock();
}
}

/*  这是用lock替代synchronized之前的set方法
  public synchronized void set(String name)
{
while(flag)           //这里本来是if(flag),由于多生产者,而且唤醒时,程序会直接从下面this开始执行,不会再判断,安全问题就再次出现了。
 //但是只需把if换成while,就可以解决,while每次唤醒都会再次判断flag。然而同时另一个问题出现了,死锁,4个线程全都wait了,没人唤醒。
 //解决这个死锁的方法:notifyAll()   全部唤醒,如果cpu切到本方线程,会回到while判断,就会继续wait,不影响。
try{this.wait();}catch(InterruptedException 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(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
flag=false;
notifyAll();
}
*/
public void out()
{
lock.lock();
try
{
while(!flag)
try{consumer_con.await();}catch(InterruptedException e){}
//try{this.wait();}catch(InterruptedException e){}
System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name);
flag=false;
producer_con.signal();
//notifyAll();
}
finally
{
lock.unlock();
}
}
/* 这是lock替换synchronized之前的out方法
public synchronized void out()
{
while(!flag)
try{this.wait();}catch(InterruptedException 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("烤鸭");
}
}
}
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 con=new Consumer(r);

Thread t0=new Thread(pro);
Thread t1=new Thread(pro);
Thread t2=new Thread(con);
Thread t3=new Thread(con);
t0.start();
t1.start();
t2.start();
t3.start();
}
}




wait和sleep区别?
1.wait可以指定时间也可以不指定。
  sleep必须指定时间。
2.在同步中时,对cpu的执行权和锁的处理不同。
  wait:释放执行权,释放锁。
  sleep:释放执行权,不释放锁。






停止线程
1.stop()   已过时,具有固有的不安全性。
2.run方法结束 
怎么控制线程的任务的run方法结束呢?
任务中都会有循环结构,只要控制住循环就可以结束任务。
控制循环通常就用定义标记来完成。


class StopThread implements Runnable
{
private boolean flag=true;
public void run()
{
while(flag)
System.out.println(Thread.currentThread().getName()+"....");

}
public void setFlag()
{
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=1;
for(;;)                 //无限循环
{
if(++num==50)
{
st.setFlag();
break;
}
System.out.println("main..."+num);
}
System.out.println("over");
}
}
但是如果线程处于了冻结状态,无法读取标记,怎么结束呢?
interrupt()方法,它可以将线程从冻结状态恢复到运行状态,让线程具备cpu的执行资格,但是会发生中断异常。


线程的一些其他方法
setDaemon()      设置为守护线程,其他和以前学的线程一样,唯一区别就是守护线程会在其它线程都结束后自动结束。
                 比如输入和输出,就可以将输出设置为守护线程,输入线程结束了,输出也就自动结束了。
join() 申请加入进来,运行。等待该线程终止。一般适用于临时加入一个线程运算。
比如在上面的例子中,主函数t1.start();下面加上t1.join();就代表主线程先别执行了,先执行线程1,等线程1终止后主线程才再次获得执行资格。































































评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值