`

Java Concurrent之Thread

 
阅读更多

  今天看着看着源码,突然发现Thread的一些概念 觉得那么的陌生。。。囧rz....所以特意整理一下 以备不时之需。

   其实线程也就是我们计算机中正在执行程序(进程)的某个单一顺序的控制流。线程不仅可以共享进程的内存空间,而且也拥有自己的工作内存(即系统分配的线程栈空间)。

   对于我们的java程序,总是会有一个默认的主线程,当JVM加载java程序时发现main方法,就会启动一个线程即主线程。如果main方法里没有其他线程,那么main返回时jvm就回结束java程序。但如果main方法中启动了其他新的线程,jvm就会在主线程和这些线程之间进行轮流切换,保证每个线程都有机会使用CPU资源。这时,我们需要注意 此时的主线程返回也不会导致java程序结束,jvm会等到该程序的所有线程结束,才会终结我们的程序。(也有例外情况,如果强制调用了Runtime的exit方法,而且安全管理器允许程序退出,则JVM也会终止程序)也即:java中的多线程其实是一种抢占机制(多个线程处于运行状态,但只允许一个线程运行,那么他们之间就会采用谁抢到算谁的的竞争方式抢占CPU)而不是分时机制。

   线程的定义:

   众所周知,java中定义一个线程既可以继承Thread类 亦可实现Runnable接口,具体如何玩,想必也不必我啰嗦。他们之间的区别在于,因为java是单根继承,所以我们一般使用Runnable来定义线程,使用Runnable方式 线程间可以共享相同的内存单元(包括代码和数据),并利用这些共享单元来实现数据交换、实时通信与必要的同步操作。对于Thread(Runnable target)创建的使用同一目标对象的线程,可以共享该目标对象的成员变量和方法。

 另外,创建目标对象类在必要时还可以是某个特定类的子类,因此,使用Runnable接口比使用Thread的子类更具有灵活性。

 (注意:具有相同目标对象的线程,run()方法中的局部变量相互独立,互不干扰)

  至于Thread的启动时利用了JNI调用操作系统底层的方法。代码为证:

 

 private native void start0();

 

 

  线程的生命周期:

  

在上图中,我们需要注意的是:使用Object.wait()/notify()方法时,这些方法其实都是针对已经获取了对象锁进行的操作,所以从语法的角度,我们必须将Object.wait()/notify()放在synchronized(obj){...}语法块中执行。从功能上来说,wait()方法是对象在获得对象锁后,主动释放对象锁同时阻塞(休眠)当前线程,直到其他线程调用该对象的notify()方法来唤醒该线程,才能继续获取对象锁继续执行。但是有一点需要我们注意的是,notify()方法唤醒线程并不是立即生效,而是在相应的synchronized(){}语法块执行结束,自动释放锁后,JVM在wait()对象锁的线程中随机选取一个赋予对象锁,唤醒线程 继续执行。Thread.sleep()和Object.wait()都可以暂停当前线程,释放CPU资源,主要区别在Object.wait()释放CPU资源的同时,释放了对象锁的控制。Sleep()不释放的锁指的是“单线程锁”  而并非是对象锁。

以下代码依然不是同步的:

/**
 * @author Sonicery_D
 * @date 2014年11月13日
 * @version 1.0.0
 * @description
 **/
public class Share {

	public void print(String str){
		System.out.print("["+str);
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
			System.out.print("thread interrupted ...");
		}
		System.out.print("]");
	}
	
	public static void main(String[] args) {
		Share share = new Share();
		Call call1 = new Call("A",share);
		Call call2 = new Call("B",share);
		Call call3 = new Call("C",share);
		Call call4 = new Call("D",share);
	}
}
class Call implements Runnable{

	private Share share;
	private String str;
	public Call(String str,Share share){
		this.str = str;
		this.share = share;
		Thread thread = new Thread(this);
		thread.start();
	}
	@Override
	public void run() {
		share.print(str);
	}
	
}

 我们需要将线程资源加锁:

public synchronized void print(String str){
		System.out.print("["+str);
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
			System.out.print("thread interrupted ...");
		}
		System.out.print("]");
	}

 
 新建状态(new):用new语句创建的线程对象处于新建状态,此时它和其它的java对象一样,仅仅在堆中被分配了内存 

就绪状态(runnable):当一个线程创建了以后,其他的线程调用了它的start()方法,该线程就进入了就绪状态。处于这个状态的线程位于可运行池中,等待获得CPU的使用权.一旦JVM将CPU使用权切换给该线程时,此线程就可以脱离创建它的主线程独立开始自己的生命周期了。

    线程创建后仅仅是占有了内存资源,在JVM管理的线程中还没有这个线程,此线程必须调用start()方法(从父类继承)通知JVM,这样JVM就知道又有一个新的线程排队等候切换。

     当JVM将CPU使用权切换给线程时,如果线程是Thread的子类创建的,该类中的run()方法立刻执行(run()方法中规定了该线程的具体使命)。Thread类中的run()方法没有具体内容,程序要在Thread类的子类中重写run()方法覆盖父类该方法。(注意:在线程没有结束run()方法前,不要再调用start方法,否则将发生ILLegalThreadStateException异常) 

运行状态(running): 一旦获取CPU(被JVM选中),线程就进入运行(running)状态,线程的run()方法才开始被执行;在运行状态的线程执行自己的run()方法中的操作,直到调用其他的方法而终止、或者等待某种资源而阻塞、或者完成任务而死亡;如果在给定的时间片内没有执行结束,就会被系统给换下来回到线程的等待状态;此时线程是活着的(alive);

阻塞状态(blocked):当线程处于阻塞状态时,java虚拟机不会给线程分配CPU,直到线程重新进入就绪状态,它才有机会转到运行状态。 

引起阻塞状态的情况大致如下:

  • (1)、JVM将CPU资源从当前线程切换给其他线程,使本线程让出CPU的使用权,并处于挂起状态。
  • (2)、线程使用CPU资源期间,执行了sleep(int millsecond)方法,使当前线程进入休眠状态。sleep(int millsecond)方法是Thread类中的一个类方法,线程执行该方法就立刻让出CPU使用权,进入挂起状态。经过参数millsecond指定的毫秒数之后,该线程就重新进到线程队列中排队等待CPU资源,然后从中断处继续运行。
  • 线程使用CPU资源期间,执行某个操作进入阻塞状态,如执行读/写操作引起阻塞。进入阻塞状态时线程不能进入线程队列,只有引起阻塞的原因消除时,线程才能进入到线程队列排队等待CPU资源,以便从中断处继续运行。
  • (3)、线程使用CPU资源期间,执行了wait()方法,使得当前线程进入等待状态。JVM就会把线程放到这个对象的等待池中, 等待状态的线程不会主动进入线程队列等待CPU资源,必须由其他线程调用notify()方法通知它,才能让该线程从新进入到线程队列中排队等待CPU资源,以便从中断处继续运行。
  • (4)、 位于对象锁中的阻塞状态,当线程处于运行状态时,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他的线程占用,JVM就会把这个线程放到这个对象的琐池中。

死亡状态(dead):线程释放了实体,即释放了分配给线程对象的内存。线程死亡的原因有两个:

 (1)、正常运行的线程完成了它的全部工作,即执行完run()方法中的全部语句,结束了run()方法。

 (2)、线程被提前强制性终止,即强制run()方法结束。

    有三种方法可以使终止线程。

1.  使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。

2.  使用stop方法强行终止线程(这个方法不推荐使用,因为stop和suspend、resume一样,也可能发生不可预料的结果)。

3.  使用interrupt方法中断线程。

 

    一般来说,当调用start方法后,线程开始执行run方法中的代码。线程进入运行状态。可以通过Thread类的isAlive方法来判断线程是否处于运行状态。当线程处于运行状态时,isAlive返回true,当isAlive返回false时,可能线程处于等待状态,也可能处于停止状态。

    在线程执行的过程中,可以通过两个方法使线程暂时停止执行。这两个方法是suspend和sleep。在使用suspend挂起线程后,可以通过resume方法唤醒线程。而使用sleep使线程休眠后,只能在设定的时间后使线程处于就绪状态(在线程休眠结束后,线程不一定会马上执行,只是进入了就绪状态,等待着系统进行调度)。

    虽然suspend和resume可以很方便地使线程挂起和唤醒,但由于使用这两个方法可能会造成一些不可预料的事情发生,因此,这两个方法被标识为deprecated(抗议)标记,这表明在以后的jdk版本中这两个方法可能被删除,所以尽量不要使用这两个方法来操作线程。

  线程中常用方法总结:

join():等待此线程死亡后再继续,可使异步线程变为同步线程

   也即, 线程联合:一个线程A在占有CPU资源期间,可以让其他线程调用join()方法和本线程联合,如: 

              B.join();

              此时称A在运行期间联合了B。这时A线程立刻中断执行,一直等到它联合的线程B执行完毕,A线程          再重新排队等待CPU资源。但如果A准备联合的线程B已经结束,则B.join()不会产生任何效果。

  守护线程

    线程默认是非守护线程,非守护线程也称用户线程,一个线程调用setDaemon(boolean on)方法可以将自       己设置成一个守护(Daemon)线程,如:

   thread.setDaemon(true);

   一个线程必须在自己运行之前设置自己是否是守护线程。守护线程是当程序中所有用户线程都已结束运行      时即使守护线程的run()方法还有需要执行的语句,守护线程也会立刻结束运行。因此守护线程用于做一些      不是很严格的工作,当线程随时结束时不会产生什么不良后果。

interrupt():Thread.interrupt()对运行的线程是不起作用的,只对阻塞的线程起作用。Thread.interrupt()不会中断一个正在运行的线程,Thread.interrupt()实际完成的是在线程处于阻塞状态时,抛出一个中断信号,这样线程就可以退出阻塞状态,重新排队获取CPU资源。更确切的说,当线程被Object.wait(),Thread.join()和Thread.sleep()三个方法之一阻塞,那么再调用该线程的interrupt()方法时得到的一个中断异常(InterruptedException),从而提早地终结被阻塞状态。

    那么当线程处于上述方法阻塞,正确中断线程的方式是设置共享变量,并调用interrupt()方法(注意:变量应该先设置),如果线程没有被阻塞,调用interrupt将不起作用,否则线程将接受到异常,逃出阻塞状态。

//main 方法中在调用 interrupt()之前 将stop 设置为true

public void run(){

    while(!stop){
         try{
            //执行业务逻辑
         }catch(InterruptedException e){
              stop = true;
             logger.info("线程被Interrupt,退出");
         }
     }

}

 

isAlive():当线程调用start()方法并占有CPU资源后该线程的run()方法开始运行,在run()方法没有结束之前调用isAlive()返回true,当线程处于新建态或死亡态时调用isAlive()返回false。

 

sleep():让当前正在执行的线程休眠,有一个用法可以代替yield函数——sleep(0)

yield():暂停当前正在执行的线程对象,并执行其他线程。也就是交出CPU一段时间

sleep和yield区别:

1、sleep()方法会给其他线程运行的机会,而不考虑其他线程的优先级,因此会给较低线程一个运行的机会;yield()方法只会给相同优先级或者更高优先级的线程一个运行的机会。 

2、当线程执行了sleep(long millis)方法后,将转到阻塞状态,参数millis指定睡眠时间;当线程执行了yield()方法后,将转到就绪状态。 

3、sleep()方法声明抛出InterruptedException异常,而yield()方法没有声明抛出任何异常 

4、sleep()方法比yield()方法具有更好的移植性 

 

如果希望明确地让一个线程给另外一个线程运行的机会,可以采取以下的办法之一:

1、 调整各个线程的优先级 

2、 让处于运行状态的线程调用Thread.sleep()方法 

3、 让处于运行状态的线程调用Thread.yield()方法 

4、 让处于运行状态的线程调用另一个线程的join()方法

 

首先,wait()和notify(),notifyAll()是Object类的方法,sleep()和yield()是Thread类的方法。

 

(1).常用的wait方法有wait()和wait(long timeout):

    void wait() 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。 

    void wait(long timeout) 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或者超过指定的时间量前,导致当前线程等待。

    wait()后,线程会释放掉它所占有的“锁标志”,从而使线程所在对象中的其它synchronized数据可被别的线程使用。

    wait()和notify()因为会对对象的“锁标志”进行操作,所以它们必须在synchronized函数或synchronized  block中进行调用。如果在non-synchronized函数或non-synchronized block中进行调用,虽然能编译通过,但在运 行时会发生IllegalMonitorStateException的异常。

使用 wait()需要注意:

    wait()方法必须放在一个循环中,因为在多线程环境中,共享对象的状态随时可能改变。当一个在对象等待池中的线程被唤醒后,并不一定立即恢复运行,等到这个线程获得了锁及CPU才能继续运行,又可能此时对象的状态已经发生了变化。

(2).Thread.sleep(long millis),必须带有一个时间参数。

    sleep(long)使当前线程进入停滞状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;

    sleep(long)可使优先级低的线程得到执行的机会,当然也可以让同优先级和高优先级的线程有执行的机会;

    sleep(long)是不会释放锁标志的。

 

(3).yield()没有参数。

    sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出CPU占有权,但让出的时间是不可设定的。

    yield()也不会释放锁标志。

 

    实际上,yield()方法对应了如下操作: 先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则继续运行原来的线程。所以yield()方法称为“退让”,它把运行机会让给了同等优先级的其他线程。

 

    sleep方法允许较低优先级的线程获得运行机会,但yield()方法执行时,当前线程仍处在可运行状态,所以不可能让出较低优先级的线程些时获得CPU占有权。 在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I/O阻塞,那么较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。

 

    yield()只是使当前线程重新回到可执行状态,所以执行yield()的线程有可能在进入到可执行状态后马上又被执行。所以yield()只能使同优先级的线程有执行的机会。

volitile 语义

volatile相当于synchronized的弱实现,也就是说volatile实现了类似synchronized的语义,却又没有锁机制。它确保对volatile字段的更新以可预见的方式告知其他的线程。

volatile包含以下语义:

(1)Java 存储模型不会对valatile指令的操作进行重排序:这个保证对volatile变量的操作时按照指令的出现顺序执行的。

(2)volatile变量不会被缓存在寄存器中(只有拥有线程可见)或者其他对CPU不可见的地方,每次总是从主存中读取volatile变量的结果。也就是说对于volatile变量的修改,其它线程总是可见的,并且不是使用自己线程栈内部的变量。也就是在happens-before法则中,对一个volatile变量的写操作后,其后的任何读操作理解可见此写操作的结果。

尽管volatile变量的特性不错,但是volatile并不能保证线程安全的,也就是说volatile字段的操作不是原子性的,volatile变量只能保证可见性(一个线程修改后其它线程能够理解看到此变化后的结果),要想保证原子性,目前为止只能加锁!

 

数据同步:

 

线程同步的特征: 

1、 如果一个同步代码块和非同步代码块同时操作共享资源,仍然会造成对共享资源的竞争。因为当一个线程执行一个对象的同步代码块时,其他的线程仍然可以执行对象的非同步代码块。(所谓的线程之间保持同步,是指不同的线程在执行同一个对象的同步代码块时,因为要获得对象的同步锁而互相牵制) 

2、 每个对象都有唯一的同步锁 

3、 在静态方法前面可以使用synchronized修饰符。

     synchronized(this)与synchronized(static XXX)的区别:

      在JAVA中的Object类型中,都是带有一个内存锁的,在有线程获取该内存锁后,其它线程无法访问该内存,从而实现JAVA中简单的同步、互斥操作。synchronized就是针对内存区块申请内存锁,this关键字代表类的一个对象,所以其内存锁是针对相同对象的互斥操作,而static成员属于类专有,其内存空间为该类所有成员共有,这就导致synchronized()对static成员加锁,相当于对类加锁,也就是在该类的所有成员间实现互斥,在同一时间只有一个线程可访问该类的实例。

4、 当一个线程开始执行同步代码块时,并不意味着必须以不间断的方式运行,进入同步代码块的线程可以执行Thread.sleep()或者执行Thread.yield()方法,此时它并不释放对象锁,只是把运行的机会让给其他的线程。 

5、 Synchronized声明不会被继承,如果一个用synchronized修饰的方法被子类覆盖,那么子类中这个方法不在保持同步,除非用synchronized修饰。

  • 大小: 353 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics