浅谈多线程

概念:
  1.进程:进程是正在运行的程序的实例.进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
 2.多线程的本质:计算机同时运行多个应用程序称为多任务处理,实际上现在大多数计算机的处理器都是个位数甚至只有一个,操作系统通过处理器分时表现出多个应用程序同时运行的假象。之所以用到这种功能,是其充分利用了处理器的时间,避免浪费。这种概念在应对单应用程序多线程的情况下同样有效。java中通过使用线程支持单个应用程序同时任务(并发)。线程就是并发任务的执行单元。
 java线程:在java中即使没有显式创建线程,有时也可能会创建多线程的任务。main()方法在执行时,就会隐式自动创建一个线程,具有GUI的应用程序也会显式的创建一个线程(AWT线程)用于图形渲染(AWT线程可以同时负责界面绘制和事件通知,这可能导致AWT线程忙于事务处理而忽略了图形绘制而出现类似程序挂起或死机的状态)
1. 创建线程   java中每个线程都有java.lang.Thread类的一个实例表示,要创建新的线程实例,只需让目标类继承Thread类或Runnable接口。因为java无法多继承的限制,而两种方法又无功能上的优劣,故通常实现Runnable接口作为创建线程的首选方法,以使得目标类可以继承其他类实现功能扩展。
  通过将实现了Runnable接口的实例传递给Thread构造函数创建线程,对于实现Runnable接口创建的线程,一旦退出run()方法(类似于main())即意味着线程结束,且无法重新启用或重用。一般不必显式调用run()方法(直接调用run()方法就是普通调用,其并不会另开线程),而采用start()方法(会另开线程)来启动线程。
2. 使用线程的缺点
 1)程序初始化(启动)变慢:利用线程池可以解决该问题
 2)资源利用:每个线程都会创建相应的栈(即包含自身变量及相应信息的存储区),所用平台可能会限制创建线程的数量。该问题通过使用线程池可以得到控制,使用线程池除了能够消除创建新线程引起的开销外,还能减少线程的创建数量(假定程序支持线程池技术,java中并未实现线程池管理器)。
 3)复杂性增加:如调试单线程应用程序时很容易观察应用程序的执行流程,但是一旦线程数增加,就很难再观察各线程的执行顺序
 关于资源共享的一个问题:如下代码中创建两个线程使用同一个runnable实例,但由于nonsharedalue变量是在run()方法中定义的,因此其实他时方法的局部变量,无法被两个线程共享

public class ThreadShared implements Runnable{
		public static void main(String args[]){
				ThreadShared ts=new ThreadShared();
				Thread t1 = new Thread(ts);
				Thread t2 = new Thread(ts);
				t1.start();
				t2.start();
		}
		public void run(){
			int nonSharedValue = 100;
			nonSharedValue +=100;
			System.out.println(nonSharedValue);
		}	
}
//输出
//200
//200
/**
但如果上述程序作如下修改:
	public static void main(String args[]){
				int nonSharedValue = 100;
				ThreadShared ts=new ThreadShared();
				Thread t1 = new Thread(ts);
				Thread t2 = new Thread(ts);
				t1.start();
				t2.start();
		}
		public void run(){
			nonSharedValue +=100;
			System.out.println(nonSharedValue);
		}	
其输出会变成以下几种(即线程不安全)
	1. 200
	   300
	2. 300
	   300
	3. 300
	   200 
*/
 

3. 使用synchronized关键字锁
  从上述例子可以看出当数据同时被多个线程修改时,可能出现损坏,这时可以采用java中的synchronized关键字实现线程锁,防止数据损坏。该关键字规定使用该关键字修饰的代码块或方法在结束之前只能由一个线程能访问或执行,直到代码段结束,从而防止数据的损坏。
  ”java.lang.Object(即java对象)都含有一个锁(即synchronized关键字),启用该关键字后,线程在进入该段代码之前必须取得被synchronized修饰的对象的线程锁,如果线程A已经取得该对象的线程锁,另一个线程B还想要访问该对象则必须等待,直到线程A对该段代码的锁释放,每个锁中还会有一份等待该锁的线程的列表,当某个线程无法获取锁时进入等待,并入表。 “
 synchronized关键字也可以作为方法的修饰符,表示同步整个方法,如:

public class StudentRoster{
	protected java.util.Vector studentList;
	
	public synchronized void remove(Student st){
		studentList.addElement(st);
		st.setEnrolled(true);
	}
}

  synchronized关键字通常与某个对象实例关联,这点对于上段代码似乎不是那么容易理解,上文中关联到了哪个对象呢?实际上,当synchronized关键字用于实例方法时,该方法所属的对象即为被锁对象,即下段代码的功能等价于在remove()调用前加上synchronized关键字:

StudentRoster sr =new StudentRoster();
Student st= new Student();
.
.
synchronized(sr){
	sr.remove(st)
}

  定义同步(即上锁)的类方法(即静态方法)时,对该方法的调用会与该类关联的class对象同步,例:

public class StudentRoster{
	public static synchronized StudentRoster getNewInstance(){
		return new StudentRoster();
	}
}

  那么getNewInstance()方法的调用将会与StudentRoster关联的class对象同步,因此在getNewInstance()的方法定义中加入同步,就相当于按如下代码调用该方法:

StudentRoster sr;
.
.
synchronized(StudentRoster.class){
sr = StudentRoster.getNewInstance();
}

4. 同步方法和同步代码块的嵌套调用
当线程尝试执行同步方法或同步代码块时,如果其他线程已经拥有关联对象的监控,那么该线程就会阻塞。不过,如果线程已经拥有了该方法锁对象,那当该线程想要进入该线程想要再次进入该锁对象的另一个锁方法或者代码块时会发生什么呢?如:

class ....{
	public synchronized void a1(){
		a2();
	}
	public synchronized void a2(){
		...
	}
}

  当线程进入a1方法时,即取得了所在对象的锁再调用a2方法时,由于线程已经拥有了相应的锁,于是就不会去获取锁,因此线程可以直接正常执行。
  每次线程成功进入某个对象的同步方法或同步代码块时,就会递增与该对象关联的一个计数值,而当线程退出该方法或代码块后,计数值就会减一。只有当对象的关联计数值变为0时,线程才会释放该对象的锁,线程得以在退出最先取得锁的代码之前始终拥有该监控。而在上例中,当调用a1时,线程取得锁并将计数器加1,当调用a2时计数器又加1,即为2,而在执行完a2之后计数器减1,退出a1之后又减1变为0,此时释放该对象的锁。

5. 同步代码块与同步方法
  上文中示范了整个方法加锁,事实上,也可以指对方法中的一段代码进行
加锁,但通常建议采用后者进行加锁。因为锁会减少程序中多线程的并发性,因此,过度使用多线程会破坏程序的并发性,违背了并发的初衷。因此,建议在不牺牲线程安全性的前提之下,尽量少的使用锁或减少锁中包含的代码行数。过多的锁会影响程序的正常(流畅运行)。

6. 死锁
定义:使用synchronized关键字对对象G,P加锁,线程A持有对象G的锁,并请求对象P的锁,线程B持对象P的锁并请求访问对象G的锁,于是线程A,B同时进入不可调和等待,即死锁。
解决办法:
 1)高层同步:对方法F加锁,其实现对G,P对象进行同步,使得原本需要的两个锁减少为了一个从而消除死锁。但该方法会降低程序的并发程度。
 2)锁定排序:由上例所示,两个线程以不同的顺序获取上锁对象会造成死锁,其根本原因是获取锁的顺序差异,如果两个线程以相同的顺序获取锁,就能解决死锁问题。

7. 线程优先级
  每个线程都会分到一个用1到10 表示的优先级,该数值用于表示该线程是否优于其他线程运行。当出现多个可运行线程(该线程已经启动,没有死亡,也没有阻塞)时,处理器则根据优先级决定那个优先运行。
  处理器在上下文切换时会优先选择优先级高的可运行线程,因此优先级高的线程运行更频繁。
 决定线程运行时长和频率的实际因素取决于应用程序运行的平台和所在的JVM实现。有些操纵系统选择优先级高的第一个可运行线程,有些操作系统则把相同优先级的线程以循环方式排到进度表中。Java虽然支持10个优先级,但底层操作系统的线程体系支持的优先级数量可能有多有少。这时,由JVM负责把分配给Thread对象的优先级值映射为相应的本地优先级。正是由于不同平台之间的差异使得Java无法担保优先级对线程的影响,因此应尽量避免使用优先级影响应用程序的假设条件。
  通常根据线程功能来分配线程的优先级。如,若线程大部分时间用于等待输入并且任务会很快完成,那么一般分配较高的优先级。相反,若执行某些无关紧要的后台任务,就应该分配较低的优先级,例如文字处理程序在用户不显示要求文档拼音检查时,应该使用低优先级进行自动检查。
 值得注意的是,即使Java线程本身采用了抢占式多任务处理,高优先级线程依然可能垄断处理器控制权,因此当谨慎分配高优先级。只为那些定期放弃处理器控制权的线程分配高优先级。

8. 监控处理程序
  线程 可分为监控线程(守护线程)和用户线程(非守护线程)。通过使用Thread类的setDaemon(Boolean b)方法指定线程类型,但必须在线程启动前调用该方法,当b为true时为监控线程,b为false时为用户线程。
  用户线程与监控线程的区别:用户线程会组织JVM退出,而JVM线程不会。例如AWT事件线程即为用户线程,其在JFrame创建时启动。假设有线程main(),用户线程A,监控线程B(长时间运行),这表示当main()退出时,由于线程A还在运行,那么JVM即不会退出,线程B也得以继续运行,但紧接着当线程A运行结束,由于JVM中无用户线程存在,JVM退出,线程B也会立刻退出。但若是线程A还在运行,线程B退出,线程A则可以继续运行。
 如果想要用户线程强制下线可以使用System.exit()强制关闭JVM。监控线程常作为持续运行的后台程序,并且不要求JVM在终止时执行整理操作,例如垃圾回收机制。

9. 线程休眠:sleep(),wait()

sleep()wait()
sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;wait()方法是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);其他线程可以访问
sleep()是Thread类的Static(静态)的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程
在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常

  注意:
 1.使用sleep()方法时,由于该方法是属于Thread类的静态方法,所以,当任何对象调用该方法时,他表示的是使当前线程(非调用对象)所在的线程进入休眠
 2.sleep()和wait()方法的最大区别是:
 sleep()睡眠时,保持对象锁,仍然占有该锁;
 而wait()睡眠时,释放对象锁。
 但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)

 3.如果其他的线程中断了一个休眠的线程,sleep方法会抛出Interrupted Exception。

  4.休眠的线程在唤醒之后不保证能获取到CPU,它会先进入就绪态,与其他线程竞争CPU。

  5.wait方法必须正在同步环境下使用,比如synchronized方法或者同步代码块。如果你不在同步条件下使用,会抛出IllegalMonitorStateException异常。另外,sleep方法不需要再同步条件下调用,你可以任意正常的使用

10. 唤醒线程:notify(),notifyAll()
  通过让另一个线程调用Object类中的notify()或notifyAll()方法唤醒线程。与wait()方法类似,线程在调用notify()或者notifyAll()方法之前也必须取得对象的锁。否则会抛异常(IllegalMonitorStateException)。
  notify() 与notifyAll()方法之间的区别:
  调用notify()方法会移除等待列表中的所有等待线程,而调用notify()方法只会移除其中的一个线程,并且不确定移除哪一个线程(实际上仍然可以通过JDK8中的java.util.concurrent.locks下的Condition 实现唤醒指定的线程)。由于不能使用notify()方法移除指定线程,因此只有在唤醒任意线程时才使用notify()方法。

11. 终止线程

12. 过实方法:suspend(),resume(),stop()
  当一个线程想挂起或停止另一个线程时,其往往并不知道该线程的当前状态知否适合立即挂起。而这三个方法就算线程不适合挂起或停止,其也会立即线程,因此应当避免使用这几个过实方法

13.线程让步:yield()
  通过调用Thread类中的静态方法yield()方法使当前线程通知另一个相同优先级的线程开始运行。概念上可以认为yield()方法把线程放到同优先级可运行线程列表的末尾,让出CPU。直觉上这会使另一个线程运行,但实际上这是不确定的,即使有很多同优先级的线程可运行线程竞争CPU,该线程也有可能立即重新获取CPU。

14. 并发工具:线程池
  q前面说到使用线程会使应用程序变得复杂且具有潜在问题。例如创建并启动一个新线程是一个相对较慢的过程,因此大量创建线程会降低应用程序的性能。这是,可以使用线程池来解决该问题,通过线程池来维护可用线程的组。当需要时,从池中取出一个线程,从而避免了创建新线程造成的系统开销。换句话说,线程池技术允许重复使用单个线程,而不必为每个任务创建新线程并在任务结束后销毁。

参考:
 1.“Pro Java Programming Second Edition” --Brett Spell
2.Java中notify和notifyAll的区别 - 何时以及如何使用
3.理解java线程的中断(interrupt)
4.Java Thread之Sleep()使用方法总结
5.Java sleep和wait的区别

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值