Java语言学习之进程与线程二

本文详细介绍了Java中线程同步的实现方式,包括同步代码块、同步方法及ReentrantLock锁的使用,探讨了如何通过synchronized关键字和Lock接口进行线程同步。此外,还讲解了线程间通信的两种方式:传统wait/notify机制和Condition条件变量,以及线程组的概念和未处理异常的处理。

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

一.线程同步

java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式如下:
synchronized(obj) {

//此处的代码就是同步代码块
}

1.同步代码块

任何时刻只能有一个线程可获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。
public class DrawThread extends Thread
{
	// 模拟用户账户
	private Account account;
	// 当前取钱线程所希望取的钱数
	private double drawAmount;
	public DrawThread(String name , Account account
		, double drawAmount)
	{
		super(name);
		this.account = account;
		this.drawAmount = drawAmount;
	}
	// 当多条线程修改同一个共享数据时,将涉及数据安全问题。
	public void run()
	{
		// 使用account作为同步监视器,任何线程进入下面同步代码块之前,
		// 必须先获得对account账户的锁定——其他线程无法获得锁,也就无法修改它
		// 这种做法符合:“加锁 → 修改 → 释放锁”的逻辑
		synchronized (account)
		{
			// 账户余额大于取钱数目
			if (account.getBalance() >= drawAmount)
			{
				// 吐出钞票
				System.out.println(getName()
					+ "取钱成功!吐出钞票:" + drawAmount);
				try
				{
					Thread.sleep(1);
				}
				catch (InterruptedException ex)
				{
					ex.printStackTrace();
				}
				// 修改余额
				account.setBalance(account.getBalance() - drawAmount);
				System.out.println("\t余额为: " + account.getBalance());
			}
			else
			{
				System.out.println(getName() + "取钱失败!余额不足!");
			}
		}
		// 同步代码块结束,该线程释放同步锁
	}
}

该同步代码块的同步监视器是account对象

2.同步方法

对于synchronized修饰的实例方法(非static方法)而言,无须显式指定同步监视器,同步方法的同步监视器就是this,也就是调用该方法的对象。


public class Account
{
	// 封装账户编号、账户余额两个成员变量
	private String accountNo;
	private double balance;
	public Account(){}
	// 构造器
	public Account(String accountNo , double balance)
	{
		this.accountNo = accountNo;
		this.balance = balance;
	}

	// accountNo的setter和getter方法
	public void setAccountNo(String accountNo)
	{
		this.accountNo = accountNo;
	}
	public String getAccountNo()
	{
		return this.accountNo;
	}
	// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
	public double getBalance()
	{
		return this.balance;
	}

	// 提供一个线程安全draw()方法来完成取钱操作
	public synchronized void draw(double drawAmount)
	{
		// 账户余额大于取钱数目
		if (balance >= drawAmount)
		{
			// 吐出钞票
			System.out.println(Thread.currentThread().getName()
				+ "取钱成功!吐出钞票:" + drawAmount);
			try
			{
				Thread.sleep(1);
			}
			catch (InterruptedException ex)
			{
				ex.printStackTrace();
			}
			// 修改余额
			balance -= drawAmount;
			System.out.println("\t余额为: " + balance);
		}
		else
		{
			System.out.println(Thread.currentThread().getName()
				+ "取钱失败!余额不足!");
		}
	}

	// 下面两个方法根据accountNo来重写hashCode()和equals()方法
	public int hashCode()
	{
		return accountNo.hashCode();
	}
	public boolean equals(Object obj)
	{
		if(this == obj)
			return true;
		if (obj !=null
			&& obj.getClass() == Account.class)
		{
			Account target = (Account)obj;
			return target.getAccountNo().equals(accountNo);
		}
		return false;
	}
}

synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器,成员变量等。

3.同步锁(Lock)

Lock是控制多个线程对共享资源进行访问的工具。

ReentrantLock锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用。

import java.util.concurrent.locks.*;
 class Account
{
	// 定义锁对象
	private final ReentrantLock lock = new ReentrantLock();
	// 封装账户编号、账户余额的两个成员变量
	private String accountNo;
	private double balance;
	public Account(){}
	// 构造器
	public Account(String accountNo , double balance)
	{
		this.accountNo = accountNo;
		this.balance = balance;
	}

	// accountNo的setter和getter方法
	public void setAccountNo(String accountNo)
	{
		this.accountNo = accountNo;
	}
	public String getAccountNo()
	{
		return this.accountNo;
	}
	// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
	public double getBalance()
	{
		return this.balance;
	}

	// 提供一个线程安全draw()方法来完成取钱操作
	public void draw(double drawAmount)
	{
		// 加锁
		lock.lock();
		try
		{
			// 账户余额大于取钱数目
			if (balance >= drawAmount)
			{
				// 吐出钞票
				System.out.println(Thread.currentThread().getName()
					+ "取钱成功!吐出钞票:" + drawAmount);
				try
				{
					Thread.sleep(1);
				}
				catch (InterruptedException ex)
				{
					ex.printStackTrace();
				}
				// 修改余额
				balance -= drawAmount;
				System.out.println("\t余额为: " + balance);
			}
			else
			{
				System.out.println(Thread.currentThread().getName()
					+ "取钱失败!余额不足!");
			}
		}
		finally
		{
			// 修改完成,释放锁
			lock.unlock();
		}
	}

	// 下面两个方法根据accountNo来重写hashCode()和equals()方法
	public int hashCode()
	{
		return accountNo.hashCode();
	}
	public boolean equals(Object obj)
	{
		if(this == obj)
			return true;
		if (obj !=null
			&& obj.getClass() == Account.class)
		{
			Account target = (Account)obj;
			return target.getAccountNo().equals(accountNo);
		}
		return false;
	}
}

使用ReentrantLock对象来进行同步,加锁和释放锁现在不同的作用范围内时,通常建议使用finally块来确保在必要时释放锁。

二.线程通信

1.传统的线程通信
借助于object类提供的wait(),notify(),notifyAll()三个方法,这三个方法并不属于Thread类,而是属于Object类。这三个方法必须由同步监视器对象来调用。
对于使用synchronized修饰的同步方法,因为该类的默认实例this就是同步监视器,所以可以在同步方法种直接调用这三个方法;
对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这三个方法;

wait(): 导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程;
notify():唤醒在此同步监视器上等待的单个线程。
notifyAll():唤醒在此同步监视器上等待的所有线程。


public class Account
{
	// 封装账户编号、账户余额的两个成员变量
	private String accountNo;
	private double balance;
	// 标识账户中是否已有存款的旗标
	private boolean flag = false;

	public Account(){}
	// 构造器
	public Account(String accountNo , double balance)
	{
		this.accountNo = accountNo;
		this.balance = balance;
	}

	// accountNo的setter和getter方法
	public void setAccountNo(String accountNo)
	{
		this.accountNo = accountNo;
	}
	public String getAccountNo()
	{
		return this.accountNo;
	}
	// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
	public double getBalance()
	{
		return this.balance;
	}

	public synchronized void draw(double drawAmount)
	{
		try
		{
			// 如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
			if (!flag)
			{
				wait();
			}
			else
			{
				// 执行取钱
				System.out.println(Thread.currentThread().getName()
					+ " 取钱:" +  drawAmount);
				balance -= drawAmount;
				System.out.println("账户余额为:" + balance);
				// 将标识账户是否已有存款的旗标设为false。
				flag = false;
				// 唤醒其他线程
				notifyAll();
			}
		}
		catch (InterruptedException ex)
		{
			ex.printStackTrace();
		}
	}
	public synchronized void deposit(double depositAmount)
	{
		try
		{
			// 如果flag为真,表明账户中已有人存钱进去,则存钱方法阻塞
			if (flag)             //①
			{
				wait();
			}
			else
			{
				// 执行存款
				System.out.println(Thread.currentThread().getName()
					+ " 存款:" +  depositAmount);
				balance += depositAmount;
				System.out.println("账户余额为:" + balance);
				// 将表示账户是否已有存款的旗标设为true
				flag = true;
				// 唤醒其他线程
				notifyAll();
			}
		}
		catch (InterruptedException ex)
		{
			ex.printStackTrace();
		}
	}

	// 下面两个方法根据accountNo来重写hashCode()和equals()方法
	public int hashCode()
	{
		return accountNo.hashCode();
	}
	public boolean equals(Object obj)
	{
		if(this == obj)
			return true;
		if (obj !=null
			&& obj.getClass() == Account.class)
		{
			Account target = (Account)obj;
			return target.getAccountNo().equals(accountNo);
		}
		return false;
	}
}

2.使用condition控制线程通信

如果程序不使用synchronized关键字来保证同步,而是直接使用lock对象来保证同步,则系统中不存在隐式的同步监视器。也就不能使用wait(),notify(),notifyAll()方法进行线程通信。

Condition实例被绑定在一个Lock对象上,要获得特定Lock实例的Condition实例,调用Lock对象的newConditoin()方法即可。
await():类是于隐式同步监视器上的wait方法;
signal():唤醒在此Lock对象上等待的单个线程。
signalAll():唤醒在此Lock对象上等待的所有线程。

import java.util.concurrent.*;
import java.util.concurrent.locks.*;
 class Account
{
	// 显式定义Lock对象
	private final Lock lock = new ReentrantLock();
	// 获得指定Lock对象对应的Condition
	private final Condition cond  = lock.newCondition();
	// 封装账户编号、账户余额的两个成员变量
	private String accountNo;
	private double balance;
	// 标识账户中是否已有存款的旗标
	private boolean flag = false;

	public Account(){}
	// 构造器
	public Account(String accountNo , double balance)
	{
		this.accountNo = accountNo;
		this.balance = balance;
	}

	// accountNo的setter和getter方法
	public void setAccountNo(String accountNo)
	{
		this.accountNo = accountNo;
	}
	public String getAccountNo()
	{
		return this.accountNo;
	}
	// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
	public double getBalance()
	{
		return this.balance;
	}

	public void draw(double drawAmount)
	{
		// 加锁
		lock.lock();
		try
		{
			// 如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
			if (!flag)
			{
				cond.await();
			}
			else
			{
				// 执行取钱
				System.out.println(Thread.currentThread().getName()
					+ " 取钱:" +  drawAmount);
				balance -= drawAmount;
				System.out.println("账户余额为:" + balance);
				// 将标识账户是否已有存款的旗标设为false。
				flag = false;
				// 唤醒其他线程
				cond.signalAll();
			}
		}
		catch (InterruptedException ex)
		{
			ex.printStackTrace();
		}
		// 使用finally块来释放锁
		finally
		{
			lock.unlock();
		}
	}
	public void deposit(double depositAmount)
	{
		lock.lock();
		try
		{
			// 如果flag为真,表明账户中已有人存钱进去,则存钱方法阻塞
			if (flag)             // ①
			{
				cond.await();
			}
			else
			{
				// 执行存款
				System.out.println(Thread.currentThread().getName()
					+ " 存款:" +  depositAmount);
				balance += depositAmount;
				System.out.println("账户余额为:" + balance);
				// 将表示账户是否已有存款的旗标设为true
				flag = true;
				// 唤醒其他线程
				cond.signalAll();
			}
		}
		catch (InterruptedException ex)
		{
			ex.printStackTrace();
		}
		// 使用finally块来释放锁
		finally
		{
			lock.unlock();
		}
	}

	// 下面两个方法根据accountNo来重写hashCode()和equals()方法
	public int hashCode()
	{
		return accountNo.hashCode();
	}
	public boolean equals(Object obj)
	{
		if(this == obj)
			return true;
		if (obj !=null
			&& obj.getClass() == Account.class)
		{
			Account target = (Account)obj;
			return target.getAccountNo().equals(accountNo);
		}
		return false;
	}
}

三.线程组和未处理的异常

Java使用ThreadGroup来表示线程组,他可以对一批线程进行分类管理,如果程序没有显式指定线程属于哪个线程组,则该线程属于默认现程组,在默认情况下,子线程和创建它的父线程处于同一个线程组,例如A线程创建B线程,B没有指定线程组,那么B线程就是输出A线程所在的线程组的。

一旦某个线程加入了指定的线程组之后,该线程就一直属于该线程组,直到线程死亡。中途不能改变。
在这里插入图片描述
在这里插入图片描述

class MyThread extends Thread
{
	// 提供指定线程名的构造器
	public MyThread(String name)
	{
		super(name);
	}
	// 提供指定线程名、线程组的构造器
	public MyThread(ThreadGroup group , String name)
	{
		super(group, name);
	}
	public void run()
	{
		for (int i = 0; i < 20 ; i++ )
		{
			System.out.println(getName() + " 线程的i变量" + i);
		}
	}
}
public class ThreadGroupTest
{
	public static void main(String[] args)
	{
		// 获取主线程所在的线程组,这是所有线程默认的线程组
		ThreadGroup mainGroup = Thread.currentThread().getThreadGroup();
		System.out.println("主线程组的名字:"
			+ mainGroup.getName());
		System.out.println("主线程组是否是后台线程组:"
			+ mainGroup.isDaemon());
		new MyThread("主线程组的线程").start();
		ThreadGroup tg = new ThreadGroup("新线程组");
		tg.setDaemon(true);
		System.out.println("tg线程组是否是后台线程组:"
			+ tg.isDaemon());
		MyThread tt = new MyThread(tg , "tg组的线程甲");
		tt.start();
		new MyThread(tg , "tg组的线程乙").start();
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值