第八篇 黑马程序员-线程

来源:互联网 发布:杭州淘宝g20怎么办 编辑:程序博客网 时间:2024/06/06 14:23

------- android培训、java培训、期待与您交流! ---------

创建线程的两种方式:
继承Thread类
实现Runnable接口
1.定义类实现Runnable接口
2.覆盖Runnable接口中的run方法
注意:复写run方法的目的是将自定义的代码存储在run方法中,让线程运行
将线程要运行的代码存放在该run方法中
3.通过Thread类建立线程对象
4.通过Runnable接口的子类对象作为实际参数传递给Thread类的构造函数
为什么呢?因为自定义的run方法所属的对象是Runnable接口的子类对象,所以要
让线程去指定指定对象的run方法。就必须明确该run方法所属对象
5.调用Thread类的start方法开启线程并调用Runnable接口子类的run方法
start方法有两个作用:
一是启动线程,二是调用run方法
线程有自己默认的名称:
格式:Thread-编号,编号从0开始。
static Thread currentThread():获取当前线程对象
getName():获取线程名称
设置线程名称:setName或者构造函数
实现方式和继承方式有什么区别呢?
实现方式好处是:避免了单继承的局限性,建议使用它
继承方式:线程代码存放在Thread子类run()方法中
实现方式:存在接口的子类的run方法中
线程安全:
我们先来看段代码:
package thread;

public class Ticket implements Runnable//extends Thread
{
 private int tick=100;
 public void run()
 {
  while(true)
  {
   if(tick>0)
   {
    System.out.println(Thread.currentThread().getName()+"....sale:"+tick--);

   }
  }
 }

}

package thread;

public class TicketDemo
{
 public static void main(String[] args)
 {
  /*Ticket t1=new Ticket();
  Ticket t2=new Ticket();
  Ticket t3=new Ticket();
  Ticket t4=new Ticket();

  t1.start();
  t2.start();
  t3.start();
  t4.start();*/
  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();
  
 }

}
分析:
在判断中,假如tick大于1,满足条件,所以Thread-0准备去执行,但是有可能出现一种情况,就是Thread-0
刚判断完正要执行的时候,但是被线程Thread-1给抢去了,而Thread-1正准备要执行,但是被Thread-2抢去了,Thread-2正要准备
去执行的时候,但是被Thread-3给抢去了,这四个全部挂着,但是它们都有执行的资格,假设当Thread-1,Thread-2,Thread-3全部挂着的时候
切换到Thread-0,它就买到1号票了,其他的线程因为不要再判断了,所以他们买到的票tick--就分别是0,-1,-2
张错票,这显然不对,所以这里线程不安全。
造成这个问题的原因:
当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,
另一个线程参与进来执行,导致共享数据的错误
解决办法:
对多条操作共享数据的语句,只能让一个都执行完,在执行过程中,其他线程不能参与进来
Java对于多线程安全问题使用同步代码块来解决:
synchronized(对象)
{
     需要被同步的代码    
}
哪些语句正在操作共享数据,那哪些代码块就需要被同步,如下代码所示:
package thread;

public class Ticket implements Runnable//extends Thread
{
 private int tick=1000;
 Object obj=new Object();
 public void run()
 {
  while(true)
  {
   synchronized(obj)
   {
    if(tick>0)
    {
     try {
      Thread.sleep(10);
     } catch (InterruptedException e) {
      
     }
     System.out.println(Thread.currentThread().getName()+"....sale:"+tick--);
         }
    }
  }
 }

}
同步代码块的原理:
用锁的思想去理解
对象如同锁,持有锁的线程可以在同步中执行,没有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。
同步的前提:
1.必须要有两个或者两个以上的线程
2.必须是多个线程使用同一个锁
必须保证同步中有一个线程在运行就可以了
弊端:多个线程需要判断锁,较为消耗资源
多线程-同步函数:
如何同步?
1.明确哪些代码是多线程运行代码
2.明确共享数据
3.明确多线程运行代码中哪些语句是操作共享数据的
在函数的返回值前面加上synchronized,它就变成同步函数了
同步函数用的是哪一个锁呢?
函数需要被对象调用,函数都有一个所属对象的引用,那就是this
所以同步函数使用的锁是this。
如果同步函数被静态修饰后,使用的锁是什么呢?
通过验证,发现不是this。因为静态方法中也不可以定义this。
静态进内存时,内存中没有本类对象,但是一定有该类对应的字节码文件对象,所以它的锁可以用字节码文件对象
格式:类名.class。
该对象的类型是Class.
单例设计模式:
饿汉式:
class Single
{
    private static final Single s=new Single();
    private Single(){}
    public static Single getInstance()
 {
      return s;
  }
}
懒汉式:
class Single
{
    private static final Single s=null;
    private Single(){}
    public static synchronized Single getInstance()
    {
         if(s==null)
               s=new Single();
         return s;
 }
 
class SingleDemo
{
     public static void main(String[] args)
     {
           System.out.println("Hello World");


class Single2
{
    private static final Single2 s=null;
    private Single2(){}
    public static  Single getInstance()
    {
         if(s==null)
         {
             synchronized(Single2.class)
             {
                 if(s==null)
                 s=new Single();
         return s;
 }
 
class SingleDemo
{
     public static void main(String[] args)
     {
           System.out.println("Hello World");

懒汉式面试经常问到。
第二种方式效率提高了。
单例设计模式涉及的知识点必须要会!。
线程间通信,其实就是多个线程在操作同一个资源
看段代码:
package thread;

public class Res
{
 String name;
 String sex;
}

package thread;

public class Input implements Runnable
{
 private Res r;
 Input(Res r)
 {
  this.r=r;
 }

 @Override
 public void run()
 {
  int x=0;
  while(true)
  {
  if(x==0)
  {
     r.name="mike";
     r.sex="man";
  }
  else
  {
     r.name="丽丽";
     r.sex="女女女女女女女女女";
 }
 
  x=(x+1)%2;
 }
 

}
}

package thread;

public class Output  implements Runnable
{
 private Res r;
 Output(Res r)
 {
  this.r=r;
 }

 
 public void run()
 {
  while(true)
  {
   System.out.println(r.name+"....."+r.sex);
  }
 }
}

package thread;

public class InputOutputDemo
{
 public static void main(String[] args)
 {
  Res r=new Res();
  
  Input in=new Input(r);
  Output out=new Output(r);
  
  Thread t1=new Thread();
  
  Thread t2=new Thread();
  
  t1.start();
  t2.start();

 }

}
上面代码有安全问题,打印出来的结果出现了mike为女,而丽丽为男的情况。这是怎么造成的呢?
这是因为当要输出mike名字时,接着准备输出性别时,而被“丽丽”给抢去了,所以会打印出
丽丽为男。
这该怎么解决呢?
出现安全问题,我们用同步。
注意:凡是出现runnable接口我们一般都得建对象。
如果两个类里的方法有一个同步了而另一个没有同步,那么也是没有满足第一个前提:至少有两个或两个以上的线程
它们是处理同一个资源。所以两边都必须同步。但是两边都同步以后还是报错,这里第一个前提已经满足了,
所以我们再来看第二个前提:必须是多个线程使用同一个锁。目前他们用的还不是同一个锁,那传个this可以吗?答案是no
因为Input有个this,Output类也有个this,是两个不同的对象,那就要思考,这内存里有没有唯一对象,那传个Input.class对象
行不行?答案是:yes,因为内存里有四个唯一的类:Input.class  Output.class  Res.class  InputOutputDemo.class.所以传个
Input.class是靠谱的。但是这太过牵强,我们看下这里哪个对象是唯一的,当然是r了,因为它是唯一对象,在程序里表示唯一的资源,打印的结果是一大片mike.....man或一大片
丽丽.......女女女女女女女女女女女女,这样看起来不爽,我们必须要它这样打印:一行一行的切换,为什么会产生
这样的现象呢?分析:
如果输入的线程即Input的线程获得CPU执行权,它存了一个mike......man,它在存的时候别个是进不来的,存完以后,
这个时候,它出了同步的话,Input和Output都有可能再次抢到CPU执行权,所以Input还有可能抢到执行权,它就把
丽丽......女输入进去了。前面的值就被覆盖掉了,它有了CPU执行权以后,就会一直输入,把前面的值覆盖掉,当输入
到某个时刻,它的执行权被Output抢走了,它会输出多次。所以造成这种现象。这是CPU切换的时候造成的。
我们现在想实现这种需求:交替执行,且是一次一次的交替执行。怎么做呢?方法是加个标记,如加个boolean flag=false;
它的原理是:当Input线程在输入数据的时候,就判断这个标记,看里面是否有值,如果为false,那就表示没值,
那输入线程就往里面存了一个mike....man,输入以后,Input线程还会有执行权,它其实要做一件事情,就是存完数据以后
就把标记改为true,那就表示有数据了,当它再次判断时候,看到有数据,它就不往里面输了,因为原来的还没输出呢,这时候就需要
它等着别动,有人会想到用sleep,那睡多长时间合适呢?这是不确定的,而它什么时候应该醒呢?应该是我把数据都输入进去完了,
然后再输出完了它才能够醒,所以这里最好的办法是用wait方法,它可以等待唤醒,一wait就会冻结,即放弃执行资格,所以Output
线程就会执行,它本来是具有执行资格,但是没执行权,当Input线程一wait,它就有执行权了,它就开始输出
它输出之前也要做一次判断,如果判断为true,即里面有数据,它就输出,输出完以后把标记置为false,这时它还有执行权,当它再次判断的时候,
一看是false,它就不会再输出,这时候它就会等着,这时Input 线程等着,它也等着,程序就挂了,所以必须保证
一个线程是活着的,这怎么办呢?方法是Output在等之前应该把Input线程叫醒,叫它往里边输入数据,这时Output线程
是等着的,当Input线程被叫醒了以后,它就去判断,一看是false,即里面没数据,所以它就往里面存数据,它这时
还有执行权,然后再去判断,一看是true,所以它就又要等着,在等之前就唤醒Output线程。这就是等待唤醒机制,这在程序开发中是
非常常见的。必须掌握。
其实线程都是放在线程池里,我们使用notify唤醒线程的时候,其实唤醒的是线程池里面的
线程,到底是先唤醒哪个线程呢?通常是唤醒第一个被等待的。
在同步里面必须标识出wait它所操作的那个线程所属的锁。这里r.wait即标识出锁r.这里wait表示持有r这个锁的线程。
为什么?因为同步会出现嵌套,即两个锁的时候。我notily的时候是唤醒r这个锁上的等待线程。
为什么wait和notify这些专门操作线程的方法定义在Object当中?
锁是任意对象,任意对象能调用的方法必然应该定义在Object当中。
等待和唤醒必须是同一个锁。
生产者和消费者的问题:
分析以下代码:
package thread2;

public 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 (InterruptedException e) {
    e.printStackTrace();
   }
  this.name = name + "--" + count++;// 这里传名字带编号,如张三+编号
  System.out.println(Thread.currentThread().getName() + "...生产者..."
    + this.name);// currentThread:返回对当前正在执行的线程对象的引用。返回:当前执行的线程。
  flag=true;
  this.notify();

 }

 public synchronized void out() {
  if(!flag)
   try {
    this.wait();
   } catch (InterruptedException e) {
    e.printStackTrace();
   }
  System.out.println(Thread.currentThread().getName() + "...消费者..."
    + this.name);
  flag=false;
  this.notify();
 }

}

package thread2;

public class Producer implements Runnable {
 private Resource res;

 Producer(Resource res) {
  this.res = res;
 }

 @Override
 public void run() {
  while (true) {
   res.set("+商品+");
  }

 }

}


package thread2;

public class Consumer implements Runnable {
 private Resource res;

 Consumer(Resource res) {
  this.res = res;
 }

 @Override
 public void run() {
  while (true) {
   res.out();

  }
 }

}


package thread2;

public class ProducerConsumeDemo {
 public static void main(String[] args) {
  Resource r = new Resource();
  Producer pro = new Producer(r);
  Consumer con = new Consumer(r);

  Thread t1 = new Thread(pro);
  Thread t2 = new Thread(pro);
  Thread t3 = new Thread(con);
  Thread t4 = new Thread(con);

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

}
以上程序会打印出生产两个商品而只消费一个商品,或者生产一个商品而被消费了两次,
为什么会出现生产两个商品而只消费一个商品呢?分析如下:
生产者有:t1,t2
消费者有:t3,t4
假设当t1获得CPU执行权的时候,t1判断标记为假,它不需要等待,它就往里面赋了一个值,然后count++一次,再打印
如果它赋的值是130,那么假设它打印的是Thread-0...生产者...+商品+--130(当然,有可能也会是Thread-1...生产者...+商品+--130),
即表示生产了一次,生产完以后,就把标记置为真了,然后执行notify,notify完以后,它这时是还有执行权的,
然后去再次判断,一看flag为true,它于是就等着,这时t2,t3,t4都有可能抢到CPU执行权,假设t2抢到了,它于是去判断标记,
一看为真,于是它也就等着了,这时只剩下t3和t4了,t3一进来判断标记!flag为假(即非真即为假,这里以前我一直弄不明白,这里解释以下,以前
我总是认为这里判断应该是!flag即为真,其实不是这样的,因为之前t1消费完以后就把flag置为true了,所以t3
去判断!flag即为假,是假的话那么就表示t3消费了商品了),不需要等,即消费了一次,到目前为止,这属于正常消费,
即生产一次,消费一次,消费完了以后,它就把标记置为false了,然后notify一次,这时它就把t1唤醒了,这时t1
有资格,但是不一定有执行权,这时执行权在t3那里,然后它再次判断标记!flag为真,即不能再消费了,这时它就wait
即等着,这时如果t4抢到执行权,它就去判断标记,一看为真,它也就wait了,这时t1是活着的,它这时它是从程序中的
“this.name = name + "--" + count++;”这段语句开始运行的,即它又count++了一次,即它又生产了一次,当t1生产完
一个新的商品以后,然后下来执行flag=true,即把标记flag置为真了,置完真以后然后notify
一次,把t2唤醒了(因为t2进线程池是先进去的,比t3,t4都早,所以唤醒的是它),这时t2获取资格,但是不一定有执行权,
这时t1还是有执行权的,然后它再次去判断标记,一看是true,它就wait了,即等着,放弃资格了,这时活着的
的只有t2,这时要注意了,这时t2不会再去判断标记了,为什么呢?因为它本来是等着的(即它原来已经判断过了,不会再判断了
,因为if语句置判断一次的,如果是while就可以判断多次)应该就往下执行了,即从this.name = name + "--" + count++;
这个语句开始执行,这时它又新生产了一个商品,把前一个商品覆盖掉了(它就把t1那时生产的商品给覆盖掉了,之前t1
生产了一个,由于t3,t4都放弃资格了,所以没有被消费,就被t2新生产的商品覆盖掉了),这就导致生产了两个,即
Thread-1...生产者...+商品+--130
Thread-0...生产者...+商品+--131
即Thread-1...生产者...+商品+--130刚生产完,又被t2又生产了一次,导致在t3消费的时候只消费了
Thread-0...生产者...+商品+--131一次,。
当t2生产完,它就notify,就把t3唤醒了,t3就消费t2生产的那个了,t1那个没消费成功,之所以出现这种情况
是因为t2没去判断标记,那我们怎样做才能要其醒的时候去判断标记呢?方法:
因为if只判断一次,我们把它改成while就可以判断多次了,但是这样做的话,就会出现他们全部等待的状况
这不是死锁,而是叫全部等待,你等待我,我等待你,全都动不了。全都冻结了。即t1把t2给唤醒了,即
把本方给唤醒了,而t3 t4都没有被唤醒。要把对方唤醒才靠谱,所以把notify换成notifyAll就可以了,
把他们全部都唤醒。
我们说过了这样做的话会导致全部等待的状况发生,要解决这个问题的话,就需要在全部唤醒的时候,不要唤醒本方,
只把对方唤醒,这该怎么办呢?我们就要用到jdk5的新特性了,jdk5中出现了Lock,Lock 就把sychronized替代
了,wait,notify,notifyAll方法 分别被condition里面的await,signal,signall给替代了,
Condition把wait,notify等对象封装了,这些condition对象怎么来呢?
答案是通过锁来。为什么呢?因为像wait等方法是定义在同步语句块中,同步语句块中必然有锁,每个wait,notify
都要标识自己所属的锁,现在是Lock,wait,notify变成了Condition,那么这个Condition怎么获取?同样道理,
它需要通过锁获取。
释放资源需要被定义在finally里,即需要把释放锁unlock定义在finally里。
为了解决这个问题,我们需要在Resource类里面定义两个Condition对象,即
private Condition condition_pro = lock.newCondition();
private Condition condition_con = lock.newCondition();
它的执行状况是这样的:
生产者获取锁,一进来,判断标记,如果为真,生产者就必须等待,即t1,t2都等待,然后消费者被唤醒,即执行condition_con.singal();
紧接着消费者获取了锁,一进来,判断标记满足条件的话,消费者许等待,所以它就下来执行condition_pro.signal();
把生产者t1唤醒了(即她只唤醒生产者这里重点强调下),这时它还有执行权,然后去判断标记!flag为真,这时它就等着,
然后t4去判断标记,一看为真,它也就等着,由于这时t1被唤醒了,他就从this.name = name + "--" + count++;// 这里传名字带编号,如张三+编号
开始执行,当执行到condition_con.signal();的时候,即唤醒消费者其中一个(这时它没去唤醒同类对象了,即没
唤醒t2,而是去唤醒消费者),这就搞定了,即生产者里面代码唤醒消费者,消费者代码里面唤醒生产者。
这就是一个锁里面可以绑定好几个Condition对象,而以前的只能绑定一个,而且挨个拿锁去区分,很麻烦,而锁
一嵌套,就死锁,这是新特性的好处。新特性中把wait,notify封装成了Condition对象,它一个锁可以对应多个Condition,以前的是一个锁对应一个waot,notify
停止线程:
stop方法已经过时,那么停止线程就只有一种方法,那就是run方法结束:
开启多线程运行,运行代码通常是循环结构,只要控制住循环,就可以要run方法结束,也就是线程结束。
在Thread类当中有一个interrupt()方法,中断线程的意思,中断线程不是停止线程的意思,当线程处于冻结状态的时候
就可以用这个方法,强制清除其冻结状态。让其恢复到运行状态中来,因为是强制的,所以会发生异常。
守护线程:
setDaemon方法:
将线程标记为守护线程或用户线程。
守护线程也叫后台线程,它随着前台线程的结束而结束。
join方法:
通俗说就是抢夺CPU执行权,
它是临时加入线程用的,当一个线程t被设置为join,那么主线程会把执行权给它,主线程这时就处于冻结状态,
它必须等到t线程结束后,主线程才可以运行。
当A线程执行到了B线程的.join方法时,A就会等待,等B线程都执行完,A才会执行。
yield方法:
暂停当前正在执行的线程对象,并执行其他线程。

-------- android培训、java培训、期待与您交流! ----------

原创粉丝点击