19.Java学习笔记第十九节——多线程(尚硅谷视频整理)

本文详细介绍了Java线程的生命周期、同步机制、死锁问题以及线程安全的处理方法,包括synchronized关键字的使用、Lock接口的应用,并对比了两者的区别。此外,还探讨了线程的通信方法wait、notify和notifyAll,以及JDK5.0引入的Callable接口和线程池的使用,强调了线程池在性能优化和资源管理上的优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


一、线程的生命周期

1.理解

JDK中用Thread.State类定义了线程的几种状态。
要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态

> 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。

> 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源。

> 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态, run()方法定义了线程的操作和功能。

> 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态。

> 死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束。

2.五种状态之间的关系

在这里插入图片描述

二、线程的同步

通过同步机制解决线程安全问题。

1.理解
  • 多个线程执行的不确定性引起执行结果的不稳定。
  • 多个线程对账本的共享,会造成操作的不完整性,会破坏数据。
2.思想
  1. 多线程出现了安全问题
  2. 问题的原因:
    当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
  3. 解决办法:
    对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。这种情况即使当前线程出现了阻塞也不能被改变。
3.处理两种线程安全问题
3.1 同步代码块处理继承Thread类的线程安全问题

Synchronized关键字:

形式:

synchronized (同步监视器)
{
	// 需要被同步的代码;
}

但是若此时创建了多个Thread对象,所以还是会出现安全问题(每个线程都拥有一个同步监视器),此时需要声明同步监视器是静态的即可。同理, 也不能用 this 。可以用当前类充当同步监视器(类名.class)。推测出类也是对象。

3.2 同步方法处理继承Thread类的线程安全问题

synchronized还可以放在方法声明中,表示整个方法为同步方法。

形式:

public static synchronized void show () //声明为静态的,//此时同步监视器是当前类
{ 
	// 需要被同步的代码;
}

static的原因同上。

3.3 同步代码块处理实现Runnable的线程安全问题

形式:

synchronized (同步监视器)
{
	// 需要被同步的代码;
}

1.操作共享数据的代码即为同步代码。
2.同步监视器,俗称锁。任何一个类的对象都可以充当锁。
          要求多个线程共用一把锁。
          为了方便,不再创建任意一个对象,可以用当前对象 this 。同步监视器的位置直接写 this 即可。

3.4 同步方法处理实现Runnable的线程安全问题

synchronized还可以放在方法声明中,表示整个方法为同步方法。

形式:

public  synchronized void show () //此时同步监视器是this 
{ 
	// 需要被同步的代码;
}

1. 同步方法仍然涉及到同步监视器,只是不需要显示声明了。
2. 非静态的同步方法,同步监视器是:this;
静态的同步方法,监视器是:当前类本身。

4.处理单例模式之懒汉式的线程安全问题
class Singleton 
{
	private Singleton() // 1.私有化构造器
	{
	}

	private static Singleton single=null;  //2.声明当前类的对象,没有初始化 4.此实例也必须静态化

	public static synchronized Singleton getInstance() 	// 3.提供公共的静态的方法,返回当前类的对象
	{
		if(single == null) 
		{
			single = new Singleton();
		}
		return single; 
	}
}

或者:

class Singleton 
{
	private Singleton() // 1.私有化构造器
	{
	}

	private static Singleton single=null;  //2.声明当前类的对象,没有初始化 4.此实例也必须静态化

	public static Singleton getInstance() 	// 3.提供公共的静态的方法,返回当前类的对象
	{
		synchronized(Bank.class)
		{
			if(single == null) 
			{
				single = new Singleton();
			}
			return single; 
		}
	}
}

这两种效率都较差。当第一个线程创建了对象后,其他线程就没必要再进去了。

class Singleton 
{
	private Singleton() // 1.私有化构造器
	{
	}

	private static Singleton single=null;  //2.声明当前类的对象,没有初始化 4.此实例也必须静态化

	public static Singleton getInstance() 	// 3.提供公共的静态的方法,返回当前类的对象
	{
	if(single==null)
	{
		synchronized(Bank.class)
		{
			if(single == null) 
			{
				single = new Singleton();
			}
		 }
	  }
	  return single; 
	}
}
5.死锁问题
5.1 定义
  • 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。
  • 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
5.2 解决方法
  • 专门的算法、原则
  • 尽量减少同步资源的定义
  • 尽量避免嵌套同步
6.处理线程安全问题方式三——Lock(锁)

从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。当用继承thread方法时,要使lock对象也是唯一的,即声明为静态的。private static final ReentrantLock lock = new ReenTrantLock();

class A
{
	private final ReentrantLock lock = new ReenTrantLock();
	public void m()
	{
		lock.lock();
		try
		{
			//保证线程安全的代码; 
		}
		finally
		{
			lock.unlock(); 
		}
  	} 
}

注意:如果同步代码有异常,要将unlock()写入finally语句块

7. synchronized 与 Lock 的对比
  1. Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放。
  2. Lock只有代码块锁,synchronized有代码块锁和方法锁。
  3. 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。

优先使用顺序:
Lock → 同步代码块(已经进入了方法体,分配了相应资源) → 同步方法(在方法体之外)

三、线程的通信

1. 三种方法

wait():一旦执行该方法,当前进程就进入阻塞状态,并释放同步监视器(与sleep不同的一点)。

notify():唤醒被wait的线程中优先级最高者。(唤醒一个)

notifyAll ():唤醒被wait的所有线程。(唤醒所有)

注:这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报java.lang.IllegalMonitorStateException异常。

这三个方法的调用者必须是同步代码块或同步方法中的同步监视器。

因为这三个方法必须有锁对象调用,而任意对象都可以作为synchronized的同步锁,因此这三个方法只能在Object类中声明。

2. wait 和 sleep 方法的异同

相同点:

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

不同点:

(1)两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()。
(2)调用的要求不同:sleep()可以在任何需要的场景下调用,wait()必须使用在同步代码块或同步方法中。
(3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放同步监视器,而wait()会释放同步监视器。

四、JDK5.0新增线程创建方式

1.实现Callable接口

与使用Runnable相比, Callable功能更强大些:
> 相比run()方法,call()可以有返回值。
> call()方法可以抛出异常,被外面的操作捕获,获取异常的信息。
> Callable支持泛型。
> 需要借助FutureTask类,比如获取返回结果。

借助FutureTask类就需要用到:

Future接口
> 可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。
> FutrueTask是Futrue接口的唯一的实现类。
> FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值

过程:
(1)创建一个实现Callable的实现类;
(2)实现call方法,将此线程需要执行的操作声明在call()中;
(3)创建Callable接口实现类的对象;
(4)将此Callable接口实现类的对象传递到FutureTask构造器中,创建FutureTask的对象;
(5)将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread的对象,并调用start();
(6)获取Callable中call方法的返回值。(可没有返回值)Object 变量名=FutureTask的对象.get();

2.使用线程池

背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。

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

好处:
> 提高响应速度(减少了创建新线程的时间)
> 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
> 便于线程管理
> corePoolSize:核心池的大小
> maximumPoolSize:最大线程数
> keepAliveTime:线程没有任务时最多保持多长时间后会终止
>

线程池相关API:
> JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors
> ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
> void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable
> Future submit(Callable task):执行任务,有返回值,一般用来执行Callable
> void shutdown() :关闭连接池
> Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
> Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
> Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
> Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
> Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

过程:
(1)提供指定线程数量的线程池
(2)执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
(3)关闭连接池。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值