多线程学习笔记

多线程学习

一、多线程是什么?为什么要用多线程?

  介绍多线程之前要介绍线程,介绍线程则离不开进程。

  首先 进程 :是一个正在执行中的程序,每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元;

  线程:就是进程中的一个独立控制单元,线程在控制着进程的执行。一个进程中至少有一个进程。

多线程:一个进程中不只有一个线程。

  为什么要用多线程:

    ①、为了更好的利用cpu的资源,如果只有一个线程,则第二个任务必须等到第一个任务结束后才能进行,如果使用多线程则在主线程执行任务的同时可以执行其他任务,而不需要等待;

    ②、进程之间不能共享数据,线程可以;

    ③、系统创建进程需要为该进程重新分配系统资源,创建线程代价比较小;

    ④、Java语言内置了多线程功能支持,简化了java多线程编程。

  

二、线程的生命周期:

  • 新建 :从新建一个线程对象到程序start() 这个线程之间的状态,都是新建状态;

  • 就绪 :线程对象调用start()方法后,就处于就绪状态,等到JVM里的线程调度器的调度;

  • 运行 :就绪状态下的线程在获取CPU资源后就可以执行run(),此时的线程便处于运行状态,运行状态的线程可变为就绪、阻塞及死亡三种状态。

  • 等待/阻塞/睡眠 :在一个线程执行了sleep(睡眠)、suspend(挂起)等方法后会失去所占有的资源,从而进入阻塞状态,在睡眠结束后可重新进入就绪状态。

  • 终止 :run()方法完成后或发生其他终止条件时就会切换到终止状态。

三、创建线程方式

1、 继承thread类 重写run()方法 调用start开启线程

public class TestThread1  extends Thread{
   @Override
   public void run() {
       for (int i = 0; i < 20; i++) {
           System.out.println("我在看代码"+i);
      }

  }

   public static void main(String[] args) {
       new TestThread1().start();
       for (int i = 0; i < 20; i++) {
           System.out.println("我在学习多线程"+i);
      }
  }
}

image-20211129155455400

2、 实现Runnable接口重写run()方法 调用start开启线程

public class TestThread2 implements Runnable{
   @Override
   public void run() {
       for (int i = 0; i < 20; i++) {
           System.out.println("我在看代码"+i);
      }

  }

   public static void main(String[] args) {
//       TestThread2 testThread2 = new TestThread2();
      new Thread(new TestThread2()).start();
       for (int i = 0; i < 20; i++) {
           System.out.println("我在学习多线程"+i);
      }
  }
}

image-20211129155544165

3、通过Callable和Future创建线程:

    实现步骤:①、创建Callable接口的实现类,并实现call()方法,改方法将作为线程执行体,且具有返回值。

         ②、创建Callable实现类的实例,使用FutrueTask类进行包装Callable对象,FutureTask对象封装了Callable对象的call()方法的返回值

         ③、使用FutureTask对象作为Thread对象启动新线程。

         ④、调用FutureTask对象的get()方法获取子线程执行结束后的返回值。

四、继承Thread类和实现Runnable接口、实现Callable接口的区别。

    继承Thread:线程代码存放在Thread子类run方法中。

        优势:编写简单,可直接用this.getname()获取当前线程,不必使用Thread.currentThread()方法。

        劣势:已经继承了Thread类,无法再继承其他类。

    实现Runnable:线程代码存放在接口的子类的run方法中。

        优势:避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。

        劣势:比较复杂、访问线程必须使用Thread.currentThread()方法、无返回值。

    实现Callable:

        优势:有返回值、避免了单继承的局限性、多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。

        劣势:比较复杂、访问线程必须使用Thread.currentThread()方法

  建议使用实现接口的方式创建多线程。

五、线程状态管理

1、线程睡眠---sleep

 线程睡眠的原因:线程执行的太快,或需要强制执行到下一个线程。

    线程睡眠的方法(两个):sleep(long millis)在指定的毫秒数内让正在执行的线程休眠。

                sleep(long millis,int nanos)在指定的毫秒数加指定的纳秒数内让正在执行的线程休眠。

public class TestSleep {
   public static void main(String[] args) throws InterruptedException {
       tendown();
       System.out.println("结束");
  }
   public static void tendown() throws InterruptedException {
       int num = 10;
       while (true) {
           Thread.sleep(1000);
           System.out.println(num--);
           if (num <= 0) {
               break;
          }

      }
  }
}

image-20211129160310810

2、线程让步---yield:

  该方法和sleep方法类似,也是Thread类提供的一个静态方法,可以让正在执行的线程暂停,但是不会进入阻塞状态,而是直接进入就绪状态。相当于只是将当前线程暂停一下,然后重新进入就绪的线程池中,让线程调度器重新调度一次。也会出现某个线程调用yield方法后暂停,但之后调度器又将其调度出来重新进入到运行状态。

public class TestYield {
   public static void main(String[] args) {
       yieldDemo ms = new yieldDemo();
       Thread t1 = new Thread(ms,"张三吃完还剩");
       Thread t2 = new Thread(ms,"李四吃完还剩");
       Thread t3 = new Thread(ms,"王五吃完还剩");
       t1.start();
       t2.start();
       t3.start();
  }
}
class yieldDemo implements Runnable{
   int count = 20;
   public void run() {
       while (true) {
           if(count>0){
               System.out.println(Thread.currentThread().getName() + count-- + "个瓜");
               if(count % 2 == 0){
                   Thread.yield();
              }
          }
      }
  }
}

image-20211129160601417

 sleep和yield的区别:

    ①、sleep方法声明抛出InterruptedException,调用该方法需要捕获该异常。yield没有声明异常,也无需捕获。

    ②、sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态。

 3、线程合并---join:

    当B线程执行到了A线程的.join()方法时,B线程就会等待,等A线程都执行完毕,B线程才会执行。

    join可以用来临时加入线程执行。

public static void main(String[] args) throws InterruptedException {    
       yieldDemo ms = new yieldDemo();
       Thread t1 = new Thread(ms,"张三吃完还剩");
       Thread t2 = new Thread(ms,"李四吃完还剩");
       Thread t3 = new Thread(ms,"王五吃完还剩");
       t1.start();
       t1.join();
       
       t2.start();
       t3.start();
       System.out.println( "主线程");
  }

4、停止线程---stop:

    原stop方法因有缺陷已经停用了,那么现在改如何停止线程?现在分享一种,就是让run方法结束。

    开启多线程运行,运行的代码通常是循环结构,只要控制住循环,就可以让run方法结束,也就是线程结束。

 5、设置优先级:

    每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。

    Thread类中提供了优先级的三个常量,代码如下:

MAX_PRIORITY   =10

MIN_PRIORITY   =1

NORM_PRIORITY   =5
public static void main(String[] args) {
       System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
       MyPriority myPriority = new MyPriority();
       Thread t1 = new Thread(myPriority);
       Thread t2 = new Thread(myPriority);
       Thread t3 = new Thread(myPriority);
       Thread t4 = new Thread(myPriority);
       Thread t5 = new Thread(myPriority);
       Thread t6 = new Thread(myPriority);
       t1.start();
       //注意是先设置优先级在启动
       t2.setPriority(2);
       t2.start();
       t3.setPriority(10);
       t3.start();
       t4.setPriority(1);
       t4.start();
       t5.setPriority(6);
       t5.start();
       t6.setPriority(7);
       t6.start();
  }
}
class MyPriority implements Runnable{

   @Override
   public void run() {
       System.out.println(Thread.currentThread().getName()+"-->"+Thread.currentThread().getPriority());
  }
}

image-20211129185802996

注意的地方,只是对线程设置优先级,优先级高的不一定先执行

6、守护线程(daemon)

线程分为用户线程和守护线程

虚拟机必须确保用户线程执行完毕

虚拟机不用等待守护线程执行完毕

如,后台记录操作日志,监控内存,垃圾回收等待。

7、线程同步与锁

并发:同一个对象被多个线程同时操作

列子:上万人同时抢100张票、两个银行同时取钱。

  public static void main(String[] args) {
       //定义三个线程,
       MySyn ms = new MySyn();
       Thread t1 = new Thread(ms, "线程1输出:");
       Thread t2 = new Thread(ms, "线程2输出:");
       Thread t3 = new Thread(ms, "线程3输出:");
       t1.start();
       t2.start();
       t3.start();
  }
}

class MySyn implements Runnable {

   int tick = 10; //共执行10次线程

   public void run() {
       while (true) {
           if (tick > 0) {
               try {
                   Thread.sleep(10);//执行中让线程睡眠10毫秒,
              } catch (InterruptedException e) {
                   e.printStackTrace();
              }
               System.out.println(Thread.currentThread().getName() + " " + tick--);
          }
      }
  }
}

image-20211129193540371

同步方法1:

    同步函数:就是用synchronize关键字修饰的方法。因为每个java对象都有一个内置锁,当用synchronize关键字修饰方法时内置锁会保护整个方法,而在调用该方法之前,要先获得内置锁,否则就会处于阻塞状态,默认锁的是this。

    代码演示:请将上方代码的第17行改为以下代码↓

    public synchronized void run() {

  同步方法2:

    同步代码块:就是拥有synchronize关键字修饰的语句块,被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。

    代码演示:将上方代码的run方法改成下方代码

image-20211129194613258

  追加问题:如果同步函数被静态修饰之后,使用的锁是什么?静态方法中不能定义this!

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

  所以静态的同步方法使用的锁是该方法所在类的字节码文件对象。 类名.class。代码如下:

 

public static mySyn(String name){
   synchronized (Xxx.class) {
       Xxx.name = name;
  }
}

总结:

 同步的前提:

  1、必须要有两个或者两个以上的线程。

  2、必须是多个线程使用同一个锁。

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

  4、只能同步方法,不能同步变量和类。

  5、不必同步类中所有方法,类可以拥有同步和非同步的方法。

  6、如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。

  7、线程睡眠时,它所持的任何锁都不会释放。

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

  弊端:多个线程需要判断,消耗资源,降低效率。

  如何找问题?

  1、明确哪些代码是多线程运行代码。

  2、明确共享数据。

  3、明确多线程运行代码中哪些语句是操作共享数据的。

8、死锁

  进程A中包含资源A,进程B中包含资源B,A的下一步需要资源B,B的下一步需要资源A,所以它们就互相等待对方占有的资源释放,所以也就产生了一个循环等待死锁。

  死锁形成的必要条件总结(都满足之后就会产生):

    ①、互斥条件:资源不能被共享,只能被同一个进程使用;

    ②、请求与保持条件:已经得到资源的进程可以申请新的资源;

    ③、非剥夺条件:已经分配的资源不能从相应的进程中强制剥夺;

    ④、循环等待条件:系统中若干进程形成环路,该环路中每个进程都在等待相邻进程占用的资源。

9、lock锁

ReentrantLock类实现了Lock,它拥有与synchronized相同的并发现和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock,可以显式加锁、释放锁。

class A{
   private final ReentrantLock lock = new ReentrantLock();
   public void m(){
       lock.lock();//加锁
       try {
           //保证线程安全的代码
      }
       finally {
           lock.lock();//解锁
           //如果同步代码有异常,要将unlock()写入finally语句块
      }
  }
}

重点:synchronized与lock的对比

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭锁) synchronized是隐式锁,出来作用域会自动释放

  • Lock只有代码块锁,synchronized有代码块锁和方法锁

  • 使用Lock锁,JVM将花费较少的时间来调度线程。性能更好。并且具有更好的扩展性(提供更多子类)

  • 优先使用顺序:

  • Lock->同步代码块(已经进入了方法体,分配了相应的资源)->同步方法(在方法体之外)

了解生产者消费者问题:处理方法:管程法、信号灯法

10、线程池

 

posted @ 2021-11-29 20:45  苏unique  阅读(140)  评论(0)    收藏  举报