Java高级之---多线程

1.谈谈你对程序、进程、线程的理解

程序:为完成特定的任务、用某种语言编写的一组指令的集合。即指一段静态的代码
进程:是程序的一次执行过程、正在运行的一个程序。作为资源分配的单位
线程:是一个程序内部的一条执行路径,作为调度和执行的最小单位,每个线程都拥有独立的运行栈,和程序计数器 (pc)

2.线程

线程是jvm调度的最小单元,也叫做轻量级进程,进程是由线程组成,线程拥有私有的程序技术器以及栈,并且能够访问堆中的共享资源。

3.多线程的创建

3.0线程的声明周期

新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。

阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。

同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态

死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。在这里插入图片描述

3.1、继承于Thread类
  • 1.创建一个继承于Thread类的子类
  • 2.重写Thread的run()—>将此线程执行的操作声明在run()中
  • 3.创建Thread类的子类的对象
  • 4.通过对象调用start():①启动当前线程② 调用当前线程的run()
//1.创建一个继承于Thread类的子类
class MyThread extends Thread {
   //2.重写Thread的run()

   @Override
   public void run() {
       for (int i = 0; i < 100; i++) {
           if (i % 2 == 0) {
               System.out.println(Thread.currentThread().getName() + ":" + i);
           }
       }
   }
}


public class ThreadTest {
   public static void main(String[] args) {
//        3.创建Thread类的子类的对象
       MyThread t1 = new MyThread();
       //4.通过对象调用start()
       t1.start();
//        t1.run();错误的

       //如下方法任在主线程中进行的
       for (int i = 0; i < 100; i++) {
           if (i % 2 == 0) {
           //获取当前的线程名
               System.out.println(Thread.currentThread().getName() + ":" +i + "*************main()**********");
           }
       }
   }
}

3.2、方式二:实现Runnable接口
  • 1.创建一个实现了Runnable的类
  • 2.实现类去实现Runnnable的抽象方法,run()
  • 3.创建实现类的对象
  • 4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
  • 5.通过Thread类的对象调用start()
//1.创建一个实现了Runnable的类
class MThread implements Runnable {

   @Override
   public void run() {
       for (int i = 0; i < 100; i++) {
           if (i % 2 == 0) {
               System.out.println(Thread.currentThread().getName() + ":" + i);
           }
       }
   }
}

public class ThreadTest1 {
   //     3.创建实现类的对象
   public static void main(String[] args) {
       MThread mThread = new MThread();
       //4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
       Thread t1 = new Thread(mThread);
       t1.setName("线程1");
//        5.通过Thread类的对象调用start()①启动线程②调用当前线程的run();-->调用了Runnable类型的target
       t1.start();
       //在开启一个线程
       Thread t2 = new Thread(mThread);
       t2.setName("线程2");
       t2.start();
   }
}


3.3 说明两个问题:

问题一:我们启动一个线程,必须调用start()方法,不能调用run()方法的方式启动线程
问题二:如果再启动一个线程,必须重新创建一个Thread子类的对象,调用此对象的start()方法

3.4、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):让当前线程睡眠(阻塞),指定毫秒数,在毫秒数时间内当前的线程是阻塞状态

  • 1.isAlive():判断当前线程是否存活

  • 线程的优先级

  • MAX_PRIORITY:10

  • MIN_PRIORITY:1

  • NORM_PRIORITY:5 默认的优先级

  • 2.如何获取和设置线程的优先级:

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

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

    说明:高优先级的线程要抢占低优先级的CPU的执行权,但是只是概率上来讲,高优先级的线程高概率下被执行,并不一定高优先级的线程执行完后,低优先级才执行
    
3.5、 例子:创建三个窗口买票,总票数为100张,使用Runable接口的方式

class Window extends Thread implements  Runnable {
   private static int ticket = 100;
   Object obj = new Object();
   @Override
   public void run() {
       while (true) {
       //解决线程安全问题
           synchronized (obj){
               if (ticket > 0) {

                   try {
                       Thread.sleep(100);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }

                   System.out.println(Thread.currentThread().getName() + ":买票,票号为:" + ticket);
                   ticket--;
               } else {
                   break;
               }
           }
       }
   }

}

public class WindowTest {

   public static void main(String[] args) {
       Window w = new Window();

       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。买票过程 中,出现了重票、错票

  • 2.问题出现的原因:当某个线程操作车票时,尚未操作完成,其他线程参与进来,也操作买票

  • 3.如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来,直到线程a操作完ticket的时候,其他线程才

  • 可以开始操作ticket,即使线程a出现了阻塞也不可以被改变

  • 4.在java中通过同步机制,解决线程安全问题

  • 方式一:同步代码块 synchronized(同步监视器){

  • //需要被同步的代码
    
  • }
    说明:1.操作共享数据的代码,即为需要被同步代码

    2.共享数据:多个线程共同操作的变量

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

    要求:多个必须公用同一把锁* 补充:使用Runable方式创建多线程的方式中,可以考虑使用this充当同步监视器

    在使用Thread类的方式创建线程的方式中,要慎用this充当同步监视器,可以考虑使用当前类充当同步监视器

方法二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将这个方法声明同步的。

5.使用同步的方式,解决了线程的安全问题 --好处

操作同步代码时,只有一个线程参与,其他线程等待,相当于一个单线程的过程,效率低

解决线程安全问题三:lock锁----JDK5.0新增

3.6、使用同步机制将单例模式中的懒汉式改写成线程安全的

1.面试题:synchronized 和 lock有何异同
*

  • 相同点:解决线程的安全问题
  • 不同:synchronized机制在执行完相应的同步代码后,自动释放同步监视器
  •  lock需要手动开启同步(lock()),同时结束同步也需要手动的实现(unlock());
    

*2.优先使用顺序:灵活度递减

  • lock锁—>同步代码块(已经进入了方法体,分配了相应资源)---->同步方法(在方法体之外)
  • lock是否还存在同步监视器,不存在,lock相当于同步监视器
public class BankTest {
  private BankTest() {

  }

  private static BankTest instance = null;

  public static BankTest getInstance() {
      //方式一:效率稍差
//        synchronized (BankTest.class) {
//
//            if (instance == null) {
//                instance = new BankTest();
//            }
//            return instance;
//        }
      //方式二:效率更高
      if (instance == null){
          synchronized (BankTest.class){
              if (instance ==null){
                  instance = new BankTest();
              }
          }
      }
      return instance;
  }
}
3.7、演示线程死锁问题
  • 1.死锁的理解:不同的线程分别占用对方需要的不同资源不放弃,

  • 都在等待对方放弃自己需要的同步资源,就形成了线程的死锁

  • 2.说明:

  • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续

public class ThreadTest {
   public static void main(String[] args) {

       StringBuffer s1 = new StringBuffer();
       StringBuffer s2 = new StringBuffer();


       new Thread() {
           @Override
           public void run() {
               synchronized (s1) {
                   s1.append("a");
                   s2.append("1");
                   try {
                       Thread.sleep(100);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }

                   synchronized (s2) {
                       s1.append("b");
                       s2.append("2");

                       System.out.println(s1);
                       System.out.println(s2);

                   }
               }

           }
       }.start();

       new Thread(new Runnable() {
           @Override
           public void run() {
               synchronized (s2) {
                   s1.append("c");
                   s2.append("3");
                   try {
                       Thread.sleep(100);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }

                   synchronized (s1) {
                       s1.append("d");
                       s2.append("4");

                       System.out.println(s1);
                       System.out.println(s2);

                   }
               }
           }
       }).start();
   }
}

4、线程通信

线程的通信例子:使用两个线程打印1-100,线程1和线程2,互打印

设计到的三个方法:只能出现在同步代码块和同步方法中

  • wait():一旦执行此方法,当前线程就会进入阻塞状态,并释放同步监视器
  • notify():一旦执行此方法,就会唤醒被wait阻塞的一个线程,如果有多个线程被wait,就唤醒优先级高的那个
  • notifyAll():一旦执行此方法,就会唤醒beiwait阻塞的所有线程。
说明:

说明:1.这三个方法都必须使用在同步代码块和同步方法中
2.这三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
否则,会出现异常报错
3.这三个方法都定义在java.lang.Object中

  • 同步代码块涉及到同步监视器和共享数据,
  • synchronized(同步监视器){
  • 操作数据的代码,保证在操作数据的过程中,是个单线程,就不会产生线程安全问题
  • }
  • 多个线程共同操纵的数据:共享数据,一个线程操作时如果另一个线程参与就会产生线程安全问题
面试题:sleep和wait的异同
  • 相同点:一旦执行方法都可以使当前线程进入阻塞状态
  • 不同点:1.两个方法声明的位置就不同 :sleep()声明在Thread类中,wait()声明在Object类中
  • 2.调用的要求不同:sleep()可以在任何需要的场景下调用。wait()只能用在同步代码块和同步方法中
  • 3.关于是否释放同步监视器的问题:如果两个方法中使用在同步代码块和同步方法中,sleep不会释放锁,wait()会
class Number implements  Runnable{
   private int number =1;

   @Override
   public void run() {
       while (true){
           synchronized (this) {
               //让线程进入就绪状态
               notify();

               if (number <= 100){
                   try {
                       Thread.sleep(10);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   System.out.println(Thread.currentThread().getName() +":" + number);
                   number++;

                   try {
                       //使得调用如下wait()方法的线程,进入阻塞状态
                       wait();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }

               }else {
                   break;
               }
           }
       }
   }
}
public class CommunicationTest {
   public static void main(String[] args) {
       Number m1 = new Number();
       Thread t1 = new Thread(m1);
       Thread t2 = new Thread(m1);

       t1.setName("线程1");
       t2.setName("线程2");

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

   }
}

5. 创建线程的方式三:实现Callable接口 ----JDK5.0新增

  • 1.创建一个实现Callable接口的实现类
  • 2.实现call()方法,将此线程需要执行的操作声明在call()中
  • 3.创建一个Callable接口实现类的对象
  • 4.将此callable实现类的对象传递到FutureTask构造器中,创建FutureTas的对象
  • 5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread类的对象,调用start()方法
  • 6.获取Callable中call的方法作为返回值
  • 如果理解实现Callable接口的方式创建多线程比Runnable接口的方式创建多线程强大?
  • 1.call()可以有返回值
  • 2.call()可以抛出异常,被外面的操作捕获,获取异常的信息
  • 3Callable是支持泛型的
//1.创建一个实现Callable接口的实现类
class  NumThread implements Callable {
//2.实现call()方法,将此线程需要执行的操作声明在call()中
   @Override
   public Object call() throws Exception {
       int sum = 0;
       for (int i = 1; i <= 100; i++) {
           if (i % 2 == 0) {
               sum += i;
               System.out.println(i);
           }
       }
       return sum;
   }

}

public class ThreadNew {
   public static void main(String[] args) {
//        3.创建一个Callable接口实现类的对象
       NumThread numThread = new NumThread();
//        4.将此callable实现类的对象传递到FutureTask构造器中,创建FutureTas的对象
       FutureTask futureTask = new FutureTask(numThread);
//        5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread类的对象,调用start()方法
       new Thread(futureTask).start();

       try {
//            6.获取Callable中call的方法作为返回值
           //get()的返回值,即为FutureTask构造器参数Callable实现重写的call的返回值
           Object sum = futureTask.get();
           System.out.println("总和为:"+sum);
       } catch (InterruptedException e) {
           e.printStackTrace();
       } catch (ExecutionException e) {
           e.printStackTrace();
       }

   }
}

6. 创建线程的方式四:使用线程池

好处:

  • 1.提高响应速度(减少了创建新线程的时间)
  • 2.减低资源消耗(重复利用线程池中的线程,不需要每次都创建)
  • 3.便于线程管理
  • corePoolsize:核心池的大小
  • maximumPoolSize:最大线程数
  • keepAliveTime:线程没有大小时最多保持多长时间后终止
class NumberThread implements Runnable {

   @Override
   public void run() {
       for (int i = 0; i < 100; i++) {
           if (i % 2 == 0) {
               System.out.println(Thread.currentThread().getName() + ":" + i);
           }
       }
   }
}

public class ThreadPool {
   public static void main(String[] args) {
       //提供指定线程数量的线程池
       ExecutorService service = Executors.newFixedThreadPool(10);

       //设置线程池的属性
       System.out.println(service.getClass());
       //2.执行指定的线程操作/需要提供Runnable接口或Callable接口实现类的对象
       //        service.submit(Callable callable)//提交,适用于Callable接口
       service.execute(new NumberThread());//适用于Runnable接口
       //3.关闭线程池
       service.shutdown();
   }

}

发布了19 篇原创文章 · 获赞 0 · 访问量 492

猜你喜欢

转载自blog.csdn.net/weixin_43244120/article/details/105010437