多线程的介绍与使用


1.什么是线程?什么是进程?

进程:进程是程序的一次动态执行过程,它需要经历从代码加载,代码执行到执行完毕的一次完整过程。
线程:每一个进程都有至少有一个执行顺序,该顺序是一个执行路径,也叫做一个控制单元。而一个独立的控制单元就是一个线程。
区别:一个进程有好多线程,比如word文档是一个进程,那拼写检查,就是一个线程,线程关闭了但进程不一定消失,但是进程关闭了,线程一定消失。

2.创建线程的两种方式

在java中如果想要实现多线程有两种方式,一种是继承Thread类,另一种是实现Runnable接口。

2.1继承Thread类

在java中,如果一个类继承了Thread类,此类就称为多线程实现类。在Thread的子类中,必须覆盖run()方法才行,因为线程要执行的内容都是要定义在run()方法内的。
public class MyThread extends Thread{
	@Override
	public void run() {
		for(int i=0;i<5;i++){
			System.out.println(Thread.currentThread().getName()+"运行: i="+i);
		}
	}
}

如果要启动线程,需要调用start()方法
	public static void main(String[] args) {
		MyThread thread1 = new MyThread();
		MyThread thread2 = new MyThread();
		thread1.start();
		thread2.start();
	}
上面的代码定义了两个线程,并启动了它们,运行结果如下(其中的一种情况):
Thread-0运行: i=0
Thread-1运行: i=0
Thread-0运行: i=1
Thread-1运行: i=1
Thread-0运行: i=2
Thread-1运行: i=2
Thread-0运行: i=3
Thread-0运行: i=4
Thread-1运行: i=3
Thread-1运行: i=4
注意:启动线程必须调用start()方法,不能调用run()方法。而且start()方法只能被调用一次否则会报错。

2.2实现Runnable接口

创建线程的另一种方式是通过实现Runnable接口来实现多线程,Runnable接口中只定义了一个方法public void run(); 示例如下:
public class MyThread implements Runnable{
	@Override
	public void run() {
		for(int i=0;i<5;i++){
			System.out.println(Thread.currentThread().getName()+"运行: i="+i);
		}
	}
	
	public static void main(String[] args) {
		MyThread thread1 = new MyThread();
		MyThread thread2 = new MyThread();
		Thread t1 = new Thread(thread1);
		Thread t2 = new Thread(thread2);
		t1.start();
		t2.start();
	}
}

2.3两种方式的联系与区别

既然java有两种方式创建多线程,那两者有什么联系和区别呢,如果查看Thread类源码会得到这样的总结(精简后的代码):
public class Thread {

	public Thread(Runnable target, String name){
		init(null, target, name, 0);
	}
	private void init(ThreadGroup,Runnable target, String name,long stackSize){
		this.target = target;
	}
	
	public void run(){
		if(target!=null){
			target.run();
		}
	}
}
可以发现,在Thread类中的run()方法调用的是Runnable接口中的run()方法,也就是说此方法是由Runnable子类完成的,所以如果要是通过继承Thread类实现多线程则必须覆盖run()方法。
这是二者之间的联系,那么区别呢?Thread类不能资源共享。例如:
public class MyThread implements Runnable{
	private int ticket = 5;
	@Override
	public void run() {
		while(true){
			if(ticket>0){
				System.out.println("卖票: ticket=" + ticket--);
			}
			if(ticket==0){
				break;
			}
		}
	}
	
	public static void main(String[] args) {
		MyThread sell = new MyThread();
		new Thread(sell).start();
		new Thread(sell).start();
		new Thread(sell).start();
	}
}
运行结果为:
卖票: ticket=5
卖票: ticket=4
卖票: ticket=3
卖票: ticket=2
卖票: ticket=1
上面启动了3个线程,但是卖票却共同卖了5张票,换句话说就是ticket属性被所有线程共享了,而继承Thread类却做不到这一点。
所以实现Runnable接口比继承Thread的好处有如下几点:
1. 适合多个相同的线程去处理同一资源
2. 可以避免java单继承特点带来的局限性
3. 增强了程序的健壮性,代码能被多个线程共享,代码与数据是独立的

3.线程的生命周期

线程的生命周期有五个状态
1. 创建状态
在程序中创建了一个线程对象后,这个线程对象便处于创建状态,此时它已经有了相应的内存空间和其他资源,但是出于不可运行状态。
2.就绪状态
当该线程调用start()方法后就启动了线程,此时线程具有了执行资格,但是CPU还没有调用这个线程,这时叫就绪状态
3.运行状态
当就绪状态的线程被调用的时候就同时具有了执行权和执行资格,线程就进入了运行状态,此时线程会执行run方法内的内容
4.阻塞状态
一个正在执行的线程在某些特殊的情况下被人为的挂起,此时CPU会暂停对该线程的执行,该线程进入阻塞状态。一般sleep(),wait()等方法都会使线程进入阻塞状态。
5.死亡状态
当线程调用stop()方法或者run()方法执行结束后,即出于死亡状态。

4.线程的同步和死锁

先观察如下代码:
	private int ticket = 5;
	@Override
	public void run() {
		while(true){
			if(ticket>0){
				try {
					Thread.sleep(100);
				} catch (Exception e) {
					// TODO: handle exception
				}
				System.out.println("卖票: ticket=" + ticket--);
			}
			if(ticket==0){
				break;
			}
		}
	}

运行结果为:
...
卖票: ticket=2
卖票: ticket=1
卖票: ticket=0
卖票: ticket=-1
程序加入了延迟操作后,运行结果出现了负数。原因分析,当票数还剩1张的时候,线程1拿到了执行权发现票数大于0,则继续执行,执行到sleep()方法后暂时sleep 100毫秒,此时线程2获得执行权,线程2发现票数还是大于0,则也进入执行,执行到sleep(100)的时候也暂时进入睡眠,这线程1醒了过来拿到了执行权,继续执行剩下的部分,对票进行-1操作,此时票数已经为0了,线程1结束。线程2醒过来之后也继续执行,而此时ticket已经变为0,所以当线程2再对票数进行-1操作的时候就得到了-1。
如果想解决这个问题就必须用到线程的同步。

4.1线程的同步

解决资源共享的同步操作,可以使用同步代码块和同步方法两种方式来完成。
同步代码块的格式为:
	private int ticket = 5;
	@Override
	public void run() {
		synchronized (this) {
			while(true){
				if(ticket>0){
					try {
						Thread.sleep(100);
					} catch (Exception e) {
						// TODO: handle exception
					}
					System.out.println("卖票: ticket=" + ticket--);
				}
				if(ticket==0){
					break;
				}
			}
		}

很多人多同步语句中的this不了解,synchronized (object)括号内放的是一个对象,有人会问,这个对象是哪个对象,任意一个对象都行吗?答案是任意一个对象都行,但是,这个对象就像是一把锁,一开始处于打开状态,如果一个线程进来了,这把锁就锁上了,其他线程发现锁被锁上了,就无法进入,当其中的线程运行完同步代码块的内容时,锁会自动打开,其他线程再进入再重复上面的步骤,所以 如果想实现同步必须保证多个线程用的是同一把锁。如果线程用的是不同的锁,同步就失效了。

在看同步方法的格式
public class MyThread implements Runnable{
	private int ticket = 5;
	@Override
	public void run() {
		this.sell();
	}
	
	public synchronized void sell(){
		while(true){
			if(ticket>0){
				try {
					Thread.sleep(100);
				} catch (Exception e) {
					// TODO: handle exception
				}
				System.out.println("卖票: ticket=" + ticket--);
			}
			if(ticket==0){
				break;
			}
		}
	}
}

synchronized方法也是持有一把锁的,它默认持有的对象就是该类的对象,static方法也可以被synchronized修饰,如果被synchronized修饰则static方法持有的对象是这个类的字节码文件。

4.2死锁

所谓死锁就是指两个线程都在等对方先执行完,造成了停滞,一般死锁都是在运行的时候发生的,下面是个死锁的例子:
public class Ticket implements Runnable{

	public static final Object obj = new Object();
	public static final Object obj1 = new Object();
	boolean flag;
	
	public Ticket(boolean flag){
		this.flag = flag;
	}
	@Override
	public void run() {
		if(flag){
		synchronized (obj) {
			System.out.println("if-obj");
			synchronized (obj1) {
				System.out.println("if-obj1");
			}
		}
		}
		else{
			synchronized (obj1) {
				System.out.println("else-lockobj1");
				synchronized (obj) {
					System.out.println("else-lockobj");
				}
			}
			
		}
	}
}

public class DeadLockDemo {

	public static void main(String[] args) {
		Ticket t1 = new Ticket(true);
		Ticket t2 = new Ticket(false);
		Thread th1 = new Thread(t1);
		Thread th2 = new Thread(t2);
		th1.start();
		th2.start();
	}
}

线程1运行的时候会进入if语句,线程2则进入else,当线程1进入if语句第一个同步块的时候,持有obj锁,此时线程切换到线程2运行,线程2进入else语句中第一个同步块,线程2持有obj1锁。然后线程切换到线程1,线程1发现obj1锁正在被线程2使用则会在这个地方等待,当线程2执行的时候发现obj锁被线程1使用则也会等待,这样就造成了线程1和线程2都在等待对方执行完的情况,这就是死锁。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值