Java多线程

1. 线程基础

1.1 进程

      进程就是应用程序的执行实例,它的特点是:动态性,并发性和独立性

1.2 线程

      线程是进程内部的一个执行单位,它是程序中一个单一的顺序控制流程。

      它的特点:

           *一个进程可以包含多个线程,而一个线程至少要一个父进程

           *线程可以有自己的堆栈,程序计数器和局部变量

           *线程与父进程的其他线程共享进程所有的全部资源,包括内存

           *独立运行,采用抢占方式

           *一个线程可以创建和删除另外一个线程

           *同一个进程中的多个线程之间可以并发执行

           *线程的调度管理是由进程来完成的

      注意:编程时必须确保线程不会妨碍同一进程的其他线程

小结:

      线程是比进程小一级的单位,它不可以独立存在,一个进程可以包含多个线程,这些线程和它对应的进程共享一个存储空间,因此在数据交换通信方面,线程比进程更具优势。线程是并发执行的,当然在一个cpu的情况下,它只是在逻辑上“同时”运行,在物理上任然是串行的,即线程在不同时间片段轮流占有cpu资源。在多个cpu的情况下,各个线程在同一时间段内运行在不同cpu上,这才真正做到并发执行。多线程的优点就是提高了cpu的执行效率,提升了应用系统的运行效率那么在什么情况下应用Java多线程编程呢?比如说要循环播放一段背景音乐,服务器向客户端推送消息广告,在以前的项目中,还用到过多线程分发邮件,后台管理员向会员分发系统邮件,像这些需要重复完成的动作就可以使用多线程来提高系统的性能。当然还有一些复杂,费时的操作也都可以使用多线程来完成。

2. 线程的创建

2.1 创建线程的第一种方法(继承Thread

   1、定义Thread的子类,重写父类的run()方法

   2、创建Thread的子类的对象

   3、调用start()方法启动该线程


class Thread1 extends Thread {
	private String accoutId;
	private String accoutName;
	@Override
	public void run() {
		super.run();
		for (int i = 0; i < 5; i++) {
			System.out.println("线程"+this.getName()+"第"+i+"次出现。");
		}
	}
	public String getAccoutId() {
		return accoutId;
	}
	public void setAccoutId(String accoutId) {
		this.accoutId = accoutId;
	}
	public String getAccoutName() {
		return accoutName;
	}
	public void setAccoutName(String accoutName) {
		this.accoutName = accoutName;
	}
}
public class MyThread1{
	public static void main(String[] args) {
		Thread1 thread1 = new Thread1();
		thread1.start();
	}
}


2.2 创建线程的第二种方法(实现Runnable接口)

       1、定义Runnable接口的实现类

       2、重写run()方法

       3、用 new  ThreadRunnable的实现类)的方式实例化一个Thread类的线程对象

       4、调用start()方法启动该线程


class Thread2 implements Runnable {
	private String accoutId;
	private String accoutName;
	public void run() {
		for (int i = 0; i < 5; i++) {
			System.out.println("线程"+Thread.currentThread().getName()+"第"+i+"次出现。");
		}
	}
	public String getAccoutId() {
		return accoutId;
	}
	public void setAccoutId(String accoutId) {
		this.accoutId = accoutId;
	}
	public String getAccoutName() {
		return accoutName;
	}
	public void setAccoutName(String accoutName) {
		this.accoutName = accoutName;
	}
}
<pre name="code" class="java">public class MyThread2{
	public static void main(String[] args) {
		Thread thread2 = new Thread(new Thread2());
		thread2.start();
	}
}

 

2.3 两种创建线程的方法的比较

 优点缺点
第一种方法(继承Thread类)编写简单,可以使用this直接访问当前线程无法再继承其他类
第二种方法(实现Runnable接口)可以继承其他类,多个线程之间可以使用同一个Runnable对象编程方式复杂,不可以直接访问当前对象,要通过currentTread()访问

3.  线程的状态和生命周期

3.1 线程的状态

1新生

    创建一个线程类的的时候,它的状态就是新生态。它可以通过start( )方法进入就绪状态,上图中应该在新生态和可运行状态之间加上一个就绪状态

2、就绪状态

    处于就绪状态的线程就已经具备的运行的条件,它有机会被分配到cpu资源变成运行状态。

3、运行状态

    运行状态的线程会占用cpu,执行run( )方法中的内容。处于运行状态的线程又可以转变成就绪状态、阻塞状态和死亡状态

    运行状态的线程通过yield( )方法,主动让出cpu资源,重新回到就绪状态,并和其他线程重新争夺cpu资源。

    运行状态的线程可以通过sleep( )方法让线程睡眠,并放弃它所占的系统资源,此时,线程的状态为阻塞状态。当然还有其他情况可使运行状态的线程变成阻塞状态,比如,在I/O上阻塞,等待I/O操作完成,或者该线程正在等待通知notify( )唤醒,还有该线程调用suspend( )方法。

4阻塞状态

    处于运行状态的线程如果执行了sleep()方法、suspend( )方法,或者等待I/O操作完成,该线程将让出CPU资源并暂时停止自己的运行,进入阻塞状态。阻塞状态不会直接变成就绪状态与其他线程争夺cpu资源,它只会等到睡眠时间到了,或者I/O操作完成后,在转为就绪态与其他线程竞争。

5、死亡状态

    当线程的run( )方法执行完以后,该线程就进入死亡状态。

3.2 线程的优先级

     * 默认情况下,一个线程继承其父类的优先级

     * 优先级表示为一个整数值

     * 优先级越高,执行的机会越大,反之,执行机会越小

     * 高优先级的线程可以抢占低优先级线程的cpu资源

     * Java提供10个优先等级,最大等级10,最小等级1,一般为5 

     线程的优先级与线程执行的效率没有必然的联系


4. 线程调度的常用方法

     1Join()   非静态方法,使用具体的进程对象调用

         join()方法的特点是:

          * join()执行时,当前正在进行的进程会被挂起,让调用join方法的进程执行

          * join进来的进程没有执行完,会一直阻塞当前进程

     2Sleep()  静态方法,可以使用在run()方法中,实现当前进程的挂起,阻塞指定的时间   

        语法: Thread.sleep( long millis )

         sleep()方法使当前进程进入睡眠状态--被阻塞状态,则此时其他等待的进程皆有机会执行。sleep()方法要进行异常处理                

     3Yield() 使当前线程进入可运行状态,当前进程本身仍然有机会再次只是暂停当前线程,

        语法: Thread.yield();  

     4setDaemon() 设置当前线程对象为后台进程    

         语法:线程对象.setDaemonboolean on;      

     5Sleep()和yield()方法的比较

        * sleep()方法使当前线程转入被阻塞的状态,它释放了系统资源。而yield()方法使当前线程转入可运行状态

        * sleep()方法总是强制当前线程停止执行,而yield()方法不一定

        * sleep()方法可以使其他等待运行的线程有同样的执行机会,而yield()方法只使相同或者更高优先级的线程获得执行机会

        * 使用 sleep 方法时需要捕获异常,而 yield 方法无需捕获异常


5. 线程的同步      

  

5.1 同步方法

    语法:

         访问修饰符 synchronized 返回类型  方法名()

         synchronized 访问修饰符 返回类型  方法名()

5.2 同步代码块

  语法:

      Synchronized (指定要收益的对象){ 同步语句块 }

5.3 存钱和取钱

        为什么要使用进程同步呢?由于各线程和它们对应的进程是共享一个存储空间的,也就是说当各线程访问并修改同一个变量时,会出现数据不同步的问题。举个最经典的例子,存钱和取钱。有一个账户,初始有1000元钱,tom在ATM机上取了400,正在ATM吐钱的时候,jerry在另一个ATM机上也取了400,但是他发现这个账户上任然是1000元,好了,问题就来了,明明tom取了400,按理说jerry再取的时候账户总金额应该是600才对,这就是数据不同步引起的数据冲突问题。用线程操作共享存储变量时一样的道理,下面我们用存取钱的例子还原一下这个问题。

public class MyThread2{
	public static void main(String[] args) {
		Account account = new Account();
		account.setAccoutAmount(1000.00);//初始账户1000.00元
		Thread2 thread2 = new Thread2();//Runnable的实现类
		thread2.setAccount(account);
		Thread tom = new Thread(thread2,"tom");//tom线程
		Thread jerry = new Thread(thread2, "jerry");//jerry线程
		Thread jack = new Thread(thread2, "jack");//jack线程
		Thread rose = new Thread(thread2, "rose");//rose线程
		tom.start();
		jerry.start();
		jack.start();
		rose.start();
	}
}
class Thread2 implements Runnable {
	private Account account;
	public void run() {
		getMoney(400.00);//取钱
	}
	//取钱
<span style="font-family:宋体;">       </span>public void getMoney(double money){
		if ((account.getAccoutAmount() - money)<=0) {
			System.out.println(Thread.currentThread().getName()+"的账户余额不足。");
			return;
		}
		try {
			System.out.println(Thread.currentThread().getName()+"的账户上有"+account.getAccoutAmount()+" 元钱。");
			account.setAccoutAmount(account.getAccoutAmount() - money);
			System.out.println(Thread.currentThread().getName()+"取走了"+money+"元,还剩"+account.getAccoutAmount()+" 元钱。");
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	//存钱
	public void setMoney(double money){
		account.setAccoutAmount(account.getAccoutAmount() + money);
	}
	public Account getAccount() {
		return account;
	}
	public void setAccount(Account account) {
		this.account = account;
	}
	
}
class Account{
	private String accoutId;
	private String accoutName;
	private double accoutAmount;
	public String getAccoutId() {
		return accoutId;
	}
	public void setAccoutId(String accoutId) {
		this.accoutId = accoutId;
	}
	public String getAccoutName() {
		return accoutName;
	}
	public void setAccoutName(String accoutName) {
		this.accoutName = accoutName;
	}
	public double getAccoutAmount() {
		return accoutAmount;
	}
	public void setAccoutAmount(double accoutAmount) {
		this.accoutAmount = accoutAmount;
	}
}
运行几次,出现的结果都不一样,这是因为线程的开始和结束时无法预测的。但问题基本都是一样 的,就是tom,jerry,jack,rose取钱的时候账户总额都是1000元,而此时他们都有取钱的动作,这与事实是不相符的。

jack的账户上有1000.0 元钱。
jack取走了400.0元,还剩600.0 元钱。
jerry的账户上有1000.0 元钱。
tom的账户上有1000.0 元钱。
tom取走了400.0元,还剩-200.0 元钱。
rose的账户上有1000.0 元钱。
rose取走了400.0元,还剩-600.0 元钱。
jerry取走了400.0元,还剩200.0 元钱。

为了解决这个问题,我们需要用到多线程的同步(synchronized关键字),具体操作就是在取钱和存钱的方法前加上 synchronized关键字,加上 synchronized关键字表示这个方法是同步方法,也就是 一个时候只允许一个线程使用这个方法。它的运行机制是,当一个线程执行这个同步方法时,它就获得了这个方法的同步锁,除非它把这个方法执行完,把锁释放掉,否则别的线程是得不到这个同步锁然后执行方法的。这样同步了的方法一次只允许一个线程执行它,就解决了数据同步引起的数据冲突问题。具体实现如下:

//取钱
	synchronized public void getMoney(double money){
		if ((account.getAccoutAmount() - money)<=0) {
			System.out.println(Thread.currentThread().getName()+"的账户余额不足。");
			return;
		}
		try {
			System.out.println(Thread.currentThread().getName()+"的账户上有"+account.getAccoutAmount()+" 元钱。");
			account.setAccoutAmount(account.getAccoutAmount() - money);
			System.out.println(Thread.currentThread().getName()+"取走了"+money+"元,还剩"+account.getAccoutAmount()+" 元钱。");
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	//存钱
	synchronized public void setMoney(double money){
		account.setAccoutAmount(account.getAccoutAmount() + money);
	}
就是在取钱和存钱方法前加上 synchronized关键字,出现的结果是这样的。

tom的账户上有1000.0 元钱。
tom取走了400.0元,还剩600.0 元钱。
jack的账户上有600.0 元钱。
jack取走了400.0元,还剩200.0 元钱。
rose的账户余额不足。
jerry的账户余额不足。

5.4 死锁

虽然synchronized关键字能解决数据同步的问题,但是它的使用影响了线程并发执行的特性,而且,不恰当的使用synchronized会引起死锁。那么什么是死锁?死锁就是两个或多个线程之间,相互占有资源,又相互等待对方释放资源,各线程因此都处于阻塞状态,产生了死锁。举个例子来说,有A,B两个线程,A线程运行并占有a资源,睡眠1秒。然后B线程运行并占有b资源,接着它也睡眠1秒。之前A线程睡眠1秒之后需要得到b资源,但此时b资源已经被B占据,没有释放,所以A线程等待B线程释放b资源。B线程睡眠了1秒之后又需要a资源,但是a资源一直被A线程占据,没有释放,所以B线程等待A线程释放a资源。而此时A线程一直在等B线程释放b资源,A ,B线程都在等待对方释放资源,这就是死锁。

因此在多线程编程时应特别注意死锁的产生。

6.  线程之间的通信

     线程间的通信首先建立在同步的范围内

6.1 线程间通信的实现

     1wait()方法   释放已有共享资源对象的锁,进入wait队列

     2notify()方法  随机唤醒

     3notifyAll()方法  唤醒全部

wait() 使得线程进入阻塞状态,它有两种形式,一种允许指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用。调用 wait( ) 方法,会释放掉已有共享资源对象的锁,进入阻塞状态,它只会等待超出指定时间或者被notify()唤醒才会有机会重新获得锁进入可执行状态。

调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。

调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。







评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值