最近系统的学了java中的进程和线程,之前学习线程的时候就觉得很有意思,因为自己经常写的程序但是单线程的,如果写一个多线程的程序,那么在某些情况下肯定是和单线程有很大区别的。确实一开始自己去尝试的写多线程程序,错误可谓是五花八门。而且通过遇到问题解决问题的学习方法让我觉得有点零碎,于是就系统的学了一下,在这里说说和大家分享。
1.线程的创建方法:
首先就从线程的创建说起吧(至于那些线程和进程的关系啥的概念就不说了,因为这些通过写程序就可以看的很明白),java 中创建线程的方法无非是两个,大家应该都能说出来——
第一个:继承Thread类。创建一个类继承Thread类,重写父类中的run()方法,把需要线程做事情的代码写到该方法中,如果想启动该线程的话可以实例化这个类,通过该类的对象调用start()方法就可以了。这里要说明的并不是调用run()方法,新手要注意。
第二个:实现Runnable接口。接下来的事情和上面的差不多,实现run()方法,但是要启动运行线程有点麻烦,首先实例化实现Runnable接口的对象,然后作为参数创建一个Thread类的对象并调用start()方法就可以了。
以上的创建线程的方法很简单就不贴代码了。简单的比较一下,大家会发现如果通过实现Runnable接口的方式去启动线程有点麻烦,但是平时个人写程序的时候自己总是用这类方法,并不是自己不嫌麻烦而是因为java大家都知道单根继承的,因此如果用第一种方法去创建线程的话有些时候就比较尴尬,这个类不能去继承其他类了,所以为了不出现这种情况,个人推荐还是用第二种方法吧,毕竟也代码也不会多多少。
2.线程的安全问题:
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
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.线程之间的通讯
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;
}
}
做出这样的改变之后再看结果:这样就保证了不会重复消费重复生产了。