- 浏览: 78559 次
- 性别:
- 来自: 北京
文章分类
最新评论
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());
发表评论
文章已被作者锁定,不允许评论。
相关推荐
操作系统实验报告——线程与进程同步,主要探讨了在Linux环境下如何实现进程和线程的同步,以解决经典的生产者-消费者问题。该实验旨在帮助学生掌握操作系统提供的同步机制,并深化对经典同步问题的理解。 实验内容...
在“多线程编程之四——线程的同步”这个文件中,可能包含了上述各种同步机制的具体实现示例和详细说明,这对于初学者来说是一份非常宝贵的参考资料。通过学习和理解这些例子,开发者可以更好地掌握如何在实际项目中...
本文将深入探讨Java中的线程概念、创建方法、状态管理以及同步机制,帮助你全面理解Java线程。 一、线程的基本概念 线程是程序执行的最小单元,每个线程都有自己的程序计数器、虚拟机栈、本地方法栈,而它们共享同...
综上所述,操作系统中的线程是并发编程的核心概念,理解和掌握其原理与实践技巧对于开发高效、可靠的多线程应用至关重要。通过阅读提供的资料,你将能够深入理解线程的各个方面,并能够在实际项目中灵活运用。
标题与描述均提到了“多线程编程之三——线程间通讯”,这明确指出了文章的核心主题:在多线程编程环境下,不同线程之间的通信机制。在现代软件开发中,尤其是涉及到高性能计算、并发处理以及分布式系统设计时,线程...
在多线程编程中,线程的同步是一个关键的概念,主要目的是确保多个线程在访问共享资源时能够有序地执行,防止数据不一致和竞态条件等问题。...理解并熟练运用这些同步技术,是编写高效、可靠的多线程应用程序的基础。
在Java编程语言中,线程是程序执行流的最小单元,一个...理解线程的基础概念、创建方式、状态转换以及线程间的同步与通信机制对于编写高质量的Java应用程序至关重要。希望本文能为你理解和运用Java线程提供一定的帮助。
【线程同步】是多线程编程中一个关键的概念,主要目的是解决多个线程并发访问共享资源时可能出现的问题,如数据不一致、竞态条件等。...理解并熟练运用这些同步机制,对于编写高效、可靠的多线程程序至关重要。
在IT领域,尤其是在服务器开发中,理解多线程的概念至关重要,因为这直接影响到服务器的性能和并发处理能力。本文将详细探讨线程在服务器端实现中的作用,以及它相对于多进程模型的优势。 首先,随着Web服务器的...
总的来说,Java中的并发编程是一门综合技术,它涉及对线程生命周期的理解、线程间通信与协作、线程同步以及并发控制等多方面知识。通过学习并发编程,可以增强处理多任务的能力,提高程序的响应性和吞吐量。这对于...
在实际开发中,理解并熟练掌握这些线程同步机制对于构建高效且稳定的多线程应用程序至关重要。除了`synchronized`关键字,Java还提供了其他并发控制工具,如`java.util.concurrent`包下的`Semaphore`、`Lock`接口...
理解并正确使用`SuspendThread`、`ResumeThread`和`TerminateThread`等函数对于编写健壮的多线程程序至关重要。同时,必须注意避免潜在的问题,如死锁和不安全的资源释放,以确保程序的稳定性和安全性。
Java线程编程是Java开发中的重要组成部分,尤其在面试中,这部分知识的掌握程度往往成为衡量开发者...这些知识点涵盖了Java面试中线程编程部分的基础和进阶问题,理解并熟练掌握这些内容对于面试和实际开发都至关重要。
总的来说,理解和正确使用线程的挂起、唤醒和终止是多线程编程中的关键技能,它们可以帮助我们更好地控制并发执行的流程,提高程序的响应性和效率。在实践中,务必确保线程的生命周期管理是安全和可靠的,避免出现...
了解这些状态有助于理解和调试多线程问题。 - **线程调度**:Java的线程调度包括抢占式调度(优先级高的线程获得更多CPU时间)和合作式调度(线程主动让出CPU)。 2. **锁**: - **内置锁(synchronized)**:...
Java线程编程是Java开发中的重要组成部分,尤其在面试中,这部分知识经常被用来测试候选人的并发编程能力。以下是对给定文件中提到的...在实际开发中,理解并熟练掌握这些内容能帮助开发者编写高效、安全的多线程代码。
### 线程IO模型知识点详解 在讨论线程IO模型之前,首先需要明确几个概念。IO模型通常指的是操作系统处理输入输出操作的...而对于Java开发者来说,理解NIO的相关原理和技术,能够更好地编写高效、可扩展的网络程序。