理解JAVA的进程和线程

       最近系统的学了java中的进程和线程,之前学习线程的时候就觉得很有意思,因为自己经常写的程序但是单线程的,如果写一个多线程的程序,那么在某些情况下肯定是和单线程有很大区别的。确实一开始自己去尝试的写多线程程序,错误可谓是五花八门。而且通过遇到问题解决问题的学习方法让我觉得有点零碎,于是就系统的学了一下,在这里说说和大家分享。

 1.线程的创建方法:

      首先就从线程的创建说起吧(至于那些线程和进程的关系啥的概念就不说了,因为这些通过写程序就可以看的很明白),java 中创建线程的方法无非是两个,大家应该都能说出来——

第一个:继承Thread类。创建一个类继承Thread类,重写父类中的run()方法,把需要线程做事情的代码写到该方法中,如果想启动该线程的话可以实例化这个类,通过该类的对象调用start()方法就可以了。这里要说明的并不是调用run()方法,新手要注意。

      第二个:实现Runnable接口。接下来的事情和上面的差不多,实现run()方法,但是要启动运行线程有点麻烦,首先实例化实现Runnable接口的对象,然后作为参数创建一个Thread类的对象并调用start()方法就可以了。

以上的创建线程的方法很简单就不贴代码了。简单的比较一下,大家会发现如果通过实现Runnable接口的方式去启动线程有点麻烦,但是平时个人写程序的时候自己总是用这类方法,并不是自己不嫌麻烦而是因为java大家都知道单根继承的,因此如果用第一种方法去创建线程的话有些时候就比较尴尬,这个类不能去继承其他类了,所以为了不出现这种情况,个人推荐还是用第二种方法吧,毕竟也代码也不会多多少。

2.线程的安全问题:

      写多线程的程序问题就会出现在这里,那就是线程的安全问题。凡事有利有弊,多线程确实调高了运行的效率,但是很多问题也随之而来了。试想,如果很多线程去访问一个变量,那么就很可能发生当一个线程去使用该变量的时候,这个变量已经被其他的线程改掉了,最终的结果可能就不是我们预想的那样。放大一点,如果很多线程去访问一个共享的资源,那么每个线程去使用的时候,其他线程可能已经改掉,那么就乱套了。针对这个问题我们可以用个生产者和消费者这个例子来说一下(接下来的例子也是用这个例子说明):
首先,解释一下这个例子,所谓的生产者消费者问题就是一个资源如果生产者生产了,那么消费者才可以消费,如果没有该资源那么消费者是不能消费的,另外,生产的资源是有界限的,假设最多生产n个资源,那么如果有n个资源了,那么生产者是不能再生产了,除非消费者消费了。好,为了简便的说明上述的安全问题贴出一个简单的小程序。
公共资源的类:
package customer_and_producer1;

public class Resource {

	private String name;
	private String sex;

	// use for producer to push commodity to container
	public void push(String name, String sex) {
		this.name = name;
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		this.sex = sex;
		System.out.println("生产了 " + this.name + "-" + this.sex);
		return;
	}

	// use for customer to take commodity from container
	public void popup() {
		System.out.println("消费了 " + this.name + "-" + this.sex);
		return;
	}
}
生产者的类:
package customer_and_producer1;

public class Producer implements Runnable {

	private Resource resource = null;

	public Producer(Resource resource) {
		this.resource = resource;
	}

	@Override
	public void run() {
		for (int i = 0; i < 50; i++) {
			produce(i);
		}

	}

	public void produce(int i) {

		if (i % 2 == 0) {
			resource.push("春哥哥", "男");
		} else {
			resource.push("凤姐姐", "女");
		}
		return;
	}

}
消费者的类:
package customer_and_producer1;

public class Customer implements Runnable {

	private Resource resource;

	public Customer(Resource resource) {
		this.resource = resource;
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		for (int i = 0; i < 5000; i++) {
			take();
		}
	}

	public void take() {
		resource.popup();
		return;
	}
}
测试类:
package customer_and_producer1;

public class TestMain {

	public static void main(String[] args) {
		// to create the common object
		Resource resource = new Resource();
		// to create the thread for customer and producer
		new Thread(new Producer(resource)).start();
		new Thread(new Customer(resource)).start();
	}
}
        说明:共享资源就是一个名字加一个性别,生产者根据奇偶关系分别生产“春哥哥”和“凤姐姐”,性别一个是男,一个是女。那么消费者就是消费资源,通过打印“春哥哥 男”或者“风姐姐 女”来表示。这是我们想看到的。但是运行结果

       
可以看出,运行情况和我们想的不一样,因为性别都乱了,这就可以说明线程带来的问题了,当消费者线程去消费的时候,生产者可能刚刚改变了姓名或者性别,这就导致了性别的紊乱。

3.关键字synchronized

        在这里引出synchronized这个关键字,这个关键字啥用呢?就是能保证被它修饰的代码块能够原子性的执行。换句话说,如果一个线程在操作这个被synchronized修饰的代码块,那么在这时候其他线程想访问这个代码块是不允许的。必须等待之前那个完整的执行完了那个代码块其他线程才能访问。
那么如果我们把这个关键字放在push和popup的方法看看什么效果:
package customer_and_producer1;

public class Resource {

	private String name;
	private String sex;

	// use for producer to push commodity to container
	synchronized public void push(String name, String sex) {
		this.name = name;
		try {
			Thread.sleep(10);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		this.sex = sex;
		System.out.println("生产了 " + this.name + "-" + this.sex);
		return;
	}

	// use for customer to take commodity from container
	synchronized public void popup() {
		System.out.println("消费了 " + this.name + "-" + this.sex);
		return;
	}
}
        其他类不变,再运行看看结果:

              这只是一部分,确实没有再出现过性别紊乱的问题。咋一看理所当然呀,毕竟synchronized这个关键字给方法加锁了,保证了原子性。可是仔细一想也不对呀,这确实保证 了原子性,但是只是保证了方法的原子性,但是两个线程访问的不是同一个方法,在生产者线程生产的时候,消费者照样可以消费呀,不还是应该出现性别紊乱的问题吗?这个问题也曾困扰过我。如果真是只是保证了方法原子性的话,确实会出现性别紊乱的现象,但是synchronized虽然只是修饰的一个方法,但是却是给整个对象加了锁。什么意思??先来说锁,就好比一个门,又两个人想进去,锁只有一把,那谁获得了锁,谁就可以进去,等他出来之后,把锁释放,其他人才可以拿到锁进去。synchronized是对整个对象加锁,那么结果就是整个对象里面只要是被synchronized修饰的方法只能同时只有一个在调用,等到这个方法执行完了,那么才会执行其他被synchronized修饰的方法。

        这样,就很好理解了,整个Resource类实例的对象中有两个被synchronized修饰的方法,那么同一个对象中的这两个方法每时每刻只能有一个方法在执行。这样就保证了消费者在消费(调用popup方法)的时候,生产者不会生产(调用push方法)。也就不会性别紊乱了。

4.线程之间的通讯

       解决了并发导致性别紊乱的问题之后,其实消费者和生产者的问题还是没有解决。从上面的截图可以看出,有时候重复消费了,又有时候重复生产了。因为一个资源被消费了之后是不能被再被消费的,同样生产的资源数量达到最大值之后(上面的程序我们就认为最大值是1)是不能再生产了。这就需要当消费者线程只有在生产者线程调用了push()方法之后才能执行popup()方法。
       怎么解决这个问题呢?单凭上面那个synchronized关键字是不行的了。那么就要用到线程通讯?什么是线程通讯?就刚刚这个例子来说,生产者生产了资源之后通知消费者可以消费了,然后生产者一直等待,直到消费者告诉生产者我消费资源了你可以再生产了,那么生产者才可以生产。
       要达到上面两个要求,要介绍两个函数:

        在这里贴出了API文档的Object类型的方法,里面指明了两个方法分别是notify()和wait()。这里值得注意的是和线程相关的这两类方法都是在Object类里面的,也就是说我们平常写的类实例化的对象都可以去调用这两类方法。其实很显然的,因为任何一个类我们都可以让它担当类似共享资源这样的角色。
        好了,开始介绍这两类方法了。为什么说是两类方法呢,大家可以看的出,notify()和wait()都有重载。在这里就简单的说一下notify()和wait()这两个不带参数的方法好了 ,弄懂了他们剩下的也就都懂了 ,另外平时就这两个方法用的多一些。wait()表示执行该方法的线程对象释放同步锁,JVM把该线程放在等待池中,等待其他的线程唤醒这个线程。notify()是表示唤醒线程,执行该方法的线程唤醒在等待池中等待的任意一个线程,把线程转到锁池中等待。还有有一个方法就是notifyAll(),这个就是唤醒等待池中所有的线程了,把线程转到锁池中等待。这里注意的是自己是唤醒不了自己的。
       懂了这两个函数之后,那我们的消费者和生产者的例子就可以比较完整的实现了。
共享资源:
package customer_and_producer;

public class Resource {

	private String name;
	private String sex;
	private Boolean empty;
	
	public Resource(){
		this.empty = true;
	}
	
	// use for producer to push commodity to container
	synchronized public void push(String name, String sex) throws InterruptedException {
		if(!empty)
		{
			this.wait();
		}
		this.name = name;
		this.sex = sex;
		System.out.println("生产了 "+this.name+"-"+this.sex);
		this.notify();
		empty = false;
		return;
	}

	// use for customer to take commodity from container
	synchronized public void popup() throws InterruptedException {
		if(empty)
		{
			this.wait();
		}
		System.out.println("消费了 "+this.name + "-" + this.sex);
		empty = true;
		this.notify();
		return;
	}
}

消费者:
package customer_and_producer;

public class Customer implements Runnable {

	private Resource resource;

	public Customer(Resource resource) {
		this.resource = resource;
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		for (int i = 0; i < 50; i++) {
			take();
		}
	}

	public void take() {
		try {
			resource.popup();
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return;
	}
}

生产者:
package customer_and_producer;

public class Producer implements Runnable {

	private Resource resource = null;

	public Producer(Resource resource) {
		this.resource = resource;
	}

	@Override
	public void run() {
		for (int i = 0; i < 50; i++) {
			produce(i);
		}

	}

	public void produce(int i) {
		try {
			if (i % 2 == 0) {
				resource.push("春哥哥", "男");
			} else {
				resource.push("凤姐姐", "女");
			}
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return;
	}

}
做出这样的改变之后再看结果:

           这样就保证了不会重复消费重复生产了。

5.线程的声明周期

          了解了以上线程的巴拉巴拉,再讲一下关于线程的声明周期问题,因为凡事有始有终,线程也不例外,线程从创建到毁灭都经历了哪些阶段呢?大体可以分成这几个部分:新建状态、可运行状态、等待状态、计时等待状态、终止状态、阻塞状态。这里贴出之前在教学视频中的一张图:
       这张图表明了各个状态之间的关,很好理解,在这里说明一点就是等待和计时等待区别还是很大的,为什么这么说呢?第一,线程等待的时候是要靠另一个线程去唤醒它的,自己是不能唤醒自己的,但是计时等待就不一样了,到达时间就可以继续去执行;第二,线程等待的时候会释放锁的,但是线程在计时等待的时候就不会去释放锁,这个区别要注意。其他的有兴趣的可以自己去了解一下这里不多说了。

6.线程的其他的小知识

       首先有个联合线程的概念,这是什么意思?举个例子:如果两个线程a,b同时在执行,那么在执行过程中我们想让b线程执行完之后再让a线程执行,达到的效果好像是ab联合起来了,因为看到的好像是不再同时执行而是有顺序的执行,联合成一个“大”的线程了。那么在这里就用到了关键方法:join()。
       其次是线程的优先级:每个线程是有优先级的,用数字表示从1到10,数字越大,优先级越高。但是要注意的是,一个线程的优先级越高并不意味着先执行,而是意味着执行的次数多点。怎么知道一个线程的优先级呢?可以通过getPriority()方法获得优先级,也可以通过setPriority来设置优先级。父进程和它创建的字进程的优先级是一样的。
       最后要说的是一个方法,一个不经常用的方法就是 yield()方法,这个方法不怎么用因为确实没啥用,是啥意思呢?就是表示一种意愿,当前线程愿意让出cpu让其他线程去执行,但是只是一种意愿而且,cpu可以忽略。而且,该线程让出之后还有可能自己再抢到占用cpu的机会。

        好了,以上就是我对线程的认识,有些还是写的比较粗糙,以后进一步理解了 线程的原理会再继续完善。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值