Java中的多线程机制

首先弄清楚什么是进程?什么是线程?
进程正在运行的一个程序。如打开任务管理器时,会看到正在运行的QQ,360等应用,每一个正在运行的应用程序就是一个进程。

线程:线程在进程里面,也可以说进程可以进一步细化为线程,是一个程序内部的一条执行路径

若同一个进程同一时间并行执行多个线程,就是支持多线程的。

举个例子,打开360安全卫士后,可以选择一边杀毒的同时还可以扫描垃圾文件,其实这两个过程就是两个线程。

线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器

单核CPU:其实是一种假的多线程,因为在一个时间单位内,也只能执行一个线程的任务。

多核CPU:能够更好的发挥多线程效率。

一个Java应用程序java.exe其实至少有三个线程:main()主线程,gc()垃圾回收机制,异常处理线程。

并行:多个CPU同时执行多个任务。

并发:一个CPU同时执行多个任务。

何时需要多线程?

  1. 当一个程序需要同时执行两个或多个任务;
  2. 程序需要实现一些需要等待的任务时,如用户输入、文件读写等;
  3. 需要一些后台运行的程序时。

线程的创建和使用

 创建多线程方式一:继承Thread类

多线程的创建,方式一:继承Thread类

步骤:

  1. 创建一个继承于Thread类的子类;
  2. 重写Thread类的run()方法;
  3. 创建该子类的对象;
  4. 通过此对象调用start()方法

 

例1:遍历10以内的所有偶数

class Mthread extends Thread
{
  //重写Thread类的run()方法
  public void run()
  {
    for (int i = 0; i <10 ; i++)
    {
      if (i % 2 == 0) {
        System.out.println(i);
      }
    }
  }
}
public class MyThread {
  public static void main(String[] args) {
    //创建该子类的对象
    Mthread m=new Mthread(); //此时只是创建线程,但并未启动线程
    //通过此对象调用start()方法
    //此时有两个作用:1)启动当前线程;2)调用当前线程的run()
    m.start();
    //如下操作是在main线程中执行的
    for (int i = 0; i <10; i++) {
      System.out.println("主线程"+i);
    }
  }
}

注意:如果线程对象直接调用run(),即m.run(),此时只是普通的对象调用方法,并未启动线程。

例2:使用匿名内部类创建两个线程。

public class MyThread {
  public static void main(String[] args) {
    //使用匿名内部类来定义子线程
    new Thread()
    {
      //重写run()
      public void run() {
        for(int i = 0; i <10 ; i++)
        {
          if (i % 2 == 0)
          {
            System.out.println(Thread.currentThread().getName()+"..."+i);
          }
        }
      }
    }.start();
    new Thread(){
      @Override
      public void run() {
        for (int i = 0; i < 10; i++)
        {
          if (i % 2 != 0)
          {
            System.out.println(Thread.currentThread().getName()+"..."+i);
          }
        }
      }
    }.start();

  }
}

Thread类的方法

  1. start():启动当前线程,调用当前线程的run();
  2. run():通常需要重写Thread类中的此方法,将创建线程要执行的操作声明在此方法中;
  3. currentThread():静态方法,返回执行当前代码的线程;
  4. getName():获取当前线程名字;
  5. setName():设置当前线程名;
  6. yield():释放当前CPU的执行权;
  7. join():在线程A中调用线程B的join(),此时线程A就进入阻塞状态,直到线程B完全执行完以后,线程A才结束阻塞状态;
  8. stop():已过时。强制结束当前线程;
  9. sleep(long millitime):单位为毫秒,让当前线程“睡眠”指定的millitime毫秒。在此时间之内,当前线程是阻塞状态;
  10. isAlive():判断当前线程是否存活,返回ture或false

线程的调度

  1.  同优先级线程组成先进先出队列;
  2. 对高优先级,会抢占CPU的执行权;

线程的优先级

  1.  线程的优先等级分为3个:MAX_PRIORITY:10 ,MIN_PRIORITY:1,NORM_PRIORITY:5;
  2. 如何获取和设置当前线程的优先级?

         getPriority():获取线程的优先级

         setPriority():设置线程的优先级

注意:高优先级的线程会抢占CPU的执行权,但这只是从概率上讲,并不意味着只有高优先级的线程执行完后才执行低优先级的线程。

 

 创建多线程方式二:实现Runnable接口

  1. 定义类实现Runnable接口;
  2. 覆盖Runnable接口中的run方法,将线程要运行的代码存放在该run方法中;
  3. 通过Thread类建立线程对象;
  4. 将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数;
  5. 调用Thread类的start方法开启线程并调用Runnable接口子类的run方法

为什么要将Runnable接口的子类对象传递给Thread的构造函数?
因为,自定义的run方法所属的对象是Runnable接口的子类对象。
所以要让线程去指定对象的run方法。就必须明确该run方法所属对象.

实现方式和继承方式创建线程有什么区别呢?

实现方式好处:避免了单继承的局限性。
在定义线程时,建议使用实现方式。

两种方式区别:
继承Thread:线程代码存放Thread子类run方法中。
实现Runnable,线程代码存在接口的子类的run方法。

例3:简单的卖票程序,三个窗口同时卖票,总票数为100张。

class Window implements Runnable
{
  private int ticket=100;
  public void run()
  {
    while(true)
    {
      if(ticket>0)
      {
        System.out.println(Thread.currentThread().getName() + "卖票:" + ticket);
        ticket--;
      }
      else
      {
        break;
      }
    }
  }
}
public class MyThread {
  public static void main(String[] args) {
    Window w=new Window(); //此时只new()了一个对象,所以一共卖100张票
    Thread t1=new Thread(w); //创建了一个线程
    Thread t2=new Thread(w); //创建了一个线程
    Thread t3=new Thread(w); //创建了一个线程
    t1.start();
    t2.start();
    t3.start();
  }
}

    

线程的生命周期:

 

 新建:使用new关键字创建Thread类或及其子类对象后,该线程就处于新建状态。此时,通过对象调用start()方法后,线程进入就绪状态。

就绪:此时线程已经具备了运行条件,但是还没有分配到CPU的执行权,处于线程就绪队列,等待系统为其分配CPU。一旦获得了CPU的执行权,那么线程就进入运行状态,并自动调用自己的run()方法。

运行:此时线程执行自己的run()方法,直到调用其他方法而终止,或等待某资源而阻塞或完成任务而死亡。

阻塞:处于运行状态的线程在某些情况下,如执行了sleep()方法后,此时将让出cpu的执行权并停止自己的运行。只有当引起阻塞的原因消除时,如睡眠时间已到,此时线程便转入就绪状态,再次等待cpu的执行权。其实阻塞状态时线程具备运行资格,但没有cpu执行权。

死亡:死亡状态是线程生命周期的最后一个阶段。线程死亡有两个原因:一是正常运行的线程完成了全部工作;二是一个线程被强制性终止,如通过stop()。

 

线程的同步

发现例3中的卖票问题,会出现重票错票的问题,此时多线程的运行出现了安全问题。

出现安全问题的原因:
    当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,
    另一个线程参与进来执行。导致共享数据的错误。

解决办法:
    对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。

Java对于多线程的安全问题提供了专业的解决方式。

就是同步代码块

同步代码块

格式:

synchronized(同步监视器)
{
    需要被同步的代码(操作共享数据的代码)

}

同步监视器:俗称,。任何一个类的对象都可以充当锁。

持有锁的线程可以在同步中执行。
没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁

同步的前提:
1,必须要有两个或者两个以上的线程。
2,多个线程必须使用同一个锁

而且必须保证同步中只能有一个线程在运行。

好处:解决了多线程的安全问题。

弊端:多个线程需要判断锁,较为消耗资源。

那么修改以后的卖票程序为:

class Window implements Runnable
{
  private int ticket=100;
  Object obj=new Object();
  public void run()
  {
    while(true)
    {
        //同步代码块
      synchronized(obj) {
        if (ticket > 0) {
            //就算此时sleep()其他的线程也进不来,因为没有获得锁
          try
          {
            Thread.sleep(10);
          }
          catch(Exception e)
          {
            e.printStackTrace();
          }
          System.out.println(Thread.currentThread().getName() + "卖票:" + ticket);
          ticket--;
        } else {
          break;
        }
      }
    }
  }
}
public class MyThread {
  public static void main(String[] args) {
    Window w=new Window(); //此时只new()了一个对象,所以一共卖100张票
    Thread t1=new Thread(w); //创建了一个线程
    Thread t2=new Thread(w); //创建了一个线程
    Thread t3=new Thread(w); //创建了一个线程
    t1.setName("窗口1");
    t2.setName("窗口2");
    t3.setName("窗口3");
    t1.start();
    t2.start();
    t3.start();
  }
}

 

我们发现使用同步代码块以后的不安全问题解决了。

同步方法

  1. 同步函数使用的锁是this;
  2. 对于static的同步函数,使用的锁不是this。是 类名.class 是该类的字节码文件对象

例:使用同步方法解决实现Runnable接口的线程安全问题

class Window implements Runnable
{
  private int ticket=100;
  Object obj=new Object();
  public void run()
  {
    while(true)
    {
      show();
    }
  }
  private synchronized void show()  //此时同步函数使用的锁是this
  {
    if (ticket > 0)
    {
      try
      {
        Thread.sleep(10);
      }
      catch(Exception e)
      {
        e.printStackTrace();
      }
      System.out.println(Thread.currentThread().getName() + "卖票:" + ticket);
      ticket--;
    }
  }
}
public class MyThread {
  public static void main(String[] args) {
    Window w=new Window(); //此时只new()了一个对象,所以一共卖100张票
    Thread t1=new Thread(w); //创建了一个线程
    Thread t2=new Thread(w); //创建了一个线程
    Thread t3=new Thread(w); //创建了一个线程
    t1.setName("窗口1");
    t2.setName("窗口2");
    t3.setName("窗口3");
    t1.start();
    t2.start();
    t3.start();
  }
}

 

如果同步函数被静态修饰后,使用的锁是什么呢?

通过验证,发现不在是this。因为静态方法中也不可以定义this。

静态进内存是,内存中没有本类对象,但是一定有该类对应的字节码文件对象。
类名.class  该对象的类型是Class

其实:静态的同步方法,使用的锁是该方法所在类的字节码文件对象。 类名.class

 

使用同步方法解决继承Thread类的线程安全问题

class Window extends Thread
{
  private static int ticket=100;
  public void run()
  {
    while(true)
    {
      show();
    }
  }
  private static synchronized void show()  //注意此时对于静态同步方法中使用的锁是 类名.class
  {
    if (ticket > 0)
    {
      try
      {
        Thread.sleep(10);
      }
      catch(Exception e)
      {
        e.printStackTrace();
      }
      System.out.println(Thread.currentThread().getName() + "卖票:" + ticket);
      ticket--;
    }
  }
}
public class MyThread {
  public static void main(String[] args) {
    Window w1=new Window();
    Window w2=new Window();
    Window w3=new Window();

    w1.setName("窗口1");
    w2.setName("窗口2");
    w3.setName("窗口3");
    w1.start();
    w2.start();
    w3.start();
  }
}

例:解决单例设计模式中的懒汉式的线程安全问题。

懒汉式特点:延迟加载。
不足:当多线程访问时会出现安全问题。

如何解决?:可以采用加同步的方式解决。
加同步的方式有同步函数和同步代码块,都低效。但是用双重判断的方式可以解决效率问题。
加同步是锁是:该类所在的字节码文件

 先说一下饿汉式为:

class Single
{
  private static Single s=new Single();
  private Single(){}
  private static Single getInstance()
  {
    return s;
  }
}

采用加同步的方式解决懒汉式:

class Single {
  private static Single s = null;
  private Single() {
  }

  public static Single getInstance() {

      synchronized (Single.class) //锁是该类的字节码文件
     {
        if (s == null)
          s = new Single(); //对象的延迟加载
      }
    return s;
  }
}

上述的方法虽然使用了同步方式,但是效率仍然会很低。

现在使用双重判断的方式可以解决效率问题。

class Single {
  private static Single s = null;
  private Single() {
  }

  public static Single getInstance() {
    if (s == null)
    {
      synchronized (Single.class) {
        if (s == null)
          s = new Single();
      }
    }
    return s;
  }
}

这样就可以解决效率低的问题了。原因在于,第一个线程创建了对象之后,之后的每一个线程再进来时不会再判断锁了,直接拿已经建立好的对象来使用,提高了效率。

死锁问题

  1. 什么是死锁? 同步中嵌套同步;
  2. 出现死锁后,不会出现异常,也不会出现提示,只是所有的线程都处于阻塞状态,无法继续,程序摆在那里了;
  3. 我们使用同步时应避免出现死锁。

 死锁的例子:

class Test implements Runnable
{
  private boolean flag;
  Test(boolean flag)
  {
    this.flag = flag;
  }

  public void run()
  {
    if(flag)
    {
      while(true)
      {  //锁locka中有lockb锁
        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)
      { //锁lockb中有locka锁
        synchronized(MyLock.lockb)
        {
          System.out.println(Thread.currentThread().getName()+"..else lockb");
          synchronized(MyLock.locka)
          {//对于static的同步函数,使用的锁不是this。是 类名.class 是该类的字节码文件对象
						System.out.println(Thread.currentThread().getName()+".....else locka");
          }
        }
      }
    }
  }
}


class MyLock
{
  static Object locka = new Object();
  static Object lockb = new Object();
}

class  DeadLockTest
{
  public static void main(String[] args)
  {
    Thread t1 = new Thread(new Test(true));
    Thread t2 = new Thread(new Test(false));
    t1.start();
    t2.start();
  }
}

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值