Thread入门与线程方法详解及多线程安全

首先区分三个概念:程序、进程和线程。

  • 程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
  • 进程(process)是程序的一次执行过程,或是正在运行的一个程序。动态过程:有它自身的产生、存在和消亡的过程。如:运行中的QQ,运行中的MP3播放器。程序是静态的,进程是动态的
  • 线程(thread),进程可进一步细化为线程,是一个程序内部的一条执行路径。若一个程序可同一时间执行多个线程,就是支持多线程的

JVM允许程序运行多个线程,它通过java.lang.Thread类来实现。Thread类的特性:

  • 每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体。
  • 通过该Thread对象的start()方法来调用这个线程

【1】Thread类

该类实现了Runnable接口。
在这里插入图片描述

① 构造方法

  • Thread():创建新的Thread对象
  • Thread(String threadname):创建线程并指定线程实例名
  • Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法
  • Thread(Runnable target, String name):创建新的Thread对象

② 两种创建方式

  • 继承Thread类
  1. 定义子类继承Thread类。
  2. 子类中重写Thread类中的run方法。
  3. 创建Thread子类对象,即创建了线程对象。
  4. 调用线程对象start方法:启动线程,调用run方法。
  • 实现Runnable接口
    1)定义子类,实现Runnable接口。
    2)子类中重写Runnable接口中的run方法。
    3)通过Thread类含参构造器创建线程对象。
    4)将Runnable接口的子类对象作为实际参数传递给
    Thread类的构造方法中。
    5)调用Thread类的start方法:开启线程,调用 Runnable子类接口的run方法。

③ 两种方法区别与优势

两种方法区别:

  • 继承Thread: 线程代码存放Thread子类run方法中。
  • 实现Runnable:线程代码存在接口的子类的run方法。

实现Runnable接口相对于继承Thread类来说,有如下显著的好处:

  • 适合多个相同程序代码的线程去处理同一资源的情况,把虚拟CPU(线程)同程序的代码,数据有效的分离,较好地体现了面向对象的设计思想。

  • 可以避免由于Java的单继承特性带来的局限。
    我们经常碰到这样一种情况,即当我们要将已经继承了某一个类的子类放入多线程中,由于一个类不能同时有两个父类,所以不能用继承Thread类的方式,那么,这个类就只能采用实现Runnable接口的方式了。

  • Runnable可以无限地创建Thread进行run,而Thread进行一次run之后就无法再次run(Thread执行了start之后不可以再次执行start)。
    要实现线程能重复运行,如果采用XXX extends Thread,那么每次运行都必须new一个XXX,这十分损耗资源;如果使用XXX implements Runnable,那每次运行只需要新开一个线程new Thread(xxx)即可,节省了很多时空消耗。

④ start()与run()的区别

当调用start()方法时,它会新建一个线程然后执行run()方法中的代码。如果直接调用run()方法,并不会创建新线程,方法中的代码会在当前调用者的线程中执行。


【2】线程生命周期

要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:

  • 新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
  • 就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件。
  • 运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态, run()方法定义了线程的操作和功能。
  • 阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态。
  • 死亡:线程完成了它的全部工作或线程被提前强制性地中止。

线程的生命周期如下图:

这里写图片描述

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

这里写图片描述


【3】线程的调度与优先级

① 调度策略

  • 时间片
    这里写图片描述

给每个线程一个一时间片,线程在消耗完时间片未结束时,则进入就绪状态等待分配时间片。

  • 抢占式

高优先级的线程抢占CPU。


② Java的调度方法

同优先级线程组成先进先出队列(先到先服务),使用时间片策略,对高优先级,使用优先调度的抢占式策略。

③ 线程的优先级

MAX_PRIORITY(10);
MIN _PRIORITY (1);
NORM_PRIORITY (5);

涉及的方法:

getPriority() :返回线程优先值 
setPriority(int newPriority) :改变线程的优先级

线程创建时继承父线程的优先级。


【4】线程方法说明

① sleep()

使当前线程(即调用该方法的线程)暂停执行一段时间,让其他线程有机会继续执行,但它监控状态依然保持,到时后会自动恢复,并不释放对象锁(wait会释放对象锁)。

也就是说如果有synchronized同步块,即使sleep了其他线程仍然不能访问共享数据。注意该方法要捕捉InterruptedException异常。

例如有两个线程同时执行(没有synchronized),一个线程优先级为MAX_PRIORITY,另一个为MIN_PRIORITY,如果没有Sleep()方法,只有高优先级的线程执行完毕后,低优先级的线程才能够执行;但是高优先级的线程sleep(500)后,低优先级就有机会执行了。

总之,sleep()可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的线程有执行的机会。

  • Thread.sleep(long millis)必须带有一个时间参数。
  • sleep(long)使当前线程进入停滞-waiting状态,所以执行sleep()的线程在指定的时间内肯定不会被执行;
  • sleep(long)可使优先级低的线程得到执行的机会,当然也可以让同优先级的线程有执行的机会;
  • sleep(long)是不会释放锁标志的;
  • sleep()方法结束后,线程处于就绪状态,而非直接运行状态。
Thread.currentThread().sleep(100);

② join()

当某个程序执行流中调用其他线程的 join() 方法时,调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止。低优先级的线程也可以获得执行

允许指定一个以毫秒为单位的参数,表示等待该线程终止的最长时间,如果没有参数则表示等待该线程执行完毕再调度其他线程。但是如果另一个线程中断了该线程,则会抛出InterruptedExceprtion异常。

注意该方法也需要捕捉异常。示例如下:

public class ThreadTest {

	static AtomicInteger i = new AtomicInteger();
	
	public static class AddThread implements Runnable{
		public void run(){
			for (int k = 0; k<10;k++){
				System.out.println(Thread.currentThread().getName()+"--k->"+k+", i-->"+i);
				i.incrementAndGet();
			}
		}
	public static void main(String[] args) throws InterruptedException {
		Thread[] ts = new Thread[2];
		for(int k=0;k<2;k++){
			ts[k] = new Thread(new AddThread());
			ts[k].setName(k+"");
		}
		System.out.println("开始启动线程");
		for(int k=0;k<2;k++){
			ts[k].start();
		}
		System.out.println("开始join");
		for(int k=0;k<2;k++){
			ts[k].join();
			System.out.println(k+"---join");
		}
		System.out.println("the end-->"+i);
		
		}
	}
}

输出结果如下:

开始启动线程
开始join
0--k->0, i-->0
1--k->0, i-->0
0--k->1, i-->1
1--k->1, i-->2
0--k->2, i-->3
0--k->3, i-->5
0--k->4, i-->6
0--k->5, i-->7
0--k->6, i-->8
0--k->7, i-->9
0--k->8, i-->10
0--k->9, i-->11
1--k->2, i-->4
1--k->3, i-->13
1--k->4, i-->14
1--k->5, i-->15
1--k->6, i-->16
1--k->7, i-->17
1--k->8, i-->18
1--k->9, i-->19
0---join
1---join
// 线程执行完,最后再执行main中的打印方法
the end-->20


③ yield()

yield()没有参数,功能与sleep()类似,只是不能由用户指定暂停多长时间,并且yield()方法只能让同优先级的线程有执行的机会,不能使低优先级线程有执行结汇。

yield方法与sleep方法区别说明如下:

  • sleep 方法使当前运行中的线程睡眠一段时间,进入不可以运行状态(停滞状态-waiting),这段时间的长短是由程序设定的 ;

  • sleep方法和join方法都可以使低优先级线程获得执行机会,但是yield方法不能使低优先级线程获得执行机会;

  • yield方法使当前线程让出CPU占有权,但让出的时间是不可设定的 ;

  • yield方法与sleep不同的是,会使线程处于就绪状态,可能下一秒就被执行;

  • yield和sleep都不会释放对象锁标志,wait会释放锁。

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

另外,sleep 方法允许较低优先级的线程获得运行机会,但yield()方法执行时,当前线程仍处在可运行状态,所以不可能让出较低优先级的线程此时获取CPU占有权。

在一个运行系统中,如果较高优先级的线程没有调用sleep方法,也没有受到I/O阻塞,那么较低优先级线程只能等待所有较高优先级的线程运行结束,方可有机会运行。yield()只是使当前线程重新回到可执行状态,所有执行yield()的线程有可能在进入到可执行状态后马上又被执行,所以yield()方法只能使同优先级的线程有执行的机会。


④ wait()和notify()、notifyAll()

这三个方法用于协调多个线程对共享数据的存取,所以必须在synchronized语句块内使用。

synchronized关键字用于保护共享数据,阻止其他线程对共享数据的存取,但是这样程序的流程就很不灵活了。

如何才能在当前线程还没退出synchronized数据块时让其他线程也有机会访问共享数据呢?

此时就用这三个方法来灵活控制。


wait()方法使当前线程暂停执行并释放对象锁,让其他线程可以进入synchronized数据块,当前线程被放入对象等待池中。

当调用notify()方法后,将从对象的等待池中移走一个任意的线程并放到锁标志等待池中,只有锁标志等待池中线程能够获取锁标志。如果锁标志等待池中没有线程,则notify()不起作用。

notifyAll()则从对象等待池中移走所有等待那个对象的线程并放到锁标志等待池中。

也就是说wait()和notify()因为会对对象的“锁标志”进行操作,所以他们必需在synchronized函数或者 synchronized block 中进行调用。

如果在non-synchronized 函数或 non-synchronized block 中进行调用,虽然能编译通过,但在运行时会发生IllegalMonitorStateException的异常。

这里写图片描述

wait():令当前线程挂起并放弃CPU、同步资源,使别的线程可访问并修改共享资源,而当前线程排队等候再次对资源的访问。
notify():唤醒正在排队等待同步资源的线程中优先级最高者结束等待
notifyAll ():唤醒正在排队等待资源的所有线程结束等待。

注意 这三个方法都是java.lang.Object的方法。


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

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

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

在当前线程中调用方法: 对象名.wait()。使当前线程进入等待(某对象)状态 ,直到另一线程对该对象发出 notify (或notifyAll) 为止。

调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)。否则也会抛出IllegalMonitorStateException异常。

这里写图片描述

调用此方法后,当前线程将释放对象监控权 ,然后进入等待。在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行。


⑤ 线程运行-run和start()

把需要处理的代码放到run()方法中,start()方法启动线程将自动调用run()方法,这个由java的内存机制规定的。并且run()方法必需是public访问权限,返回值类型为void。


⑥ 关键字synchronized

该关键字用于保护共享数据,当然前提条件是要分清哪些数据是共享数据。

每个对象都有一个锁标志,当一个线程访问到该对象,被synchronized修饰的数据将被"上锁",阻止其他线程访问。当前线程访问完这部分数据后释放锁标志,其他线程就可以访问了。


⑦ sleep与wait区别

sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。

wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。


⑧ stop

强制线程生命期结束。


⑨ suspend

挂起一个线程,状态变为阻塞。

这种方法已被弃用,因为它本质上是死锁倾向的。如果目标线程在监视器上保持一个锁,当它被挂起时,保护一个关键系统资源,没有线程可以访问该资源,直到目标线程被恢复。如果要恢复目标线程的线程试图在调用resume()之前锁定此监视器,则死锁结果。这种死锁通常表现为“冻结”过程。


⑩ resume

唤醒一个suspend线程,状态变为就绪。

因为suspend已经过时,该方法也过时。


【5】线程的分类

Java中的线程分为两类:一种是守护线程,一种是用户线程。它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。

守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程。

Java垃圾回收就是一个典型的守护线程。若JVM中都是守护线程,当前JVM将退出。


【6】互斥锁

在Java语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。

每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

关键字synchronized 来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问。

同步的局限性:导致程序的执行效率要降低。

同步监视器:俗称锁,任何一个类的对象都可以充当锁。如果想保住线程安全,要求所有线程使用同一把锁。

同步方法(非静态的)的锁默认为this。

同步方法(静态的)的锁为默认当前类本身。

需要注意的是,在同步代码块的时候,如果使用继承Thread类的方式,锁慎用this。implementsf方式,锁可以使用this。

示例如下:

public class TestThread implements Runnable {
    int i = 1;

    @Override
    public void run() {
        // TODO Auto-generated method stub
        while(true){
/*指代的为 t,因为使用的是implements方式。若使用继承Thread类的方式,慎用this*/
            synchronized (this) {
            /*唤醒另外一个线程,注意是this的方法,而不是Thread*/
            notify();
            try {
            /*使其休眠100毫秒,放大线程差异*/
            //Thread,currentThread().sleep(0,100);使其休眠100纳秒
                Thread.currentThread().sleep(100);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
                if (i<=100) {

                    System.out.println(Thread.currentThread().getName() + ":"+ i);
                    i++;
                    try {
                    /*放弃资源,等待*/
                        wait();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
        }
    }
    }

    public static void main(String[] args) {
        /*只有一个TestThread对象*/
        TestThread t = new TestThread();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        t1.setName("线程1");
        t2.setName("线程2");

        t1.start();
        t2.start();

    }
}
            

【7】锁的释放

① 以下情况将会释放锁

当前线程的同步方法、同步代码块执行结束。

当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。

当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。

当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁(sleep不会释放锁)。


② 以下情况不会释放锁

线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。

线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。

应尽量避免使用suspend()和resume()来控制线程。


【8】死锁

① 死锁的形成与解决
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。

解决方法

专门的算法、原则,尽量减少同步资源的定义。


② 死锁实例

package com.web.maven.thread;

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) {
	//获取A对象的引用传递
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 进入了B实例的bar方法"); // ②
		try {
		//睡眠0.2s,免得过快执行
			Thread.sleep(200);
		} catch (InterruptedException ex) {
			ex.printStackTrace();
		}
		System.out.println("当前线程名: " + Thread.currentThread().getName()
				+ " 企图调用A实例的last方法"); // ④
		//企图使用a对象,调用a对象的方法。如果此时拿不到a对象又不释放b对象资源,
		//将会形成死锁
		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();
		//另起线程
		new Thread(dl).start();
		//主线程
		dl.init();
	}
}

测试结果如下:

当前线程名: 主线程 进入了A实例的foo方法
当前线程名: 副线程 进入了B实例的bar方法
当前线程名: 副线程 企图调用A实例的last方法
当前线程名: 主线程 企图调用B实例的last方法

当两个线程在保持自身对象锁的情况下,企图请求对方对象资源,将会形参死锁,相互等待。


【9】stop()和suspend()方法为何不推荐使用

反对使用stop(),是因为它不安全。

它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态,那么其他线程能在那种状态下检查和修改它们。结果很难检查出真正的问题所在。

suspend()方法容易发生死锁。

调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被"挂起"的线程恢复运行。

对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend(),而应在自己的Thread类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用wait()命其进入等待状态。若标志指出线程应当恢复,则用一个notify()重新启动线程。


【10】经典例题-生产者消费者

生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。

这里可能出现两个问题:

生产者比消费者快时,消费者会漏掉一些数据没有取到。
消费者比生产者快时,消费者会取相同的数据。

实例如下:

public class TestProduct {
	
	public static void main(String[] args) {
		Clerk clerk = new Clerk();
		Thread productorThread = new Thread(new Productor(clerk));
		Thread consumerThread = new Thread(new Consumer(clerk));
		productorThread.start();
		consumerThread.start();
	}

}
//售货员
class Clerk{  
	private int product = 0;
	public synchronized void addProduct(){
		if(product >= 20){
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}else{
			product++;
			System.out.println("生产者生产了第"+product+"个产品");
			notifyAll();
		}
	}
	public synchronized void getProduct(){
		if(this.product <= 0){
			try {
				wait();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}else{
			System.out.println("消费者取走了第"+product+"个产品");
			product--;
			notifyAll();
		}
	}
}
/********************************生产者***************************************/
class Productor implements Runnable{  
	Clerk clerk;
	public Productor(Clerk clerk){
		this.clerk = clerk;
	}
	public void run(){
		System.out.println("生产者开始生产产品");
		while(true){
			try {
				Thread.sleep((int)Math.random()*1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			clerk.addProduct();
		} 
	}  
}
/********************************消费者***************************************/
class Consumer implements Runnable{  
	Clerk clerk;
	public Consumer(Clerk clerk){
		this.clerk = clerk;
	}
	public void run(){
		System.out.println("消费者开始取走产品");
		while(true){
			try {
				Thread.sleep((int)Math.random()*1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			clerk.getProduct();
		}  
	}  
}


【11】处理多线程安全的常见手段

① 同步机制

  • 监视器锁(synchronized);
  • 显示锁(ReentrantLock、RReadWriteLock);
  • 原子变量(AtomicInteger/AtomciLong);
  • Volatile;

注意同步机制具体使用要看性能和用途。jdk6之前,监视器锁性能弱于显示锁。JDK6之后,JVM对监视器锁进行了优化,性能大于显示锁。

另外原子类和Volatile是有局限性的,尤其是Volatile通常不能保证原子性。

参考博文:Java虚拟机的锁优化策略


② 线程封闭

当访问共享的可变数据时,通常需要同步。一种避免同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步,这种技术称为线程封闭。

  • 栈封闭
  • ThreadLocal

③ 其他方案

不可变对象一定是线程安全的。当满足以下条件时的对象才是不可变的:

  • 对象创建后不可更改;
  • 对象的所有域都是final;

最佳方案:使用线程安全的对象实现线程安全,参考java.util.concurrent包下的对象。

参考博文:
认真学习Java中线程实现和调度

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流烟默

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值