Java 多线程

多线程

基本概念:

程序:

程序(program)是为完成特定任务、用某种语言编写的一组指令的集合 即指一 段静态的代码,静态对象。

进程:

进程(process)是程序的一次执行过程,或是正在运行的一个程序 是一个动态的过程
进程作为资源分配的单位,系统在运行时会为每个进程分配不同的内存区域

线程:

线程(thread)
进程可进一步细化为线程,是一个程序内部的一条执行路径。
若一个进程同一时间 并行 执行多个线程,就是支持多线程的
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小

  • 一个进程中的多个线程共享相同的内存单元/内存地址空间
  • 它们从同一堆中分配对象,可以 访问相同的变量和对象。这就使得线程间通信更简便、高效 但多个线程操作共享的系统资源可能就会带来安全的隐患

单核CPU和多核CPU的理解:

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

  • 有多车道,但是收费站只有一个工作人员在收费,只有收了费才能通过:
  • 那么CPU就好比收费人员。如果有某个人不想交钱,那么收费人员可以 把他“挂起”晾着他,等他想通了,准备好了钱,再去收费
  • 收费的车就相当于一个个的线程任务,收费员收费执行线程 收完费用执行结束 对于大的任务, 也可以进行挂起,或一点点的处理...
  • 因为CPU时 间单元特别短,因此感觉不出来 一个个的在工作运行,感觉是一起执行的!

如果是多核的话,才能更好的发挥多线程的效率。 CPU一个核就相当于一个工作人员... 现在的服务器都是多核的)
一个Java应用程序java.exe,其实至少有三个线程:

  • main()主线程,gc() 垃圾回收线程,异常处理线程 当然如果发生异常,会影响主线程

并行 与 并发:

并行:

  • 多个CPU同时执行多个任务: 比如:多个人同时做不同的事

并发:

  • 一个CPU(采用时间片)同时执行多个任务: 比如:秒杀、多个人做同一件事

并行与并发:
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
并发的关键是你有处理多个任务的能力,不一定要同时
并行的关键是你有同时处理多个任务的能力
它们最关键的点就是:是否是『同时』

多线程优点:

  1. 提高应用程序的响应。对图形化界面更有意义 可增强用户体验
  2. 提高计算机系统CPU的利用率
  3. 改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和 修改

但:多线程并不一定提高效率: 注意是不一定!

  • 以单核CPU为例,使用单个线程先后完成多个任务(调用多个方 法),肯定比用多个线程来完成用的时间更短
  • 多线程还要计算,cpu 分配不同时间片来执行时,切换的时间...

何时需要多线程:

程序需要同时执行两个或多个任务

  • 程序需要实现一些需要等待的任务时,如用户输入、文件读写 操作、网络操作、搜索等;
    用户在输入,此时压力并不是特别大,后台还可以做其它事情:校验输入数据 
    文件上传时间较长可以开个线程缓慢上传… 让用户不用一直等待
  • 需要一些后台运行的程序时… 可以提高程序响应速度..

线程的创建 和 使用:

JAVA实现多线程方式: 4种 JDK5.0之后新增两种:

线程的创建和启动:

Java语言的JVM允许程序运行多个线程,它通过 Java.lang.Thread 类来体现

Thread类:

Java中每个线程都是通过某个特定Thread对象的run()方法来完成操作的 把run()方法的主体称为线程体
通过该Thread对象的start()方法来启动这个线程,而非直接调用run()
构造器:

  • Thread():创建新的Thread对象
  • Thread(String threadname):创建线程并指定线程实例名
  • Thread(Runnable target, String name):创建新的Thread对象
  • Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口,中的 run() 方法;

常用方法()

其实 Thread 类本身也是实现了 Runnable 接口 因此 Therad 不是抽象类(类 重写了Runable接口 run(); 方法
Java 通过 Thread类将线程,所必需的功能都封装了起来
Thread类: 支持多线程类提供了大量的方法,控制操作线程

void		Thread();					//分配新的 Thread 对象;		
void		Thread(Runnable);			//分配新的 Thread 对象 参数 Runnable 实现类 对象 
void		Thread(Runnable,String);	//分配新的 Thread 对象 参数1 Runnable 实现类 对象 ,参数2 线程名....

void		run();						//通常需要子类重写,将线程执行的操作声明在方法中,对象直接调用单纯执行run()方法; 不是以线程方式;
void		start();					//使线程开始执行 启动当前线程;java虚拟机调用 run() 方法; 
										//注意:一个对象不可以调两次start();方法; 启动两次??  (底层固定好了!)
										
void		sleep(long);				//Theread静态方法(); 在指定毫秒数内让当前正在执行线程,休眠(暂停);需要处理异常!!

String		getName();					//返回线程 名称;
void		setName(String);			//修改线程名; 注意:要在 start(); 之前改名;
int			getPriority();				//返回线程 优先级; 0-10 默认5; 越高 CPU 获取几率越大,也只是概率~ 	
void		setPriority(int);			//更改线程 优先级
static	    Thread.currentTherad();		//Theread类静态方法 返回当前正在执行线程的线程对象Thread; 
										//注意: main();是java主线程,方法内调用 指main();中的线程;
											
boolean		isAlive();					//检查线程是否属于 运行状态;		需要处理异常!!
void		join();						//等待终止线程; 使当前线程运行完毕!
										//线程A调用,此时A进入阻塞状态,直到B线程完全执行完,A才结束阻塞;
										
void		interrupt();				//终止线程;
void		yieId();					//礼让: 暂停当前线程对象允许其他线程获得CPU资源; 
										(停一下则立刻就绪!)该线程仍处于就绪状态,系统随机选择:就绪线程运行; 有可能该线程继续获取到资源!
										
void		stop();						//已过时,当执行此方法,强制结束当前线程; 生命周期结束;
void		wait();						//线程 run()中调用,当前线程就进入阻塞状态,并开启锁,其它线程可访问数据;
void		notify()/notfyAll();		//线程 run()中调用,notify();随机取消一个线程设置wait();  notfyAll();把所有的wait();阻塞线程都取消阻塞;

wait() notfyAll() notfy()
三个方法是在 Java.lang.Object类中
三个方法的调用者必须是:当前同步代码块/同步监视器 否则会出现,IllegalMonitorStateExeption异常;

sleep()和wait()方法的异同:

  • sleep() wait()一旦执行,都可以使当前线程进入阻塞状态;
  • sleep()可以在任何场景下使用,wait()只能在同步代码块/同步方法中 wait需要由 同步监视器来调用!
  • 关于同步建视器,如果两个方法都在同步代码块/同步方法中使用
    sleep()不会释放锁(同步监视器);
    wait()会释放锁(同步监视器);

线程的分类:

Java中的线程分为两类:守护线程 用户线程 它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开
守护线程:
是用来服务用户线程的,
通过在 start() 方法前调用 thread.setDaemon(true) 可以把一个用户线程变成一个守护线程
Java垃圾回收就是一个典型的守护线程 main 就是一个用户线程!
若JVM中都是守护线程,当前JVM将退出
守护线程,随着用户线程消亡而消亡 守护线程和其它线程生命周期不同…

线程的生命周期:

在这里插入图片描述

线程一般具有五种状态: 创建 就绪 运行 堵塞 死亡

1.创建状态:
	//用构造函数 创建 出 
	( Thread )或实现( Runnable )类 的 类对象; 就是线程 创建; 
2.就绪状态:
	//调用了 .start(); 方法的 就是线程 就绪;
3.运行状态:	
	//线程 从就绪到运行 获得了CPU 的资源进入运行状态....失去CUP权限/yieId();就绪专题....
4.堵塞状态:
	//一个 正在运行 的线程 因某种原因不在运行时(一个线程 不能获取 CPU资源) 进入 阻塞状态....
	//常见: 1. .sleep(int); (休眠)  2.yieId(); (线程显示让出CPU资源) 3.I/O 事件阻塞...等待同步锁...
5.死亡状态:
	//线程 的run(); 方法 中代码执行完毕... 不在具有 继续运行的 能力!

JDK中用Thread.State类定义了线程的几种状态: Java线程有6中状态
在这里插入图片描述

  1. 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
  2. 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
    线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选 中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
  3. 阻塞(BLOCKED):表示线程阻塞于锁。
  4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
  5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
  6. 终止(TERMINATED):表示该线程已经执行完毕。

线程的创建:

JDK1.5之前创建新执行线程有两种方法:

  • 继承Thread类的方式
    定义子类继承Thread类
    子类中重写Thread类中的run方法
    创建Thread子类对象,即创建了线程对象
    调用线程对象start方法:启动线程,调用run方法
  • 实现Runnable接口的方式

注意:

  • 想要启动多线程,只有调用:start(); 方法
  • 多线程中的run()方法,不要手动调用,那样并不是多线程执行…只是正常的调用run() 方法调用!
    只有通过:线程对象.start(); 才是正确的多线程就绪方法, Java底层会去开启多线程调用run(); 多线程执行run() 中的步骤!
  • Java一个线程对象只能调用一次start()方法启动,如果重复调用了 则将抛出以上 的异常“IllegalThreadStateException”

继承Thread类:

在这里插入图片描述

Thread.java

package com.wsm.thread;
/**
 * 多线程的创建,方式一:继承于Thread类
 * 1. 创建一个继承于Thread类的子类
 * 2. 重写Thread类的run() --> 将此线程执行的操作声明在run()中
 * 3. 创建Thread类的子类的对象
 * 4. 通过此对象调用start()
 * <p>
 * 例子:遍历100以内的所有的偶数
 *
 */

//1. 创建一个继承于Thread类的子类
class MyThread extends Thread {
    //2. 重写Thread类的run()
    @Override
    public void run() {
        //循环输出100行数据
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                //不设置默认线程名,默认Tread-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():①启动当前线程 ② 调用当前线程的run()
        t1.start();
        //问题一:我们不能通过直接调用run()的方式启动线程。
//      t1.run();

        //问题二:再启动一个线程,遍历100以内的偶数。不可以还让已经start()的线程去执行。会报IllegalThreadStateException
//      t1.start();

        //我们需要重新创建一个线程的对象
        MyThread t2 = new MyThread();
        t2.start();

        //如下操作仍然是在main线程中执行的。
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i + "***********main()************");
            }
        }
    }
}

在这里插入图片描述
运行可以看到多线程,交替执行…

扩:匿名子类的方式创建线程

匿名类,当程序只有一处地方使用到该类,则可以创建一个匿名子类 内存用完即丢,快速回收~

创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数

public static void main(String[] args) {

   //创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历100以内的奇数
   //创建Thread类的匿名子类的方式
   new Thread(){
       @Override
       public void run() {
           for (int i = 0; i < 100; i++) {
               if(i % 2 == 0){
                   System.out.println(Thread.currentThread().getName() + ":" + i);

               }
           }
       }
   }.start();

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

                }
            }
        }
    }.start();
	
	}
}
总结:

继承 Thread 类创建线程:

//使用此方式创建线程类    
//此线程类需要继承 Thread类, 并重写类中 run()方法;
//因为Thread类中的 run()方法是,线程要执行的操作任务方法;  所以,线程要执行的操作代码需要写 在 run()方法中;   //并通过子类实例.调用 start();方法来启动线程;
操作:
	A 类 继承Thread;
	A a = new A();	//创建 A 类实例/线程
	a.start();		//启动 线程.....
	//创建两个线程就是 A a1 = new A(); 
	//每 new A(); 创建一个即每个线程都是独立类对象..只有static修饰的类属性才共享...

实现Runnable接口:

在这里插入图片描述
RunnableTest.java

package com.wsm.thread;

/**
 * 创建多线程的方式二:实现Runnable接口
 * 1. 创建一个实现了Runnable接口的类
 * 2. 实现类去实现Runnable中的抽象方法:run()
 * 3. 创建实现类的对象
 * 4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
 * 5. 通过Thread类的对象调用start()
 *
 * 比较创建线程的两种方式。
 * 开发中:优先选择:实现Runnable接口的方式
 * 原因:1. 实现的方式没有类的单继承性的局限性
 *      2. 实现的方式更适合来处理多个线程有共享数据的情况。
 *
 * 联系:public class Thread implements Runnable
 * 相同点:两种方式都需要重写run(),将线程要执行的逻辑声明在run()中。
 */
//1. 创建一个实现了Runnable接口的类
class MThread implements Runnable {

    //2. 实现类去实现Runnable中的抽象方法: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 RunnableTest{
    public static void main(String[] args) {
        //3. 创建实现类的对象
        MThread mThread = new MThread();
        //4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(mThread);
        t1.setName("线程1");
        //5. 通过Thread类的对象调用start():① 启动线程 ②调用当前线程的run()-->调用了Runnable类型的target的run()
        t1.start();

        //再启动一个线程,遍历100以内的偶数
        Thread t2 = new Thread(mThread);
        t2.setName("线程2");
        t2.start();
    }
}

在这里插入图片描述

  • 上面的线程:t1 和 t2 都是同一个Runnable实现类对象创建的,类内部的数据是可以共享的!
  • 这里没有表现出,后面举例…

总结:

实现Runnable接口:

//因为 Java是单继承的 
//所以如果 一个类继承一个类,又要是 线程类,所以:还可以实现接口来 创建线程了;
//Runnable 
//在java.lang; 包中 其中 有一个 抽象方法 run(); 
//通过实现 Runable 接口重写run(); 方法,方法中写代码完成线程要完成操作;
操作: 
	A 实现了 Runnable接口;
	A  a = new A();			   //创建 A类对象 a;
	Thread xc = new Thread(a); //创建 线程 xc;
	xc.start();				   // 启动线程;
	....................		

注意:
	//如果想要在创建一个线程就不需要 
	new A();
	//只需要在 Thread xc1 = new Thread(a); 即创建新的线程,但每次参数都是实例 a,即创建线程的属性值共享;
	//不想共享某些属性,可以重新 
	A a1 new A();  
	Thread n = new Thread(a1); 
	//当然也可以使用:匿名内部类:匿名子类/匿名实现类形式, 调用启动线程; 前提这个线程只用一次;	

JDK5.0 新增创建方式 👍

实现Callable接口:

多线程计算,100以内偶数和
在这里插入图片描述
CallableTest.Java

package com.wsm.thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * 创建线程的方式三:实现Callable接口。 --- JDK 5.0新增
 *
 * 如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
 * 1. call()可以有返回值的。
 * 2. call()可以抛出异常,被外面的操作捕获,获取异常的信息
 * 3. Callable是支持泛型的
 */
//1.创建一个实现Callable的实现类
//class NumThread implements Callable<Integer> {            //Callable<T> 支持泛型: 实现的call返回值类型,就得是 <T> 泛型指定的类型!
class NumThread implements Callable {
//2.实现call方法,将此线程需要执行的操作声明在call()中
    @Override
//  public Integer call() throws Exception {                //Integer 可以和 Object 进行自动类型转换!
    public Object call() throws Exception {                 //“默认 Object类型”;
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }

}

public class CallableTest {
    public static void main(String[] args) {
        //3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        //4.将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
            //FutureTask<T>         支持泛型
            //futureTask.get();     返回的就是 <T> 泛型类型, “默认 Object类型”;
        FutureTask<Integer> futureTask = new FutureTask<Integer>(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();
        }
    }
}

在这里插入图片描述

总结:

实现Callable 接口:

与使用Runnable相比, Callable功能更强大些
	相比run()方法,可以有返回值
	方法可以抛出异常
	支持泛型的返回值
	需要借助 FutureTask 类,比如获取返回结果;
	
Futrue 接口:
	可以对具体RunnableCallable任务的执行结果进行取消、查询是 否完成、获取结果等
	FutrueTaskFutrue接口的唯一的实现类...
	
FutureTask 实现类:
	FutureTask 同时实现了Runnable, Future接口
	它既可以作为 Runnable被线程执行, 又可以作为 Future得到 Callable的返回值;
	
操作:
	A 实现了 Callable接口, "重写call方法()";
	A a = new A();							"创建A类对象"
	
	将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
	FutureTask<Objetc> futureTask = new FutureTask<Objetc>(a);FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
	new Thread(futureTask).start();
	...
	
	之后还可以通过 Future接口实现类接口, 调用方法();
	 Object sum = futureTask.get();			"方法获取方法的返回值!"

线程池创建线程: 🙃 开发中常用~

后面单独写一个文章…


窗口买票Demo:线程不安全

Thread 窗口买票Demo 线程不安全

在这里插入图片描述

ThreadWindow.java

package com.wsm.thread;
//继承Thread类完成窗口买票!

/**
 * 例子:创建三个窗口卖票,总票数为100张.使用继承Thread类的方式
 * 存在线程的安全问题,待解决。
 */
class Window extends Thread{
    /* 使用 static修饰和不用static修饰 */
    //private int ticket = 100;
    private static int ticket = 100;
    /**static
     *      static 修饰的属性,属于类,该类的所有的对象都共享
     *      因为, 继承Thread 实现线程,没一次new 创建线程对象,内部属性不是static修饰, 每次都是一个新的对象100 这样三个窗口就相当于都有100个票
     *      而, 不是一共由100 张票的了!
     */
    @Override
    public void run() {
        while(true){
            if(ticket > 0){
                System.out.println(getName() + ":卖票,票号为:" + ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}

public class ThreadWindow {
    public static void main(String[] args) {
        Window t1 = new Window();
        Window t2 = new Window();
        Window t3 = new Window();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

使用Static 与 不使用Static
在这里插入图片描述

  • 没有使用 static ,三个窗口都相当于有100 张票 大家各买各的!
  • 使用了 static 修饰,类的属性,属于类所有,三个new 线程对象共享 static 修饰的票数 三个窗口一共100张票
  • 至于出现重票问题!

Runnable 窗口买票Demo 线程不安全

Runnable 可以实现多线程类数据共享!
在这里插入图片描述

RunnableWindow.java

package com.wsm.thread;
//实习Runnable接口,实现窗口买票!

/**
 * 例子:创建三个窗口卖票,总票数为100张.使用实现Runnable接口的方式
 * 存在线程的安全问题,待解决。
 */
class Window1 implements Runnable{

    //Runnable实现线程,内部数据可以共享!
    private int ticket = 100;
    @Override
    public void run() {
        while(true){
            if(ticket > 0){
                System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}

public class RunnableWindow {
    public static void main(String[] args) {
        //创建Runnable 实现类
        Window1 w = new Window1();
        //根据w 创建三个线程,因为都是用的是通一个对象w 所以,内部的实例数据共享! 不需要static修饰可以实现票共享!
        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();
    }
}

在这里插入图片描述

重票问题:

RunnableWindow举例, (ThreadWindow.java也一样)

  • 虽然,我们使用Runnable实现方式 Therad 中使用 static修饰,使多线程数据共享: 三个窗口共买100个票
  • 但也正是因为数据共享导致线程不安全!
    多线程共享数据,因为三个线程会进行资源抢占,A执行中突然停止,B来执行也停止,C进来…
    如上:
    A线程正在执行买第一张票 100,判断票数大于0进入,并将票100打印 还没有进行 --减减
    B线程进来了,因为 票还没有 -- 则,输出 100
    C同上!

线程同步解决线程安全问题:买票

多线程共享数据引发线程安全问题:

  • 一个线程类如果是通过 Runnable 接口实现:
    那么,类中属性有可能被多个类线程对象共享,就可能会引发线程不安全问题;
  • 继承Thread类中的 static属性也是数据共享的
  • eg:
    A B微信绑定同一张银行卡余额500, A取500,B取500 (程序判断余额够500 够就取到并余额-500)
  • 同步异常:
    A取判断余额够500进入条件成立可以取钱,这时A线程结束了余额还未减,
    B执行条件符合也可以取钱了,A和B都取了500,银行哭了…

线程同步解决:

当 A 符合条件开始取钱时,其它线程不可以参与进来,直到A操作完之后,其它线程才可以进来 即使 A线程阻塞也不可以改变; 必须A执行完毕;
采用: 线程同步来控制多线程执行 (两种方式:

Synchronized 同步代码块:

在这里插入图片描述
ThreadWindow.Java

package com.wsm.sync;
/* 使用: 同步代码块; */

/**
 * 例子:创建三个窗口卖票,总票数为100张.使用继承Thread接口的方式
 *
 * 1.问题:卖票过程中,出现了重票、错票 -->出现了线程的安全问题
 * 2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。
 * 3.如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他
 *            线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
 *
 * 4.在Java中,我们通过同步机制,来解决线程的安全问题:
 *  方式一:同步代码块
 *
 *   synchronized(同步监视器){
 *      //需要被同步的代码
 *   }
 *  说明:1.操作共享数据的代码,即为需要被同步的代码。  -->不能包含代码多了,也不能包含代码少了。
 *       2.共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
 *       3.同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。
 *          要求:多个线程必须要共用同一把锁。
 *       补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。 `但对于继承,Thread的类,不建议使用this!!`
 *  方式二:同步方法。
 *     如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。
 *  5.同步的方式,解决了线程的安全问题。---好处
 *    操作同步代码时,只能有一个线程参与,其他线程等待。
 *    相当于是一个单线程的过程,效率低。(很多时候,安全大于一切!) ---局限性;
 */
class Window extends Thread{

    private static int ticket = 100;

    @Override
    public void run() {
        while(true){
            synchronized (Window.class){
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
    
}

public class ThreadWindow {
    public static void main(String[] args) {
        Window t1 = new Window();
        Window t2 = new Window();
        Window t3 = new Window();

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

在这里插入图片描述
ok,执行线程安全了!
Synchronized 同步代码块内的, 多线程操作会一步步的执行…一次只有一个线程可以操作!

Synchronized () 代码块:

同步代码块:synchronized (:同步  读:C抠奈zs
使用: synchronized 关键字修饰代码块,:"同步代码块"

写法: 
	synchronized(同步建视器){ 需要同步的代码.... }; 
	//操作共享的数据即为,需要被同步的代码:
	//卡余额500就是共享数据,线程A B都可以访问...且互相影响; 把存在这些共享数据的操作都包裹在 synchronized(Object){ 代码块 };

同步监视器: (俗称: 锁🔒
	"锁",可以是任何对象都可以,但必须得有,
	注意:
		多线程情况下,要求多个线程用的是同一把锁(对象)  `多个人轮流带钥匙去仓库拿东西,仓库的锁得是同一个锁🔒,不然打不开门🚪!`
		
		实现Runnable接口,的多线程可以使用: this "因为Runnable实现类对象可以创建多个线程,this表示的是同一个对象!"
		继承Thread,可以使用: static静态对象 或 类.class; 确保是同一个对象()  "继承Therad的实现类,没new 就是一个新的线程,this表示的是不同的对象!"

Synchronized 同步方法:

在这里插入图片描述

RunnableWindow.java

package com.wsm.sync;
/* 使用: 同步方法解决; */
class Window1 implements Runnable {

    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            show();
        }
    }

    private synchronized void show(){//同步监视器:this
        //synchronized (this){
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
            ticket--;
        }
        //}
    }

}

public class RunnableWindow {
    public static void main(String[] args) {
        Window1 w = new Window1();

        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();
    }
}

在这里插入图片描述

同步代码方法 Synchronized

同步代码方法
	其实就是在: 同步代码块的基础上,把同步代码块,定义成了个方法...让重写run(); 内部调用..

写法: 
	public synchronized void 方法名(){ ..需要同步的代码.. }; 
	//Runnable实现,没有指定 锁(对象),其实就是默认this...
	public static synchronized void 方法名(){ ..需要同步的代码.. };
	//Thread  继承,没有指定 锁(对象),方法是静态的了,就是默认锁为:类.Class 为当前的类对象了;

注意:

  • public synchronized void 方法名(){ ..需要同步的代码.. };
    Runnable实现,没有指定 锁(对象),其实就是默认this…
  • public static synchronized void 方法名(){ ..需要同步的代码.. };
    Thread 继承,没有指定 锁(对象),方法是静态的了,就是默认锁为:类.Class 为当前的类对象了;

Lock 锁:

在这里插入图片描述
RunnableLock.Java

package com.wsm.lock;
import java.util.concurrent.locks.ReentrantLock;

/* RunnableLock 实现Lock锁 */
class Window implements Runnable{
    private int ticket = 100;
    //1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while(true){
            try{
                //2.调用锁定方法lock()
                //以下的操作,只能允许单线程同行操作... 一个线程调用lock() 除非调用 unlock() 不然其它线程,不能在进入锁执行!
                lock.lock();
                if(ticket > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }finally {
                //3.调用解锁方法: unlock()
                lock.unlock();
            }
        }
    }
}

public class RunnableLock {
    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();
    }
}

Lock

 Lock()
 	从JDK 5.0开始,Java提供了更强大的线程同步机制
 	通过显式定义:同步锁对象来实现同步
 	同步锁使用Lock对象充当

LockJDk5.0 新增的一个接口  Java.util.concurrent.locks.Lock
是控制多个线程对共享资源进行访问的工具
	锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象 加锁, "线程开始访问共享资源之前应先获得Lock对象"

ReentrantLock类实现了Lock接口 (:重入 读:瑞啊吹特

语法:
	// 创建 ReentrantLock类对象;
	ReentrantLock lock = new ReentrantLock();
	//对象调用.lock();方法手动给指定代码上锁, 改对象调用之后,下面的方法只允许单线程通行了 ”一次只允许一个线程执行...“
	lock.lock();
	...需要被同步的代码... 
	//对象调用.unlock();方法手动开锁:  ”其它线程才可以进行改lock() 进行使用!“
	lock.unlock(); 

注意:
	//因为,
	lock(); 	手动上锁
	unlock(); 	需要 unlock(); 手动关锁,不然锁资源一直不释放会导致死锁 🔒
	//为了避免这个问题,建议使用 try-finall 保证, 锁资源的回收
	
	ReentrantLock lock = new ReentrantLock();
	try{
		lock.lock();
		...需要被同步的代码... 
	}finally(
		lock.unlock(); 	
	)

lock 锁挺好用的, 只需要将 数据共享的操作代码 进行 lock() unlock() 即可~
因为:没有synchronized 所以也没有 wait() 不用考虑… 最开始学习时候,看的多了,总是搞混..记乱感觉很难!

synchronized 与 Lock的异同?

相同:

  • 二者都可以解决线程安全问题

不同:

  • synchronized机制在执行完相应的同步代码以后, 自动的释放同步监视器
  • Lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())

Lock Synchronized代码块 Synchronized方法 执行顺序

Lock > Synchronized代码块 > Synchronized方法

  • Lock 直接在方法内部编写调用…
  • Synchronized代码块 还需要创建 代码块, 局部变量…
  • Synchronized方法 创建方法, 还得调用…

线程的通信:😉

线程的通信,别被这个名词给吓到了, 归根结底就是多个线程之间的 操作共享数据,数据安全的一个通信过程…

使用两个线程打印 1-100。线程1, 线程2 交替打印

这里使用 synchronized 来进行线程通信,lock 有自己的方式…


CommunicationTest.Java

package com.wsm.threadsync;

/**
 * 线程通信的例子:使用两个线程打印 1-100。线程1, 线程2 交替打印
 *
 * 涉及到的三个方法:
 * wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
 * notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就唤醒优先级高的那个。
 * notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
 *
 * 说明:
 * 1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
 * 2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。
 *    否则,会出现IllegalMonitorStateException异常
 * 3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
 *
 * 面试题:sleep() 和 wait()的异同?
 * 1.相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。
 * 2.不同点:1)两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
 *          2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
 *          3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
 */
class Number implements Runnable{

    private int number = 1;

    private Object obj = new Object();
    @Override
    public void run() {
        while(true){
            synchronized (obj) {
                //随机唤醒一个级别高的线程
                obj.notify();

                if(number <= 100){
                    try {
                        //使当前线程休眠,但是不释放锁,其它线程任然在 synchronized 锁,外面等待...  (需要处理异常!
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //打印 ++
                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;

                    try {
                        //使得调用如下wait()方法的线程进入阻塞状态, 但会释放锁, 其它线程可以进入方法... 需要 notify() notifyall() 才能唤醒!
                        //作用:
                            //为了实现, 多个线程互相通信,A线程休眠,进入锁,并唤醒阻塞线程,让他完成之后的步骤,因为我进入锁了,其它线程又进不来了...
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else{
                    break;
                }
            }
        }
    }

}

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

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

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

在这里插入图片描述
交替执行,并操作共享数据!

wait() 与 notify() 和 notifyAll()

  • 这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报: Java.lang.IllegalMonitorStateException异常
  • 因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁 因此这三个方法只能在Object类中声明!
  • 所有类都继承Object 所有类都又这个方法!

wait():

  • 在 synchronized代码块/方法中调用 使当前线程进入 阻塞阶段 并释放锁🔒
  • 使别的线程可访问并修改共享资源, 当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒
  • 唤醒后等待重新获得 CPU时间片 对监视器的所有权后才能继续执行。

notify():

  • 随机唤醒 唤醒正在排队等待同步资源的线程中优先级最高者结束等待

notifyAll ():

  • 唤醒正在排队等待资源的所有线程结束等待

解释上述代码:

线程通信小Demo
	开启两个线程,循环输出 100 内的数字
	创建:线程A 线程B
	
1.线程A进行锁🔒, 立刻调用 notify(); 唤醒一个堵塞线程!
2.判断数字是否还在100~进入: if{}
3.线程A休眠sleep() 10毫秒~
4.打印当前数 并 ++
5.调用 wait(); 线程进入阻塞,并释放了锁... (为了,让线程B 获得锁...

6.B获得锁: 因为A "进入了阻塞,并释放了锁" B获得锁"立刻调用 notify(); 唤醒了堵塞A线程!"
7.B执行了: sleep() 进入了休眠~ "但,sleep() 线程进入阻塞,持有锁...A还是进不来~"
8.B调用 wait(); 线程进入阻塞,并释放了锁...
9.A获得了锁,一次循环执行结束...开始下次循环

10.A唤醒B
11.A休眠 不释放锁...
12.A打印 ++
13.A阻塞 释放锁
14.B获得锁,继续执行 🙃

一直循环, 值则跳出~while() run() 方法结束!
只要记住!

  • wait(); 会使当前线程阻塞,并释放锁🔒! 线程调用了 wait() 方法之后,它就不在执行了! 并且也没有了锁🔒!
    就处于休眠状态:只有其它线程调用notify() / notifyall() 才能恢复!
  • 带锁的方法/代码块 一次只能有一个线程使用!
  • 一般将,需要共享操作数据的代码进行上锁! 避免多线程同时操作问题!

sleep() 和 wait()的异同?

相同点:

  • 一旦执行方法,都可以使得当前的线程进入阻塞状态

不同点:

  • 两个方法声明的位置不同:
    Thread类中声明sleep()
    Object类中声明wait()
  • 调用的要求不同:
    sleep()可以在任何需要的场景下调用
    wait()必须使用在同步代码块或同步方法中
  • 关于是否释放同步监视器:
    如果两个方法都使用在同步代码块或同步方法中
    sleep()不会释放锁,wait()会释放锁。
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java.慈祥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值