理解——线程

1.线程

几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能有多个顺序执行流,每个顺序执行流就是一个线程。

1.1 线程与进程

当一个程序进入内存运行,即变成一个进程。进程是出于运行中的程序,并且具有一定独立功能,进程是系统进行资源分配和调度的一个独立单位。

进程特征:

  • 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间
  • 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合
  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会影响。(并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得宏观上具有多个进程同时执行的效果)

线程时进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不再拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。因为多个线程共享父进程里的全部资源,因此编程更加方便;但必须更加小心,我们必须确保线程不会妨碍同一进程里的其他线程。

 

线程是独立运行的,它并不知道进程中是否还有其他线程存在。线程的执行时抢占式的,也就是说,当前运行的线程在任何时候都可能被挂起,以便另外一个线程可以运行。

一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。线程的调度和管理由进程本身负责完成,而不是操作系统。

 

简而言之:一个程序运行后至少有一个进程,一个进程里可以包含多个线程,但至少要包含一个线程。

 

1.2 多线程的优势

线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性:多个线程将共享同一个进程虚拟空间。线程共享的环境包括:进程代码段、进程的公有数据等。利用这些共享的数据,线程很容易实现相互之间的通信。

总结,使用多线程编程包含以下几个优点:

  • 进程之间不能共享内存,但线程之间共享内存很容易
  • 系统创建进程需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程效率高
  • java语言内置多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了java的多线程编程

2. 线程的创建和启动

所有线程对象都必须是Thread类或其子类的实例。每条线程的作用是完成一定的任务,实际上就是执行一段程序流。Java用run()方法来封装这样一段程序流。在java中要想实现多线程,有两种手段,一种是继承Thread类,另外一种是实现Runable接口。

2.1 继承Thread类
//通过继承Thread类来创建线程类
public class FirstThread extends Thread
{
	private int i ;
	//重写run方法,run方法的方法体就是线程执行体
	public void run()
	{
		for ( ; i < 100 ; i++ )
		{
			//当线程类继承Thread类时,可以直接调用getName()方法来返回当前线程的名。
			//如果想获取当前线程,直接使用this即可
			//Thread对象的getName返回当前该线程的名字
			System.out.println(getName() +  " " + i);
		}
	}
	
    public static void main(String[] args) 
    {
        for (int i = 0; i < 100;  i++)
        {
			//调用Thread的currentThread方法获取当前线程
			System.out.println(Thread.currentThread().getName() +  " " + i);
			if (i == 20)
			{
				//创建、并启动第一条线程
				new FirstThread().start();
				//创建、并启动第二条线程
				new FirstThread().start();
			}
        }
    }
}

 执行后查看输出结果,可以发现两条线程输出的i变量不连续,所以这两条线程不能共享数据。

  • 注意:使用继承Thread类的方法来创建线程类,多条线程之间无法共享线程类的实例变量。
2.2 实现Runnable接口创建线程类

 

//通过实现Runnable接口来创建线程类
public class SecondThread implements Runnable
{
	private int i ;
	//run方法同样是线程执行体
	public void run()
	{
		for ( ; i < 100 ; i++ )
		{
			//当线程类实现Runnable接口时,
			//如果想获取当前线程,只能用Thread.currentThread()方法。
			System.out.println(Thread.currentThread().getName() + "  " + i);
		}
	}
	
    public static void main(String[] args) 
    {
        for (int i = 0; i < 100;  i++)
        {
			System.out.println(Thread.currentThread().getName() + "  " + i);
			if (i == 20)
			{
				SecondThread st = new SecondThread();
				//通过new Thread(target , name)方法创建新线程
				new Thread(st , "新线程1").start();
				new Thread(st , "新线程2").start();
			}
        }
    }
}

 

实现Runnable接口的实例只是作为Thread的Target来创建Thread对象,该Thread对象才是真正的线程对象。

从执行结果可以看出,采用实现Runnable接口的方式来创建的多条线程可以共享线程类的实例属性。

  • 这是因为在这种情况下,程序所创建的Runnable对象只是线程的target,而多条线程可以共享同一个target,所以多条线程可以共享一个线程类(实际上应该是线程的target类)的实例属性。
 实现Runnable接口比继承Thread类所具有的优势:
1):适合多个相同的程序代码的线程去处理同一个资源
2):可以避免java中的单继承的限制
3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。
  • 注意:启动线程使用start方法,而不是run方法!永远不要调用线程对象的run方法!调用start方法来启动线程,系统会把该run方法当成线程执行体来处理。但如果直接调用线程对象的run方法,则run方法立即就会被执行,而且在run方法返回之前,其他线程无法并发执行——也就是说系统把线程对象当成了一个普通对象,而run方法也是一个普通方法,而不是线程执行体。

3. 控制线程

    3.1 join线程

Thread提供了让一个线程等待另一个线程完成的方法:join()方法。当在某个程序执行流中调用其他线程的join方法时,调用线程将被阻塞,直到被join方法加入的join线程完成为止。

public class JoinThread extends Thread
{
	//提供一个有参数的构造器,用于设置该线程的名字
	public JoinThread(String name)
	{
		super(name);
	}
	//重写run方法,定义线程执行体
	public void run()
	{
		for (int i = 0; i < 10 ; i++ )
		{
			System.out.println(getName() + "  " + i);
		}
	}
    public static void main(String[] args) throws Exception
    {
		//启动子线程
		new JoinThread("新线程").start();
		for (int i = 0; i < 10 ; i++ )
		{
			if (i == 5)
			{
				JoinThread jt = new JoinThread("被Join的线程");
				jt.start();
				//main线程调用了jt线程的join方法,main线程
				//必须等jt执行结束才会向下执行
				jt.join(); 
			}
			System.out.println(Thread.currentThread().getName() + "  " + i);
		}
    }
}

 "被Join的线程"被join到主线程中,则当i==5时,主线程被阻塞,此时只有“新线程”和 "被Join的线程"是并发执行的了。

     3.2 后台线程

有一种线程,它是在后台运行的,它的任务是为其他的线程提供服务,这种线程被称为“后台线程(Daemon Thread)”,又被称为“守护线程”或“精灵线程”。JVM的垃圾回收线程就是典型的后台线程。
后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡。
调用Thread对象setDaemon(true)方法可将指定线程设置成后台线程。

public class DaemonThread extends Thread
{
	//定义后台线程的线程执行体与普通线程没有任何区别
	public void run()
	{
		for (int i = 0; i < 1000 ; i++ )
		{
			System.out.println(getName() + "  " + i);
		}
	}

    public static void main(String[] args) 
    {
        DaemonThread t = new DaemonThread();
		//将此线程设置成后台线程
		t.setDaemon(true);
		//启动后台线程
		t.start();
		for (int i = 0 ; i < 10 ; i++ )
		{
			System.out.println(Thread.currentThread().getName()
				+ "  " + i);
		}
		//------程序执行到此处,前台线程(main线程)结束------
		//后台线程也应该随之结束
    }
}

 

注意:前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程。
前台线程死亡后,JVM会通知后台线程死亡,但从它接受指令,到它做出响应,需要一定时间。而且要将某个线程设置为后台线程,必须在该线程启动之前设置,也就是说setDaemon(true)必须在start()方法之前调用。

 3.3 线程睡眠:sleep

如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread类的静态sleep方法。
当当前线程调用sleep方法进入阻塞状态后,在其sleep时间段内,该线程不会获得执行的机会,即使系统中没有其他可以运行的线程,处于sleep中的线程也不会运行,因此sleep方法常用来暂停程序的执行。

public class TestSleep
{
    public static void main(String[] args) throws Exception
    {
		for (int i = 0; i < 10 ; i++ )
		{
			System.out.println("当前时间: " + new Date());
			//调用sleep方法让当前线程暂停1s。
			Thread.sleep(1000);
		}
    }
}

 

上面的代码使得主线程每停止1s后再执行。

3.4 线程让步:yield

yield方法也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,完全可能的情况是:当前某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。

实际上:当某个线程调用了yield方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程才会获得执行的机会。

public class TestYield extends Thread
{
	public TestYield()
	{
	}
	public TestYield(String name)
	{
		super(name);
	}
	//定义run方法作为线程执行体
	public void run()
	{
		for (int i = 0; i < 50 ; i++ )
		{
			System.out.println(getName() + "  " + i);
			//当i等于20时,使用yield方法让当前线程让步
			if (i == 20)
			{
				Thread.yield();
			}
		}
	}
    public static void main(String[] args) throws Exception
    {
		//启动两条并发线程
		TestYield ty1 = new TestYield("高级");
		//将ty1线程设置成最高优先级
		//ty1.setPriority(Thread.MAX_PRIORITY);
		ty1.start();
		TestYield ty2 = new TestYield("低级");
		//将ty1线程设置成最低优先级
		//ty2.setPriority(Thread.MIN_PRIORITY);
		ty2.start();	
    }
}

 

通常不要依靠yield方法来控制并发线程的执行。

3.5 改变线程优先级

每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。

public class PriorityTest extends Thread
{
	public PriorityTest(){}
	//定义一个有参数的构造器,用于创建线程时指定name
	public PriorityTest(String name)
	{
		super(name);
	}
	public void run()
	{
		for (int i = 0 ; i < 50 ; i++ )
		{
			System.out.println(getName() +  ",其优先级是:"
				+ getPriority() + ",循环变量的值为:" + i);
		}
	}
	public static void main(String[] args) 
	{
		//改变主线程的优先级
		Thread.currentThread().setPriority(6);
		for (int i = 0 ; i < 30 ; i++ )
		{
			if (i == 10)
			{
				PriorityTest low  = new PriorityTest("低级");
				low.start();
				System.out.println("创建之初的优先级:" + low.getPriority());
				//设置该线程为最低优先级
				low.setPriority(Thread.MIN_PRIORITY);
			}
			if (i == 20)
			{
				PriorityTest high = new PriorityTest("高级");
				high.start();
				System.out.println("创建之初的优先级:" + high.getPriority());
				//设置该线程为最高优先级
				high.setPriority(Thread.MAX_PRIORITY);
			}
		}
	}
}

 

注:虽然java提供了10个级别的优先级(1-10),但这些优先级级别需要操作系统的支持。不幸的是,不同的操作系统优先级并不相同。所以我们应该尽量避免直接为线程指定优先级,而应该使用MAX_PRIORITY、MIN_PRIORITY、NORM_PRIORITY三个静态常量来设置优先级,这样才可以保证程序具有最好的可移植性。

4. 线程同步

当使用多个线程来访问同一个数据时,非常容易出现线程安全问题,这是由于系统的线程调度具有一定的随机性。

    4.1 同步代码块

之所以出现线程安全问题,是因为run方法的方法体不具有同步安全性。为了解决这个问题,java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。同步代码块语法如下:

synchronized(obj){
      ...
      //此处的代码就是同步代码块
}

上面语法格式中的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须获得对同步监视器的锁定。任何时刻只能有一条线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然释放了对该同步监视器的锁定。

  • 虽然java程序允许使用任何对象来作为同步监视器,但想一下同步监视器的目的:阻止两条线程对同一个共享资源进行并发访问。因此通常推荐使用可能被并发访问的共享资源充当同步监视器。

以取款为例:

实体Account:

public class Account
{
	//封装账户编号、账户余额两个属性
	private String accountNo;
	private double balance;
	public Account(){}
	//构造器
	public Account(String accountNo , double balance)
	{
		this.accountNo = accountNo;
		this.balance = balance;
	}
	public void setAccountNo(String accountNo)
	{
		this.accountNo = accountNo;
	}
	public String getAccountNo()
	{
		 return this.accountNo;
	}

	public void setBalance(double balance)
	{
		 this.balance = balance;
	}
	public double getBalance()
	{
		 return this.balance;
	}


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

 取款线程类DrawThread:

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() + "取钱失败!余额不足!");
			}
		}
	}
}

 

测试类TestDraw:

public class TestDraw
{
    public static void main(String[] args) 
    {
		//创建一个账户
		Account acct = new Account("1234567" , 1000);
		//模拟两个线程对同一个账户取钱
		new DrawThread("甲" , acct , 800).start();
		new DrawThread("乙" , acct , 800).start();
    }
}

 

对上面的程序,我们就考虑使用用户账户(Account)作为同步监视器。

    4.2 同步方法

与同步代码块对应的,java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。对于同步方法而言,无须显示指定同步监视器,同步方法的同步监视器是this,也就是该对象本身。

通过使用同步方法可以非常方便地将某类变成线程安全的类,线程安全的类的特征如下:

  • 该类的对象可以被多个线程安全的访问
  • 每个线程调用该类的任意方法之后都将得到正确的结果
  • 每个线程调用该类的任意方法之后,该对象状态依然保持合理状态

上面取钱的例子中Account就是一个线程不安全的类,当两个线程同时修改Account的balance属性时,程序就出现了异常,现在将Account类变成一个线程安全的类,只需要把修改balance的方法修饰成同步方法即可。

Account:

public class Account
{
	private String accountNo;
	private double balance;


	public Account(){}

	public Account(String accountNo , double balance)
	{
		this.accountNo = accountNo;
		this.balance = balance;
	}

	public void setAccountNo(String accountNo)
	{
		this.accountNo = accountNo;
	}
	public String getAccountNo()
	{
		 return this.accountNo;
	}

	public double getBalance()
	{
		 return this.balance;
	}
	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() +
				"取钱失败!余额不足!");
		}
	}

	public int hashCode()
	{
		return accountNo.hashCode();
	}
	public boolean equals(Object obj)
	{
		if (obj != null && obj.getClass() == Account.class)
		{
			Account target = (Account)obj;
			return target.getAccountNo().equals(accountNo);
		}
		return false;
	}
}

 

上面程序将代表取钱的方法draw()使用了synchronized修改为同步方法了,同步方法的同步监视器是this,就当前对象(Account的实例),因此对于同一个Account而言,任意时刻只能有一条线程获得对Account对象的锁定,然后进入draw方法进行取钱,这样既可保证多条线程并发取钱时的线程安全了。此时,取钱的线程类DrawThread,只需要调用Account对象的draw方法既可。

DrawThread:

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

 

可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响,可以采用如下策略:

  • 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源(共享资源)的方法进行同步。例如Account的balance属性
  • 如果可变类有两种运行环境:单线程、多线程环境,则应该为该可变类提供两种版本:线程不安全版本和线程安全版本。在单线程环境中使用线程不安全版本以保证性能,在多线程环境中使用线程安全版本。
    4.3 同步锁

java提供了另外一种线程同步的机制:它通过显示定义同步锁对象来实现同步,在这种机制下,同步锁应该使用Lock对象充当。
Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。不过,某些锁可能允许对共享资源并发访问,如ReadWriteLock(读写锁)。在实现线程安全的控制中,通常喜欢使用ReentrantLock(可重入锁)。

通过Lock对象我们可以将Account修改为如下形式,它依然是线程安全的:

Account:

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

	public void setAccountNo(String accountNo)
	{
		this.accountNo = accountNo;
	}
	public String getAccountNo()
	{
		 return this.accountNo;
	}

	public double getBalance()
	{
		 return this.balance;
	}
	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();
		}
	}

	public int hashCode()
	{
		return accountNo.hashCode();
	}
	public boolean equals(Object obj)
	{
		if (obj != null && obj.getClass() == Account.class)
		{
			Account target = (Account)obj;
			return target.getAccountNo().equals(accountNo);
		}
		return false;
	}
}
  • ReentrantLock锁具有重入性,也就是说线程可以对它已经加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计算器来追踪lock方法的嵌套调用,线程在每次调用lock()加锁后,必须显示调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。
    4.4 死锁

当两个线程相互等待对方释放同步监视器时就会发生死锁,JVM没有检测,也没用采用措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁的出现。一旦出现死锁,整个程序既不会发生任何异常,也不会给任何提示,只是所有线程处于阻塞状态,无法继续。死锁是很容易发生的,尤其在系统中出现多个同步监视器的情况下。

 

class A
{
    public synchronized void foo( B b )
	{
		System.out.println("当前线程名: " + 
			Thread.currentThread().getName() + " 进入了A实例的foo方法" );
		try
		{
			Thread.sleep(200);
		}
		catch (InterruptedException ex)
		{
			ex.printStackTrace();
		}
		System.out.println("当前线程名: " + 
		Thread.currentThread().getName() + " 企图调用B实例的last方法");
		b.last();
	}

	public synchronized void last()
	{
		System.out.println("进入了A类的last方法内部");
	}
}

class B
{
	public synchronized void bar( A a )
	{
		System.out.println("当前线程名: "
			+ Thread.currentThread().getName() + " 进入了B实例的bar方法" );
		try
		{
			Thread.sleep(200);
		}
		catch (InterruptedException ex)
		{
			ex.printStackTrace();
		}
		System.out.println("当前线程名: " 
		+ Thread.currentThread().getName() + " 企图调用A实例的last方法");
		a.last();
	}

	public synchronized void last()
	{
		System.out.println("进入了B类的last方法内部");
	}
}

public class DeadLock implements Runnable
{
	A a = new A();
	B b = new B();

	public void init()
	{
		Thread.currentThread().setName("主线程");
		//调用a对象的foo方法
		a.foo(b);
		System.out.println("进入了主线程之后");
	}
	public void run()
	{
		Thread.currentThread().setName("副线程");
		//调用b对象的bar方法
		b.bar(a);
		System.out.println("进入了副线程之后");
	}
	public static void main(String[] args)
	{
		DeadLock dl = new DeadLock();
		//以dl为target启动新线程
		new Thread(dl).start();
		//执行init方法作为新线程
		dl.init();
	}
}

 上述程序中,主线程调用A对象的同步方法foo()时对A进行加锁,sleep过后,副线程得到执行调用B对象的同步方法bar(),sleep过后,主线程继续执行。此时,主线程需要访问B对象的last()方法,由于此时B对象被副线程加锁了,无法访问,而副线程由于同样的原因也无法访问A对象的last()方法。这样,两个线程都在等待对方先释放锁,程序就“僵住”了,无法执行下去,也不会报任何异常。

 5 线程通信

    5.1 线程的协调运行

可以借助Object类提供的wait()、notify()、notifyAll()三个方法。这三个方法必须由同步监视器对象来调用,可以分为两种情况:

  • 对于同步方法,因为同步监视器就是this,所以可以在同步方法中直接调用这三个方法
  • 对于同步代码块,同步监视器是synchronized后括号里的对象,所以必须使用该对象调用这三个方法

关于这三个方法的解释:

  • wait():调用wait()方法的当前线程会释放对该同步监视器的锁定,直到其他线程调用该同步监视器的notify()或notifyAll()方法来唤醒该线程
  • notify():唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会随机选择唤醒其中一个线程。只有当前线程放弃对该同步监视器的锁定后(使用wait()方法),才可以执行被唤醒的线程
  • notifyAll():唤醒在此同步监视器上等待的所有线程

以一个取款和存款的线程为例,存款后立即取款,不允许多次存款或多次取款。

Account:

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

	public void setAccountNo(String accountNo)
	{
		this.accountNo = accountNo;
	}
	public String getAccountNo()
	{
		 return this.accountNo;
	}

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

	public int hashCode()
	{
		return accountNo.hashCode();
	}
	public boolean equals(Object obj)
	{
		if (obj != null && obj.getClass() == Account.class)
		{
			Account target = (Account)obj;
			return target.getAccountNo().equals(accountNo);
		}
		return false;
	}
}

 

存款线程DepositThread:

public class DepositThread extends Thread
{
	//模拟用户账户
	private Account account;
	//当前取钱线程所希望存款的钱数
	private double depositAmount;

	public DepositThread(String name , Account account , 
		double depositAmount)
	{
		super(name);
		this.account = account;
		this.depositAmount = depositAmount;
	}

	//重复100次执行存款操作
	public void run()
	{
		for (int i = 0 ; i < 100 ; i++ )
		{
			account.deposit(depositAmount);
		}		
	}
}

 

取款线程DrawThread:

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

	//重复100次执行取钱操作
	public void run()
	{
		for (int i = 0 ; i < 100 ; i++ )
		{
			account.draw(drawAmount);
		}
	}
}

 

测试类TestDraw:

public class TestDraw
{
	public static void main(String[] args) 
	{
		//创建一个账户
		Account acct = new Account("1234567" , 0);
		new DrawThread("取钱者" , acct , 800).start();
		new DepositThread("存款者甲" , acct , 800).start();
		new DepositThread("存款者乙" , acct , 800).start();
		new DepositThread("存款者丙" , acct , 800).start();
	}
}

 

程序最后会被阻塞,这是由于一共有三个存款者而只有一个取款者,300次存款操作只有100次取款操作,存款线程需要取款线程释放同步监视器锁,故而存款操作无法继续下去,所以最后被阻塞。注意,阻塞并不是死锁。

    5.2 使用条件变量控制协调

如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器对象,也就不能使用wait、notify、notifyAll方法来协调进程的运行。
当使用Lock对象来保证同步时,java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程。

Lock替代了同步方法或同步代码块,Condition替代了同步监视器的功能。实例实质上被绑定到一个Lock对象上,要获得特定Lock实例的Condition实例,调用Lock对象的newCondition()方法即可。Condition类提供了三个方法:

  • await():同wait()
  • signal:同notify()
  • signalAll:同notifyAll()

Account:

public class Account
{
	//显示定义Lock对象
	private final Lock lock = new ReentrantLock();
	//获得指定Lock对象对应的条件变量
	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;
	}

	public void setAccountNo(String accountNo)
	{
		this.accountNo = accountNo;
	}
	public String getAccountNo()
	{
		 return this.accountNo;
	}

	public double getBalance()
	{
		 return this.balance;
	}
	public void draw(double drawAmount)
	{
		//加锁
		lock.lock();
		try
		{
			//如果账户中还没有存入存款,该线程等待
			if (!flag)
			{
				cond.await();
			}
			else
			{
				//执行取钱操作
				System.out.println(Thread.currentThread().getName() + 
					" 取钱:" +  drawAmount);
				balance -= drawAmount;
				System.out.println("账户余额为:" + balance);
				//将标识是否成功存入存款的旗标设为false
				flag = false;
				//唤醒该Lock对象对应的其他线程
				cond.signalAll();
			}
		}
		catch (InterruptedException ex)
		{
			ex.printStackTrace();
		}
		//使用finally块来确保释放锁
		finally
		{
			lock.unlock();
		}
	}
	public void deposit(double depositAmount)
	{
		lock.lock();
		try
		{
			//如果账户中已经存入了存款,该线程等待
			if(flag)
			{
				cond.await();				
			}
			else
			{
				//执行存款操作
				System.out.println(Thread.currentThread().getName() + 
					" 存款:" +  depositAmount);
				balance += depositAmount;
				System.out.println("账户余额为:" + balance);
				//将标识是否成功存入存款的旗标设为true
				flag = true;
				//唤醒该Lock对象对应的其他线程
				cond.signalAll();
			}
		}
		catch (InterruptedException ex)
		{
			ex.printStackTrace();
		}
		//使用finally块来确保释放锁
		finally
		{
			lock.unlock();
		}
	}

	public int hashCode()
	{
		return accountNo.hashCode();
	}
	public boolean equals(Object obj)
	{
		if (obj != null && obj.getClass() == Account.class)
		{
			Account target = (Account)obj;
			return target.getAccountNo().equals(accountNo);
		}
		return false;
	}
}

 执行效果和前一个方法一样。

    5.3 使用管道流

管道流有3种形式:PipedInputStream和PipedOutputStream,PipedReader和PipedWriter以及Pipe.SinkChannel和Pipe.SourceChannel,它们分别是管道字节流、管道字符流和新IO的管道Channel。

class ReaderThread extends Thread
{
	private PipedReader pr;
	//用于包装管道流的BufferReader对象
	private BufferedReader br;
	public ReaderThread(){}
	public ReaderThread(PipedReader pr)
	{
		this.pr = pr;
		this.br = new BufferedReader(pr);
	}
	public void run()
	{
		String buf = null;
		try
		{
			//逐行读取管道输入流中的内容
			while ((buf = br.readLine()) != null)
			{
				System.out.println(buf);
			}
		}
		catch (IOException ex)
		{
			ex.printStackTrace();
		}
		//使用finally块来关闭输入流
		finally
		{
			try
			{
				if (br != null)
				{
					br.close();
				}
			}
			catch (IOException ex)
			{
				ex.printStackTrace();
			}
		}
	}
}
class WriterThread extends Thread
{
	String[] books = new String[]
	{
		"Struts2权威指南",
		"ROR敏捷开发指南",
		"基于J2EE的Ajax宝典",
		"轻量级J2EE企业应用指南"
	};
	private PipedWriter pw;
	public WriterThread(){}
	public WriterThread(PipedWriter pw)
	{
		this.pw = pw;
	}
	public void run()
	{
		try
		{
			//循环100次,向管道输出流中写入100个字符串
			for (int i = 0; i < 100 ; i++)
			{
				pw.write(books[i % 4] + "\n");
			}
		}
		catch (IOException ex)
		{
			ex.printStackTrace();
		}
		//使用finally块来关闭管道输出流
		finally
		{
			try
			{
				if (pw != null)
				{
					pw.close();
				}
			}
			catch (IOException ex)
			{
				ex.printStackTrace();
			}
		}
	}
}

public class PipedCommunicationTest
{
	public static void main(String[] args)
	{
		PipedWriter pw = null;
		PipedReader pr = null;
		try
		{
			//分别创建两个独立的管道输出流、输入流
			pw = new PipedWriter();
			pr = new PipedReader();
			//连接管道输出流、出入流
			pw.connect(pr);

			//将连接好的管道流分别传入2个线程,
			//就可以让两个线程通过管道流进行通信
			new WriterThread(pw).start();
			new ReaderThread(pr).start();
		}
		catch (IOException ex)
		{
			ex.printStackTrace();
		}
	}
}

 

通常没有必要使用管道流来控制两个线程之间的通信,因为两个线程属于同一个进程,它们可以非常方便的共享数据,这种方式才是线程之间进行信息交换的最好方式,而不是使用管道流。

6 ThreadLocal类

线程局部变量(ThreadLocal)的功能非常简单,就是为每一个使用该变量的线都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。

class Account
{
	/*定义一个ThreadLocal类型的变量,该变量将是一个线程局部变量
	  每个线程都会保留该变量的一个副本*/
	private ThreadLocal<String> name = 
		new ThreadLocal<String>();
	//定义一个初始化name属性的构造器
	public Account(String name)
	{
		this.name.set(name);
		//下面代码看到输出“初始名”
		System.out.println("------" + this.name.get());
	}
	//定义了name属性的setter和getter方法
	public String getName()
	{
		return name.get();
	}
	public void setName(String str)
	{
		this.name.set(str);
	}
}
class MyTest extends Thread
{
	//定义一个Account属性
	private Account account;
	public MyTest(Account account, String name)
	{
		super(name);
		this.account = account;
	}
	public void run()
	{
		//循环10次
		for (int i = 0 ; i < 10 ; i++)
		{
			//当i == 6时输出将账户名替换成当前线程名
			if (i == 6)
			{
				account.setName(getName());
			}
			//输出同一个账户的账户名和循环变量
			System.out.println(account.getName()
				+ " 账户的i值:" + i);
		}
	}
}
public class ThreadLocalTest
{
	public static void main(String[] args) 
	{
		//启动两条线程,两条线程共享同一个Account
		Account at = new Account("初始名");
		/*
		虽然两条线程共享同一个账户,即只有一个账户名
		但由于账户名是ThreadLocal类型的,所以两条线程将
		导致有同一个Account,但有两个账户名的副本,每条线程
		都完全拥有各自的账户名副本,所以从i == 6之后,将看到两条
		线程访问同一个账户时看到不同的账户名。
		*/
		new MyTest(at , "线程甲").start();
		new MyTest(at , "线程乙").start();
	}
}

 

同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式;而ThreadLocal是隔离多个线程的数据共享,从根本上避免了多个线程之间的共享资源(变量),也就不需要对多个线程进行同步了。

7 包装线程不安全的集合

java集合ArrayList、LinkedList、HashSet、TreeSet、HashMap等都是线程不安全的,也就是有可能当多个线程向这些集合中放入一个元素时,可能会破坏这些集合数据的完整性。
如果有多条线程可能访问以上这些集合,我们可以使用Collections提供的静态方法来把这些集合包装成线程安全的集合。

如:

//使用Collections的synchronizedMap方法将一个普通的HashMap包装成线程安全的类
HashMap hashMap = Collections.synchronizedMap(new HashMap());

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值