09-JavaSE【线程安全,线程状态,线程通讯,线程池】

掌握内容:

  • 造成线程不安全的问题原因
  • 解决线程安全问题的方式:
  • 同步代码块
  • 同步方法
  • Lock方法
  • 掌握线程的通讯:wait()、notify()
  • 线程池: Runnable、Callable

一、线程安全

1、线程安全问题:

  • 条件:两个或多个线程

  • 多个线程在对共享数据进行读改写的时候,可能导致的数据错乱就是线程的安全问题了。

  • 此时:线程不安全

    当多个线程只对共享数据进行读操作时,不会出现线程不安全的情况

2、解决线程安全

核心思想:把操作共享数据的代码,放到一个同步锁中执行,要让某个线程操作数据时,不要让其他线程使用。

1)同步代码块

因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证该变量的唯一性和准确性。

"同步锁",把操作共享数据的代码包含起来,让其他线程排斥在外等待。

同步代码块中的锁是什么有什么要求:

  1. 是一个对象 可以是任意类型任意对象

  2. 多个线程需要使用同一个锁对象

  • 格式:

    • synchronized(对象锁){
              
              
          //操作共享数据的代码
      }
      
  • 示例:

    • public class Ticket implements Runnable{
              
              
          //共享数据
          int number=100;
          //对象锁
          Object lock=new Object();
         
          public void run(){
              
              
              //同步代码块
             synchronized(lock){
              
               
              if(number>0){
              
              
                  System.out.println(线程名称+"正在卖出第"+number"号票");
                  number--;
              }
             }
          }
      }
      
2)同步方法

使用synchronized修饰的方法,就叫做同步方法,保证线程执行该方法的时候,其他线程只能在方法外等着(多个线程并发访问时只能互斥访问)

  • 格式:

    • public synchronized 方法名(参数列表){
              
              
          //操作共享数据的代码
      }
      
    • 同步方法上使用的同步锁是隐式的,分为两种:

      • this锁 : 非静态方法
      • 类.class锁 : 静态方法
  • 示例:

    • public class Ticket implements Runnable{
              
              
          //共享数据
          int number=100;
         
          public void run(){
              
              
              //调用同步方法
          }
          //同步方法  
          public synchronized void method(){
              
                   
              if(number>0){
              
              
                  System.out.println(线程名称+"正在卖出第"+number"号票");
                  number--;
              }
          }
      }
      
      void main(String[] args){
              
              
          //共享资源
          Ticket tick = new Ticket();///线程任务
          
          new Thread( tick , "窗口1" ).start();
           new Thread( tick , "窗口2" ).start();
           new Thread( tick , "窗口3" ).start();
      }
      
3)Lock锁

1、java.util.concurrent.locks.Lock机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有。

2、Lock是接口,其中的两个方法我们需要用到:

public void lock():加同步锁。

public void unlock():释放同步锁。

3、我们无需实现该接口,可以借助其子类ReentrantLock来实例化。

使用步骤:

1)创建Lock锁对象

2)调用lock()方法获取锁

3)调用unLock()释放锁

**注意:**多个线程使用相同的Lock锁对象,需要互斥访问的代码放在lock()和unLock()方法之间。一定要确保unlock最后能够调用。

  • 使用方式:

    • //多态的形式: 父引用  = 子类对象
      Lock lock = new ReenTrantLock();
      
  • Lock中的常用方法:

    • lock() 获取锁
    • unlock() 释放锁
  • 示例:

    • public class Ticket implements Runnable{
              
              
          //共享数据
          int number=100;
          //Lock锁
          Lock lock = new ReenTrantLock();
         
          public void run(){
              
              
              lock.lock(); //获取锁
              if(number>0){
              
              
                  System.out.println(线程名称+"正在卖出第"+number"号票");
                  number--;
              }
              lock.unlock();//释放锁
          }
      }
      

二、线程死锁

死锁是一种少见的,而且难于调试的错误。

当两个线程对两个同步锁对象具有循环依赖时,就会大概率的出现死锁。

  1. 死锁的现象
    各进程、线程相互等待对方手里的资源,导致阻塞状态,程序无法向下运行。
  2. 死锁的产生的原因
    互斥条件
    对必须互斥使用的资源的抢夺才会导致死锁
    不剥夺条件
    进程保持的资源只能主动释放,不可强行剥夺
    请求和保持条件
    保持着某些资源不放的同时,请求别的资源
    循环等待条件
    存在一种进程资源的循环等待,循环等待未必死锁,当不在循环内的进程释放了一些资源后,即可能解除循环等待现象。死锁一定有循环等待
  • 在并发编程中,当使用嵌套的同步代码块时,会发生:死锁现象

    • 示例:

      //A锁
      Object objA = new Object();
      //B锁
      Object objB = new Object()
          
      线程一中
      synchronized (objA) {
              
              
      	System.out.println("嵌套1 objA");
      	synchronized (objB) {
              
              
      		System.out.println("嵌套1 objB");
      	}
      }
      
      线程二中
      synchronized (objB) {
              
              
      	System.out.println("嵌套2 objB");
      	synchronized (objA) {
              
              
      		System.out.println("嵌套2 objA");
      	} 
      }
      
  • 避免死锁:

    • 书写并发代码时,不使用嵌套的同步代码块
    • 如果使用嵌套的同步代码块,不要在嵌套代码块中交替使用对象锁

三、线程的状态

在这里插入图片描述

  • 在Thread类中,有一个枚举:State。 枚举中有6种线程状态
    • 创建

      • 当创建Thread对象时,进行new状态
    • 可运行

      • 当调用start()方法时,进入run状态
    • 锁阻塞

      • 当没有获取到锁对象时,进入锁阻塞状态
    • 无限等待

      • 当调用wait()方法时,进入到无限等待状态
        • 如果没有其他线程唤醒处理等待状态的线程,等待状态的线程会一直处于等待
      • 使用notify()或notifyAll()唤醒
    • 计时等待

      • 当调用:sleep(毫秒)、wait(毫秒)方法时,进入计时等待状态
        • 时间超过时,会进行到"run状态"或"锁阻塞"
    • 死亡(终止)

      • 当线程执行完后,进行到死亡状态

在这里插入图片描述

四、线程的通讯

1、什么是线程间通讯

线程间的通讯技术就是通过等待和唤醒机制,来实现多个线程协同操作完成某一项任务,例如经典的生产者和消费者案例

本质:就是使用wait()和notify()实现两个不同线程之间的通讯

若A线程进入等待状态,可通过B线程通过唤醒,让A线程进入可运行状态

2、等待和唤醒的方法调用注意事项

1)等待方法

void wait() 让线程进入无限等待。

void wait(long timeout) 让线程进入计时等待

以上两个方法调用会导致当前线程释放掉锁资源。

2)唤醒方法

void notify() 唤醒在此对象监视器(锁对象)上等待的(任意)单个线程。

void notifyAll() 唤醒在此对象监视器上等待的所有线程。

以上两个方法调用不会导致当前线程释放掉锁资源。

注意:

1.等待和唤醒的方法,都要使用锁对象调用(需要在同步代码块中调用)。

2.等待和唤醒方法应该使用相同的锁对象调用。

Object lock = new Object();

synchronized(lock){
    
    
    
    lock.wait();//当前获取锁的线程,进入无限等待状态 (释放锁)
    
} 

其他线程:
    lock.notify();//唤醒使用lock锁对象上的任意一个处于等待状态的线程
    lock.notifyAll();//唤醒使用lock锁对象上的所有的线程

3、生产者消费者案例

定义一个集合当做盘子,包子铺线程完成生产包子,包子添加到集合中;吃货线程完成购买包子,包子从集合中移除。

  1. 当包子没有时,吃货线程等待

  2. 包子铺线程生产包子,并通知吃货线程去吃包子

  3. 当盘子装满(100)时,包子铺线程等待

4.吃货线程去吃包子,并通知包子铺去做包子

import java.util.ArrayList;

// 生产者
class Producer extends Thread {
    
    
    // 共享资源
    ArrayList<String> list = null;

    public Producer(String name, ArrayList<String> list) {
    
    
        super(name);
        this.list = list;
    }

    @Override
    public void run() {
    
    
        while (true) {
    
    
            synchronized (list) {
    
      // 对象锁可以是任意类型的
                String name = Thread.currentThread().getName();
                if (list.isEmpty()) {
    
    

                    // 生产包子
                    System.out.println(name + ": 生产包子");
                    list.add("包子");

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

                    // 唤醒 顾客一 吃包子
                    System.out.println(name + ": 叫 吃货吃包子");
                    list.notify();

                } else {
    
    
                    try {
    
    
                        list.wait();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
            }
        }
    }

}


// 消费者
class Consumer extends Thread {
    
    
    // 共享资源
    ArrayList<String> list = null;

    public Consumer(String name, ArrayList<String> list) {
    
    
        super(name);
        this.list = list;
    }

    @Override
    public void run() {
    
    
        while (true) {
    
    
            synchronized (list) {
    
    
                String name = Thread.currentThread().getName();

                if (list.isEmpty()) {
    
      // 没有包子
                    // 唤醒生产者 --> 生产包子
                    System.out.println("\t\t"+name + ": 叫包子铺生产包子");
                    list.notify();  // 唤醒时不会释放锁

                    // 进入等待 --> 释放锁了
                    try {
    
    
                        list.wait();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }


                } else {
    
    
                    System.out.println("\t\t"+name + " 吃包子");
                    list.remove(0);  // 吃包子

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


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

        ArrayList<String> list = new ArrayList<>();

       new Producer("包子铺", list).start();
       new Consumer("吃货", list).start();

    }

}

五、线程池

1、 线程使用存在的问题

 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

 如果大量线程在执行,会涉及到线程间上下文的切换,会极大的消耗CPU运算资源。

2 、线程池的认识

其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

java.util.concurrent.ExecutorService 是线程池接口类型。使用时我们不需自己实现,JDK已经帮我们实现好了。

获取线程池我们使用工具类java.util.concurrent.Executors的静态方:

public static ExecutorService newFixedThreadPool (int num) 指定线程池最大线程池数量获取线程池

3、线程池使用大致流程

  1. 创建线程池指定线程开启的数量

  2. 提交任务给线程池,线程池中的线程就会获取任务,进行处理任务。

  3. 线程处理完任务,不会销毁,而是返回到线程池中,等待下一个任务执行。

  4. 如果线程池中的所有线程都被占用,提交的任务,只能等待线程池中的线程处理完当前任务。

4、请简述线程池使用的好处

 降低资源消耗。

 提高响应速度。

 提高线程的可管理性。

5、线程池处理Runnable任务

1、Runnable接口使用步骤

  1. 指定线程数量获取线程池
  2. 定义Runnable任务类型
  3. 创建任务对象,提交给线程池

2、Runnable实现类代码

public class MyRunnable implements Runnable {
    
    
    @Override
    public void run() {
    
    
        System.out.println("我要一个教练");
        try {
    
    
            Thread.sleep(2000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("教练来了: " + Thread.currentThread().getName());
        System.out.println("教我游泳,交完后,教练回到了游泳池");
    }
}

线程池测试类:

public class ThreadPoolDemo {
    
    
    public static void main(String[] args) {
    
    
        // 1. 创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
        // 2. 创建Runnable实例对象
        MyRunnable r = new MyRunnable();

        //自己创建线程对象的方式
        // Thread t = new Thread(r);
        // t.start(); ---> 调用MyRunnable中的run()

        // 3. 从线程池中获取线程对象,然后调用MyRunnable中的run()
        service.submit(r);
        // 再获取个线程对象,调用MyRunnable中的run()
        service.submit(r);
        service.submit(r);
        
        // 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
        // 将使用完的线程又归还到了线程池中
        // 关闭线程池
        //service.shutdown();
    }
}

6、线程池处理Callable任务

1 、Callable接口概述

public interface Callable<V> {
    
     

V call() throws Exception;

}
------------------------------------
public interface Runnable{
    
    

void run();

}

**Callable与Runnable的不同点:**

1. Callable支持结果返回,Runnable不行

2. Callable可以 抛出异常,Runnable不行

2、 Callable任务处理使用步骤

  1. 创建线程池对象,并指定线程池中Thread的数量

    • //线程池:ExecutorService (接口类型)
      //实例化线程池:
      ExecutorService es = Executors.newFixedThreadPool(10);//线程池中有10个Thread
      
  2. 把线程任务,提交给线程池(线程池会分配一个Thread对象,来执行当前的任务),当Thread执行完任务之后,会回归到线程池中

    • // <T> Future<T> submit(Callable<T> task) 提交Callable任务方法
      //方法: submit(Runnable task)
       Future<Integer> f = es.submit(task);
      
  3. 获取执行结果

    • // 返回值类型Future的作用就是为了获取任务执行的结果。
      // Future是一个接口,里面存在一个get方法用来获取值。
      Integer integer = f.get();
      

代码实现:

// 计算1-100之间的和
package cn.itcast.day10.demo01Lambda;

import java.util.concurrent.*;

class Task implements Callable<Integer>{
    
    
    private int n;
    public Task(int n){
    
    
        this.n = n;
    }

    @Override
    public Integer call() throws Exception {
    
    
        Thread.sleep(3000);
        int sum = 0;
        for (int i = 0; i <= n; i++) {
    
    
            sum += i;
        }
        System.out.println("任务完成--》我计算完了!回到线程池...");
        return sum;
    }
}

public class Demo02{
    
    
    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        // 1.创建线程池 有五个线程
        ExecutorService es = Executors.newFixedThreadPool(2);

        // 2.定义callable任务
        Task task = new Task(5);
        Task task2 = new Task(10);
        Task task3 = new Task(100);

        // 3.把callable任务--》提交给线程池
        Future<Integer> f = es.submit(task);
        Future<Integer> f2 = es.submit(task2);
        Future<Integer> f3 = es.submit(task3);

        // 4.获取执行结果
        Integer integer = f.get();
        Integer integer2 = f2.get();
        Integer integer3 = f3.get();

        System.out.println("输出结果:");
        System.out.println(integer);
        System.out.println(integer2);
        System.out.println(integer3);

        es.shutdown();
    }
}


面试题 :java线程的创建有几种方式?

  • Thread
  • Runnable
  • Callable (必须使用线程池来实现)

猜你喜欢

转载自blog.csdn.net/mmmmmCJP/article/details/115331976