Java基础之线程

多线程编程

  • Java 给多线程编程提供了内置的支持
并发 并行
  • 并发:同一时刻多个任务交替执行
    • 貌似”同时执行“
    • 单核CPU实现多任务就是并发
  • 并行:同时间多个任务同时执行
    • 多核CPU可以实现并行、并发

进程和线程

进程

应用程序的执行实例,有独立的内存空间和系统资源

  • 一个进程包括由操作系统分配的内存空间

    • 包含一个或多个线程
  • 一个线程不能独立的存在,必须是进程的一部分

  • 一个进程一直运行,直到所有的非守护线程都结束运行后才能结束

线程

CPU 调度和分派的基本单位,进程中执行运算的最小单位,可完成一个独立的顺序控制流程

  • 一条线程指进程中一个单一顺序的控制流

    • 一个进程中可以并发多个线程,每条线程并行执行不同的任务
  • 多线程是多任务的一种特别形式,但多线程使用更小的资源开销

  • 多线程能编写高效率的程序来达到充分利用 CPU 的目的

  • 多线程就是分时利用 CPU,宏观上让所有线程一起执行

    • 也叫并发
进程和线程的关系
  1. 一对多:一个线程只能属于一个进程

    • 一个进程可以有多个线程,但至少有一个线程
    • 线程是操作系统可识别的最小执行和调度单位
  2. 资源分配给进程,同一进程的所有线程共享该进程的所有资源

    • 同一进程中的多个线程共享代码段(代码和常量)、数据段(全局变量和静态变量)、扩展段(堆存储),但每个线程拥有自己的栈段
    • 栈段又叫运行时段,用来存放所有局部变量和临时变量
  3. 处理机分给线程:真正在处理机上运行的是线程

  4. 线程在执行过程中,需要协作同步

    • 不同进程的线程间要利用消息通信的办法实现同步

使用线程

三种方法
  1. 实现 Runnable 接口
  2. 继承 Thread 类本身
  3. 实现 CallableFuture 创建线程
Runnable
  • 创建线程最简单的方法是实现 Runnable 接口

    1. 类实现 Runnable 接口
      • 需要重写 run() 方法
        • public void run();
      • 需要使用线程实现的业务逻辑放在 run() 方法里
      • run() 可以调用其他方法,使用其他类,并声明变量
    2. 实例化子类对象,并传入 Thread 类构造参数
      • Thread(Runnable threadOb,String threadName);
        • threadOb:实现 Runnable 接口的类的实例
        • threadName:自定义新线程的名字
    3. 线程创建之后,调用 start() 方法才会运行
      • void start(){};
      • start() 方法是 Thread 类方法
        • 所以实现 Runnable 接口的子类需要 Thread 类代理启动线程
  • 实例

    class RunnableDemo implements Runnable {						// 实现 Runnable 接口
        private Thread t;											// 封装线程对象
        private String threadName;									// 线程名
        public RunnableDemo( String name) {							// 创建线城市指定线程名
            threadName = name;
            System.out.println("创建线程: " +  threadName );
        }
    
        @Override
        public void run() {											// 重写 run() 方法,实现业务逻辑
            System.out.println("运行: " +  threadName );
            try {
                for(int i = 2; i > 0; i--) {
                    System.out.println("线程: " + threadName + "-" + i);
                    Thread.sleep(50);								// 线程休眠 50ms
                }
            }catch (InterruptedException e) {
                System.out.println("Thread " +  threadName + " interrupted.");
            }
        }
    
        public void start () {										// 自定义线程启动方法,调用 Thread 类 start 方法
            if (t == null) {
                t = new Thread (this, threadName);					// 通过本类对象实例化 Thread 类对象
                t.start ();											// 实际启动线程方法
            }
        }
    }
    
    public class TestThread { 
        public static void main(String args[]) {
            RunnableDemo R1 = new RunnableDemo( "Thread-1" );		// 实例化第一个线程对象
            Thread thread = new Thread(R1);							// 作为参数创建 Thread 类对象
            thread.start();											// 通过 Thread 类启动线程
    
            RunnableDemo R2 = new RunnableDemo( "Thread-2");		// 实例化第二个线程对象
            R2.start();												// 通过自定义 start 方法启动线程
            														/* 实际两种方式完全一样 */
        }   
    }
    /**
    创建线程: Thread-1
    创建线程: Thread-2
    运行: Thread-1
    运行: Thread-2
    线程: Thread-2-2
    线程: Thread-1-2
    线程: Thread-2-1
    线程: Thread-1-1
    */
    /* 两个线程同时运行,执行顺序不固定 */
    
Thread
实现
  • 必须重写 run() 方法

    • 该方法是新线程的入口点
    • 必须调用 start() 方法才能执行
  • 本质上也是实现 Runnable 接口的一个实例

    • Thread 类实现了 Runnable 接口
class ThreadDemo extends Thread {
    private String threadName;								// 指定线程名

    public ThreadDemo( String name) {
        threadName = name;
        System.out.println("创建 " +  threadName );
    }

    public void run() {
        System.out.println("运行线程 " +  threadName );
        try {
            for(int i = 2; i > 0; i--) {
                System.out.println("线程: " + threadName + ", " + i);
                Thread.sleep(50);
            }
        }catch (InterruptedException e) {
            System.out.println("Thread " +  threadName + " interrupted.");
        }
        System.out.println("线程 " +  threadName + " 退出.");
    }
}

public class TestThread {
    public static void main(String args[]) {
        ThreadDemo T2 = new ThreadDemo( "Thread-2");
        T2.start();
    }   
}
/*
创建 Thread-2
运行线程 Thread-2
线程: Thread-2, 2
线程: Thread-2, 1
线程 Thread-2 退出.
*/
Thread 方法
  • 通过 Thread 对象调用

  • 常用

    • 方法作用
      public void start()使该线程开始执行;Java 虚拟机调用该线程的 run 方法
      public void run()如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回
      public final void setName (String name)设置线程名称
      public final void setPriority (int priority)更改线程的优先级
      public final void setDaemon (boolean on)将该线程标记为守护线程或用户线程
      Public final void join (long millisec)等待该线程终止的时间最长为 millis 毫秒
      public void interrupt()中断线程
      public final boolean isAlive()测试线程是否处于活动状态
  • 静态方法

    • 方法描述
      public static void yield ()礼让线程,暂停当前正在执行的线程对象,并执行其他线程
      public static void sleep (long millisec)线程休眠(暂停执行)millisec 毫秒,受到系统计时器和调度程序精度和准确性的影响
      public static boolean holdsLock (Object x)当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true
      public static Thread currentThread ()返回对当前正在执行的线程对象的引用
      public static void dumpStack()当前线程的堆栈跟踪打印至标准错误流
实例
class DisplayMessage implements Runnable {					// 实现 Runnable 接口
    private String message;
    public DisplayMessage(String message) {
        this.message = message;
    }
    @Override
    public void run() {
        while(true) {
            System.out.println(message);					// 死循环,线程一直运行
        }
    }
}
class GuessANumber extends Thread {							// 继承 Thread 类
    private int number;
    public GuessANumber(int number) {						// 传入一个数
        this.number = number;
    }

    @Override
    public void run() {									  	// 重写 run 方法
        int counter = 0;
        int guess = 0;
        do {
            guess = (int) (Math.random() * 100 + 1);		// 生成随机数
            System.out.println("第" +  counter++ + "次;" +  this.getName() + " 猜的数为:" + guess);
        } while(guess != number);							// do-while 循环直到 随机数与参数相同
        System.out.println("猜到了!" + this.getName() + " 在第 " + counter + " 次成功");
    }
}
public class Test {
    public static void main(String args[]) {
        Runnable hello = new DisplayMessage("Hello");			// Runnable 实现类
        Thread thread1 = new Thread(hello);						
        thread1.setDaemon(true);								// 将此线程设置为守护线程,随其他线程停止而停止
        thread1.setName("hello");								// 设置线程名
        thread1.start();										// 启动线程
        //每一次运行的结果都不一样

        Runnable bye = new DisplayMessage("Goodbye");			// Runnable 实现类
        Thread thread2 = new Thread(bye);
        thread2.setPriority(Thread.MIN_PRIORITY);				// 设置线程最小优先级:最后执行
        thread2.setDaemon(true);								// 设置守护线程	
        thread2.start();										// 启动线程
        //每一次运行的结果都不一样

        Thread thread3 = new GuessANumber(27);					// Thread 子类,参数 27
        thread3.start();										// 启动线程
        try {
            thread3.join();										// 加入 thread3 优先执行
        }catch(InterruptedException e) {
            System.out.println("Thread interrupted.");
        }
        //每一次运行的结果都不一样

        Thread thread4 = new GuessANumber(75);					// Thread 子类,参数 75
        thread4.start();
        //每一次运行的结果都不一样
    }
}
/*
Hello
Hello									//后面仍有可能出现
Goodbye									//后面仍有可能出现
Goodbye
......
第46次;Thread-0 猜的数为:27			 
猜到了!Thread-0 在第 46 次成功			//次数不定,直到数字相等为止
.......
第504次;Thread-1 猜的数为:75			 //次数不定,直到数字相等为止
猜到了!Thread-1 在第 504 次成功
Hello
Godbye								
......
*/
Callable
实现
  • 实现 Callable 接口
    • 需要重写call()方法
    • 给出泛型类型作为 call() 方法返回值类型
    • call() 方法需要抛出异常
  • 使用步骤
    1. 创建线程类对象:TestCallable testCallable = new TestCallable();
    2. 创建执行服务:ExecutorSeervice ser = Executors.newFixedThreadPool(1);
      • 1:线程池大小
    3. 提交执行:Future<Boolean> result = ser.submit(testCallable);
      • TestCallable:实现接口的线程类对象
    4. 获取结果:boolean r = result.get();
    5. 关闭服务:ser.shutdownNow();
实例
public class CallableThreadTest implements Callable<Integer> {
    public static void main(String[] args)  {  
        CallableThreadTest ctt = new CallableThreadTest();  			   // 创建子类实例对象
        FutureTask<Integer> ft = new FutureTask<>(ctt);  				   // 注册线程服务
        for(int i = 0; i < 3; i++)  {
            System.out.println( Thread.currentThread().getName() + " 的循环变量i的值 " + i );		// 循环获取 main 线程名
            if(i == 2)  {  
                new Thread(ft, "有返回值的线程").start();  			 	 // 启动线程,设置线程名为:有返回值的线程
            }  
        }  
        try  {  
            System.out.println("子线程的返回值:"+ ft.get());  				// 获取 call 方法返回值
        } catch (InterruptedException e)  {  
            e.printStackTrace();  
        } catch (ExecutionException e)  {  
            e.printStackTrace();  
        }  
    }
    @Override  
    public Integer call() throws Exception{
        int i = 0
        for( ; i < 3; i++)  {  
            System.out.println(Thread.currentThread().getName() + " " + i);		// 输出当前线程名和变量 i  
        }  
        return i;
    }  
}
/*
main 的循环变量i的值 0
main 的循环变量i的值 1
main 的循环变量i的值 2
有返回值的线程 0
有返回值的线程 1
有返回值的线程 2
子线程的返回值:3
*/
对比
  • 实现 RunnableCallable 接口方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类
  • 使用继承 Thread 类的方式创建多线程编写简单
    • 需要访问当前线程无需使用 Thread.currentThread() 方法,直接使用 this 即可
  • RunnableCallable
    • Runnable 是在 JDK1.0 的时候提出的多线程的实现接口
      • Callable 是在 JDK 1.5之后提出的
    • java.lang.Runnable 接口之中只提供有一个 run()方法,并且没有返回值
    • java.util.concurrent.Callable 接口提供有 call()方法,有返回值
原理
  • 调用 start() 后,线程会被放到等待队列,等待 CPU 调度,并不一定马上开始执行,只是将线程置于可动行状态

    • start() 启动线程是因为调用本地的 start0() 方法,再根据具体环境分配资源以启动线程
  • 通过 JVM,线程 Thread 会调用 run() 方法,执行本线程的线程体

    • 先调用 start 后调用 run()

      • 为了实现多线程的优点,没 start 不行
    • start() 方法来启动线程,真正实现了多线程运行

      • 无需等待 run() 方法体执行完毕,可直接继续执行下面的代码
      • 通过调用 Thread 类的 start() 方法启动一个线程,此线程处于就绪状态,没有运行
      • 然后通过此 Thread 类调用方法 run() 来完成其运行操作,CPU 再调度其它线程
  • run() 方法当作普通方法直接调用时

    • 程序是顺序执行:run() 方法体执行后,才继续执行下面的代码
    • 程序中只有主线程, 程序执行路径只有一条,没有达到写线程的目的
run()、start()
  • 每个线程都是通过某个特定 Thread 对象所对应的方法 run() 来完成操作
    • 方法 run() 称为线程体,包含要执行的线程的内容, run 方法运行结束, 此线程终止
  • 通过调用 Thread 类的 start() 方法来启动一个线程
    • start() 启动线程是调用本地的 start0() 方法,再根据具体环境分配资源以启动线程

在这里插入图片描述

线程生命周期

  • 线程是一个动态执行的过程,有从产生到死亡的过程
五种状态
  1. 新建状态:NEW
    • new 一个线程对象后,该线程对象就处于新建状态
    • 保持这个状态直到程序 start() 这个线程
  2. 就绪状态:RUNNABLE
    • 线程对象调用 start() 方法后,该线程进入就绪状态:READING
    • 就绪状态的线程处于就绪队列中,要等待 JVM 里线程调度器的调度
  3. 运行状态:RUNNING
    • 就绪状态的线程获取 CPU 资源执行 run(),线程处于运行状态
    • 运行状态的线程最为复杂:可以变为阻塞状态、就绪状态和死亡状态
  4. 阻塞状态:BLOCKED
    • 线程执行了 sleep(睡眠)、suspend(挂起)等方法,失去所占用资源
      • 该线程从运行状态进入阻塞状态
      • 睡眠时间已到或获得设备资源后可以重新进入就绪状态。分为三种:
        1. 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态
        2. 同步阻塞:线程获取 synchronized 同步锁失败
          • 同步锁被其他线程占用
        3. 其他阻塞:调用线程的 sleep()join() 发出了 I/O 请求时,线程就会进入到阻塞状态
          • sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态
  5. 死亡状态:TERMINATED
    • 运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态(run() 方法结束)

在这里插入图片描述

在这里插入图片描述

七种状态
  • NEW:刚创建未启动

  • RUNNABLE:可运行状态

    • Ready:就绪状态
    • Runinng:运行状态
  • BLOCKED:阻塞状态(等待获取锁)

  • WAITING:等待状态(直到被唤醒)

  • TIMED_WITING:超时等待(有时间限制的等待)

  • TERMINATED:线程终止
    在这里插入图片描述

线程操作

通知线程
  • B 线程中掌握 A 线程的对象,在 A 线程中使用变量控制程序运行

  • B 线程中改变该变量以通知 A 线程停止

  • public class RunnableDemo  extends Thread {
    	static volatile  Boolean flag = true;   						// 控制程序停止的变量
    
        @Override
        public void run() {
            System.out.println(this.getName() + " 启动");
            while(RunnableDemo.flag){									// 默认死循环
                System.out.println(this.getName() + " 在运行");
            }
            System.out.println(this.getName() + " 停止运行");
        }
    }
    class  Test{
        public static void main(String[] args) {
            RunnableDemo runnableDemo = new RunnableDemo();				// 创建线程对象
            runnableDemo.setName("主线程");
            runnableDemo.start();										// 启动线程
    
            Thread thread = new Thread(() -> {							// lambda 表达式实现 Runnable 线程使用
                while(true) {
                    for(int i = 0; i < 5; i++) {
                        System.out.println(Thread.currentThread().getName() + " - " + i);
                        if(i == 2) {				
                            RunnableDemo.flag = false;					// 判断控制主线程停止
                        }
                    }
                }
            }, "控制线程");
            thread.setDaemon(true);										// 设置为守护线程
            thread.start();												// 启动
        }
    }
    
线程插队
  • join():一旦成功先执行完插队的线程任务再继续原来的线程任务

  • yield():线程礼让,但礼让的时间不确定,所以不一定成功

  • public class RunnableDemo  extends Thread {
        static volatile  Boolean flag = true;
    
        @Override
        public void run() {
            System.out.println(this.getName() + " 启动");
            while(flag){
                for(int i = 0; i < 20; i++) {
                    System.out.println(this.getName() + " " + i);
                }
                flag = false;
            }
            System.out.println(this.getName() + " 停止运行");
        }
    }
    class  Test{
        public static void main(String[] args) {
            RunnableDemo runnableDemo = new RunnableDemo();
            runnableDemo.setName("主线程");
            // runnableDemo.start();
    
            Thread thread = new Thread(() -> {
                    for(int i = 0; i < 10; i++) {
                        System.out.println(Thread.currentThread().getName() + " - " + i);
                        if(i == 5) {
                            try {
                                runnableDemo.start();						// 此时启动主线程
                                runnableDemo.join();						// 让主线程插队,先执行完毕主险成再继续原来线程任务
                                // Thread.yield() 							// 线程礼让,但可能失败,原线程继续执行 
                            } catch( InterruptedException e ) {
                                throw new RuntimeException(e);
                            }
                        }
                    }
            }, "控制线程");
            thread.start();
        }
    }
    
守护线程
  • setDaemon(true):当其他所有线程结束时,被设为守护线程的线程自动退出
中断线程
  • interrupted():判断线程是否在执行
  • interrupt():中断当前执行的线程
    • 所有正在执行的线程都可以中断
    • 中断线程必须处理异常
优先级
  • Java 线程都有优先级、

    • 有助于操作系统确定线程的调度顺序
  • 优先级是一个整数,取值范围是:[1, 10]

    • 1:Thread.MIN_PRIORITY

    • 10:Thread.MAX_PRIORITY

    • 默认线程分配优先级:NORM_PRIORITY = 5

  • 较高优先级的线程对程序更重要,应该在低优先级的线程之前分配处理器资源

    • 优先级不能保证线程执行的顺序,而且非常依赖于平台

    • 优先级高被调用几率比较大,但是并非绝对先被调用

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

  • setPriority():设置当前线程优先级

线程池

介绍
  • 容纳多个线程的容器,其中的线程可以反复使用

    • 省去频繁创建线程对象的操作
    • 无需反复创建线程而消耗过多资源因
  • 如果每个请求到达就创建一个新线程,资源消耗相当大

    • 实际使用中创建和销毁线程花费的时间和消耗的系统资源都相当大

      • 可能比处理实际的用户请求的时间和资源要多的多
    • 活动的线程也需要消耗系统资源

      • 一个JVM创建太多的线程,可能使系统过度消耗内存或切换过度,导致系统资源不足
    • 为防止资源不足,需要限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数

      • 特别是资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务
作用
  • 主要用来解决线程生命周期开销问题和资源不足问题

    • 通过对多个任务重复使用线程,线程创建的开销被分摊到多个任务

    • 由于请求到达时线程已存在,消除线程创建带来的延迟,可以立即为请求服务

创建
相关API
  • 通过线程池工厂创建,调用线程池中的方法获取线程,通过线程去执行任务方法

  • JDK 5.0 提供线程池相关的 API

    • Executors:工具类,线程池的工厂类;用于创建并返回不同类型的线程池

      • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象
    • ExecutorService:真正的线程池接口

      • 常见子类 ThreadPoolExecutor
      • void execute(Runnable run):执行任务、命令
        • 无返回值,一般用来执行 Runnable
      • <T> Future <T> submit(Callable<T> task):执行任务
        • 有返回值,一般用来执行 Callable
      • Future<?> submit(Runnable task)
        • 执行 Runnable 的有返回值方法
      • void shutdown():关闭连接池
    • Future 接口:用来记录线程任务执行完毕后产生的结果

使用
class Test{
    public static void main(String[] args){
        ExecutorService service = Executors.newFixedThreadPool(10);						// 创建服务:创建线程池对象;大小为 10
        service.submit(() -> System.out.println(Thread.currentThread().getName()));		// lambda 表达式启动 Runnable
        service.execute(() -> System.out.println(Thread.currentThread().getName()));	// lambda 表达式启动 Runnbale
        Integer num = service.submit(() -> {
            System.out.println(Thread.currentThread().getName());
            return 1;
        }).get();																		// lambda 表达式启动 Callable,并获取返回值
        System.out.println("num = " + num);
        service.shutdown();																// 关闭服务
    }
}

线程安全

synchronized
  • 作用

    • 同一进程的多个线程共享同一块存储空间,可能会有访问冲突问题

    • 保证数据在被访问时的正确性,在访问中加入锁机制

    • synchronized:当一个线程拿到对象的排他锁会独占资源,其他线程必须等待,使用后释放锁

  • 问题

    • 一个线程持有锁会导致其他所需要此锁的线程挂起

    • 多线程竞争下,加锁、释放锁会导致较多的上下文切换和 调度延时,引起性能问题

    • 优先级高的线程等待优先级低的线程释放锁会导致优先级倒置,引起性能问题

  • 使用

    • synchronized():同步(互斥锁)

      • 同一时间只能有一个对象拿到锁访问,会导致效率降低
        • 非静态方法锁默认为:this,即本对象
          • 多个线程间必须为同一个对象
        • 静态方法锁默认:类名.class
      class A extends Thread{
          synchronized (this){}						// 同步代码块,得到对象的锁才能操作
          public synchronized void m(){				// 同步方法
              // 需要同步的方法实现
          }	
          public synchronized static void m(){		// 静态同步方法;锁为 A.class
              // 需要同步的方法实现
          }
          public static void m(){						// 静态方法
              synchronized(A.clss) {}   				// 静态方法中同步代码块,锁为 A.class
          }
      } 
      
  • 死锁:多个线程间抢夺资源,各自需要对方手中的锁才能继续执行

    • 容易形成死锁:无法释放自身资源,同时无法拿到对方资源
Lock
介绍
  • JDK5 可以通过显式定义同步锁对象,同步锁使用 Lock 对象充当

    • Lock 锁需要手动释放
    • JDK1.5 之后出现
  • java.util.concurrent.locks.Lock 接口时控制多个线程对共享资源进行访问的工具

    • 锁提供对共享资源的独占访问
    • 每次只有一个对象对 Lock 对象加锁
    • 线程开始前要先获得 Lock 对象
  • java.util.concurrent.locks.ReentrantLock 类实现了 Lock:可重入锁

    • Lock lock = new ReentranttLock();

      • Reentran:可重用的
    • Synchronzied 相同的并发性和内存语义

    • 线程安全的控制中,较常用 ReentrantLock,可以显式加锁、释放锁

      • void lock():上锁
      • void unlock():释放锁
示例
  • 步骤

    1. 创建一个 ReentrantLock 对象

    2. 可能出现线程安全问题的代码前,调用 Lock 接口中的方法 lock 获取锁对象

    3. 可能出现线程安全问题的代码后,调用 Lock 接口中的方法 unlock 释放锁对象

      private final ReentrantLock lock = new ReentrantLock();			// 创建锁对象,定义为私有不可改变属性
      @Override
      public void run() {
          lock.lock();												// 上锁
          System.out.println("你好");								  // 执行同步方法
          lock.unlock();												// 解锁
      }
      
  • public class RunnableImpl implements Runnable{
        private int ticket = 100;						// 定义共享的票源
        Lock l = new ReentrantLock();					// 创建 ReentrantLock 对象
        
        @Override
        public void run() {								// 设置线程任务:卖票
            while(true){								// 死循环,卖票重复的执行
                l.lock();								// lock 方法获取锁对象
                if(ticket > 0){							// 判断票是否大于 0
                    try {
                        Thread.sleep(10);				// 提高线程安全问题出现的几率,让程序睡眠 10 毫秒
                        System.out.println(Thread.currentThread().getName() + "正在卖第 " + ticket + " 张票!");
                        ticket--;						// 票数自减
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        l.unlock();						// unlock 方法释放锁对象,无论是否异常,都释放锁对象,节约内存,提高程序的效率
                    }
                }
            }
        }
    }
    
synchronzied 和 Lock
  • Lock:显式锁,手动开关锁

    • synchronzied :隐式锁,作用域之外自动释放
  • Lock:只有代码块锁

    • synchronzied:代码块锁和方法锁
  • LockJVM 花费较少时间调度线程,性能更好

    • 并且有更好的拓展性,Lock 接口有很多子类
  • Lock 实现提供比使用 synchronized 方法和语句更广泛的锁定操作

  • 优先使用顺序

    • Lock > 同步代码块(已进入方法体,分配资源后)> 同步方法(方法体外)
锁释放
  • 释放锁

    • 当前线程的同步方法、同步代码块执行结束
    • 当前线程在同步方法、同步代码块遇到 breakreturn
    • 当前线程的同步方法、同步代码块出现未处理的 ErrorException,异常结束
    • 当前线程的同步方法、同步代码块执行了线程对象的 wait() 方法,当前线程暂停并释放锁
  • 不会释放锁

    • 当前线程的同步方法、同步代码块中调用 Thread.sleep()Thread.yield()方法暂停当前线程执行,不会释放锁
    • 当前线程的同步代码块其他线程调用该线程的 suspend() 挂起线程
      • suspend()resume() 尽量避免使用,方法已过时
volatile
  • 定义在属性上使用

  • 正常变量处理流程

    1. 获取变量原有数据内容副本
    2. 利用副本进行数学计算
    3. 将计算后的变量保存在原始空间中
  • 加上 volatile

    1. 不使用副本操作,直接操作原始数据
    2. 保证数据的可见性,进制指令重排序
    3. 不保证操作的原子性
ThreadLocal
  • 解决静态属性存放数据的线程同步问题

    • 多线程情况下静态属性被所有线程对象共享

      • 很容易被其他线程将数据覆盖造成资源隐患
    • ThreadLocal 类对线程资源单独存放

      • 当前线程的资源只允许自己访问
      • 同时保存 线程对象 和对应的 数据对象
        • 线程对象:自动保存执行时的当前线程对象
      • 结构:Map<Thread,T>
        • 使用 Map 保存数据
        • key:``Thread.currentThread()`,当前线程对象
        • value:泛型 T,线程的共享变量
  • Java 中每个线程对象都有 ThreadLocal

    • 常用来解决数据库连接、Session 管理等
  • 创建对象:ThreadLocal<T> threadLocal = new ThreadLocal<T>();

    • 执行方法时的当前线程对象为 key
  • 设置数据:threadLocal.set(T value)

    • 许多程序不使用此功能,只依赖于 initialValue() 方法设置线程局部变量值
    • 程序中一般重写方法给特定初始值
  • 取出数据:threadLocal.get()

    • 返回此线程局部变量当前线程副本的值
    • 第一次调用该方法创建并初始化此副本
  • 删除数据:threadLocal.remove()

    • 移除此线程局部变量的值,有助于减少存储需求
    • 再次访问此线程局部变量默认将拥有其 initialValue
  • 返回初始值:protected T initialValue() {return null;}

    • 返回此线程局部变量当前线程的初始值
    • 最多在每次访问线程获得线程局部变量时调用一次
      • 即 第一次使用 get() 方法访问变量时
      • 若线程先调用 set() 方法则不会载调用此方法
    • 该方法只返回 null,若要将线程局部变量初始化为其他值
      • ThreadLocal 创建子类重写该方法
      • 通常使用匿名内部类
        • 调用适当构造方法返回新构造对象
  • 在这里插入图片描述

    class Channel {
        // 封装为私有静态全局常量,ThreadLocal 类按线程分别存放数据,只有对应线程才可以获得自己的资源
        private static final ThreadLocal<Message> THREAD_LOCAL = new ThreadLocal<>();
        // 设置消息
        public static void setMessage(Message mess) {
            THREAD_LOCAL.set(mess);							// 存放在 ThreadLocal 中
        }
        public static void send() { 						// 发送消息
            // 从 ThreadLocal 实例对象中获取当前线程对应的资源
            System.out.println(Thread.currentThread().getName() + "发送消息: " + THREAD_LOCAL.get().getInfo());
        }
    }
    class Message { 										// 要发送的消息体
        private String info;
        public Message(String info){ this.info = info;}
        public String getInfo() {return info;}
    }
    public class Test{
        public static void main(String[] args) {
            // 启动多个线程同时操作消息
            new Thread(() -> {
                Channel.setMessage(new Message("废物"));				 // 发送消息
                Channel.send();
            }, "线程A").start();
            
            new Thread(() -> {
                Channel.setMessage(new Message("菜狗"));
                Channel.send();
            }, "线程B").start();
            
            new Thread(() -> {
                Channel.setMessage(new Message("小点心"));
                Channel.send();
            }, "线程C").start();
        }
    }
    /*
    运行结果: 按线程自动抢占资源的快慢执行顺序有所不同,但消息内容各自保存不会被覆写
    线程C发送消息: 小点心
    线程B发送消息: 菜狗
    线程A发送消息: 废物
    */
    

线程通信

Object类的线程方法
  • notify() :随机唤醒一个该对象上等待的线程

    • 返回的前提是该线程获取到了对象的锁
  • notifyAll():唤醒所有等待在该对象上的线程

  • wait():调用该方法的线程进入 WAITING 状态

    • 被该对象的其他线程的唤醒或线程中断才会返回
    • 调用 wait() 方法后,会释放对象的锁
  • wait(long timeout) :超时等待一段时间

    • 参数:毫秒,即等待长达 timeout 毫秒,如果没有通知就超时返回
  • wait(long timeout, int nanos) :超时等待一段时间后可以额外等待更精确的时间

    • 对于超时时间更细粒度的控制,可以达到纳秒
    • timeout:超市等待,单位:毫秒
    • nanos:额外等待,单位:纳秒
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值