Java多线程基础总结

发布于:2025-08-13 ⋅ 阅读:(18) ⋅ 点赞:(0)


在这里插入图片描述

一、Java多线程实现方式:

多线程需要线程体,线程体有继承要求:

  • Thread类
  • Runnable接口
  • Callable接口(JDK 1.5后支持)
  • 使用线程池

二、Java多线程实现步骤:

1. 通过类实现Thread类:

java.lang.Thread 是一个负责线程操作的类,任何的类只要继承了Thread类就可以成为一个线程的主类,同时线程类中需要明确覆写父类中的run()方法(方法定义:“public void run()”),这样当产生了若干个线程类对象时,这些对象就会并发执行run()方法中的代码。

实现步骤

  1. 自定义一个类继承Thread 类,这个类也叫(线程类)。
  2. 重写run() 方法,编写线程执行体(就是在 重写的run() 方法中编写我们要处理的业务代码)。
  3. 创建我们定义的线程类对象,并用该对象调用 start()方法启动线程。
/**
 * Description:
 *
 * @Author Js
 * @Create 2025-08-12 21:35
 * @Version 1.0
 */
public class StrartThread extends Thread {
    
    //线程入口点
    @Override
    public void run() {
       //线程体
        for (int i = 0; i < 20; i++) {
            System.out.println("我在执行线程任务" + i);
        }
    }
}

//---------------------------
/**
 * Description:
 *
 * @Author Js
 * @Create 2025-08-12 21:34
 * @Version 1.0
 */
public class Main {
    public static void main(String[] args) {
        //创建线程对象
        StrartThread strartThread = new StrartThread();
        //启动线程
        strartThread.start();

    }
}
1.1、注意:
同一个线程对象只能调用一次 start()方法,否则就会报:
    Exception in thred "main" java.lang.IllegalThreadStateException 异常
   
1.2、多线程启动运行过程:

在这里插入图片描述

1.3、重要结论:

重要结论:以后只是要进行Java多线程的启动,必须强制性的使用Thread类中的Start() 方法进行启动

2. 通过Runnable接口实现多线程:

使用Thread类的确是可以方便的进行多线程的实现,但是这种方式最大的缺点就是单继承局限,为此在java之中也可以利用Runnable接口来实现多线程,此接口的定义如下:

@FunctionalInterface	// 从JDK 1.8引入了Lambda表达式之后就变为了函数式接口
public interface Runnable {
	public void run​() ;//线程的方法
}

2.1、传统方法:

步骤如下

  1. 定义 Runnable接口 的实现类并重写该接口的 run()方法,该run()方法的方法体同样是该线程的线程执行体。
  2. 创建 Runnable实现类的实例,并以此实例作为Thread的 target参数来创建Thread对象,该Thread对象才是真正 的线程对象。new Thread(Runnable实现类的实例)
  3. 调用线程对象的start()方法,启动线程。new Thread(Runnable实现类的实例).start()

在这里插入图片描述

代码如下

package com.js.thread;
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
        }
    }
}

此时虽然定义了一个线程主体类,同时这个类本身也避免了 Thread 继承时的单继承局限,但是会存在有另外的一个问题:按照多线程的实现机制来讲,所有的多线程一定要通过 Thread 类的 stat()方法才可以启动,而现在如果实现了 Rumnable 接口,则这个接口没有 start()方法。
通过 Thread 类的源代码可以发现,Thread 类实现Runable 接口,并且方法参数中可以接收 Runable 接口对象,同时在 Thread 类的内部也提供有一个 Rumnable 接口对象名称为“target”,而通过 Thread 类内部的原生的 run()方法可以发现,如果此时 target 属性不为空,则调用的是 target 对象的 run() 方法

测试类

package com.js.thread;

public class TestMyRunnable {
    public static void main(String[] args) {
        //创建Runnable实现类对象
        MyRunnable mr = new MyRunnable();
        //创建线程对象
        Thread t = new Thread(mr, "长江");//Thread 类中可以接收 Runable 接口对象
        t.start();
        for (int i = 0; i < 20; i++) {
            System.out.println("黄河 " + i);
        }
    }
}

通过实现Runnable接口,使得该类有了多线程类的特征。所有的分线程要执行的代码都在run方法里面。
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。

实际上,所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现 Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。

说明:Runnable对象仅仅作为Thread对象的target,Runnable实现类里包含的run()方法仅作为线程执行体。而实际的线程对象依然是Thread实例,只是该Thread线程负责执行其target的run()方法。

2.2、使用匿名内部类对象来实现线程的创建和启动

该接口是在JDK 1.0 的时候提供的,同时在JDK 1.8之后支持了 Lambda 表达式,用户只需要去实现该接口,并且覆写run()方法就可以实现线程主类的定义:

public class ThreadDemo {
	public static void main(String[] args) {
		for (int x = 0; x < 3; x++) {
			String title = "线程对象-" + x;
			new Thread(() -> {// Lambda实现线程体
				for (int y = 0; y < 10; y++) {
					System.out.println(title + "运行,y = " + y);
				}
			}).start();	// 启动线程对象
		}
	}
}

3. 对比 Thread 与 Runnable 两种方式:

3.1、联系

分析可以发现 Thread 类和 Runmable 之间彼此是有联系的,而且所有的多线程的对象,都建议通过 Runnable 接口来实现,同时只要是多线程的启动那么一定要通过 Thread 类来完成,于是这个时候来观察一下当前的 Thread 类的定义:。

public class Thread extends Object implements Runnable.

通过 Thread 类的继承结构可以非常清楚的发现,其实是 Runmable 接口子类,同时如果要想进行多线程的启动,还需要将 Runmable接口的子类对象实例传递到Thread类中

3.2、线程继承结构:

在这里插入图片描述

3.3、区别
  • 继承Thread:线程代码存放Thread子类run方法中。

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

3.4、实现Runnable接口比继承Thread类所具有的优势
  • 避免了单继承的局限性
  • 多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
  • 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
  • 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类。

4. 实现Callable接口

  • 从JDK 1.5开始对于多线程的实现提供了一个新的接口:java.util.concurrent.Callable,此接口定义如下:
@FunctionalInterface
public interface Callable<V> {
	public V call() throws Exception ;
}

在这里插入图片描述

  • 与使用Runnable相比, Callable功能更强大些
    • 相比run()方法,可以有返回值
    • 方法可以抛出异常
    • 支持泛型的返回值
    • 需要借助FutureTask类,比如获取返回结果
  • Future接口
    • 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
    • FutureTask是Futrue接口的唯一的实现类
    • FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值

在这里插入图片描述
步骤如下

第一种方式

  1. 实现Callable接口,需要定义返回值类型。
  2. 重写call方法,需要抛出异常。
  3. 创建Callable接口实现类的对象numThread
  4. 创建执行服务: FutureTask futureTask = new FutureTask(numThread);
  5. 提交执行:new Thread(futureTask).start();
  6. 获取结果: futureTask.get();

第二种方式

  1. 实现Callable接口,需要定义返回值类型。
  2. 重写call方法,需要抛出异常。
  3. 创建Callable接口实现类的对象numThread
  4. 创建执行服务,使用线程池: ExecutorService ser = Executors.newFixedThreadPool(1);
  5. 通过线程池中的线程提交执行任务:Future<Boolean> result1 = ser.submit(numThread);
  6. 获取结果: boolean r1= result1.get();
  7. 关闭服务:ser.shutdownNow();

代码举例:

/*
 * 创建多线程的方式三:实现Callable (jdk5.0新增的)
 */
	//1.创建一个实现 Callable 的实现类
	class NumThread implements Callable<int> {//需要定义返回值类型
	//2.重写 call方法,将此线程需要执行的操作声明在call()方法中
	    @Override
	    public int call() throws Exception {
	        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 futureTask = new FutureTask(numThread);
        //5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
        new Thread(futureTask).start();


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

}

5. 使用线程池

现有问题:

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

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

思路:提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

在这里插入图片描述

好处:

  • 提高响应速度(减少了创建新线程的时间)

  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)

  • 便于线程管理

    • corePoolSize:核心池的大小
    • maximumPoolSize:最大线程数
    • keepAliveTime:线程没有任务时最多保持多长时间后会终止

线程池相关API

  • JDK5.0之前,我们必须手动自定义线程池。从JDK5.0开始,Java内置线程池相关的API。在java.util.concurrent包下提供了线程池相关API:ExecutorServiceExecutors
  • ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
    • void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
    • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行Callable
    • void shutdown() :关闭连接池
  • Executors:一个线程池的工厂类,通过此类的静态工厂方法可以创建多种类型的线程池对象。
    • Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
    • Executors.newFixedThreadPool(int nThreads); 创建一个可重用固定线程数的线程池
    • Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
    • Executors.newScheduledThreadPool(int corePoolSize):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

代码举例:

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

class NumberThread1 implements Runnable{

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

class NumberThread2 implements Callable {
    @Override
    public Object call() throws Exception {
        int evenSum = 0;//记录偶数的和
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                evenSum += i;
            }
        }
        return evenSum;
    }

}

public class ThreadPoolTest {

    public static void main(String[] args) {
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//        //设置线程池的属性
//        System.out.println(service.getClass());//ThreadPoolExecutor
        service1.setMaximumPoolSize(50); //设置线程池中线程数的上限

        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        service.execute(new NumberThread1());//适合适用于Runnable

        try {
            Future future = service.submit(new NumberThread2());//适合使用于Callable
            System.out.println("总和为:" + future.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
        //3.关闭连接池
        service.shutdown();
    }

}

三、多线程运行状态:

在整个的计算机操作系统之中所有的进程是受到操作系统的控制的,同时每一个进程都有自己的生命周期,而在进程之中的线程也存在有类似的生命周期。

在这里插入图片描述

1. [创建状态]:

在 Java 程序里面如果要进行多线程对象创建则一定要有线程的主体类,同时要通过关键字 new 进行 Thread 类的实例化对象的创建,等价于如下的代码执行:

Thread thread = new Thread(myThread) :

2. [就绪状态] :

当线程对象创建完成之后肯定需要进行线程的启动,而线程的启动肯定要通过 Thread 类的内部提供的 start0方法来完成,但是调用 start0方法的时候并不是立刻就进行多线程的执行的,它本身需要等待 CPU 的调度;

3. [执行状态] :

当系统为线程分配了相关的硬件资源之后,所有的线程将按照其定义的核心业务的功能进行执行,但是这个执行并不是一直执行的,而是需要进行资源的抢占,运行一段时间之后一个线程就会让出当前的资源,而后重新等待调度;

4. [阻塞状态] :

当某一个线程对象让出了当前的资源之后,那么该线程对象将进入到一种阻塞状态(此时线程之中未完成的代码暂时不执行了),其他的线程继续完成自己先前未完成的任务,除了这种自动的系统级控制之外,也可以利用 Thread 类之中的些线程的控制方法来进行线程的阻塞操作;

5. [终止(死亡)状态] :

当线程的方法全部执行完毕之后,该线程就将释放掉所占用的全部资源,并且结束全部的执行。
在这里插入图片描述


四、线程操作方法:

1.线程的命名和取得:

方法 类型 描述
public Thread(Runnable target, String name) 构造 实例化线程对象,接收Runnable接口子类对象,同时设置线程名称
public final void setName(String name) 普通 设置线程名字
public final String getName() 普通 取得线程名字

​ 但是在获取线程名称的时候需要有一点注意:所有的线程都是不固定的执行状态,可以获得的线程名称一般都属于当前正在运行的线程对象,而要想获取当前正在运行的线程对象,就必须依靠 Thread 类里面提供的一个重要方法:

public static Thread currentThread0 该方法使用 static 定义,可以直接通过类名称进行调用。
实例:
package com.js;

public class RunnableTest {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                System.out.println("当前线程的名称为:"+Thread.currentThread().getName());
            },"线程对象的名称为:-"+i).start();
        }
    }
}

在这里插入图片描述

​ 通过“Thread.currentThread0.getName ( ) 代码可以直接在方法的内部来获取当前执行的线程对象的名称,这个操作的方法的返回的线程对象是动态的返回结果,但是这个时候的线程对象名称都是手工命名的,那么如果说此时没有定义线程名称呢?

实例:
package com.js;

public class RunnableTest {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                System.out.println("当前线程的名称为:"+Thread.currentThread().getName());
            }).start();
        }
    }
}

在这里插入图片描述

默认情况下所返回的线程名称采用了“Thread-xxx”的形式进行定义,而且所有的对象都会有一个自动增长的数据编号

范例:线程的命名操作
package cn.mldn.demo;
class MyThread implements Runnable {
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName());// 当前线程名称
	}
}
public class ThreadDemo {
	public static void main(String[] args) throws Exception {
		MyThread mt = new MyThread();
		new Thread(mt, "线程A").start(); // 设置了线程的名字
		new Thread(mt).start(); 	// 未设置线程名字
		new Thread(mt, "线程B").start(); // 设置了线程的名字
	}
}

2.线程休眠 sleep:

方法 类型 描述
public static void sleep(long millis) throws InterruptedException 静态 设置线程休眠的毫秒数,时间一到自动唤醒
public static void sleep(long millis, int nanos) throws InterruptedException 静态 设置线程休眠的毫秒数与纳秒数,时间一到自动唤醒
列子:
public class ThreadDemo {
	public static void main(String[] args) throws Exception {
		Runnable run = () -> {	// Runnable接口实例
			for (int x = 0; x < 10; x++) {
				System.out.println(Thread.currentThread().getName() + "、x = " + x);
				try {
					Thread.sleep(1000); // 暂缓1秒(1000毫秒)执行
				} catch (InterruptedException e) {// 强制性异常处理
					e.printStackTrace();
				}
			}
		} ;
		for (int num = 0; num < 5; num++) {
			new Thread(run, "线程对象 - " + num).start();// 启动线程
		}
	}
}

当程序执行的时候可以发现,对于当前的线程执行过程之中,每一次休眠的时候可以发现整个的休眠操作好像是所有的线程部一起进入到了休眠,到了休眠的时间之后会一起进行自动的唤醒,但是这其中实际广是会存在有先后顺序的。

在这里插入图片描述

在这里插入图片描述

​ 在多线程编程的实现机制里面进入到 run0方法的线程的个数可能非常的多,但是所有的线程一旦遇见了休眠之后将按照各自时间进行休眠处理,由于线程的执行速度非常的快,所以从整个的执行上来讲就会觉得好像是若工个线程一起休眠一起唤醒。

run方法是主体方法,主体方法在所有线程启动的时候,都将执行,但是里面的方法体,是根据每个线程的执行顺序,而有所不同,进行最终的调度的

3.线程的中断 interrupt

按照正常的设计来讲,每一个线程只要启动了,就会按照既定的代码方式从头到尾依次执行,如果线程的方法体执行完毕之后,那么整个的线程就将停止正常的执行。

在这里插入图片描述

一个线程的是无法自己进行中断,一定是由其他线程引起到

No. 方法 类型 描述
1 public boolean isInterrupted() 普通 判断线程是否被中断
2 public void interrupt() 普通 中断线程执行

所有的线程中断一旦产生,那么一定会产生有一个与之匹配的 InterruptedException,实际上这个异常在 sleep0方法上面都存在有自动的抛出,那么意味如果休眠时间不到的时候,并且中断了该线程,则就会抛出此异常。

列:
public class ThreadDemo {
	public static void main(String[] args) throws Exception {
		Thread thread = new Thread(() -> {
			System.out.println("【BEFORE】准备睡觉10秒钟的时间,不要打扰我");
			try {
				Thread.sleep(10000); // 预计准备休眠10秒
				System.out.println("【FINISH】睡醒了,开始工作和学习生活");
			} catch (InterruptedException e) {
				System.out.println("【EXCEPTION】睡觉被打扰了,坏脾气像火山爆发一样袭来 ");
			}
		});
		thread.start(); // 线程启动
		Thread.sleep(1000); // 保证子线程先运行1秒
		if (!thread.isInterrupted()) {// 该线程中断?
			System.out.println("【INTERRUPT】敲锣打鼓欢天喜地的路过你睡觉的地方!");		
			thread.interrupt(); // 中断执行
		}
	}
}

4.线程的强制执行 joini:

​ 在正常的执行状态下,每一个线程执行的过程之中都需要轮流进行资源的抢占,那么抢占到资源之后才可以正常的执行,但是如果说现在某一个线程非常的重要,需要强制性的霸占资源执行处理,这个时候就需要使用 join()方法进行控制。

方法名称 类型 描述
public final void join() throws InterruptedException 此方法一旦调用,那么此线程将持续执行到结束
public final void join(long millis) throws InterruptedException 设置一个线程的强制执行占用时间,如果超过了此时间也要释放掉资源
public final void join(long millis, int nanos) throws InterruptedException 设置一个线程的强制执行占用时间,如果超过了此时间也要释放掉资源

在当前给出的强制执行的处理方法里面可以发现所有的操作都会抛出有-“InterruptedException有的线程的强制运行是可以被中断的,使用interrupt0方法完成中断处理
异常信息,那么就证明所

在这里插入图片描述

5.线程礼让yield():

在多线程的执行过程之中,所有的线程肯定要轮流进行 CPU 资源的抢占,那么既然有这样的抢占处理,每当有一个线程抢占到了资源之后,可以通过一种礼让的形式让出当前抢占的资源,如果要想实现这种礼让操作可以使用 yield()方法完成:

Thread.yield():

6.线程优先级:

在Java的线程操作中,所有的线程在运行前都会保持在就绪状态,那么此时,那个线程的优先级高,那个线程就有可能会先被执行

No. 方法或常量 类型 描述
1 public static final int MAX_PRIORITY 常量 最高优先级,数值为10
2 public static final int NORM_PRIORITY 常量 中等优先级,数值为5
3 public static final int MIN_PRIORITY 常量 最低优先级,数值为1
4 public final void setPriority(int newPriority) 普通 设置线程优先级
5 public final int getPriority(); 普通 取得线程优先级
public class ThreadDemo {
	public static void main(String[] args) throws Exception {
		Runnable run = () -> {// 线程类对象
			for (int x = 0; x < 10; x++) {
				try {
					Thread.sleep(1000);// 暂缓执行
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "执行。");
			}
		};
		Thread threadA = new Thread(run, "线程对象A");// 线程对象
		Thread threadB = new Thread(run, "线程对象B");// 线程对象
		Thread threadC = new Thread(run, "线程对象C");// 线程对象
		threadA.setPriority(Thread.MIN_PRIORITY);// 修改线程优先级
		threadB.setPriority(Thread.MIN_PRIORITY);// 修改线程优先级
		threadC.setPriority(Thread.MAX_PRIORITY);// 修改线程优先级
		threadA.start();	// 线程启动
		threadB.start();	// 线程启动
		threadC.start();	// 线程启动
	}

在这里插入图片描述

7.线程同步与死锁

在这里插入图片描述

同步问题引出:
class MyThread implements Runnable {	// 定义线程执行类
	private int ticket = 6;	// 总票数为6张
	@Override
	public void run() {
		while (true) {	// 持续卖票
			if (this.ticket > 0) {// 还有剩余票
				try {
					Thread.sleep(100);// 模拟网络延迟
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + 
					"卖票,ticket = " + this.ticket--);
			} else {
				System.out.println("***** 票已经卖光了 *****");
				break;	// 跳出循环
			}
		}
	}
}

在这里插入图片描述

在这里插入图片描述

​ 此时由于休眠的出现,导致了票数修改的操作延迟调用,那么其余的线程也就可以正常的通过当前的剩余票数的判断,当休民时间一到,所有的线程都要在原始的票数上进行减少的操作,最终的结果就会数据的修改错误.

线程同步操作

​ 造成并发资源访问不同步的主要原因在于没有将若干个程序逻辑单元进行整体性的锁定,即:当判断数据和修改数据时只允许一个线程进行处理,而其它线程需要等待当前线程执行完毕后才可以继续执行,这样就使得在同一个时间段内,只允许一个线程执行操作,从而就实现了同步的处理

在这里插入图片描述

而要想实现这样的锁处理机制,在 Java 里面就可以通过同步代码块以及同步方法两种机制来完成,而这两种机制都会依赖于个重要的关键字**“synchronized”。**

使用同步代码块:

同步代码块是使用 synchronized 定义的一个代码块,在使用同步代码块的时候一般都需要进行一个同步对象的锁定,
语法:

synchronized(同步对象){
// 若干操作
}

一般这个锁定的同步对象往往可以直接使用当前对象(当前的Thread) 来完成锁定处理操作。

//Thread.currentThread():获取当前线程对象

class MyThread implements Runnable {	// 定义线程执行类
	private int ticket = 3; // 总票数为6张
	@Override
	public void run() {
		while (true) {// 持续卖票
			synchronized(this) {	// 同步代码块
				if (this.ticket > 0) {	// 还有剩余票
					try {
						Thread.sleep(100);	// 模拟网络延迟
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(Thread.currentThread().getName() + 
						"卖票,ticket = " + this.ticket--);
				} else {
					System.out.println("***** 票已经卖光了 *****");
					break;// 跳出循环
				}
			}
		}
	}
}

使用同步方法:
class MyThread implements Runnable {	// 定义线程执行类
	private int ticket = 3; // 总票数为6张
	@Override
	public void run() {
		while (this.sale()) {	// 调用同步方法
			;
		}
	}
	public synchronized boolean sale() {	// 售票操作
		if (this.ticket > 0) {
			try {
				Thread.sleep(100); 	// 模拟网络延迟
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println(Thread.currentThread().getName() + 
				"卖票,ticket = " + this.ticket--);
			return true;
		} else {
			System.out.println("***** 票已经卖光了 *****");
			return false;
		}
	}
}

8.线程死锁

通过线程的同步的确是可以实现数据的正确的操作,但是这种正确操作的背后隐藏着一个基本的逻辑:线程之间的互相等待A线程要等待B 线程释放资源之后才可以继续执行,在这之前A 线程都将进入到阳赛状态,那么假设有如下一种场景。

  • A线程对 B线程说:你给我你的画,我再借你看我的书,如果不给画我绝对不给你书:
  • B 线程对A 线程说:你借我你的书,我再给我的画,如果不给书我绝对不给你画。

在这里插入图片描述

package com.js;

class Book{}
class Paint{}
public class ThreadTest {
    public static void main(String[] args) {
        Book book = new Book();
        Paint paint = new Paint();

        Thread threadA = new Thread(() -> {
            synchronized (book) {//A线程锁定图书
                System.out.println("A线程对 B线程说:你给我你的画,我再借你看我的书,如果不给画我绝对不给你书:");
                try {
                    Thread.sleep(1000);
                    synchronized (paint) {
                        System.out.println("A得到了B的画");
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });



        Thread threadB = new Thread(() -> {
            synchronized (paint) {//B线程锁定画
                System.out.println("A线程对 B线程说:你给我你的画,我再借你看我的书,如果不给画我绝对不给你书:");
                try {
                    Thread.sleep(1000);
                    synchronized (book) {
                        System.out.println("B得到了A的书");
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        threadA.start();
        threadB.start();


    }
}

在这里插入图片描述

结论:多个线程更新同一资源的时候必须考虑到同步,而同步所带来的问题就是线程的死锁

9.生产者和消费者模型:

​ 在多线程的程序开发过程之中,生产者和消费者的模型是一个最为基础的不同线程之间的通讯模型,这种模型主要特点是在于:一个线程负责生产业务,另外一个线程负责将生产完成的内容取走。

在这里插入图片描述

通过以上的程序结构来说,可以发现在生产者与消费者的模型之间实际上需要如下的协同处理:

  • 如果要想生产完成一个产品,则一定要有很多的工序,如果有一道工序没有完成,则消费者无法取走商品:
  • 如果说现在生产者生产力很强,并且由干消费者的消费能力弱,那么就会有可能出现一种情况。当某个商品已经生产完成但是还未被取走的时候,那么生产应该等待消费者取走之后再进行后续的生产:
  • 如果说现在的消费者的能力很强,并日发现没有新的产品可以取走,那么消费者也需要进行等待,等待有了新的产品生产出来之后再进行取走。

10.解决数据同步问题

​ 在Java 程序之中如果要想实现数据的同步处理那么肯定要使用 synchronized来完成,如果要完成的话那么肯定就需要同步代马块或者是同步方法,之所以现在出现了不同步的问题,主要是没有在生产的过程里面对数据的操作进行锁定。

在这里插入图片描述

以上的代码一旦引入了同步的处理机制之后就会发现,在整个的程序里面对于重复生产和重复消费的问题并没有得到解决,因为这里面仅仅是解决了多个线程并行修改的数据的困扰,但是并没有实现多线程之间彼此协作的沟通,例如:生产者生产数据的时候消费者不要操作,消费者获取数据的时候生产者不要操作。当消费完成了通知生产者继续生产,当生产完成了也要通知消费者赶紧取走。
而要想实现这样的功能就必须采用线程的同步、等待与唤醒的处理机制,而这样的处理机制的操作全部定义在了 Obiect 类里面,在 Obiect 类中提供有如下的几个与线程有关的操作方法。“

Object类对多线程的支持
方法 类型 描述
public final void wait() throws InterruptedException 普通 线程的等待 ,傻傻的等待,一直到被唤醒为止,不见不散
public final void wait(long timeout) throws InterruptedException 普通 设置线程等待毫秒数 ,如果达到了超时时间则停止等待,过期不侯
public final void wait(long timeout, int nanos) throws InterruptedException 普通 设置线程等待毫秒数和纳秒数 ,过期不侯
public final void notify() 普通 唤醒等待线程,所有线程按照等待顺序依次恢复第一个等待线程
public final void notifyAll() 普通 唤醒全部等待线程

特别重要的提示: 以上这儿个处理方法是进行线程控制的,但是这些方法都是在 synchronized方法中才去用的,同时这些方法属于最原始的多线程协作控制,如果真的用这些操作进行开发,那么整个的项目里面基本上对于死锁的情况就会频发。

11.线程优雅的停止

线程生命周期:
  • 被废弃的线程生命周期控制方法:

  • 停止多线程:public void stop();

  • 挂起线程:public final void suspend();

  • 恢复挂起的线程执行:public final void resume();

多线程完整运行状态:

在这里插入图片描述

12.守护线程

Java中的线程分为两类:用户线程、守护线程。守护线程(Daemon)是一种运行在后台的线程服务线程,当用户线程存在时守护线程也可以同时存在,如果用户线程全部消失(程序执行完毕,JVM进程结束)时守护线程也会消失。

守护线程操作方法:
方法 类型 描述
public final void setDaemon(boolean on) 普通 设置为守护线程
public final boolean isDaemon() 普通 判断是否为守护线程
public class ThreadDemo {
	public static void main(String[] args) throws Exception {
		Thread userThread = new Thread(() -> {
			for (int x = 0; x < 2; x++) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "正在运行、x = " + x);
			}
		}, "用户线程"); 		// 完成核心的业务
		Thread daemonThread = new Thread(() -> {
			for (int x = 0; x < Integer.MAX_VALUE; x++) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + "正在运行、x = " + x);
			}
		}, "守护线程"); 		// 完成核心的业务
		daemonThread.setDaemon(true); 	// 设置为守护线程
		userThread.start();	// 启动用户线程
		daemonThread.start();	// 启动守护线程
	}}

13.volatile 关键字

​ 在整个的多线程开发的过程之中,核心的本质就在于多个线程彼此协作可以更好的发挥出所有硬件的性能,并且可以在某一个单位时间内执行更多的处理任务,例如:在多线程实现处理机制之中出现的协作卖票程序。

但是既然已经说到了多线程就必须强调一个很容易被所有人混淆的概念: volatile 关键字,这个关键字被很多的初学者误认为实现同步机制,这个关键字主要是定义在属性的声明上。

列子:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

volatile可以直接进行原始变量操作
class MyThread implements Runnable {
	private volatile int ticket = 3; // 直接内存操作
	@Override
	public void run() {
		synchronized (this) {	// 同步处理
			while (this.ticket > 0) {
				try {
					Thread.sleep(100);// 延迟模拟
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println(Thread.currentThread().getName() + 
						"卖票处理,ticket = " + this.ticket--);
			}
		}
	}
}
public class ThreadDemo {
	public static void main(String[] args) throws Exception {
		MyThread mt = new MyThread();
		new Thread(mt, "售票员A").start();
		new Thread(mt, "售票员B").start();
		new Thread(mt, "售票员C").start();
	}
}

使用 volatile 定义的变量可以直接进行底层内存变量直接操作,这样可以让数据的同步更加高效。


网站公告

今日签到

点亮在社区的每一天
去签到