关于java基础知识的归档整理(八)——多线程

第一、 多线程入门

一、进程和线程的概念
进程:正在执行中的程序,其实是应用程序在内存中的那片空间
线程:进程中的一个执行单元,负责进程中程序的运行,一个进程至少有一个线程,一个进程中是可以有多个线程的,这个应用程序也可以成为多线程程序

程序启动多线程有什么应用呢?
可以实现多部分程序同时执行,也称之为并发
多线程的使用可以合理的利用CPU资源,但是如果线程过多,则会降低性能

CPU处理程序时时通过快速的切换完成的,在我们看来,像随机一样

总结:什么时候需要用到多线程技术呢?
当部分代码需要同时执行时,就需要使用多线程技术

二、主线程
在之前的代码中,JVM启动后,必然有一个执行路径(线程),从main方法开始,一直执行到main方法结束,这个个线程在java中就称之为主线程

但是当主线程在程序的执行时,如果遇到循环而导致在指定位置停留时间过长,无法执行下面的程序
可不可以实现一个主线程负责执行其中一个循环,由另一个线程负责其他代码的执行,实现多部分代码同时执行,这就是多线程技术的应用

三、线程的创建
通过API中的英文Thread类的查询,通过对Thread类的描述,了解到线程的创建有两种方式
1、继承Thread类
1.1、定义一个类,继承thread
1.2、重写run()方法
1.3、创建子类对象,就是创建线程对象
1.4、调用start方法,开启线程并让线程执行,同时还会告诉JVM调用run方法
为什么这么做呢?
继承Thread类,因为Thread类是描述线程事物,具备线程应该有的功能
那为什么不直接创建Thread类的对象呢?
如:Thread t=new Thread();
t.start();
其实这么做是可以的,但是该start调用的是Thread类中的run方法,但是Thread类中的run方法没有做任何事情,更重要的是这个run方法并没有定义我们所需要让线程执行的代码

创建线程的目的是什么?
是为了建立单独的执行路径,让多部分代码实现同时执行
也就是说,线程的创建并执行需要给定的代码(线程的任务)
对于之前创建的主线程,其任务定义在主函数中
自定义的线程需要执行的任务都定义在run方法中
Thread类中的run方法内部的任务并不是我们所需要的,只要重写这个run方法即可
既然Thread类已经定义了线程任务的位置,只要在位置中定义任务代码即可,所以进行了重写run方法的动作

多线程执行时,在栈内存中,其实每一个执行线程都有一片自己所属的栈内存空间
当执行线程的任务结束了,线程自动在栈内存中释放,但当所有的线程都结束了,那么进程也就结束了

获取线程名称:
Thread:currentThread()获取当前线程对象。怎么名称呢?getName();
Thread.currentThread().getName();

主线程的名称: main
自定义的线程: Thread-1 线程多个时,数字顺延。Thread-2…

class Demo extends Thread
{
	private String name;
	Demo(String name)
	{
		this.name = name;	
	}
	public void run()
	{
		int[] arr = new int[3];
		System.out.println(arr[4]);
		for(int x=1; x<=20; x++)
		{
			System.out.println("name="+name+"..."+Thread.currentThread().getName()+"..."+x);
		}
	}
}


class  ThreadDemo
{
	public static void main(String[] args) 
	{
		//创建了两个线程对象。
		Demo d1 = new Demo("小强");
		Demo d2 = new Demo("旺财");
		d2.start();//将d2这个线程开启。
		d1.run();//由主线程负责。		
	}
}

线程对象调用 run方法和调用start方法区别?
调用run方法不开启线程。仅是对象调用方法。
调用start开启线程,并让jvm调用run方法在开启的线程中执 行。

2、线程创建的第二种方式:实现runable接口
2.1、定义类实现runable接口
2.2、覆盖接口中的run方法,将线程任务代码定义到run方法中
2.3、创建Thread类的对象
2.4、将runable接口的子类对象作为参数传递给Thread类的构造函数
2.5、调用Thread类的start方法开启线程

第二种方式实现Runable接口避免了单继承的局限性,所以较为常用
实现Runable接口的方式,更加的符合面向对象,线程分为两部分,一部分是线程对象,一部分是线程任务
继承Thread类,线程对象和线程任务耦合在一起,一旦创建了Thread类的子类对象,既是线程对象,又有线程任务,

实现Runable接口, 将线程任务单独分离出来封装成了对象,类型就是Runable接口类型,Runable接口对线程对象和线程任务进行了解耦

通过源码的形式讲解了一下将runnable接口的子类对象作为参数传递给Thread构造函数的原因

/*class Thread{

	private Runnable target;

	Thread(Runnable target)
	{
		this.target = target;
	}
	public void run() {
        if (target != null) {
            target.run();
        }
    }
	public void start()
	{
		run();
	}
}

Runnable d = new Demo();
Thread t = new Thread(d);
t.start();

*/

class Demo implements Runnable
{
	private String name;
	Demo(String name)
	{
		this.name = name;
	}
	//覆盖了接口Runnable中的run方法。
	public void run()
	{
		for(int x=1; x<=20; x++)
		{
			System.out.println("name="+name+"..."+Thread.currentThread().getName()+"..."+x);
		}
	}
}

class ThreadDemo2 
{
	public static void main(String[] args) 
	{
		//创建Runnable子类的对象。注意它并不是线程对象。
		Demo d = new Demo("Demo");
		//创建Thread类的对象,将Runnable接口的子类对象作为参数传递给Thread类的构造函数。
		Thread t1 = new Thread(d);
		Thread t2 = new Thread(d);
		//将线程启动。
		t1.start();
		t2.start();
		
		System.out.println(Thread.currentThread().getName()+"----->");


	}
}

第二、 多线程的安全问题

一、
案例:售票的例子
售票的动作需要同时执行,所以使用多线程技术,但是发生了线程安全问题:出现了错误的数据0、-1、-2
问题产生的原因:
1、线程任务中在操作共享的数据
2、线程任务操作共享数据的代码有多条(运算有多个)

解决思路:
只要让一个线程在执行线程任务时,将多条操作共享数据的代码执行完,在执行过程中,不要让其他线程参与运算,不就可以了吗?
java中解决此类问题,通过代码块来完成
同步代码块:synchronized
格式:

synchronized(对象){
	//需要被同步的代码
}

同步的好处:解决多线程安全问题
弊端:降低了程序的性能

同步前提:
必须保证多个线程在同步中使用的是同一个锁。
解决了什么问题?
当多线程安全问题发生时,加入了同步后,
问题依旧,就要通过这个同步的前提来判断同步是否写正确。
同步代码块解决多线程安全问题的图解

同步的另一种体现形式:同步函数。

同步函数使用的锁是哪个?
经过分析:大概猜的是this,因为函数必须被对象调用。

验证:
写一个同步代码块,写一个同步函数,如果同步代码块中的锁对象和同步函数中的锁对象是同一个,
就同步了,就没有错误的数据了。如果不是同一个锁对象,就不同步出现错误数据。

让两个线程,一个线程在同步代码块中执行,一个线程在同步函数中执行。

总结:同步函数使用的锁时this。

同步函数和同步代码块有什么区别吗?

同步函数使用的锁是固定的this。当线程任务只需要一个同步时完全可以使用同步函数。
同步代码块使用的锁可以是任意对象。当线程任务中需要多个同步时,必须通过锁来区分,这时必须使用同步代码块。
同步代码块较为常用。

同步的另一个弊端:

情况之一:当线程任务中出现了多个同步(多个锁)时,如果同步中嵌套了其他的同步。
这时容易引发一种现象:死锁。
这种情况能避免就避免掉。

死锁的代码体现:

class Test implements Runnable
{
	private boolean flag;
	Test(boolean flag)
	{
		this.flag = flag;
	}

	public void run()
	{
		if(flag)
		{
			while(true)
			{
				synchronized(MyLock.LOCKA)
				{
					System.out.println(Thread.currentThread().getName()+"...if......locka");
					synchronized(MyLock.LOCKB)
					{
						System.out.println(Thread.currentThread().getName()+"...if......lockb");
					}
				}
			}
		}
		else
		{
			while(true)
			{
				synchronized(MyLock.LOCKB)
				{
					System.out.println(Thread.currentThread().getName()+"...else......lockb");
					synchronized(MyLock.LOCKA)
					{
						System.out.println(Thread.currentThread().getName()+"...else......locka");
					}
				}
			}
		}
	}
}
//定义一个用于存储锁对象类。
class MyLock
{
	public static final Object LOCKA = new Object();
	public static final Object LOCKB = new Object();
}

class DeadLockTest 
{
	public static void main(String[] args) 
	{
		//创建两个线程任务。
		Test t1 = new Test(true);
		Test t2 = new Test(false);
		
		Thread t11 = new Thread(t1);
		Thread t22 = new Thread(t2);
		t11.start();
		t22.start();

	}
}

多线程中最为常见的应用案例:
生产者消费者问题。
生产和消费同时执行,需要多线程。
但是执行的任务却不相同,处理的资源确实相同的:线程间的通信。

1,描述一下资源。
2,描述生产者,因为具备着自己的任务。
3,描述消费者,因为具备着自己的任务。

问题1:数据错误:已经被生产很早期的商品,才被消费到。
出现线程安全问题,加入了同步解决。使用同步函数。
问题已解决:不会在消费到之前很早期的商品。

问题2:发现了连续生产却没有消费,同时对同一个商品进行多次消费。
希望的结果应该是生产一个商品,就被消费掉。生产下一个商品。
搞清楚几个问题?
生产者什么时候生产呢?消费者什么时候应该消费呢?
当盘子中没有面包时,就生产,如果有了面包,就不要生产。
当盘子中已有面包时,就消费,如果没有面包,就不要消费。

1,描述资源。属性:商品名称和编号,  行为:对商品名称赋值,获取商品。
class Resource
{
	private String name;
	private int count = 1;
	
	//1,提供设置的方法。
	public synchronized void set(String name)
	{
		//给成员变量赋值并加上编号。
		this.name = name + count;
		//编号自增。
		count++;
		//打印生产了哪个商品。
		System.out.println(Thread.currentThread().getName()+"......生产者...."+this.name);
	}
	public synchronized void out()
	{
		System.out.println(Thread.currentThread().getName()+"....消费者...."+this.name);
	}
}

//2,描述生产者。
class Producer implements Runnable
{
	private Resource r ;
	// 生产者一初始化就要有资源,需要将资源传递到构造函数中。
	Producer(Resource r)
	{
		this.r = r;
	}
	public void run()
	{
		while(true)
		{
			r.set("面包");
		}
	}
}

//3,描述消费者。
class Consumer implements Runnable
{
	private Resource r ;
	// 消费者一初始化就要有资源,需要将资源传递到构造函数中。
	Consumer(Resource r)
	{
		this.r = r;
	}
	public void run()
	{
		while(true)
		{
			r.out();
		}
	}
}




class ThreadDemo8
{
	public static void main(String[] args) 
	{
		//1,创建资源对象。
		Resource r = new Resource();

		//2,创建线程任务。
		Producer pro = new Producer(r);
		Consumer con = new Consumer(r);

		//3,创建线程。
		Thread t1 = new Thread(pro);
		Thread t2 = new Thread(con);

		t1.start();
		t2.start();
	}
}

多线程中最为常见的应用案例:
生产者消费者问题。
生产和消费同时执行,需要多线程。
但是执行的任务却不相同,处理的资源确实相同的:线程间的通信。

1,描述一下资源。
2,描述生产者,因为具备着自己的任务。
3,描述消费者,因为具备着自己的任务。

问题1:数据错误:已经被生产很早期的商品,才被消费到。
出现线程安全问题,加入了同步解决。使用同步函数。
问题已解决:不会在消费到之前很早期的商品。

问题2:发现了连续生产却没有消费,同时对同一个商品进行多次消费。
希望的结果应该是生产一个商品,就被消费掉。生产下一个商品。
搞清楚几个问题?
生产者什么时候生产呢?消费者什么时候应该消费呢?
当盘子中没有面包时,就生产,如果有了面包,就不要生产。
当盘子中已有面包时,就消费,如果没有面包,就不要消费。

生产者生产了商品后应该告诉消费者来消费。这时的生产者应该处于等待状态。
消费者消费了商品后,应该告诉生产者,这时消费者处于等待状态。

等待:wait();
告诉:notify();//唤醒

问题解决:实现生产一个消费一个。

=====================
等待/唤醒机制。
wait(): 会让线程处于等待状态,其实就是将线程临时存储到了线程池中。
notify():会唤醒线程池中任意一个等待的线程。
notifyAll():会唤醒线程池中所有的等待线程。

记住:这些方法必须使用在同步中,因为必须要标识wait,notify等方法所属的锁。
同一个锁上的notify,只能唤醒该锁上的被wait的线程。

为什么这些方法定义在Object类中呢?
因为这些方法必须标识所属的锁,而锁可以是任意对象,任意对象可以调用的方法必然时Object类中的方法。

举例:小朋友抓人游戏。

多生产多消费。
问题1;生产了商品没有被消费,同一个商品被消费多次。
Thread-0…生产者…面包2499//没有被消费。
Thread-1…生产者…面包2500
Thread-3…消费者…面包2500

被唤醒的线程没有判断标记,造成问题1的产生。
解决:只要让被唤醒的线程必须判断标记就可以了。将if判断标记的方式改为while判断标记。记住:多生产多消费,必须时while判断条件。

问题2:发现while判断后,死锁了。
原因:生产方唤醒了线程池中生产方的线程。本方唤醒了本方。
解决:希望本方要唤醒对方,没有对应的方法,所以只能唤醒所有。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值