现在的位置: 首页 > 综合 > 正文

Java基础/Java多线程

2018年05月26日 ⁄ 综合 ⁄ 共 3150字 ⁄ 字号 评论关闭

一、线程的基础知识

1. 多线程使用经典案例:

你有一家餐厅,并且你雇佣了一名服务员(相当于一条单线程)。当你的餐厅的食客数量不多的时候,一名服务员可以按顺序去做,招呼客人,点菜,上菜,清理桌面等工作。

但是当客人非常多时,你发现一名服务员不够用了,这是你会在招几名服务员(新开了几条线程),来更好的服务客人。

2. 进程

通常运行中的服务对应一个进程,就像上述提供餐饮的这个服务,可以比喻成一个进程,在这里进程里,所有的服务员共享餐厅里的物品,比如服务员都可以收拾桌子,都可以上菜。(所有线程共享进程内的全部资源),但是如果有2家餐厅,那么A餐厅肯定不能随便去B餐厅拿桌子的,除非B餐厅允许A餐厅去拿东西。(在没有进程允许下,不允许访问该进程的资源)

3.并发性 与 并行性

同一时刻只能有一条指令被执行,因为cpu处理速度非常快,不同的指令会被快速轮换,造成宏观并行的效果。(并发性)

同一时刻有多条指令被执行。(并行性)

4、关于多线程的并发问题

可以想象一下,服务员A辛辛苦苦的把盘子洗了一半,然后老板叫A去上菜了,接着服务员B不知道盘子已经洗过一半了,又去把前一半洗了一遍。。造成急需盘子的时候,B还在洗盘子,导致客人都跑光了。。。B也被解雇了 :(

二、多线程的创建

1. 继承自Thread类

public class MyThread extends Thread{

	private int i ;
	public MyThread(String name){
		super(name);
	}
	//run 方法称为线程执行体
	public void run() {
		for(;i<20;i++){
			System.out.println(Thread.currentThread().getName()+"-"+i);
		}
	}
	public static void main(String[] args){
		System.out.println(Thread.currentThread().getName()+"-主线程");
		new MyThread("新线程A").start();
		new MyThread("新线程B").start();
	}

}

2. 实现Runnable接口

public class MySecondThread implements Runnable{
	
	private int i ;
	@Override
	public void run() {
		// TODO Auto-generated method stub
		for(;i<10;i++){
			System.out.println(Thread.currentThread().getName()+"-"+i);
		}
	}
	public static void main(String[] args){
		System.out.println(Thread.currentThread().getName()+"-主线程");
		MySecondThread mt = new MySecondThread();
		Thread a = new Thread(mt);
		Thread b = new Thread(mt);
		a.setName("线程A");
		b.setName("线程B");
		a.start();
		b.start();
	}
}

这两者区别在:  数据同步上,继承方法实现的Thread并没有实现数据共享,所以,两个线程需要执行40次.而实现Runnable接口实现了数据同步,仅仅2个线程体,执行同一个操作的时候,只需要执行10次。

三、线程的生命周期

1. 线程的生命周期有5个状态:

新建 --> 就绪 --> 运行  --> 阻塞 --> 死亡

1.1 当一个Thread对象被创建时, 线程处于 新建状态,类似于你买下了一块地皮,准备开餐厅。

1.2 当Thread调用start()方法后, 线程处于就绪状态,类似于你的店装修完毕,开张了等待客人状态。

1.3 运行 ,是等待JVM里的程序调度器的调度 ,就像客人来与不来,我们控制不了,这得看客人心情的。

1.4 阻塞 ,当其他线程轮换后,会阻塞当前线程 。 就像,我们在餐厅,排队打饭菜,排在前面的人A刚好接了个电话,然后无法同时打饭菜,后面的B看到了就上前先打饭菜了,这时A就被阻塞了。


2.不要对线程调用2次以上的start()方法,已经死亡的线程无法使用start()让他重新被调用,否则会抛出IllegalThreadStateException异常。

3.运行 与 阻塞状态的 切换

3.1 线程调用sleep()方法主动放弃所占用的系统资源,因为cpu是不会偷懒的,它会在当前线程sleep时,主动唤醒运行其他就绪的线程。

3.2 线程主动调用一个阻塞式的IO方法,在方法返回之前,该线程将被阻塞。

3.3 该线程试图获得一个同步监视器,但是该同步监视器被其他线程所持有。

3.4 线程等待某个通知(notify)。

3.5 线程调用suspend方法将线程挂起。该方法容易导致死锁,尽量避免使用该犯法。

4. 线程死亡

4.1 run方法体执行完成

4.2 线程抛出异常

4.3 使用stop()方法结束该进程。但是该方法容易导致死锁。

可以使用isAlive()方法判断该线程是否 死亡 或 新建 return false;其他状态 return true;

四、控制线程

1. Join () 该方法需 throws InterruptedException

MyThread join = new MyThread("Join线程");
		join.start();
		join.join();

这时,将会阻塞main线程,直到join线程被执行完后,其他线程才有被执行的机会。

几个比较常用的方法:

sleep() 线程休眠,

yield() 线程让步,

setPriority();设置线程优先级

五、线程的同步

线程间最重要的问题就是线程安全问题,因为正在执行的线程随时可能被停止轮换到另外一个线程,

这时候线程间对,共享数据的读写将操作互斥,造成读取 “脏数据” 。

典型的例子,取钱问题。你的账户有1000,你与你朋友同时去取,每人取800。

这个问题的逻辑很简单

if( 余额 > 800){

   输出余额;
/*****   我是分割线   ****/
   计算新余额 = 余额 - 800;

}


因为有两个人并发去操作假如A与B,当A运行到 【分割线】位置,该线程被JVM调度器阻塞掉,让B去执行。(因为线程随时可能被阻塞),此时B读取的余额数 仍然是1000,

B执行的很顺畅,直接执行完毕之后,现在的余额其实是200,但是因为A被阻塞的位置在if()语句块内,所以在解除阻塞后,会接着计算新的余额,也就是 -600。1000块钱的卡,

能取出1600么?还记得前几个月,有人因为取款机多取出钱,被枪毙的事情吗?惊悚啊。。这时就用到了线程同步。


使用synchronizated(obj){}同步代码块,监视obj对象,同时只允许一个线程操作obj对象。当一个线程操作obj时,对obj加锁,结束操作后对obj解锁。

synchronizated 也可以修饰方法

public synchronizated void function(){}


同步锁 Lock ,锁的类别有


代码的格式

	private final ReentrantLock lock = new ReentrantLock();

	@SuppressWarnings("unused")
	private void EnsureSavetyThreadMethod(){
		
		lock.lock();
		try {
			// 需要保证线程安全的代码
		} catch (Exception e) {
			// TODO: handle exception
		} finally {
			//保证结束后释放锁
			lock.unlock();
		}
	}


死锁

死锁发生的原因在于,两个线程互相等待对象释放加锁的资源,造成的僵持。就像两个人,一个进门一个出门,互相礼让,最后谁也没能走出这扇门





抱歉!评论已关闭.